??1.synchronized的優(yōu)化手段
??1.1鎖膨脹/升級(jí)
前面我們說(shuō)過(guò)synchronized關(guān)鍵字加的鎖既是輕量級(jí)鎖也是重量級(jí)鎖,它是根據(jù)實(shí)際情況自適應(yīng)加鎖的,這種自適應(yīng)是基于鎖膨脹或者說(shuō)是鎖升級(jí)這樣的優(yōu)化手段來(lái)實(shí)現(xiàn)的。
??鎖升級(jí)過(guò)程:
- 當(dāng)沒有線程加鎖的時(shí)候,此時(shí)為無(wú)鎖狀態(tài)。
- 當(dāng)首個(gè)線程進(jìn)行加鎖的時(shí)候,此時(shí)進(jìn)入偏向鎖的狀態(tài),偏向鎖不是真的加鎖,而是在對(duì)象頭做個(gè)標(biāo)記而已,
- 當(dāng)有其他線程進(jìn)行加鎖,導(dǎo)致產(chǎn)生了鎖競(jìng)爭(zhēng)時(shí),此時(shí)進(jìn)入輕量級(jí)鎖狀態(tài)。
-
如果競(jìng)爭(zhēng)進(jìn)一步加劇,進(jìn)入重量級(jí)鎖狀態(tài)。
image.png
像上面根據(jù)鎖競(jìng)爭(zhēng)的程度來(lái)逐步升級(jí)鎖的情況,就是鎖的膨脹或者稱為鎖的升級(jí)。
??1.2鎖粗化
所謂鎖粗化就是將synchronized的加鎖代碼塊范圍增大,加鎖的代碼塊中的內(nèi)容越多,鎖就越粗,否則鎖就越細(xì)。
一般我們認(rèn)為,鎖越細(xì),多線程間的并發(fā)性越高,鎖越粗,加鎖解鎖的開銷就會(huì)更小。編譯器會(huì)對(duì)你加的鎖做一個(gè)優(yōu)化,如果編譯器判定加的鎖過(guò)細(xì),就會(huì)自動(dòng)粗化,從而提高程序運(yùn)行效率。
??1.3鎖消除
有些代碼,編譯器認(rèn)為沒有加鎖的必要,就會(huì)自動(dòng)把你加的鎖自動(dòng)去除,像類似這樣的優(yōu)化,就是鎖消除。
??2.java中的JUC
java中的JUC就是來(lái)自java.util.concurrent包下的一些標(biāo)準(zhǔn)類或者接口,都是有關(guān)并發(fā)或者有關(guān)多線程的一些類和接口。
??2.1Callable接口
前面我們創(chuàng)建線程的時(shí)候,有兩種方式,一是繼承Thred類并重寫run方法來(lái)創(chuàng)建線程,二是通過(guò)Runnable接口來(lái)創(chuàng)建線程,除上述兩種方式,我們還可以通過(guò)Callable接口配合FutureTask類來(lái)創(chuàng)建線程,使用該方法創(chuàng)建線程能夠支持帶返回值的任務(wù),而最開始的那兩種方法是不支持帶回返回值的。
其中通過(guò)實(shí)現(xiàn)Callable接口的call方法來(lái)描述帶有返回值的任務(wù),FutureTask就是對(duì)于具體的Runnable或者Callable任務(wù)的執(zhí)行結(jié)果進(jìn)行取消、查詢是否完成、獲取返回值。必要時(shí)可以通過(guò)get方法獲取執(zhí)行結(jié)果(返回值),如果任務(wù)還沒有執(zhí)行完畢,該方法會(huì)阻塞直到任務(wù)返回結(jié)果。
在創(chuàng)建線程的時(shí)候,傳入的引用不能是Callable類型,而應(yīng)該是FutrueTask類型,根據(jù)Thread的構(gòu)造方法,傳入的任務(wù)類型需是Runnable類,Callable與Runnable沒有關(guān)系,而FutrueTask類實(shí)現(xiàn)了Runnable類,所以在此之前我們需要把實(shí)現(xiàn)Callable接口的對(duì)象引用傳給FutrueTask類的實(shí)例對(duì)象。 [圖片上傳中...(image-c0aaa5-1654604115299-4)]

