java 多執行緒-3

宇宙砍柴人發表於2020-09-18

十、同步機制解決Thread繼承安全問題

建立三個視窗買票,共100張票。用繼承來實現

  1. 方式一:同步程式碼塊

    public class RunMainExtends {
        public static void main(String[] args) {
            Win win1 = new Win();
            Win win2 = new Win();
            Win win3 = new Win();
            // 設定執行緒的名字
            win1.setName("視窗一:");
            win2.setName("視窗二:");
            win3.setName("視窗三:");
            // 開啟執行緒
            win1.start();
            win2.start();
            win3.start();
        }
    }
    
    /* 方式一:同步程式碼塊*/
    class Win extends Thread {
        /*
        注意:ticket、object必須加static。因為例項化的三個執行緒物件,是不同的物件,他們各自有自己的棧和程式計數器。只有加了static才能讓這個三個物件共享ticket、object。
        */
        private static int ticket = 100; 
        private static Object object = new Object();
        @Override
        public void run() {
            while (true) {
                synchronized (object){// 同步程式碼塊,同步監視器,必須是同一把鎖
                    if (ticket > 0) {
                        try {
                            Thread.sleep(100);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"獲取到了第"+ticket+"票");
                        ticket--;
                    }else{
                        break;
                    }
                }
            }
        }
    }
    
    
  2. 方式二:同步方法

    public class RunMainExtends {
        public static void main(String[] args) {
            Win win1 = new Win();
            Win win2 = new Win();
            Win win3 = new Win();
            // 設定執行緒的名字
            win1.setName("視窗一:");
            win2.setName("視窗二:");
            win3.setName("視窗三:");
            // 開啟執行緒
            win1.start();
            win2.start();
            win3.start();
        }
    }
    
    
    /*方式二:同步方法
     * */
    class Win extends Thread {
        private static int ticket = 100;
        private static Object object = new Object();
        @Override
        public void run() {
            while (true) {
                show();
                if (ticket == 0) {
                    break;// 用於跳出迴圈
                }
            }
        }
    
        /* 定義一個同步方法,注意必須把這個方法定義為一靜態方法
         * */
        private static synchronized void show() {
            if (ticket > 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread().getName()+"獲取到了第"+ticket+"票");
                ticket--;
            }
        }
    }
    

    同步方法總結:

    /**
     * 同步機制解決“繼承Thread類”的執行緒安問題
     * 關於同步方法的總結:
     * 1. 同步方法任然設計來到同步監視器,只是不需要我們顯示的宣告。
     * 2. 非靜態同步方法,同步監視器是this
     * 3. 靜態同步方法,同步監視器是類本身
     */
    

十一、執行緒的死鎖問題

11.1 死鎖簡介

  1. 不同的執行緒分別佔用對方需要的同步資源不放棄,都在等待對方放棄自己需要的同步資源,就形成了執行緒的死鎖。【如:有多把鎖】
  2. 出現死鎖後,不會出現異常,不會出現提示,只是所有的執行緒都處於阻塞狀態,無法繼續

11.2 死鎖解決方法

  1. 專門的演算法、原則

  2. 儘量減少同步資源的定義

  3. 儘量避免巢狀同步

  4. 寫程式碼時,要儘量避免死鎖

十二、方式三:Lock(鎖)

12.1 Lock簡介

  1. 從JDK 5.0開始,Java提供了更強大的執行緒同步機制——通過顯式定義同步鎖物件來實現同步同步鎖使用Lock物件充當
  2. java.util.concurrent.locks.Lock介面是控制多個執行緒對共享資源進行訪問的工具。鎖提供了對共享資源的獨佔訪問,每次只能有一個執行緒對Lock物件加鎖,執行緒開始訪問共享資源之前應先獲得Lock物件。
  3. ReentrantLock類實現了Lock,它擁有與synchronized相同的併發性和記憶體語義,在實現執行緒安全的控制中,比較常用的是ReentrantLock,可以顯式加鎖、釋放鎖。

總結:Lock鎖是java在5.0之後提出來的一種執行緒同步機制,我們可以把它 列為解決執行緒安全的第三種方式

12.2 列子

// 賣票
public class RunMainLock {
    public static void main(String[] args) {
        Wins wins = new Wins();
        /*建立三個執行緒*/
        Thread win1 = new Thread(wins);
        Thread win2 = new Thread(wins);
        Thread win3 = new Thread(wins);
        /*設定執行緒名字*/
        win1.setName("視窗一:");
        win2.setName("視窗二:");
        win3.setName("視窗三:");
        /*開啟執行緒*/
        win1.start();
        win2.start();
        win3.start();
    }
}

class Wins implements Runnable{
    private  int ticket = 100;
    /*定義Lock鎖*/
    /*第一步:例項化ReentrantLock*/
    private ReentrantLock lock = new ReentrantLock();/* 引數為true,執行緒先進先出;預設為false,即cpu輪到誰就誰,看運氣*/
    @Override
    public void run() {

        while (true) {
            try{
                /*第二步:加鎖*/
                lock.lock();
                if (ticket > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + "賣票——" + ticket);
                    ticket--;
                } else {
                    break;
                }
            }finally {
                /*第三步:解鎖*/
                lock.unlock();
            }
        }
    }
}

