synchronized的優(yōu)化手段與java中的JUC

??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類,CallableRunnable沒有關(guān)系,而FutrueTask類實(shí)現(xiàn)了Runnable類,所以在此之前我們需要把實(shí)現(xiàn)Callable接口的對(duì)象引用傳給FutrueTask類的實(shí)例對(duì)象。 [圖片上傳中...(image-c0aaa5-1654604115299-4)]

image.png

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

image.png

比如,現(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ì)列:

  1. ArrayBlockingQueue 基于數(shù)組實(shí)現(xiàn)的阻塞隊(duì)列
  2. LinkedBlockingQueue 基于鏈表實(shí)現(xiàn)的阻塞隊(duì)列
  3. PriorityBlockingQueue 基于堆實(shí)現(xiàn)的帶優(yōu)先級(jí)的阻塞隊(duì)列
  4. 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)假即可。


image.png

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容