Java核心(三)并發(fā)中的線程同步與鎖

樂(lè)觀鎖、悲觀鎖、公平鎖、自旋鎖、偏向鎖、輕量級(jí)鎖、重量級(jí)鎖、鎖膨脹...難理解?不存的!來(lái),話不多說(shuō),帶你飆車(chē)。

上一篇介紹了線程池的使用,在享受線程池帶給我們的性能優(yōu)勢(shì)之外,似乎也帶來(lái)了另一個(gè)問(wèn)題:線程安全的問(wèn)題。

那什么是線程的安全問(wèn)題呢?

一、線程安全問(wèn)題的產(chǎn)生

線程安全問(wèn)題:指的是在多線程編程中,同時(shí)操作同一個(gè)可變的資源之后,造成的實(shí)際結(jié)果與預(yù)期結(jié)果不一致的問(wèn)題。

比如:A和B同時(shí)向C轉(zhuǎn)賬10萬(wàn)元。如果轉(zhuǎn)賬操作不具有原子性,A在向C轉(zhuǎn)賬時(shí),讀取了C的余額為20萬(wàn),然后加上轉(zhuǎn)賬的10萬(wàn),計(jì)算出此時(shí)應(yīng)該有30萬(wàn),但還未來(lái)及將30萬(wàn)寫(xiě)回C的賬戶(hù),此時(shí)B的轉(zhuǎn)賬請(qǐng)求過(guò)來(lái)了,B發(fā)現(xiàn)C的余額為20萬(wàn),然后將其加10萬(wàn)并寫(xiě)回。然后A的轉(zhuǎn)賬操作繼續(xù)——將30萬(wàn)寫(xiě)回C的余額。這種情況下C的最終余額為30萬(wàn),而非預(yù)期的40萬(wàn)。

如果上面的內(nèi)容您還沒(méi)有理解,沒(méi)關(guān)系,我們來(lái)看下面非安全線程的模擬代碼:

public class ThreadSafeSample {
    public int number;
    public void add() {
        for (int i = 0; i < 100000; i++) {
            int former = number++;
            int latter = number;
            if (former != latter-1){
                System.out.printf("非相等 former=" +  former + " latter=" + latter);
            }
        }
    }
    public static void main(String[] args) throws InterruptedException {
        ThreadSafeSample threadSafeSample = new ThreadSafeSample();
        Thread threadA = new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafeSample.add();
            }
        });
        Thread threadB = new Thread(new Runnable() {
            @Override
            public void run() {
                threadSafeSample.add();
            }
        });
        threadA.start();
        threadB.start();
        threadA.join();
        threadB.join();
    }
}

我電腦運(yùn)行的結(jié)果: 非相等 => former=5555 latter=6061

可以看到,僅僅是兩個(gè)線程的低度并發(fā),就非常容易碰到 former 和 latter 不相等的情況。這是因?yàn)?,在兩次取值的過(guò)程中,其他線程可能已經(jīng)修改了number.

二、線程安全的解決方案

線程安全的解決方案分為以下幾個(gè)維度(參考《碼出高效:Java開(kāi)發(fā)手冊(cè)》):

  • 數(shù)據(jù)單線程可見(jiàn)(單線程操作自己的數(shù)據(jù)是不存在線程安全問(wèn)題的,ThreadLocal就是采用這種解決方案);
  • 數(shù)據(jù)只讀;
  • 使用線程安全類(lèi)(比如StringBuffer就是一個(gè)線程安全類(lèi),內(nèi)部是使用synchronized實(shí)現(xiàn)的);
  • 同步與鎖機(jī)制;

解決線程安全核心思想是:“要么只讀,要么加鎖”,解決線程安全的關(guān)鍵在于合理的使用Java提供的線程安全包java.util.concurrent簡(jiǎn)稱(chēng)JUC。

三、線程同步與鎖

Java 5 以前,synchronized是僅有的同步手段,Java 5的時(shí)候增加了ReentrantLock(再入鎖)它的語(yǔ)義和synchronized基本相同,比synchronized更加靈活,可以做到更多的細(xì)節(jié)控制,比如鎖的公平性/非公平性指定。

3.1 synchronized

synchronized 是 Java 內(nèi)置的同步機(jī)制,它提供了互斥的語(yǔ)義和可見(jiàn)性,當(dāng)一個(gè)線程已經(jīng)獲取當(dāng)前鎖時(shí),其他試圖獲取的線程只能等待或者阻塞在那里。

3.1.1 synchronized 使用

synchronized 可以用來(lái)修飾方法和代碼塊。

3.1.1.1 修飾代碼塊

synchronized (this) {
    int former = number++;
    int latter = number;
    //...
}

3.1.1.2 修飾方法

public synchronized void add() {
    //...
}

