Java基礎(七)——多線程


一、概述

1、介紹

  Java VM 啟動的時候會有一個進程Java.exe,該進程中至少有一個線程負責Java程序的執行。而且這個線程運行的代碼存在於main方法中,該線程稱之為主線程。其實從細節上來說,JVM不止啟動了一個線程,其實至少有三個線程。除了main() 主線程,還有 gc() 負責垃圾回收機制的線程,異常處理線程。當然如果發生異常,會影響主線程。
  局部的變量在每一個線程區域中都有獨立的一份。

2、程序、進程、線程

  程序(program)是為完成特定任務、用某種語言編寫的一組指令的集合 。即一段靜態的代碼 ,靜態對象。
  進程(process)是程序的一次執行過程,或是正在運行的一個程序。是一個動態過程,有它自身的產生、存在和消亡的過程——生命周期。如運行中的QQ、運行中的 MP3播放器。
  線程(thread)是一個程序內部的一條執行路徑。由進程可進一步細化為線程。若一個進程同一時間並行執行多個線程,就是支持多線程的。

  理解:
  程序是靜態的,進程是動態的。
  進程作為資源分配的基本單位,系統在運行時會為每個進程分配不同的內存區域。每一個進程執行都有一個執行順序,該順序是一個執行路徑,或者叫一個控制單元。一個進程中至少有一個線程。一個進程當中有可能會存在多條執行路徑。
  線程作為調度和執行的基本單位,每個線程擁有獨立的運行棧和程序計數器(pc),線程切換的開銷小。是進程中一個獨立的控制單元,線程在控制着進程的執行。是進程中的內容。
  一個進程中的多個線程共享相同的內存單元/內存地址空間,它們從同一堆中分配對象,可以訪問相同的變量和對象。這就使得線程間通信更簡便、高效。但多個線程操作共享的系統資源可能就會帶來安全隱患。
  形象比喻:一個寢室就是一個進程,能住四個人,就有四個線程,四個人共享陽台和廁所,就是訪問相同的變量和對象。而各自的書桌與床是各自私有的。

  為什么有安全隱患?Java內存模型:

  Class Loader:類加載器。
  Execution Engine:執行引擎負責解釋命令,提交操作系統執行。
  Native Interface:本地接口。
  Runtime Data Area:運行時數據區。

  進程可以細化為多個線程。每個線程,擁有自己獨立的棧和程序計數器;多個線程共享同一個進程中的方法區和堆。也就是說:
  虛擬機棧、程序計數器:一個線程一份,線程私有。
  方法區、堆:一個進程一份,也就是多個線程共享一份。
  結論:多個線程可以共享(一個進程中的)堆(有變量和對象)和方法區,所以實現多個線程間的通信是比較方便的,但是也導致多個線程操作共享資源可能會帶來安全隱患。線程同步即用來解決上面的安全隱患。

3、單核與多核

  單核CPU:其實是一種假的多線程,因為在一個時間單元內,只能執行一個線程的任務。例如:雖然有多車道,但是收費站只有一個工作人員在收費,只有收了費才能通過,那么CPU 就好比收費人員。如果有某個人不想交錢,那么收費人員可以把他"掛起"(晾着他,等他想通了,准備好了錢,再去收費),但是因為CPU時間單元特別短,因此感覺不出來。
  多核CPU:如果是多核的話,才能更好的發揮多線程的效率。現在的服務器都是多核的。

4、並行與並發

  並行:多個CPU同時執行多個任務。比如:多個人同時做不同的事。
  並發:一個CPU同時執行多個任務(采用時間片原理)。比如:秒殺、多個人做同一件事。

5、多線程優點

  提高應用程序的響應。對圖形化界面更有意義,可增強用戶體驗;提高計算機系統CPU的利用率;改善程序結構,將既長又復雜的進程分為多個線程,獨立運行,利於理解和修改。

二、線程的創建和使用

1、介紹

  Thread 類的特性:
  每個線程都是通過某個特定 Thread 對象的 run() 方法來完成操作的,經常把 run() 方法的主體稱為線程體。
  通過該 Thread 對象的 start() 方法來啟動這個線程,而非直接調用 run()。

  說明:
  run() 方法由 JVM 調用,什么時候調用,執行的過程控制都由操作系統的 CPU 調度決定 。
  想要啟動多線程,必須調用 start() 方法 。
  一個線程對象只能調用一次 start() 方法啟動,如果重復調用了,則將拋出異常"IllegalThreadStateException"。