綜上,Callable用來(lái)描述任務(wù),FutureTask類用來(lái)管理Callable任務(wù)的執(zhí)行結(jié)果。

比如,現(xiàn)在我們需要使用線程來(lái)計(jì)算一個(gè)值,并通過(guò)返回值的方式獲取執(zhí)行結(jié)果。
??參考代碼:
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
public class Main {
public static void main(String[] args) throws ExecutionException, InterruptedException {
Callable<Integer> callable = new Callable<Integer>() {
@Override
public Integer call() throws Exception {
return 100 * (1 + 100) / 2;
}
};
FutureTask<Integer> task = new FutureTask<>(callable);
Thread thread = new Thread(task);
thread.start();
//獲取執(zhí)行結(jié)果
System.out.println(task.get());
}
}
??運(yùn)行結(jié)果:
5050
Process finished with exit code 0
??2.2ReentrantLock類(可重入鎖)
ReentrantLock其實(shí)就是可重入鎖,使用方式是通過(guò)lock方法加鎖,unlock方法解鎖,注意加鎖和解鎖兩個(gè)過(guò)程是分開的,而synchronized關(guān)鍵字加鎖解鎖是一步到位的。
由于加鎖解鎖兩個(gè)操作是分開的,相比于加鎖解鎖一體化這就很容易造成死鎖問題,這是因?yàn)橐环矫婕渔i后容易忘記去解鎖,造成死鎖,另一方面加鎖后解鎖前中間的代碼萬(wàn)一出了問題,可能會(huì)導(dǎo)致解鎖無(wú)法正常執(zhí)行導(dǎo)致解鎖失敗,造成死鎖。
所以使用ReentrantLock類時(shí),一般要搭配finally使用。
ReentrantLock lock = new ReentrantLock();
//dosomething
lock.lock();
try {
// working
} finally {
lock.unlock()
}
??ReentrantLock類與synchronized關(guān)鍵字區(qū)別:
-
ReentrantLock是一個(gè)java標(biāo)準(zhǔn)類,是使用java代碼實(shí)現(xiàn)的,synchronized是一個(gè)關(guān)鍵字,是基于JVM內(nèi)部實(shí)現(xiàn)的,是C/C++代碼。 -
ReentrantLock需要手動(dòng)解鎖,需謹(jǐn)防忘記解鎖,而synchronized加鎖解鎖一體化,不需要手動(dòng)解鎖。 - 如果出現(xiàn)鎖競(jìng)爭(zhēng),
ReentrantLock競(jìng)爭(zhēng)失敗時(shí)可以阻塞等待,也可以通過(guò)trylock方法直接返回退出,而synchronized競(jìng)爭(zhēng)失敗時(shí)只能阻塞等待。 -
ReentrantLock構(gòu)造實(shí)例對(duì)象時(shí),可以指定fair參數(shù)來(lái)決定該鎖對(duì)象是公平鎖還非公平鎖,synchronized加的鎖是非公平鎖,不能指定為公平鎖。 -
ReentrantLock類衍生出的等待機(jī)制是Condition類,synchronized關(guān)鍵字衍生的等待機(jī)制是wait/notify等待機(jī)制。
??2.3Semaphore類(信號(hào)量)
這個(gè)概念比較抽象,我們來(lái)打個(gè)比方,有個(gè)停車場(chǎng),停車場(chǎng)門口有一個(gè)燈牌,會(huì)顯示停車位還剩余多少個(gè),每進(jìn)去一輛車,顯示的停車位數(shù)量就減一,每出去一輛出,顯示的停車位數(shù)量就加一。
上面顯示停車位數(shù)量的燈牌其實(shí)就是信號(hào)量,信號(hào)量是一更加廣義的鎖,描述了可用資源的個(gè)數(shù)。
每次申請(qǐng)一個(gè)可用資源,信號(hào)量中的計(jì)數(shù)器就減一(P操作)。
每次釋放一個(gè)可用資源,信號(hào)量中的計(jì)數(shù)器就加一(V操作)。
當(dāng)可用資源數(shù)量為0時(shí),再次進(jìn)行P操作,會(huì)陷入阻塞等待狀態(tài)。
鎖我們可以理解為“二元信號(hào)量”,因?yàn)橛?jì)數(shù)器的取值不是0就是1,它的可用資源就一個(gè)。
??Semaphore類的常用方法:
| 序號(hào) | 方法 | 方法類型 | 作用 |
|---|---|---|---|
| 1 | public Semaphore(int permits) | 構(gòu)造方法 | 構(gòu)造可用資源為permits個(gè)的信號(hào)量對(duì)象 |
| 2 | public Semaphore(int permits, boolean fair) | 構(gòu)造方法 | 相比于方法1,該構(gòu)造方法還能指定信號(hào)量是否是公平性質(zhì)的 |
| 3 | public void acquire() throws InterruptedException | 普通方法 | 申請(qǐng)可用資源 |
| 4 | public void release() | 普通方法 | 釋放可用資源 |
??代碼演示:
import java.util.concurrent.Semaphore;
public class Main {
public static void main(String[] args) throws InterruptedException {
//構(gòu)造方法中的permits參數(shù)表示可用資源的個(gè)數(shù)
Semaphore semaphore = new Semaphore(4);
//每次使用一個(gè)可用資源,信號(hào)量就會(huì)減少1
semaphore.acquire();
System.out.println("申請(qǐng)成功");
semaphore.acquire();
System.out.println("申請(qǐng)成功");
semaphore.acquire();
System.out.println("申請(qǐng)成功");
semaphore.acquire();
System.out.println("申請(qǐng)成功");
//此時(shí)可用資源為0,線程進(jìn)入阻塞,需要使用release方法釋放資源,線程才能繼續(xù)執(zhí)行
semaphore.release();
System.out.println("釋放成功");
semaphore.acquire();
System.out.println("申請(qǐng)成功");
}
}
復(fù)制代碼
??執(zhí)行結(jié)果:
申請(qǐng)成功
申請(qǐng)成功
申請(qǐng)成功
申請(qǐng)成功
釋放成功
申請(qǐng)成功
Process finished with exit code 0
??2.4CountDownLatch同步工具類
CountDownLatch是一個(gè)同步工具類,它允許一個(gè)或多個(gè)線程一直等待,直到其他線程執(zhí)行完后再執(zhí)行。
打個(gè)比方,假設(shè)有一場(chǎng)跑步比賽,一個(gè)有5個(gè)遠(yuǎn)動(dòng)員參賽,只有當(dāng)最后一個(gè)遠(yuǎn)動(dòng)員沖過(guò)終點(diǎn)線時(shí),裁判才能宣布比賽結(jié)束。
這里的運(yùn)動(dòng)員就相當(dāng)于線程,裁判就相當(dāng)于CountDownLatch類。
??CountDownLatch同步工具類常用方法:
| 序號(hào) | 方法 | 方法類型 | 作用 |
|---|---|---|---|
| 1 | public CountDownLatch(int count) | 構(gòu)造方法 | 構(gòu)造實(shí)例對(duì)象,count表示CountDownLatch對(duì)象中計(jì)數(shù)器的值 |
| 2 | public void await() throws InterruptedException | 普通方法 | 使所處的線程進(jìn)入阻塞等待,直到計(jì)數(shù)器的值清零 |
| 3 | public void countDown() | 普通方法 | 將計(jì)數(shù)器的值減1 |
| 4 | public long getCount() | 普通方法 | 獲取計(jì)數(shù)器最初的值 |
??使用方式:
- 創(chuàng)建
CountDownLatch對(duì)象,并初始化計(jì)數(shù)器的值。 - 在每個(gè)線程執(zhí)行的最后使用
countDown方法,表示當(dāng)前線程執(zhí)行完畢,計(jì)數(shù)器的值減1。 - 在主線程中使用
await方法,等待CountDownLatch對(duì)象的計(jì)數(shù)器清零,表示所管理的線程全部執(zhí)行完畢,起到線程同步的作用。
??參考代碼:
import java.util.concurrent.*;
public class Main {
public static final int COUNT = 5;
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(COUNT);
for (int i = 0; i < COUNT; i++) {
Thread thread = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "任務(wù)執(zhí)行完畢!");
countDownLatch.countDown();
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
//等待計(jì)數(shù)器清零,清零前,線程處于阻塞等待狀態(tài),清零后,即全部任務(wù)執(zhí)行完畢
countDownLatch.await();
System.out.println("任務(wù)全部完成!");
}
}
這樣的場(chǎng)景在實(shí)際開發(fā)當(dāng)中,也是很常見的,比如要下載一個(gè)較大的文件的時(shí)候,常常將文件拆分,使用多線程并發(fā)下載。
而在這樣一個(gè)場(chǎng)景中,需要等待最后一個(gè)線程也下載完畢,才能說(shuō)整個(gè)文件下載完畢,也就是使用CountDownLatch對(duì)象進(jìn)行計(jì)數(shù),等計(jì)數(shù)器清零了await方法就會(huì)返回,表示文件下載完成。
??2.5有關(guān)數(shù)據(jù)結(jié)構(gòu)的線程安全類
??2.5.1多線程使用順序表
ArrayList在多線程中是線程不安全的,多線程環(huán)境中使用基于寫實(shí)拷貝實(shí)現(xiàn)的CopyOnWriteArrayList。
所謂寫實(shí)拷貝,就是寫的時(shí)候會(huì)創(chuàng)建一個(gè)副本,再副本上進(jìn)行修改,同時(shí)如果存在讀操作會(huì)在原文件數(shù)進(jìn)行查詢,等修改完畢后就會(huì)將副本“轉(zhuǎn)正”。
??2.5.2多線程使用隊(duì)列
??多線程情況下常常使用阻塞隊(duì)列:
- ArrayBlockingQueue 基于數(shù)組實(shí)現(xiàn)的阻塞隊(duì)列
- LinkedBlockingQueue 基于鏈表實(shí)現(xiàn)的阻塞隊(duì)列
- PriorityBlockingQueue 基于堆實(shí)現(xiàn)的帶優(yōu)先級(jí)的阻塞隊(duì)列
- TransferQueue 最多只包含一個(gè)元素的阻塞隊(duì)列
??2.5.3多線程使用哈希表
HashMap本身是線程不安全的,將HashMap中的重要方法使用synchornized加鎖后,就得到了HashTable類,雖然HashTable類是線程安全的,但是由于是對(duì)方法進(jìn)行無(wú)腦加鎖,本質(zhì)加鎖的對(duì)象是HashTable類的實(shí)例對(duì)象,這樣就會(huì)導(dǎo)致鎖競(jìng)爭(zhēng)概率加大,就相當(dāng)于公司里所有的員工需要請(qǐng)假時(shí)都需要找老板簽字批準(zhǔn),這樣會(huì)導(dǎo)致老板非常地忙,這個(gè)老板就相當(dāng)于加鎖的哈希表對(duì)象,最終會(huì)造成哈希表的效率下降。
!](https://upload-images.jianshu.io/upload_images/28168001-faf8ed9196403ae5.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240)
為了解決這個(gè)問題,java提供了ConcurrentHashMap類,該類是基于哈希表中的每一個(gè)鏈表對(duì)象進(jìn)行加鎖,線程需要對(duì)哪個(gè)鏈表對(duì)象進(jìn)行操作,就在哪里加鎖,由于哈希表中鏈表數(shù)量很多,鏈表對(duì)象的元素個(gè)數(shù)較少,可以有效地降低鎖競(jìng)爭(zhēng)的概率,相當(dāng)于公司中的老板將權(quán)力下放給各個(gè)部門,員工請(qǐng)假時(shí)只需向所在的部門領(lǐng)導(dǎo)請(qǐng)假即可。

到這里,Java多線程有關(guān)內(nèi)容基本上都介紹完畢