3.1.2 synchronized 底層實(shí)現(xiàn)原理

synchronized 是由一對(duì) monitorenter/monitorexit 指令實(shí)現(xiàn)的,Monitor 對(duì)象是同步的基本實(shí)現(xiàn)單元。在 Java 6 之前,Monitor的實(shí)現(xiàn)完全是依靠操作系統(tǒng)內(nèi)部的互斥鎖,因?yàn)樾枰M(jìn)行用戶(hù)態(tài)到內(nèi)核態(tài)的切換,所以同步操作是一個(gè)無(wú)差別的重量級(jí)操作,性能也很低。但在Java 6的時(shí)候,JVM 對(duì)此進(jìn)行了大刀闊斧地改進(jìn),提供了三種不同的 Monitor 實(shí)現(xiàn),也就是常說(shuō)的三種不同的鎖:偏向鎖(Biased Locking)、輕量級(jí)鎖和重量級(jí)鎖,大大改進(jìn)了其性能。

3.1.2.1 偏向鎖/輕量級(jí)鎖/重量級(jí)鎖

偏向鎖是為了解決在沒(méi)有多線程的訪問(wèn)下,盡量減少鎖帶來(lái)的性能開(kāi)銷(xiāo)。

輕量級(jí)鎖是指當(dāng)鎖是偏向鎖的時(shí)候,被另一個(gè)線程所訪問(wèn),偏向鎖就會(huì)升級(jí)為輕量級(jí)鎖,其他線程會(huì)通過(guò)自旋的形式嘗試獲取鎖,不會(huì)阻塞,提高性能。

重量級(jí)鎖是指當(dāng)鎖為輕量級(jí)鎖的時(shí)候,另一個(gè)線程雖然是自旋,但自旋不會(huì)一直持續(xù)下去,當(dāng)自旋一定次數(shù)的時(shí)候,還沒(méi)有獲取到鎖,就會(huì)進(jìn)入阻塞,該鎖膨脹為重量級(jí)鎖。重量級(jí)鎖會(huì)讓其他申請(qǐng)的線程進(jìn)入阻塞,性能降低。

3.1.2.2 鎖膨脹(升級(jí))原理

Java 6 之后優(yōu)化了 synchronized 實(shí)現(xiàn)方式,使用了偏向鎖升級(jí)為輕量級(jí)鎖再升級(jí)到重量級(jí)鎖的方式,減低了鎖帶來(lái)的性能消耗,也就是我們常說(shuō)的鎖膨脹或者叫鎖升級(jí),那么它是怎么實(shí)現(xiàn)鎖升級(jí)的呢?

鎖膨脹(升級(jí))原理: 在鎖對(duì)象的對(duì)象頭里面有一個(gè)ThreadId字段,在第一次訪問(wèn)的時(shí)候ThreadId為空,JVM讓其持有偏向鎖,并將ThreadId設(shè)置為其線程id,再次進(jìn)入的時(shí)候會(huì)先判斷ThreadId是否尤其線程id一致,如果一致則可以直接使用,如果不一致,則升級(jí)偏向鎖為輕量級(jí)鎖,通過(guò)自旋循環(huán)一定次數(shù)來(lái)獲取鎖,不會(huì)堵塞,執(zhí)行一定次數(shù)之后就會(huì)升級(jí)為重量級(jí)鎖,進(jìn)入堵塞,整個(gè)過(guò)程就是鎖膨脹(升級(jí))的過(guò)程。

3.1.2.3 自旋鎖

自旋鎖是指嘗試獲取鎖的線程不會(huì)立即阻塞,而是采用循環(huán)的方式去嘗試獲取鎖,這樣的好處是減少線程上下文切換的消耗,缺點(diǎn)是循環(huán)會(huì)消耗CPU。

3.1.2.4 樂(lè)觀鎖/悲觀鎖

悲觀鎖和樂(lè)觀鎖并不是某個(gè)具體的“鎖”而是一種是并發(fā)編程的基本概念。

悲觀鎖認(rèn)為對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,一定是會(huì)發(fā)生修改的,哪怕沒(méi)有修改,也會(huì)認(rèn)為修改。因此對(duì)于同一個(gè)數(shù)據(jù)的并發(fā)操作,悲觀鎖采取加鎖的形式。悲觀的認(rèn)為,不加鎖的并發(fā)操作一定會(huì)出問(wèn)題。

樂(lè)觀鎖則與 Java 并發(fā)包中的 AtomicFieldUpdater 類(lèi)似,也是利用 CAS 機(jī)制,并不會(huì)對(duì)數(shù)據(jù)加鎖,而是通過(guò)對(duì)比數(shù)據(jù)的時(shí)間戳或者版本號(hào),來(lái)實(shí)現(xiàn)樂(lè)觀鎖需要的版本判斷。