2、創建的四種方式

  JDK 1.5 之前創建線程有兩種方法:繼承 Thread 類的方式、實現 Runnable 接口的方式。
  JDK 5.0 之后,新增兩種方法:實現 Callable 接口的方式、線程池。
  代碼示例:方式一、繼承 Thread 類

 1 public class Main {
 2     public static void main(String[] args) {
 3         MyThread myThread = new MyThread();
 4         myThread.start();
 5     }
 6 }
 7 
 8 class MyThread extends Thread {
 9 
10     @Override
11     public void run() {
12         for (int i = 0; i < 100; i++) {
13             if (i % 2 == 0) {
14                 System.out.println(Thread.currentThread().getName() + ":" + i);
15             }
16         }
17     }
18 }
19 
20 // 匿名方式
21 public class Main {
22     public static void main(String[] args) {
23 
24         new Thread() {
25             @Override
26             public void run() {
27                 for (int i = 0; i < 100; i++) {
28                     if (i % 2 == 0) {
29                         System.out.println(Thread.currentThread().getName() + ":" + i);
30                     }
31                 }
32             }
33         }.start();
34     }
35 }
繼承Thread類

  代碼示例:方式二、實現 Runnable 接口
  調用 Thread 類的 start 方法開啟線程,會調用當前線程的 run() 方法。

 1 public class Main {
 2     public static void main(String[] args) {
 3         MyThread myThread = new MyThread();
 4         Thread thread1 = new Thread(myThread);
 5         Thread thread2 = new Thread(myThread);
 6 
 7         // 這里開啟了兩個線程.
 8         thread1.start();
 9         thread2.start();
10     }
11 }
12 
13 class MyThread implements Runnable {
14     
15     @Override
16     public void run() {
17         for (int i = 0; i < 100; i++) {
18             if (i % 2 == 0) {
19                 System.out.println(Thread.currentThread().getName() + ":" + i);
20             }
21         }
22     }
23 }
24 
25 // 匿名方式
26 public class Main {
27     public static void main(String[] args) {
28         new Thread(new Runnable() {
29             public void run() {
30                 for (int i = 0; i < 100; i++) {
31                     if (i % 2 == 0) {
32                         System.out.println(Thread.currentThread().getName() + ":" + i);
33                     }
34                 }
35             }
36         }).start();
37     }
實現Runnable接口

  代碼示例:方式三、實現 Callable 接口

 1 public class Main {
 2     public static void main(String[] args) throws Exception {
 3         Number number = new Number();
 4 
 5         FutureTask<Integer> futureTask = new FutureTask<>(number);
 6         // 通過線程啟動這個任務
 7         new Thread(futureTask).start();
 8 
 9         final Integer sum = futureTask.get();
10         System.out.println(sum); // 2550
11     }
12 }
13 
14 class Number implements Callable<Integer> {
15 
16     @Override
17     public Integer call() throws Exception {
18         int sum = 0;
19         // 求100以內的偶數和
20         for (int i = 0; i <= 100; i++) {
21             if (i % 2 == 0) {
22                 sum = sum + i;
23             }
24         }
25         return sum;
26     }
27 }
實現Callable接口

  值得注意:
  將futureTask作為Runnable實現類傳遞,本質為方式二。而調用 Thread 類的 start 方法開啟線程,會調用當前線程的 run() 方法
  當前線程的 run()方法 --> futureTask.run() --> callable.call()。
  實例都是通過構造器初始化的。
  返回值即為 call() 方式的返回值。

1 // Thread類
2 @Override
3 public void run() {
4     if (target != null) {
5         target.run(); // futureTask.run()
6     }
7 }

  方式四:線程池
  見標簽:聊聊並發

3、三種方式的區別

  ①相比繼承,實現Runnable接口方式
  好處:避免了單繼承的局限性。通過多個線程可以共享同一個接口實現類的對象,天然就能體現多個線程處理共享數據的情況(有共享變量)。參考賣票案例。
  應用:創建了多個線程,多個線程共享數據,則使用實現接口的方式,數據天然的就是共享的。
在定義線程時,建議使用實現接口的方式。

1 public class Thread extends Object implements Runnable

  ②相比Runnable,Callable功能更強大些:
  比run()方法,call()有返回值;call()可以拋出異常。被外面捕獲,獲取異常信息;支持泛型的返回值;需要借助FutureTask類,比如獲取返回結果。

4、線程有關方法

  void start():啟動線程,並執行對象的 run() 方法。
  run():線程在被調度時執行的方法體。
  String getName():返回線程的名稱。
  void setName(String name):設置該線程的名稱。
  static Thread currentThread():返回當前線程對象 。在 Thread 子類中就是 this ,通常用於主線程和 Runnable 實現類。

  static void yield():線程讓步,釋放當前CPU的執行權。下一刻可能又立馬得到。暫停當前正在執行的線程,把執行機會讓給優先級相同或更高的線程。若隊列中沒有同優先級的線程,忽略此方法。

  join():在線程A中調用線程B的join(),此時線程A就進入阻塞狀態,直到線程B完全執行完以后,線程A才結束阻塞狀態(相當於插隊)。低優先級的線程也可以獲得執行。

  static void sleep(long millis): 讓當前線程睡眠指定毫秒數,在睡眠期間,當前線程是阻塞狀態。不會釋放鎖。令當前活動線程在指定時間段內放棄對 CPU 控制,使其他線程有機會被執行,時間到后重排隊。拋出 InterruptedException 異常。

  stop():強制線程生命期結束,不推薦使用。(已過時)。
  boolean isAlive():返回 boolean,判斷線程是否還活着。

  代碼示例:join() 使用

 1 public class Main {
 2     public static void main(String[] args) throws InterruptedException {
 3         MyThread myThread = new MyThread();
 4         myThread.setName("線程一");
 5         myThread.start();
 6 
 7         Thread.currentThread().setName("主線程");
 8         for (int i = 0; i < 100; i++) {
 9             if (i % 2 == 0) {
10                 System.out.println(Thread.currentThread().getName() + ":" + i);
11             }
12             // 主線程拿到CPU執行權到i=40時,會進入阻塞,等 線程一 執行完才結束
13             if (i == 40) {
14                 myThread.join();
15             }
16         }
17     }
18 }
19 
20 class MyThread extends Thread {
21 
22     @Override
23     public void run() {
24         for (int i = 0; i < 100; i++) {
25             if (i % 2 == 0) {
26                 System.out.println(Thread.currentThread().getName() + ":" + i);
27             }
28         }
29     }
30 }

5、線程調度

  調度策略:時間片輪訓;搶占式,高優先級的線程搶占CPU。

  Java 的調度方法:同優先級線程組成先進先出隊列(先到先服務),使用時間片策略;對高優先級,使用優先調度的搶占式策略。

6、線程的優先級

  等級:

  MAX_PRIORITY:10
  MIN PRIORITY:1
  NORM_PRIORITY:5

  涉及的方法:

  getPriority():返回線程優先級
  setPriority(int newPriority):設置線程的優先級

  說明:線程創建時繼承父線程的優先級;高優先級的線程要搶占低優先級線程cpu的執行權。只是從概率上講,高優先級的線程高概率被執行,低優先級只是獲得調度的概率低,並非一定是在高優先級線程執行完之后才被調用。

7、線程的分類

  Java中的線程分為兩類:一種是守護線程,一種是用戶線程 。
  它們在幾乎每個方面都是相同的,唯一的區別是判斷 JVM 何時離開。
  守護線程是用來服務用戶線程的,通過在 start() 方法前調用thread.setDaemon(true)可以把一個用戶線程變成一個守護線程。
  Java 垃圾回收就是一個典型的守護線程。main() 主線程就是一個用戶線程。
  若 JVM 中都是守護線程,當前 JVM 將退出 。當用戶線程執行完畢,守護線程也結束。形象理解:兔死狗烹,鳥盡弓藏。

8、線程使用-售票

  代碼示例:方式一、三個窗口同時售100張票

 1 // 繼承Thread類來完成
 2 public class Main {
 3     public static void main(String[] args) {
 4         // new 了三次
 5         Window window1 = new Window();
 6         Window window2 = new Window();
 7         Window window3 = new Window();
 8         
 9         window1.setName("窗口一");
10         window2.setName("窗口二");
11        window3.setName("窗口三");
12 
13        window1.start();
14        window2.start();
15        window3.start();
16     }
17 }
18 
19 class Window extends Thread {
20     
21     private int ticket = 100;
22 
23     @Override
24     public void run() {
25         while (ticket > 0) {
26             System.out.println(getName() + ":賣票,票號為:" + ticket);
27             ticket--;
28         }
29     }
30 }

  由於 new 了三次,結果三個窗口各自出售了100張(共300張)。要想三個窗口共同賣 100 張票,對共享變量的訪問。修改如下:

1 private static int ticket = 100;

  結果:100號(也可能是其他號)票依然有重復,這里就存在線程安全問題。

  代碼示例:方式二、三個窗口同時售100張票

 1 // 實現Runnable接口來完成
 2 public class Main {
 3     public static void main(String[] args) {
 4         Window window = new Window();
 5 
 6         // 這里體現了三個線程天然的共享一個對象 window 
 7         Thread window1 = new Thread(window);
 8         Thread window2 = new Thread(window);
 9         Thread window3 = new Thread(window);
10 
11         window1.setName("窗口一");
12         window2.setName("窗口二");
13         window3.setName("窗口三");
14 
15         window1.start();
16         window2.start();
17         window3.start();
18     }
19 }
20 
21 class Window implements Runnable {
22 
23     private int ticket = 100;
24 
25     public void run() {
26         while (ticket > 0) {
27             System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
28             ticket--;
29         }
30     }
31 }

  結果:100號(也可能是其他號)票依然有重復,這里就存在線程安全問題。
  上面兩種方式,只是實現不同,但都存在線程安全問題。后面會解決。

三、線程的生命周期

1、五種狀態

  JDK 中用 Thread.State 枚舉定義了線程的幾種狀態。要想實現多線程必須在主線程中創建新的線程對象。Java 語言使用 Thread 類及其子類的對象來表示線程,在它的一個完整的生命周期中通常要經歷如下的五種狀態:
  新建:當一個 Thread 類或其子類的對象被聲明並創建時,新生的線程對象處於新建狀態。
  就緒:處於新建狀態的線程被 start() 后,將進入線程隊列等待 CPU 時間片,此時它已具備了運行的條件,只是沒分配到 CPU 資源。
  運行:當就緒的線程被調度並獲得 CPU 資源時便進入運行狀態,開始執行 run(),run() 方法定義了線程的操作和功能。
  阻塞:在某種特殊情況下,被人為掛起或執行輸入輸出操作時,讓出 CPU 並臨時中止自己的執行,進入阻塞狀態。
  死亡:線程完成了它的全部工作或線程被提前強制性的中止或出現異常導致結束。

2、狀態轉換圖

3、涉及方法

  線程類 Thread 的方法:Thread.yield()、Thread.sleep()
  對象類 Object 的方法:wait()、notify()、notifyAll()
  線程對象的方法:其余都是。

  suspend():掛起。為什么過時?因為可能會導致死鎖。已過時
  resume():結束掛起的狀態。容易導致死鎖。已過時
  stop():線程終止。已過時

  suspend()、resume():容易導致死鎖,這兩個操作就好比播放器的暫停和恢復。但這兩個 API 是過期的,也就是不建議使用的。
  不推薦使用 suspend() 去掛起線程的原因,是因為 suspend() 導致線程暫停的同時,並不會去釋放任何鎖資源。其他線程都無法訪問被它占用的鎖。直到對應的線程執行 resume() 方法后,被掛起的線程才能繼續,從而其它被阻塞在這個鎖的線程才可以繼續執行。
  但是,如果 resume() 操作出現在 suspend() 之前執行,那么線程將一直處於掛起狀態,同時一直占用鎖,這就產生了死鎖。而且,對於被掛起的線程,它的線程狀態居然還是 Runnable。

四、線程的同步

1、售票的問題

  上述售票案例中,不管是方式一還是方式二,都存在重票的情況,這里讓線程睡眠0.1s,暴露出錯票的情況。這就是線程安全問題。
  代碼示例:有線程安全問題的售票

 1 // 方式一、繼承Thread類來完成
 2 class Window extends Thread {
 3 
 4     private static int ticket = 100;
 5 
 6     public void run() {
 7         while (ticket > 0) {
 8 
 9             try {
10                 // 增大錯票的概率
11                 Thread.sleep(100);
12             } catch (InterruptedException e) {
13                 e.printStackTrace();
14             }
15 
16             System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
17             ticket--;
18         }
19     }
20 }
21 
22 // 方式二、實現Runnable接口來完成
23 class Window implements Runnable {
24 
25     private int ticket = 100;
26 
27     public void run() {
28         while (ticket > 0) {
29 
30             try {
31                 // 增大錯票的概率
32                 Thread.sleep(100);
33             } catch (InterruptedException e) {
34                 e.printStackTrace();
35             }
36 
37             System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
38             ticket--;
39         }
40     }
41 }
42 
43 // 可能的結果.這里只放最后 10 行打印
44 窗口二:賣票,票號為:9
45 窗口三:賣票,票號為:9
46 窗口一:賣票,票號為:7
47 窗口二:賣票,票號為:6
48 窗口三:賣票,票號為:6
49 窗口一:賣票,票號為:4
50 窗口二:賣票,票號為:3
51 窗口三:賣票,票號為:2
52 窗口一:賣票,票號為:1
53 窗口二:賣票,票號為:0
54 窗口三:賣票,票號為:-1

  問題:很明顯,上述售票有重票(9),還是錯票(-1),那么如何解決這種線程安全問題呢?
  注意:這里並不是加了sleep之后,才出現重票錯票的情況。sleep只是將這種情況出現的概率提高了。
  解決:解決線程安全問題有兩種方式
  ①synchronize(隱式鎖):同步代碼塊、同步方法
  ②Lock(顯式鎖)

2、同步代碼塊(synchronize)

1 synchronized (同步監視器) {
2     // 需要被同步的代碼
3 }

  ①什么是需要被同步的代碼?
  有沒有共享數據:多個線程共同操作的變量。案例中的 ticket。
  有操作共享數據的代碼,即為需要被同步的代碼。
  同步監視器,俗稱:鎖。任何一個類的對象,都可以充當鎖。要求:多個線程必須共用同一把鎖。
  ②優缺點?
  優點:同步的方式,解決了線程安全的問題。
  缺點:對同步代碼的操作,只能有一個線程參與,其他線程必須等待。相當於是一個單線程的過程,效率低。

  代碼示例:處理"實現Runnable的線程安全問題"
  注意:以下的 object 可以換成 this。

 1 class Window implements Runnable {
 2 
 3     private int ticket = 100;
 4     private Object object = new Object();
 5 
 6     public void run() {
 7         while (true) {
 8             synchronized (object) { // this 唯一的 Window 對象
 9                 if (ticket > 0) {
10                     try {
11                         // 增大錯票的概率
12                         Thread.sleep(100);
13                     } catch (InterruptedException e) {
14                         e.printStackTrace();
15                     }
16 
17                     System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
18                     ticket--;
19                 } else {
20                     break;
21                 }
22             }
23         }
24     }
25 }

  代碼示例:處理"繼承Thread類的線程安全問題"
  仿造前一個的方案:傳入一個object,是行不通的。原因:不是同一把鎖。有三個object。寫 this 也是不對的,正確寫法:
  以下的 object 可以換成 Window.class。類:也是對象。Window.class 只會加載一次。

 1 class Window extends Thread {
 2 
 3     private static int ticket = 100;
 4     private static Object object = new Object();
 5 
 6     public void run() {
 7         while (true) {
 8             synchronized (object) { // Window.class
 9                 if (ticket > 0) {
10                     try {
11                         // 增大錯票的概率
12                         Thread.sleep(100);
13                     } catch (InterruptedException e) {
14                         e.printStackTrace();
15                     }
16 
17                     System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
18                     ticket--;
19                 } else {
20                     break;
21                 }
22             }
23         }
24     }
25 }

  深刻理解:synchronized包含的代碼塊一定不能包含了while。這樣會導致,第一個拿到鎖的線程把票賣光之后,才釋放鎖,這不符合題意。

3、同步方法(synchronize)

  如果操作共享數據的代碼完整的聲明在一個方法中,那么不妨將此方法聲明為同步的。
  代碼示例:處理"實現Runnable的線程安全問題"

 1 class Window implements Runnable {
 2 
 3     private int ticket = 100;
 4 
 5     public void run() {
 6         while (true) {
 7             show();
 8         }
 9     }
10 
11    // 這里使用了默認的鎖:this
12    private synchronized void show() {
13         if (ticket > 0) {
14             try {
15                 // 增大錯票的概率
16                 Thread.sleep(100);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20 
21             System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
22             ticket--;
23         }
24     }
25 }

  代碼示例:處理"繼承Thread類的線程安全問題"

 1 class Window extends Thread {
 2 
 3     private static int ticket = 100;
 4 
 5     public void run() {
 6         while (true) {
 7             show();
 8         }
 9     }
10     
11    // 這里使用 Window.class
12    private static synchronized void show() {
13         if (ticket > 0) {
14             try {
15                 // 增大錯票的概率
16                 Thread.sleep(100);
17             } catch (InterruptedException e) {
18                 e.printStackTrace();
19             }
20 
21             System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
22             ticket--;
23         }
24     }
25 }

  總結:同步方法仍然涉及到鎖,只是不需要我們顯式聲明;非靜態的同步方法,鎖是this,靜態的同步方法,鎖是類對象。

5、死鎖的問題

  死鎖:不同的線程分別占用對方需要的同步資源不放棄,都在等待對方釋放自己需要的同步資源,就形成了線程的死鎖。
  出現死鎖后,不會出現異常,不會出現提示,只是所有的線程都處於阻塞狀態,無法繼續。我們同步時,應盡量避免出現死鎖。
  解決方法:專門的算法、原則。盡量減少同步資源的定義。盡量避免嵌套同步。
  代碼示例:死鎖

 1 public class Main {
 2     public static void main(String[] args) {
 3         final StringBuffer buffer1 = new StringBuffer();
 4         final StringBuffer buffer2 = new StringBuffer();
 5 
 6         // 繼承的匿名方式
 7         new Thread() {
 8             @Override
 9             public void run() {
10                 synchronized (buffer1) {
11                     buffer1.append("a");
12                     buffer2.append("1");
13                     // 可以暴露出死鎖的問題
14 //                    try {
15 //                        Thread.sleep(200);
16 //                    } catch (InterruptedException e) {
17 //                        e.printStackTrace();
18 //                    }
19 
20                     synchronized (buffer2) {
21                         buffer1.append("b");
22                         buffer2.append("2");
23                         System.out.println(getName() + ":" + buffer1 + "-" + buffer2);
24                     }
25                 }
26             }
27         }.start();
28 
29         // 實現的匿名方式
30         new Thread(new Runnable() {
31             @Override
32             public void run() {
33                 synchronized (buffer2) {
34                     buffer1.append("c");
35                     buffer2.append("3");
36 
37                     synchronized (buffer1) {
38                         buffer1.append("d");
39                         buffer2.append("4");
40 
41                         System.out.println(Thread.currentThread().getName() + ":" + buffer1 + "-" + buffer2);
42                     }
43                 }
44             }
45         }).start();
46     }
47 }
48 
49 // 可能的結果
50 Thread-0:ab-12
51 Thread-1:abcd-1234
52 
53 // 還可能是別的結果

  讓線程一在獲取到buffer1的時候,睡眠0.1s。線程二獲取到buffer2的時候,睡眠0.1s,可以讓死鎖的問題暴露出來。

6、Lock(接口)

  從JDK 5.0 開始,Java提供了更強大的線程同步機制——通過顯示定義同步鎖對象來實現同步。同步鎖使用Lock對象充當。
  java.util.concurrent.locks.Lock接口是控制多個線程對共享資源進行訪問的工具。鎖提供了對共享資源的獨占訪問,每次只能有一個線程對Lock對象加鎖,線程開始訪問共享資源之前應先獲得Lock對象。
  ReentrantLock類(可重入鎖)實現了Lock,它擁有與synchronize相同的並發性和內存語義,在實現線程安全的控制中,比較常用的是ReentrantLock,可以顯示加鎖、釋放鎖。
  代碼示例:用 Lock 解決賣票的同步問題。

 1 class Window implements Runnable {
 2 
 3     private int ticket = 100;
 4     private Lock lock = new ReentrantLock(true);
 5 
 6     public void run() {
 7         while (true) {
 8             try {
 9                 // 獲取鎖
10                 lock.lock();
11                 if (ticket > 0) {
12                     try {
13                         Thread.sleep(100);
14                     } catch (InterruptedException e) {
15                         e.printStackTrace();
16                     }
17 
18                     System.out.println(Thread.currentThread().getName() + ":賣票,票號為:" + ticket);
19                     ticket--;
20                 } else {
21                     break;
22                 }
23             } finally {
24                 // 釋放鎖
25                 lock.unlock();
26             }
27         }
28 
29         System.out.println(Thread.currentThread().getName() + "A");
30     }
31 }
32 
33 // 結果.這里只截取了最后幾行
34 …………
35 窗口一:賣票,票號為:4
36 窗口二:賣票,票號為:3
37 窗口三:賣票,票號為:2
38 窗口一:賣票,票號為:1
39 窗口二A
40 窗口三A
41 窗口一A

  說明:即使遇到break,finally里的代碼塊也會被執行。
  ReentrantLock(boolean fair):含參構造器,fair:true,公平鎖、線程先進先出。保證當線程一、線程二、線程三來了之后,線程一執行完之后,線程二拿到鎖,而不是線程一又拿到鎖。false:非公平鎖、多個線程搶占式。
  ReentrantLock():無參構造器,fair為false。則不難理解為什么是上述結果了。
  值得注意的是:try{}並非為了捕獲異常,此次代碼也沒有catch塊,是為了執行finally塊將鎖釋放出來。否則會導致死循環(賣票情況正常,且沒有同步問題)。
  不寫try,finally的結果:死循環

  說明:窗口一賣出最后一張票,且釋放了鎖。窗口二獲取到鎖以后,此時ticket = 0,則執行break,窗口二跳出while循環,但此時並沒有釋放鎖。而另外兩個線程一直在等待獲取鎖,導致了死循環。

7、synchronize與Lock的異同

  相同:都用於解決線程安全問題
  不同:synchronize機制在執行完相應的同步代碼之后,會自動的釋放鎖。而Lock需要手動獲取鎖,同時需要在finally中手動釋放鎖。
  Lock是一個接口,而synchronized是關鍵字。
  Lock是顯示鎖(必須手動開啟與釋放),synchronize是隱式鎖,出了作用域自動釋放。
  Lock可以讓等待鎖的線程響應中斷,而synchronize不會,線程會一直等下去。
  Lock可以知道線程有沒有拿到鎖,而synchronize不能。
  Lock可以實現多個線程同時讀操作,而synchronize不能,線程必須等待。
  Lock只有代碼塊鎖,synchronize有代碼塊鎖和方法鎖。
  使用Lock鎖,JVM將花費較少的時間來調度線程,性能更好,並且具有更好的擴展性(提供更多的子類)。
  Lock有比synchronize更精確的線程語義和更好的性能,Lock還有更強大的功能,例如,它的tryLock方法可以非阻塞的方式去拿鎖。

  優先使用順序:
  Lock-->同步代碼塊(進入了方法體,分配了相應資源)-->同步方法(在方法體外)

  代碼示例:銀行有一個賬戶,兩個儲戶分別向同一個賬戶存3000元,每次存1000,存3次。每次存完打印賬戶余額。

 1 public class AccountTest {
 2     public static void main(String[] args) {
 3         Account account = new Account(0);
 4 
 5         Customer customer1 = new Customer(account);
 6         Customer customer2 = new Customer(account);
 7 
 8         customer1.setName("甲");
 9         customer2.setName("乙");
10 
11         customer1.start();
12         customer2.start();
13     }
14 }
15 
16 class Customer extends Thread {
17     private Account account;
18 
19     public Customer(Account account) {
20         this.account = account;
21     }
22 
23     @Override
24     public void run() {
25         for (int i = 0; i < 3; i++) {
26             account.deposit(1000);
27         }
28     }
29 }
30 
31 class Account {
32     private double balance;
33 
34     public Account(double bal) {
35         this.balance = bal;
36     }
37 
38     public synchronized void deposit(double amt) {
39         if (amt > 0) {
40             balance += amt;
41             System.out.println(Thread.currentThread().getName() + "存錢后余額為:" + balance);
42         }
43     }
44 }
45 
46 // 可能的一種結果
47 甲存錢后余額為:1000.0
48 甲存錢后余額為:2000.0
49 乙存錢后余額為:3000.0
50 乙存錢后余額為:4000.0
51 乙存錢后余額為:5000.0
52 甲存錢后余額為:6000.0

五、線程的通信

1、wait、notify、notifyAll

  問:wait 等待中的線程被 notify 喚醒了會立馬執行嗎?
  答:不會。被喚醒的線程需要重新競爭鎖對象,獲得鎖的線程可以從wait處繼續往下執行。

  代碼示例:使用兩個線程交替打印1—100。

 1 // 錯誤示例,結果:交錯式打印
 2 class Number1 implements Runnable {
 3 
 4     private int number = 1;
 5 
 6     public void run() {
 7         while (true) {
 8             synchronized (this) {
 9                 if (number <= 100) {
10                     System.out.println(Thread.currentThread().getName() + ":" + number);
11                     number++;
12                 } else {
13                     break;
14                 }
15             }
16         }
17     }
18 }
錯誤示例

  這里需要用到線程的通信,正確方式如下:

 1 /**
 2  * 分析:很自然的想到,線程一打印了1之后,需要讓一阻塞,然后讓線程二打印2
 3  * 然后將一喚醒,二再阻塞,依次內推。
 4  */
 5 // 正確示例,結果:交替打印
 6 public class Main {
 7     public static void main(String[] args) {
 8         Number num = new Number();
 9         Thread thread1 = new Thread(num);
10         Thread thread2 = new Thread(num);
11 
12         thread1.start();
13         thread2.start();
14     }
15 }
16 
17 class Number implements Runnable {
18 
19     private int number = 1;
20 
21     @Override
22     public void run() {
23         while (true) {
24             // this : num
25             synchronized (this) {
26                 // 喚醒一個線程
27                 notify();
28                 if (number <= 100) {
29                     System.out.println(Thread.currentThread().getName() + ":" + number);
30                     number++;
31                     try {
32                         // 使得調用該方法的線程進入阻塞狀態
33                         wait();
34                     } catch (InterruptedException e) {
35                         e.printStackTrace();
36                     }
37                 } else {
38                     break;
39                 }
40             }
41         }
42     }
43 }
44 
45 // 結果
46 實現交替打印
交替打印

  注意:上面兩個線程公用同一把鎖 num,this 指num。
  若此時將同步監視器換成

1 private final Object object = new Object();
2 synchronized (object) {}

  會報異常"IllegalMonitorStateException"。同步監視器非法

  原因:默認情況下,方法前有一個this,而鎖卻是Object。

1 this.notify();
2 this.wait();

  如果要用Object當鎖,需要修改為:

1 object.notify();
2 object.wait();

  理解:其實不難理解,解決線程同步問題,是要求要同一把鎖。鎖是object,而喚醒卻是this,就不知道要喚醒誰了呀?應該喚醒跟我(當前線程)共用同一把鎖的線程,喚醒別人(別的鎖)有什么意義呢?而且本身還是錯的。
  形象理解:一個寢室四個人(四個線程)有一個廁所(共享資源),共用廁所(多個線程對共享資源進行訪問)有安全隱患,如何解決?加鎖。當甲進入廁所時,將廁所門前掛牌(鎖)拿走(獲得鎖),然后使用廁所(此時其他人都進不來,必須在門外等待獲得掛牌,即時此時甲在廁所睡着了sleep(),其他人依然要等待,因為甲依然拿着掛牌,線程沒有釋放鎖),使用完畢后,將掛牌掛於門前(線程釋放鎖),其他三人方可使用,使用前先競爭鎖。
若要求兩人交替使用廁所,那么當甲使用完畢,通知(notify)乙使用,甲去等待(wait)下一次使用,自然而然,甲需要釋放鎖。就是甲使用時,乙等待,甲用完了,通知乙(我用完了,你去吧),乙使用時,甲等待,乙用完了,通知甲(我用完了,你去吧)。那么很自然的問題是,甲用完后,通知誰?是通知和我競爭同一個廁所的人,不會去通知隔壁寢室的人(即我用完了,釋放出鎖,通知競爭這把鎖的線程)。
  總結:
  wait():一旦執行此方法,當前線程進入阻塞狀態,並釋放鎖。
  notify():一旦執行此方法,就會喚醒一個被wait()的線程。如果有多個,就喚醒優先級高的,如果優先級一樣,則隨機喚醒一個。
  notifyAll():一旦執行此方法,會喚醒所有wait()的線程。
  以上三個方法必須使用在同步代碼塊或同步方法中,這里才有鎖。如果是Lock,有別的方式(暫未介紹,可自行百度)。
  以上三個方法的調用者必須是同步代碼塊或同步方法中的同步監視器。否則會出現異常。
  而任何一個類的對象,都可以充當鎖。則當鎖是object時,根據上一條,以上三個方法調用者就是object,所以定義在java.lang.Object類中。

2、sleep與wait的異同

  相同:都可以使當前線程進入阻塞狀態。
  不同:聲明位置不同,Thread類中聲明sleep(),Object類中聲明wait();sleep()隨時都可以調用,wait()必須在同步代碼塊或同步方法中;sleep()不會釋放鎖,wait()會釋放鎖;sleep(),超時或者調用interrupt()方法就可以喚醒,wait(),等待其他線程調用對象的notify()或者notifyAll()方法才可以喚醒。

3、生產者與消費者

  見標簽:聊聊並發


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM