Java學習多執行緒第二天

  • 2019 年 10 月 3 日
  • 筆記

內容介紹

  •  執行緒安全
  •  執行緒同步
  •  死鎖
  •  Lock鎖
  •  等待喚醒機制

1    多執行緒

1.1     執行緒安全

如果有多個執行緒在同時運行,而這些執行緒可能會同時運行這段程式碼。程式每次運行結果和單執行緒運行的結果是一樣的,而且其他的變數的值也和預期的是一樣的,就是執行緒安全的。

 我們通過一個案例,演示執行緒的安全問題:

電影院要賣票,我們模擬電影院的賣票過程。假設要播放的電影是 “功夫熊貓3”,本次電影的座位共100個(本場電影只能賣100張票)。

我們來模擬電影院的售票窗口,實現多個窗口同時賣 “魔童哪吒”這場電影票(多個窗口一起賣這100張票)

需要窗口,採用執行緒對象來模擬;需要票,Runnable介面子類來模擬

  •  測試類
public class ThreadDemo {      public static void main(String[] args) {          //創建票對象          Ticket ticket = new Ticket();            //創建3個窗口          Thread t1  = new Thread(ticket, "窗口1");          Thread t2  = new Thread(ticket, "窗口2");          Thread t3  = new Thread(ticket, "窗口3");            t1.start();          t2.start();          t3.start();      }  }

  •   模擬票

public class Ticket implements Runnable {      //共100票      int ticket = 100;        @Override      public void run() {          //模擬賣票          while(true){              if (ticket > 0) {                  //模擬選坐的操作                  try {                      Thread.sleep(1);                  } catch (InterruptedException e) {                      e.printStackTrace();                  }                  System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);              }          }      }  } 

運行結果發現:上面程式出現了問題

  •   票出現了重複的票
  •   錯誤的票 0、-1

其實,執行緒安全問題都是由全局變數及靜態變數引起的。若每個執行緒中對全局變數、靜態變數只有讀操作,而無寫操作,一般來說,這個全局變數是執行緒安全的;若有多個執行緒同時執行寫操作,一般都需要考慮執行緒同步,否則的話就可能影響執行緒安全。

1.2     執行緒同步(執行緒安全處理Synchronized)

java中提供了執行緒同步機制,它能夠解決上述的執行緒安全問題。

         執行緒同步的方式有兩種:

