一、概述
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
程式碼示例:使用兩個執行緒交替列印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、生產者與消費者
見標籤:聊聊併發