12.3 synchronized與Lock的異同

  1. 相同點:

    1. 都能解決執行緒的安全問題
  2. 不同點

    1. Lock是顯式鎖(手動開啟和關閉鎖,別忘記關閉鎖),synchronized是隱式鎖,出了作用域自動釋放
    2. Lock只有程式碼塊鎖,synchronized有程式碼塊鎖和方法鎖
    3. 使用Lock鎖,JVM將花費較少的時間來排程執行緒,效能更好。並且具有更好的擴充套件性(提供更多的子類)
  3. 總結:

    優先使用順序(建議):
    Lock>同步程式碼塊(已經進入了方法體,分配了相應資源)>同步方法(在方法體之外)

十三、執行緒的通訊問題

13.1 初步程式碼

讓執行緒執行一次之後,阻塞,把cpu資源給其他執行緒。

/**
 * 使用兩個執行緒列印 1-100。執行緒1, 執行緒2 交替列印
 */
public class Test {
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        Thread thread1 = new Thread(test1);
        Thread thread2 = new Thread(test1);
        thread1.setName("執行緒一:");
        thread2.setName("執行緒二:");
        thread1.start();
        thread2.start();
    }
}
class Test1 implements Runnable{
    private int j = 1;
    @Override
    public  void run() {
        while (true) {
            synchronized (this) {// this的使用,指向呼叫該方法的物件的引用test1。在該處做為鎖
                if (j <= 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":列印"+j);
                    j++;       
                    try {
              /*第一個執行緒列印一個數字之後,呼叫wait方法便阻塞了,同時釋放了鎖。
              第二個執行緒獲得鎖之後,列印一個數字之後,呼叫wait方法也阻塞了,同時釋放了鎖。
              此時這兩個執行緒都被阻塞了,因此只列印了2次
              */
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    
                }else{
                    break;
                }
            }
        }
    }
}

結果:

總結wait方法

從執行結果我們不難推斷出,wait方法至少有2個作用或者功能:

  1. 阻塞當前執行緒
  2. 釋放鎖【不釋放鎖的的話,第二個執行緒怎麼進得來】

13.2 最終程式碼

/**
 * 使用兩個執行緒列印 1-100。執行緒1, 執行緒2 交替列印
 */
public class Test {
    public static void main(String[] args) {
        Test1 test1 = new Test1();
        Thread thread1 = new Thread(test1);
        Thread thread2 = new Thread(test1);
        thread1.setName("執行緒一");
        thread2.setName("執行緒二");
        thread1.start();
        thread2.start();
    }
}
class Test1 implements Runnable{
    private int j = 1;
    @Override
    public  void run() {
        while (true) {
            synchronized (this) {
                /*程式開始:假設此時第一個執行緒拿著鎖(this)進來了,第二個執行緒在外面等待著。
                第一個執行緒執行notify()方法沒有什麼影響,接著進入判斷,列印了“執行緒一:列印1”後,呼叫wait()方法後阻塞,
                同時釋放鎖(this)【這步操作很重要】。此時第二個執行緒拿到了鎖進來了,執行notify()方法,喚醒第一個執行緒
                【注意此時此刻,第二個執行緒還拿著鎖,所以第一個執行緒被喚醒之後,也只能在外面等待】,接著第二個執行緒進入判斷,
                列印“執行緒二:列印2”後,呼叫wait()方法後阻塞,同時釋放鎖。此時此刻第一個執行緒又拿到了鎖,接著執行。
                【後面就是重複的了,第一個執行緒和第二個執行緒之間,不停的被阻塞、又不停的被喚醒,直到迴圈結束】
                * 
                * */
                //第二步
                notify();
                if (j <= 100) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName()+":列印"+j);
                    j++;
                    try {
              /*第一個執行緒列印一個數字之後,呼叫wait方法便阻塞了,同時釋放了鎖。
              第二個執行緒獲得鎖之後,列印一個數字之後,呼叫wait方法也阻塞了,同時釋放了鎖。
              此時這兩個執行緒都被阻塞了,因此只列印了2次
              */
                        //第一步
                        wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }else{
                    break;
                }
            }
        }
    }
}

結果:實現了交替列印

總結 notify():

  1. 喚醒一個執行緒【因為還有notifyAll()方法】

13.3 執行緒通訊總結

執行緒通訊涉及到的三個方法:

  1. wait():一旦執行此方法,當前執行緒就進入阻塞狀態,並釋放同步監視器
  2. notify():一旦執行此方法,就會喚醒被wait方法的一個執行緒,如果有多個執行緒被wait,就喚醒優先順序高的那個,如果優先順序相同,就隨機喚醒一個
  3. notifyAll():一旦執行此方法,就會喚醒被wait方法的所有執行緒

注意:

  1. wait(),notify(),notifyAll()三個方法必須使用在同步程式碼塊或同步方法中。【lock中不能使用】

  2. wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。

    否則會出現IllegalMonitorStateException異常

  3. wait(),notify(),notifyAll()三個方法是定義在java.lang.object類中。

    為什麼是定義在Object類中?

    • 因為你要保證“2.wait(),notify(),notifyAll()三個方法的呼叫者必須是同步程式碼塊或同步方法中的同步監視器。”
    • 我們說過總結過任何一個物件都可以充當同步監視器,所有的物件都可以呼叫這三個方法,那麼這三個方法必然是是定義在object類中的,因為所有的類都繼承Object類。【注意Java中是單繼承,但是可以多層繼承。即如:Object---Parent---Child】

相關文章