  •   方式1:同步程式碼塊
  •   方式2:同步方法

1.2.1    同步程式碼塊

同步程式碼塊: 在程式碼塊聲明上 加上synchronized

synchronized (鎖對象) {      可能會產生執行緒安全問題的程式碼  }

同步程式碼塊中的鎖對象可以是任意的對象;但多個執行緒時,要使用同一個鎖對象才能夠保證執行緒安全。

使用同步程式碼塊,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {      //共100票      int ticket = 100;      //定義鎖對象      Object lock = new Object();      @Override      public void run() {          //模擬賣票          while(true){              //同步程式碼塊              synchronized (lock){                  if (ticket > 0) {                      //模擬電影選坐的操作                      try {                          Thread.sleep(10);                      } catch (InterruptedException e) {                          e.printStackTrace();                      }                      System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);                  }              }          }      }  }

當使用了同步程式碼塊後,上述的執行緒的安全問題,解決了。

1.2.3    同步方法

  •   同步方法:在方法聲明上加上synchronized
public synchronized void method(){         可能會產生執行緒安全問題的程式碼  }

同步方法中的鎖對象是 this

使用同步方法,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {      //共100票      int ticket = 100;      //定義鎖對象      Object lock = new Object();      @Override      public void run() {          //模擬賣票          while(true){              //同步方法              method();          }      }    //同步方法,鎖對象this      public synchronized void method(){          if (ticket > 0) {              //模擬選坐的操作              try {                  Thread.sleep(10);              } catch (InterruptedException e) {                  e.printStackTrace();              }              System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);          }      }  }

  •   靜態同步方法: 在方法聲明上加上static synchronized
public static synchronized void method(){  可能會產生執行緒安全問題的程式碼  }

靜態同步方法中的鎖對象是 類名.class

1.3     死鎖

同步鎖使用的弊端:當執行緒任務中出現了多個同步(多個鎖)時,如果同步中嵌套了其他的同步。這時容易引發一種現象:程式出現無限等待,這種現象我們稱為死鎖。這種情況能避免就避免掉。

synchronzied(A鎖){      synchronized(B鎖){    }  }

我們進行下死鎖情況的程式碼演示:

  •   定義鎖對象類
public class MyLock {      public static final Object lockA = new Object();      public static final Object lockB = new Object();  }

  •   執行緒任務類
public class ThreadTask implements Runnable {      int x = new Random().nextInt(1);//0,1      //指定執行緒要執行的任務程式碼      @Override      public void run() {          while(true){              if (x%2 ==0) {                  //情況一                  synchronized (MyLock.lockA) {                      System.out.println("if-LockA");                      synchronized (MyLock.lockB) {                          System.out.println("if-LockB");                          System.out.println("if大口吃肉");                      }                  }              } else {                  //情況二                  synchronized (MyLock.lockB) {                      System.out.println("else-LockB");                      synchronized (MyLock.lockA) {                          System.out.println("else-LockA");                          System.out.println("else大口吃肉");                      }                  }              }              x++;          }      }  }

  •   測試類
public class ThreadDemo {      public static void main(String[] args) {          //創建執行緒任務類對象          ThreadTask task = new ThreadTask();          //創建兩個執行緒          Thread t1 = new Thread(task);          Thread t2 = new Thread(task);          //啟動執行緒          t1.start();          t2.start();      }  }

1.4     Lock介面

查閱API,查閱Lock介面描述,Lock 實現提供了比使用 synchronized 方法和語句可獲得的更廣泛的鎖定操作。

Lock提供了一個更加面對對象的鎖,在該鎖中提供了更多的操作鎖的功能。

我們使用Lock介面,以及其中的lock()方法和unlock()方法替代同步,對電影院賣票案例中Ticket類進行如下程式碼修改:

public class Ticket implements Runnable {      //共100票      int ticket = 100;        //創建Lock鎖對象      Lock ck = new ReentrantLock();        @Override      public void run() {          //模擬賣票          while(true){              //synchronized (lock){              ck.lock();                  if (ticket > 0) {                      //模擬選坐的操作                      try {                          Thread.sleep(10);                      } catch (InterruptedException e) {                          e.printStackTrace();                      }                      System.out.println(Thread.currentThread().getName() + "正在賣票:" + ticket--);                  }              ck.unlock();              //}          }      }  }

1.5     等待喚醒機制

在開始講解等待喚醒機制之前,有必要搞清一個概念——執行緒之間的通訊:多個執行緒在處理同一個資源,但是處理的動作(執行緒的任務)卻不相同。通過一定的手段使各個執行緒能有效的利用資源。而這種手段即—— 等待喚醒機制。

等待喚醒機制所涉及到的方法:

  •   wait() :等待,將正在執行的執行緒釋放其執行資格 和 執行權,並存儲到執行緒池中。
  •   notify():喚醒,喚醒執行緒池中被wait()的執行緒,一次喚醒一個,而且是任意的。
  •   notifyAll(): 喚醒全部:可以將執行緒池中的所有wait() 執行緒都喚醒。

其實,所謂喚醒的意思就是讓 執行緒池中的執行緒具備執行資格。必須注意的是,這些方法都是在 同步中才有效。同時這些方法在使用時必須標明所屬鎖,這樣才可以明確出這些方法操作的到底是哪個鎖上的執行緒。

仔細查看JavaAPI之後,發現這些方法 並不定義在 Thread中,也沒定義在Runnable介面中,卻被定義在了Object類中,為什麼這些操作執行緒的方法定義在Object類中?

因為這些方法在使用時,必須要標明所屬的鎖,而鎖又可以是任意對象。能被任意對象調用的方法一定定義在Object類中。

接下里,我們先從一個簡單的示例入手:

如上圖說示,輸入執行緒向Resource中輸入name ,sex , 輸出執行緒從資源中輸出,先要完成的任務是:

  •   1.當input發現Resource中沒有數據時,開始輸入,輸入完成後,叫output來輸出。如果發現有數據,就wait();
  •   2.當output發現Resource中沒有數據時,就wait() ;當發現有數據時,就輸出,然後,叫醒input來輸入數據。

下面程式碼,模擬等待喚醒機制的實現:

  •  模擬資源類
public class Resource {      private String name;      private String sex;      private boolean flag = false;        public synchronized void set(String name, String sex) {          if (flag)              try {                  wait();              } catch (InterruptedException e) {                  e.printStackTrace();              }          // 設置成員變數          this.name = name;          this.sex = sex;          // 設置之後,Resource中有值,將標記該為 true ,          flag = true;          // 喚醒output          this.notify();      }        public synchronized void out() {          if (!flag)              try {                  wait();              } catch (InterruptedException e) {                  e.printStackTrace();              }          // 輸出執行緒將數據輸出          System.out.println("姓名: " + name + ",性別: " + sex);          // 改變標記,以便輸入執行緒輸入數據          flag = false;          // 喚醒input,進行數據輸入          this.notify();      }  }

  • 輸入執行緒任務類
public class Input implements Runnable {      private Resource r;        public Input(Resource r) {          this.r = r;      }        @Override      public void run() {          int count = 0;          while (true) {              if (count == 0) {                  r.set("小明", "男生");              } else {                  r.set("小花", "女生");              }              // 在兩個數據之間進行切換              count = (count + 1) % 2;          }      }  }

  • 輸出執行緒任務類
public class Output implements Runnable {      private Resource r;        public Output(Resource r) {          this.r = r;      }        @Override      public void run() {          while (true) {              r.out();          }      }  }

  • 測試類
public class ResourceDemo {      public static void main(String[] args) {          // 資源對象          Resource r = new Resource();          // 任務對象          Input in = new Input(r);          Output out = new Output(r);          // 執行緒對象          Thread t1 = new Thread(in);          Thread t2 = new Thread(out);          // 開啟執行緒          t1.start();          t2.start();      }  }

  •  多執行緒有幾種實現方案,分別是哪幾種?

         a, 繼承Thread類

         b, 實現Runnable介面

         c, 通過執行緒池,實現Callable介面

  •   同步有幾種方式,分別是什麼?

         a,同步程式碼塊

         b,同步方法

           靜態同步方法

  •   啟動一個執行緒是run()還是start()?它們的區別?

         啟動一個執行緒是start()

         區別:

                  start: 啟動執行緒,並調用執行緒中的run()方法

                  run  : 執行該執行緒對象要執行的任務

  •   sleep()和wait()方法的區別

         sleep: 不釋放鎖對象, 釋放CPU使用權

                           在休眠的時間內,不能喚醒

                  wait(): 釋放鎖對象, 釋放CPU使用權

                           在等待的時間內,能喚醒

  •  為什麼wait(),notify(),notifyAll()等方法都定義在Object類中

         鎖對象可以是任意類型的對象