3.1.2.5 公平鎖/非公平鎖

公平鎖是指多個(gè)線程按照申請(qǐng)鎖的順序來(lái)獲取鎖。

非公平鎖是指多個(gè)線程獲取鎖的順序并不是按照申請(qǐng)鎖的順序,有可能后申請(qǐng)的線程比先申請(qǐng)的線程優(yōu)先獲取鎖。

如果使用 synchronized 使用的是非公平鎖,是不可設(shè)置的,這也是主流操作系統(tǒng)線程調(diào)度的選擇。通用場(chǎng)景中,公平性未必有想象中的那么重要,Java 默認(rèn)的調(diào)度策略很少會(huì)導(dǎo)致 “饑餓”發(fā)生。非公平鎖的吞吐量大于公平鎖。

非公平鎖吞吐量大于公平鎖的原因:

比如A占用鎖的時(shí)候,B請(qǐng)求獲取鎖,發(fā)現(xiàn)被A占用之后,堵塞等待被喚醒,這個(gè)時(shí)候C同時(shí)來(lái)獲取A占用的鎖,如果是公平鎖C后來(lái)者發(fā)現(xiàn)不可用之后一定排在B之后等待被喚醒,而非公平鎖則可以讓C先用,在B被喚醒之前C已經(jīng)使用完成,從而節(jié)省了C等待和喚醒之間的性能消耗,這就是非公平鎖比公平鎖吞吐量大的原因。

3.2 ReentrantLock

ReentrantLock只能修飾代碼塊,使用ReentrantLock必須手動(dòng)unlock釋放鎖,不然鎖永遠(yuǎn)會(huì)被占用。

3.2.1 ReentrantLock 使用

ReentrantLock reentrantLock = new ReentrantLock(true); // 設(shè)置為true為公平鎖,默認(rèn)是非公平鎖
reentrantLock.lock();
try {

}finally {
    reentrantLock.unlock();
}

3.2.2 ReentrantLock 優(yōu)勢(shì)

  • 具備嘗試非阻塞地獲取鎖的特性:當(dāng)前線程嘗試獲取鎖,如果這一時(shí)刻鎖沒(méi)有被其他線程獲取到,則成功獲取并持有鎖;

  • 能被中斷地獲取鎖的特性:與synchronized不同,獲取到鎖的線程能夠響應(yīng)中斷,當(dāng)獲取到鎖的線程被中斷時(shí),中斷異常將會(huì)被拋出,同時(shí)鎖會(huì)被釋放;

  • 超時(shí)獲取鎖的特性:在指定的時(shí)間范圍內(nèi)獲取鎖;如果截止時(shí)間到了仍然無(wú)法獲取鎖則返回。

3.2.3 ReentrantLock 注意事項(xiàng)

  • 在finally中釋放鎖,目的是保證在獲取鎖之后,最終能夠被釋放;
  • 不要將獲取鎖的過(guò)程寫(xiě)在try塊內(nèi),因?yàn)槿绻讷@取鎖時(shí)發(fā)生了異常,異常拋出的同時(shí),也會(huì)導(dǎo)致鎖無(wú)故被釋放;
  • ReentrantLock提供了一個(gè)newCondition的方法,以便用戶(hù)在同一鎖的情況下可以根據(jù)不同的情況執(zhí)行等待或喚醒的動(dòng)作;

3.3 synchronized和ReentrantLock區(qū)別

從性能角度,synchronized 早期的實(shí)現(xiàn)比較低效,對(duì)比 ReentrantLock,大多數(shù)場(chǎng)景性能都相差較大。但是在 Java 6 中對(duì)其進(jìn)行了非常多的改進(jìn),在高競(jìng)爭(zhēng)情況下,ReentrantLock 仍然有一定優(yōu)勢(shì)。在大多數(shù)情況下,無(wú)需太糾結(jié)于性能,還是考慮代碼書(shū)寫(xiě)結(jié)構(gòu)的便利性、可維護(hù)性等。

主要區(qū)別如下:

  1. ReentrantLock使用起來(lái)比較靈活,但是必須有釋放鎖的配合動(dòng)作;
  2. ReentrantLock必須手動(dòng)獲取與釋放鎖,而synchronized不需要手動(dòng)釋放和開(kāi)啟鎖;
  3. ReentrantLock只適用于代碼塊鎖,而synchronized可用于修飾方法、代碼塊等;

參考資料

《碼出高效:Java開(kāi)發(fā)手冊(cè)》

Java核心技術(shù)36講:http://t.cn/EwUJvWA

Java中的鎖分類(lèi):https://www.cnblogs.com/qifengshi/p/6831055.html

課程推薦:

課程推薦
?著作權(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)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

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

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