多執行緒詳解
1. 多執行緒快速入門
1.1 進程與執行緒
-
什麼是進程?
CPU從硬碟中讀取一段程式到記憶體中,該執行程式的實例就叫做進程。
一個程式如果被CPU多次讀取到記憶體中,則變成多個獨立的進程。
-
什麼是執行緒?
執行緒是程式執行的最小單位,在一個進程中可以有多個不同的執行緒同時執行。
-
為什麼在進程中還需要執行緒呢?
例如,一個文本編輯器進程,在編輯器中,需要同時做很多事情:監聽用戶按下的鍵盤事件、將文本渲染到螢幕上,將文本內容持久化到硬碟,這三件事就是三個執行緒。執行緒是最小的並行單位。
-
為什麼需要使用多執行緒?
採用多執行緒的形式執行程式碼,目的就是為了提高程式的效率。
比如:一個項目只有一個程式設計師開發,需要開發的模組需求有會員模組、支付模組、訂單模組等,該程式設計師要按順序依次將各個模組完成。而當有三個程式設計師同時完成不同的模組,那麼就可以大大提高開發效率了。
-
串列與並行的區別
串列也就是單執行緒執行,程式碼執行效率非常低,程式碼從上到下執行。
並行就是多個執行緒一起執行,效率比較高。
-
多執行緒的應用場景有哪些?
- 客戶端(/移動App)開發
- 非同步發送簡訊/郵件
- 將執行比較耗時的程式碼改用多執行緒非同步執行
- 非同步寫入日誌 日誌框架底層
- 多執行緒下載
-
同步與非同步的區別
同步:程式碼從頭到尾執行
非同步:單獨分支執行,相互之間沒有任何影響
1.2 繼承Thread類創建執行緒
public class ThreadTest01 extends Thread {
/**
* 執行緒執行的程式碼在run方法
*/
@Override
public void run() {
//獲取當前執行緒名稱
System.out.print(Thread.currentThread().getName());
System.out.println("子執行緒執行...");
}
public static void main(String[] args) {
//獲取當前執行緒名稱
System.out.println(Thread.currentThread().getName());
//啟動執行緒 調用start方法而不是run方法
//調用start()執行緒不是立即被CPU調度執行。
new ThreadTest01().start();
new ThreadTest01().start();
}
}
1.3 實現Runnable介面創建執行緒
public class ThreadTest02 implements Runnable {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "子執行緒執行...");
}
public static void main(String[] args) {
//啟動執行緒
new Thread(new ThreadTest02()).start();
//使用匿名內部類的形式創建執行緒
new Thread(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + "子執行緒執行...");
}
}).start();
//使用Lambda創建多執行緒
new Thread(() -> System.out.println(Thread.currentThread().getName() + "子執行緒執行...")).start();
}
}
1.4 使用Callable和Future創建執行緒
Callable和Future執行緒可以獲取到返回結果,拋出異常,底層基於LockSupport
從Java1.5開始,Java提供了Callable介面,該介面是Runnable介面的增強版,Callable提供了一個call()方法,可以看作是執行緒的執行體,但call()方法比run()方法更強大。
假設有三個連續的程式碼塊(程式碼塊1,2,3),本屬於單執行緒(執行緒1)執行是從頭到尾依次執行,此時要求程式碼2使用Callable模式(執行緒2),也就是使用非同步執行且帶返回結果。執行緒2就會是一個單獨的執行緒執行:執行緒1在執行完程式碼1執行到程式碼2的時候,會單獨創建一個執行緒,執行程式碼2,執行緒1需要拿到程式碼2整個執行的返回結果,在拿到以後執行緒1繼續執行。
-
call()方法可以有返回值
-
all()方法可以聲明拋出異常
public class ThreadTest03 implements Callable<Integer> { /** * 當前執行緒需要執行的程式碼 返回結果 * * @return * @throws Exception */ @Override public Integer call() throws Exception { System.out.println(Thread.currentThread().getName()+"子執行緒開始執行..."); try { Thread.sleep(3000); }catch (Exception e){ } System.out.println(Thread.currentThread().getName()+"返回1"); return 1; } }
public class ThreadTest04 { public static void main(String[] args) throws ExecutionException, InterruptedException { ThreadTest03 threadCallable = new ThreadTest03(); FutureTask<Integer> futureTask = new FutureTask<>(threadCallable); new Thread(futureTask).start(); //調用get方法時 主執行緒阻塞 子執行緒執行完畢 再喚醒主執行緒 Integer result = futureTask.get(); System.out.println(Thread.currentThread().getName()+" "+result); } }
1.5 使用執行緒池創建執行緒
public static void main(String[] args) {
ExecutorService executorService = Executors.newCachedThreadPool();
executorService.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"開始執行子執行緒...");
}
});
}
JUC並發中會詳細說明
1.6 @Async非同步註解創建執行緒
項目中會使用Spring的@Async註解和執行緒池來實現多執行緒
在方法上添加@Async
註解,當調用此方法時,就會創建新的執行緒來非同步執行此方法。若沒有添加非同步註解,順序執行程式,調用到該方法時,如果該方法有sleep,會一直等到該方法執行完畢才會繼續執行。
因此,一般將比較耗時的程式碼添加@Async註解。
1.7 執行緒同步/執行緒安全性問題
執行緒如何實現同步?(如何保證執行緒安全性問題)
核心思想:上鎖。當多個執行緒共享同一個全局變數時,將可能會發生執行緒安全的程式碼上鎖,最終只能有一個執行緒能夠獲取到鎖,保證只有拿到鎖的執行緒才可以執行該程式碼,沒有拿到鎖的執行緒不可以執行,需要經歷鎖的升級過程,如果一直沒有獲取到鎖,則會一直阻塞等待。
如果執行緒A獲取鎖,但是執行緒A一直不釋放鎖,執行緒B就一直獲取不到鎖,會一直阻塞等待。
- 使用synchronized鎖
- 使用Lock鎖(屬於JUC並發包)。底層基於aqs+cas實現
- 使用Threadlocal
- 原子類CAS非阻塞式
2. synchronized鎖
2.1 概述
什麼是執行緒安全問題?
當多個執行緒共享同一個全局變數,做寫的操作時,可能會受到其他執行緒的干擾,就會發生執行緒安全問題。
public class ThreadCount implements Runnable {
private int count = 100;
@Override
public void run() {
while (true){
if (count > 1) {
try {
//運行狀態->休眠狀態——CPU的執行權讓給其他執行緒
Thread.sleep(30);
} catch (Exception e) {
e.printStackTrace();
}
count--;
System.out.println(Thread.currentThread().getName() + ":" + count);
}else{
break;
}
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
//開啟執行緒
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
在這個程式中,兩個執行緒很大概率會同時對count進行操作。
上synchronized鎖:那麼程式碼的哪一塊需要上鎖?——可能發生執行緒安全性問題的程式碼需要上鎖
如果將synchronized鎖加在run方法上,那麼就會變成單執行緒,因為兩個執行緒有非公平鎖的特性,即誰拿到鎖/搶到鎖,誰就可以執行run方法,誰搶不到,誰就會一直阻塞等待。又因為run方法有死循環,不會釋放鎖,另一個執行緒就會一直阻塞等待
public class ThreadCount implements Runnable {
private int count = 100;
@Override
public synchronized void run() {
...
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
//開啟執行緒
new Thread(threadCount).start();
new Thread(threadCount).start();
}
}
因此在加鎖的時候並不是一次將整塊程式碼都上鎖,可能會使執行緒變為單執行緒,而且加鎖後,可能會影響程式的執行效率,因為執行該程式碼前要競爭鎖的資源。
正確加鎖:
public class ThreadCount implements Runnable {
private int count = 100;
@Override
public void run() {
while (true){
if (count > 1) {
...
synchronized (this) {
count--;
System.out.println(Thread.currentThread().getName() + ":" + count);
}
}else{
break;
}
}
}
public static void main(String[] args) {
ThreadCount threadCount = new ThreadCount();
//開啟執行緒
new Thread(threadCount).start(); //執行緒0
new Thread(threadCount).start(); //執行緒0
}
}
執行緒0、執行緒1同時獲取this鎖,假設執行緒0獲取到this鎖,意味著執行緒1沒有獲取到鎖,則會阻塞等待。等執行緒0執行完count–,釋放鎖之後,就會喚醒執行緒1重新競爭鎖資源。
synchronized獲取鎖和釋放鎖底層已經由虛擬機實現,會自動獲取鎖、釋放鎖並喚醒其他阻塞執行緒競爭鎖資源。
2.2 synchronized鎖的基本用法
-
修飾程式碼塊,指定加鎖對象,對給定對象加鎖,進入同步程式碼塊前要獲得給定對象的鎖
synchronized(對象鎖){ 需要保證執行緒安全的程式碼 }
對象鎖需要保證是同一個對象
比如:
ThreadCount threadCount1 = new ThreadCount(); ThreadCount threadCount2 = new ThreadCount(); //開啟執行緒 new Thread(threadCount1).start(); new Thread(threadCount2).start();
兩個執行緒並不是同一個對象鎖,這時也會出現執行緒安全問題
@Override public void run() { while (true){ cal(); } } public void cal(){ if (count > 1) { try { //運行狀態->休眠狀態——CPU的執行權讓給其他執行緒 Thread.sleep(30); } catch (Exception e) { e.printStackTrace(); } synchronized (this) { count--; System.out.println(Thread.currentThread().getName() + ":" + count); } } } public static void main(String[] args) { ThreadCount threadCount = new ThreadCount(); //開啟執行緒 new Thread(threadCount).start(); new Thread(threadCount).start(); }
-
修飾實例方法,作用與當前實例加鎖,進入同步程式碼前要獲得當前實例的鎖
@Override public void run() { while (true) { if (count > 1) { try { //運行狀態->休眠狀態——CPU的執行權讓給其他執行緒 Thread.sleep(30); } catch (Exception e) { e.printStackTrace(); } cal(); } else { break; } } } public synchronized void cal() { count--; System.out.println(Thread.currentThread().getName() + ":" + count); }
將synchronized加在實例方法上,則默認使用的是this鎖
-
修飾靜態方法,作用於當前類對象(當前類.class)加鎖,進入同步程式碼前要獲得當前類對象的鎖
2.3 synchronized死鎖問題
我們如果在使用synchronized 需要注意 synchronized鎖嵌套的問題,避免死鎖的問題發生。
案例:
public class DeadlockThread implements Runnable {
private int count = 1;
private String lock = "lock";
@Override
public void run() {
while (true) {
count++;
if (count % 2 == 0) {
// 執行緒1需要獲取lock鎖 再獲取a方法this鎖
// 執行緒2需要獲取this鎖 再獲取b方法lock鎖
synchronized (lock) {
a();
}
} else {
synchronized (this) {
b();
}
}
}
}
public synchronized void a() {
System.out.println(Thread.currentThread().getName() + ",a方法...");
}
public void b() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName() + ",b方法...");
}
}
public static void main(String[] args) {
DeadlockThread deadlockThread = new DeadlockThread();
Thread thread1 = new Thread(deadlockThread);
Thread thread2 = new Thread(deadlockThread);
thread1.start();
thread2.start();
}
}
執行緒1先獲取自定義對象的lock鎖,進入a方法需要獲取this鎖
執行緒2先獲取this鎖,進入b方法需要獲取自定義對象的lock鎖
當兩個執行緒同時執行,開始執行緒1和執行緒2分別拿到了lock鎖和this鎖,之後兩個執行緒都需要對方已經持有的鎖,最終出現死鎖問題。
如何排查synchronized死鎖問題
使用synchronized 死鎖診斷工具:JDK安裝目錄\jdk\jdk8\bin\jconsole.exe
3. 執行緒之間通訊
等待/通知機制
等待/通知的相關方法是任意Java對象都具備的,因為這些方法被定義在所有對象的超類java.lang.Object上,方法如下:
- notify() :通知一個在對象上等待的執行緒,使其從main()方法返回,而返回的前提是該執行緒獲取到了對象的鎖
- notifyAll():通知所有等待在該對象的執行緒
- wait():調用該方法的執行緒進入WAITING狀態,只有等待其他執行緒的通知或者被中斷,才會返回。需要注意調用wait()方法後,會釋放對象的鎖 。
注意:wait,notify和notifyAll要與synchronized一起使用
wait/notify的簡單用法
public class Thread03 extends Thread {
@Override
public void run() {
try {
synchronized (this) {
System.out.println(Thread.currentThread().getName() + ">>當前執行緒阻塞,同時釋放鎖!<<");
this.wait();
}
System.out.println(">>run()<<");
} catch (InterruptedException e) {
}
}
public static void main(String[] args) {
Thread03 thread = new Thread03();
thread.start();
try {
Thread.sleep(3000);
//3s後喚醒子執行緒
} catch (Exception e) {
}
synchronized (thread) {
// 喚醒正在阻塞的執行緒
thread.notify();
}
}
}
多執行緒通訊實現生產者與消費者
看以下案例:
package com.mark.sunchronized;
/**
* @author Mark
* @version 1.0
* @className Thread
* @date 2022/11/6 18:41
*/
public class Thread04 {
/**
* 共享對象Res
*/
class Res {
/**
* 姓名
*/
private String userName;
/**
* 性別
*/
private char sex;
}
/**
* 輸入執行緒
*/
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
if (count == 0) {
res.userName = "張三";
res.sex = '男';
} else {
res.userName = "李四";
res.sex = '女';
}
count = (count + 1) % 2;
}
}
}
/**
* 輸出執行緒
*/
class OutPutThread extends Thread {
private Res res;
public OutPutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
System.out.println(res.userName + "," + res.sex);
}
}
}
public static void main(String[] args) {
new Thread04().print();
}
private void print() {
//全局對象
Res res = new Res();
//輸入執行緒
InputThread inputThread = new InputThread(res);
//輸出執行緒
OutPutThread outPutThread = new OutPutThread(res);
inputThread.start();
outPutThread.start();
}
}
可以發現,輸入輸出執行緒公用Res對象,該程式存在執行緒安全問題。
修改:加synchronized鎖
/**
* 輸入執行緒
*/
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (res) {
if (count == 0) {
res.userName = "張三";
res.sex = '男';
} else {
res.userName = "李四";
res.sex = '女';
}
}
count = (count + 1) % 2;
}
}
}
/**
* 輸出執行緒
*/
class OutPutThread extends Thread {
private Res res;
public OutPutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
System.out.println(res.userName + "," + res.sex);
}
}
}
}
那麼如何實現交替進行輸出,而不是一直在一段時間裡輸出相同的姓名性別?
在Res中添加一個flag標記,輸入執行緒為false,輸出執行緒為true
/**
* 輸入執行緒
*/
class InputThread extends Thread {
private Res res;
public InputThread(Res res) {
this.res = res;
}
@Override
public void run() {
int count = 0;
while (true) {
synchronized (res) {
if (res.flag) {
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
if (count == 0) {
res.userName = "張三";
res.sex = '男';
} else {
res.userName = "李四";
res.sex = '女';
}
res.flag = true;
//喚醒輸出執行緒
res.notify();
}
count = (count + 1) % 2;
}
}
}
/**
* 輸出執行緒
*/
class OutPutThread extends Thread {
private Res res;
public OutPutThread(Res res) {
this.res = res;
}
@Override
public void run() {
while (true) {
synchronized (res) {
//如果 res.flag = false 則輸出的執行緒主動釋放鎖 也就是讓輸出執行緒進入WAITING狀態,阻塞輸出執行緒
if (!res.flag) {
try {
res.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(res.userName + "," + res.sex);
//輸出完畢,改變狀態
res.flag = false;
res.notify();
}
}
}
}
}
4. 多執行緒核心API
4.1 Join的底層原理
public static void main(String[] args){
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t1");
Thread t2 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t2");
Thread t3 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t3");
t1.start();
t2.start();
t3.start();
}
執行上述程式碼發現,三個進程並不是按start的先後順序啟動。那麼如何實現三個執行緒按期望的順序去執行呢?
public static void main(String[] args) {
Thread t1 = new Thread(() -> System.out.println(Thread.currentThread().getName() + ",執行緒執行"), "t1");
Thread t2 = new Thread(() -> {
try {
//t1執行完才執行t2
t1.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",執行緒執行");
}, "t2");
Thread t3 = new Thread(() -> {
try {
//t2執行完才執行t3
t2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + ",執行緒執行");
}, "t3");
t1.start();
t2.start();
t3.start();
}
Join底層原理是基於wait封裝的,喚醒的程式碼在jvm Hotspot 源碼中。jvm在關閉執行緒之前會檢測線阻塞在t1執行緒對象上的執行緒,然後執行notfyAll(),這樣t2就被喚醒了。
4.2 多執行緒的七種執行狀態
- 初始化狀態
- 就緒狀態
- 運行狀態
- 死亡狀態
- 阻塞狀態
- 等待狀態
- 超時等待
start()
:調用start()方法會使得該執行緒開始執行,正確啟動執行緒的方式。、wait()
:調用wait()方法,進入等待狀態,釋放資源,讓出CPU。需要在同步快中調用。sleep()
:調用sleep()方法,進入超時等待,不釋放資源,讓出CPUstop()
:調用sleep()方法,執行緒停止,執行緒不安全,不釋放鎖導致死鎖,過時。join()
:調用sleep()方法,執行緒是同步,它可以使得執行緒之間的並行執行變為串列執行。yield()
:暫停當前正在執行的執行緒對象,並執行其他執行緒,讓出CPU資源可能立刻獲得資源執行。yield()的目的是讓相同優先順序的執行緒之間能適當的輪轉執行notify()
:在鎖池隨機喚醒一個執行緒。需要在同步快中調用。notifyAll()
:喚醒鎖池裡所有的執行緒。需要在同步快中調用。
使用sleep方法避免cpu空轉 防止cpu佔用100%
sleep(long millis) 執行緒睡眠 millis 毫秒
sleep(long millis, int nanos) 執行緒睡眠 millis 毫秒 + nanos 納秒
public static void main(String[] args) {
new Thread(() -> {
while (true) {
try {
//執行緒每隔30ms休眠一次
Thread.sleep(30);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
}
wait/join和sleep之間的區別
sleep(long)方法在睡眠時不釋放對象鎖
Wait(long)方法在等待的過程中釋放對象鎖
join(long)方法先執行另外的一個執行緒,在等待的過程中釋放對象鎖底層是基於wait封裝的
4.3 守護執行緒與用戶執行緒
java中執行緒分為兩種類型:用戶執行緒和守護執行緒。通過Thread.setDaemon(false)
設置為用戶執行緒;通過Thread.setDaemon(true)
設置為守護執行緒。如果不設置屬性,默認為用戶執行緒。
- 守護執行緒依賴於用戶執行緒,用戶執行緒退出了,守護執行緒就會退出,典型的守護執行緒如垃圾回收執行緒。
- 用戶執行緒是獨立存在的,不會因為其他用戶執行緒退出而退出。
4.4 安全停止執行緒
-
調用stop方法(不推薦)
stop:中止執行緒,並且清除監控器鎖的資訊,但是可能導致執行緒安全問題,JDK不建議用。
destroy: JDK未實現該方法。
-
Interrupt
Interrupt 打斷正在運行或者正在阻塞的執行緒。
-
如果目標執行緒在調用Object class的wait()、wait(long)或wait(long, int)、join()、join(long, int)或sleep(long, int)方法時被阻塞,那麼Interrupt會生效,該執行緒的中斷狀態將被清除,拋出InterruptedException異常。
public class Thread02 extends Thread { @Override public void run() { while (true) { try { System.out.println("1"); Thread.sleep(1000000); System.out.println("2"); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) { Thread02 thread02 = new Thread02(); thread02.start(); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("中斷..."); thread02.interrupt(); } }
-
如果目標執行緒是被I/O或者NIO中的Channel所阻塞,同樣,I/O操作會被中斷或者返回特殊異常值。達到終止執行緒的目的。
如果以上條件都不滿足,則會設置此執行緒的中斷狀態。
-
-
標誌位
在程式碼邏輯中,增加一個判斷,用來控制執行緒執行的中止。
private volatile boolean isFlag = true; @Override public void run() { while (isFlag) { } } public static void main(String[] args) { Thread07 thread07 = new Thread07(); thread07.start(); // thread07.isFlag = false; }
4.5 多執行緒優先順序
-
在java語言中,每個執行緒都有一個優先順序,當執行緒調控器有機會選擇新的執行緒時,執行緒的優先順序越高越有可能先被選擇執行,執行緒的優先順序可以設置1-10,數字越大代表優先順序越高
注意:Oracle為Linux提供的java虛擬機中,執行緒的優先順序將被忽略,即所有執行緒具有相同的優先順序。
所以,不要過度依賴優先順序。
-
執行緒的優先順序用數字來表示,默認範圍是1到10,即Thread.MIN_PRIORITY到Thread.MAX_PRIORTY.一個執行緒的默認優先順序是5,即Thread.NORM_PRIORTY
-
如果cpu非常繁忙時,優先順序越高的執行緒獲得更多的時間片,但是cpu空閑時,設置優先順序幾乎沒有任何作用。
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
int count = 0;
for (; ; ) {
System.out.println(Thread.currentThread().getName() + "," + count++);
}
}, "t1執行緒:");
Thread t2 = new Thread(() -> {
int count = 0;
for (; ; ) {
System.out.println(Thread.currentThread().getName() + "," + count++);
}
}, "t2執行緒:");
t1.setPriority(Thread.MIN_PRIORITY);
t1.setPriority(Thread.MAX_PRIORITY);
t1.start();
t2.start();
}
5. Lock鎖的使用
在jdk1.5後新增的ReentrantLock類同樣可達到鎖的效果,且在使用上比synchronized更加靈活。
相關API:
- 使用ReentrantLock實現同步
- lock()方法:上鎖
- unlock()方法:釋放鎖
- 使用Condition實現等待/通知,類似於 wait()和notify()及notifyAll()
- Lock鎖底層基於AQS實現,需要自己封裝實現自旋鎖。
Synchronized屬於JDK關鍵字,底層通過C++JVM虛擬機底層實現
Lock鎖底層基於AQS實現,變為重量級鎖
Synchronized底層原理:鎖的升級過程。推薦使用Synchronized鎖
使用Lock鎖過程中要注意獲取鎖、釋放鎖
5.1 ReentrantLock用法
使用synchronized獲取鎖和釋放鎖全部由虛擬機來完成
而使用Lock鎖需要手動獲取鎖和釋放鎖,需要開發者自己定義
public class Thread04 {
/**
* 定義鎖
*/
private Lock lock = new ReentrantLock();
public static void main(String[] args) {
Thread04 thread04 = new Thread04();
thread04.print1();
try {
Thread.sleep(500);
System.out.println("開始執行執行緒2搶鎖");
} catch (InterruptedException e) {
e.printStackTrace();
}
thread04.print2();
}
private void print1() {
new Thread((() -> {
//獲取鎖
lock.lock();
System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
}), "t1").start();
}
public void print2() {
new Thread((() -> {
System.out.println("1");
lock.lock();
System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
}), "t2").start();
}
}
/*
t1獲取鎖成功
開始執行執行緒2搶鎖
1
*/
上述程式中,t1未釋放鎖,則t2無法獲取鎖,阻塞。
因此在獲取鎖後要釋放鎖。
private void print1() {
new Thread((() -> {
try {
//獲取鎖
lock.lock();
System.out.println(Thread.currentThread().getName() + "獲取鎖成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}), "t1").start();
}
5.2 Condition用法
Condition
介面提供了與Object阻塞(wait())與喚醒(notify()或notifyAll())相似的功能,只不過Condition
介面提供了更為豐富的功能,如:限定等待時長等
public class Thread05 {
private Lock lock = new ReentrantLock();
/**
* 定義
*/
private Condition condition = lock.newCondition();
public static void main(String[] args) {
Thread05 thread05 = new Thread05();
thread05.cal();
try {
Thread.sleep(3000);
} catch (Exception e) {
}
//釋放鎖
thread05.signal();
}
public void signal() {
try {
//獲取鎖
lock.lock();
//喚醒執行緒
condition.signal();
} catch (Exception e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void cal() {
//喚醒執行緒
new Thread(() -> {
try {
lock.lock();
System.out.println("1");
//釋放鎖,變為阻塞狀態
condition.await();
System.out.println("2");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
//釋放鎖
lock.unlock();
}
}).start();
}
}
6.多執行緒綜合案例實戰
6.1 執行緒安全性問題分析
分析執行緒安全性問題需要站在下面幾個維度考慮:
-
位元組碼角度
JVM已經把底層封裝得很好,很難了解底層,因此需要從位元組碼彙編指令分析執行緒安全性問題
-
上下文切換
單核CPU上的多執行緒,並不是真正意義上的多執行緒,而是執行緒切換實現多執行緒
-
JMM java記憶體模型
public class Run extends Thread{
private static int sum = 0;
@Override
public void run() {
sum();
}
public void sum(){
for (int i = 0 ; i <10000; i++){
sum ++;
}
}
public static void main(String[] args) throws InterruptedException {
Run run1 = new Run();
Run run2 = new Run();
run1.start();
run2.start();
run1.join();
run2.join();
System.out.println(sum);
}
}
不考慮執行緒安全問題,上述程式碼應當輸出20000,然而,輸出的卻比20000小。
通過反編譯來查看過程:
- target中找到Run.class文件
- 打開Terminal,將Run.class所在目錄拖到Terminal
- 輸入命令:
javap -p -v Run.class
分析:
共享變數值 sum=0
假設現CPU執行到t1執行緒,t1執行緒執行完++但是還沒有保存sum,就切換到t2執行緒執行,t2執行緒將靜態變數sum=0改成sum=1,CPU又切換到t1執行緒,使用之前的sum++ 得到的sum=1賦值給共享變數sum,導致最終結果為sum1,然而現在sum++實際上已經執行了兩次,最終結果卻為1。
6.2 Callable和FutureTask原理分析
public interface MarkCallable<V> {
/**
* 當前執行緒執行完畢返回的結果
* @return
* @throws Exception
*/
V call();
}
public class MarkFutureTask<V> implements Runnable {
private MarkCallable<V> markCallable;
private Object lock = new Object();
private V result;
public MarkFutureTask(MarkCallable<V> markCallable) {
this.markCallable = markCallable;
}
@Override
public void run() {
//執行緒需要執行程式碼
result = markCallable.call();
//如果子執行緒執行完畢,喚醒主執行緒,可以拿到返回結果
synchronized (lock) {
lock.notify();
}
}
public V get() {
//獲取子執行緒非同步執行完畢後的返回結果
//主執行緒阻塞
synchronized (lock) {
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
return result;
}
}
public class MarkCallableImpl implements MarkCallable<Integer>{
@Override
public Integer call(){
try {
System.out.println(Thread.currentThread().getName()+",子執行緒執行");
Thread.sleep(3000);
}catch (Exception e){
}
//耗時程式碼執行完畢,返回1
return 1;
}
}
public static void main(String[] args) {
MarkCallableImpl markCallable = new MarkCallableImpl();
MarkFutureTask<Integer> markFutureTask = new MarkFutureTask<Integer>(markCallable);
new Thread(markFutureTask).start();
Integer result = markFutureTask.get();
System.out.println(result);
}
使用LockSupport實現:
LockSupport:不需要實現synchronized即可實現wait和notify相似的操作
public class MarkFutureTask<V> implements Runnable {
private MarkCallable<V> markCallable;
private Object lock = new Object();
private V result;
private Thread currentThread;
public MarkFutureTask(MarkCallable<V> markCallable) {
this.markCallable = markCallable;
}
@Override
public void run() {
//執行緒需要執行程式碼
result = markCallable.call();
if (currentThread != null) {
LockSupport.unpark(currentThread);
}
}
00 public V get() {
//獲取子執行緒非同步執行完畢後的返回結果
//主執行緒阻塞
currentThread = Thread.currentThread();
LockSupport.park();
return result;
}
}
7. ConcurrentHashMap
7.1 HashTable與HashMap的區別
- 在多執行緒情況下,同時對一個共享HashMap使用put方法做寫操作,底層會共享一個table數組,發生執行緒安全問題,在多執行緒操作中,需要使用synchronized關鍵字。而HashTable執行緒是安全的,在每個公共方法上都使用了synchronized。
- HashMap是允許key和value為null的,key為null的hash值為0,存在index=0的位置,而HashTable不允許key和value為空
- HashMap需要重新計算hash值作為hashCode,而HashTable直接使用對象的hashCode
- HashMap繼承了AbstractMap類,而HashTable繼承了Didtionary類
7.2 Hashtable集合的缺陷
- 使用傳統的Hashtable保證執行緒問題,是採用synchronized鎖將整個Hashtable中的數組鎖住,在多執行緒中只允許一個執行緒訪問put或get,效率非常低,但是能夠保證執行緒安全問題。當多個執行緒對Hashtable在get或put時,會發生this鎖的競爭,多個執行緒競爭鎖,最終只會有一個執行緒獲取到this鎖,獲取不到的阻塞等待,最終只能單執行緒get/put。所以在多執行緒並不推薦使用Hashtable,因為其效率非常低。
7.3 ConcurrentHashMap1.7實現原理
數據結構實現:數組+Segments分段鎖+HashEntry鏈表實現
鎖的實現:Lock鎖+CAS樂觀鎖+UNSAFE類
擴容實現:支援多個Segment同時擴容
原理就是將大的Hashtable拆分成n多個小的Hashtable集合,默認16個。——分段鎖
分段鎖的核心思想是減少多個執行緒對鎖的競爭:不會再訪問到同一個Hashtable(每個小的HashTable都有一個獨立鎖,多個執行緒訪問大的Hashtable,會先根據key計算存放具體小的Hashtable的位置,然後進行操作)
ConcurrentHashMap get()方法沒有鎖的競爭,而Hashtable get()方法有鎖的競爭
而在JDK1.8取消了分段鎖。
在多執行緒情況下訪問ConcurrentHashMap1.7版本進行操作,如果多個執行緒操作的key最終計算落地到不同的小的Hashtable集合中,就可以實現多執行緒同時操作Hashtable而不會發生鎖的競爭。但是如果多個執行緒操作的key最終計算落地到同一個小的Hashtable集合中就會發生鎖的競爭。
(實際在ConcurrentHashMap中,並不是叫HashTable,而是叫Segments和Segment)
7.4 ConcurrentHashMap的使用
使用方法與HashMap一樣
7.5 手寫ConcurrentHashMap
- 提前創建固定數組容量大小的小的Hashtable集合
- 通過構造函數初始化Hashtable數組
public class MarkConcuurentHashMap<K, V> {
/**
* 創建一個存放小的HashTable集合
*/
private Hashtable<K, V>[] hashTables;
public MarkConcuurentHashMap() {
//默認情況下 初始化16個小的HashTable
hashTables = new Hashtable[16];
for (int i = 0; i < hashTables.length; i++) {
hashTables[i] = new Hashtable<>();
}
}
public void put(K k, V v) {
//先計算key存放到哪個具體小的HashTable集合中
int hashTableIndex = k.hashCode() % hashTables.length;
//將key存入到具體小的HashTable集合中
hashTables[hashTableIndex].put(k, v);
}
public void get(K k) {
//先計算key存放到了哪個具體小的HashTable集合中
int hashTableIndex = k.hashCode() % hashTables.length;
//根據key從具體小的HashTable集合中get
hashTables[hashTableIndex].get(k);
}
}
7.6 分段鎖設計概念
ConcurrentHashMap底層採用分段鎖設計,將一個大的HashTable執行緒安全的集合拆封成n多個小的HashTable集合,默認初始化16個小的HashTable集合。如果多個執行緒最終根據key計算出的index值落地到不同的小的HashTable集合,不會發生鎖的競爭,同時支援多個執行緒訪問ConcurrentHashMap進行寫的操作,效率非常高。
ConcurrentHashMap會計算兩次index值:
- 第一次計算index的值,計算key具體存放到哪個小的HashTable
- 第二次計算index的值,計算key存放到具體小的HashTable對應具體數組index的哪個位置(HashTable底層也是通過數組+鏈表實現的)