2019-07-25

轉(zhuǎn)載:http://m.itdecent.cn/p/ae25eb3cfb5d

Java:CAS(樂觀鎖)

本文講解CAS機(jī)制,主要是因為最近準(zhǔn)備面試題,發(fā)現(xiàn)這個問題在面試中出現(xiàn)的頻率非常的高,因此把自己學(xué)習(xí)過程中的一些理解記錄下來,希望能對大家也有幫助。

什么是悲觀鎖、樂觀鎖?在java語言里,總有一些名詞看語義跟本不明白是啥玩意兒,也就總有部分面試官拿著這樣的詞來忽悠面試者,以此來找優(yōu)越感,其實理解清楚了,這些詞也就唬不住人了。

  • synchronized是悲觀鎖,這種線程一旦得到鎖,其他需要鎖的線程就掛起的情況就是悲觀鎖。
  • CAS操作的就是樂觀鎖,每次不加鎖而是假設(shè)沒有沖突而去完成某項操作,如果因為沖突失敗就重試,直到成功為止。

在進(jìn)入正題之前,我們先理解下下面的代碼:

 private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        count++;
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

請問cout的輸出值是否為200?答案是否定的,因為這個程序是線程不安全的,所以造成的結(jié)果count值可能小于200;

那么如何改造成線程安全的呢,其實我們可以使用上Synchronized同步鎖,我們只需要在count++的位置添加同步鎖,代碼如下:

private static int count = 0;

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        synchronized (ThreadCas.class){
                            count++;
                        }
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

加了同步鎖之后,count自增的操作變成了原子性操作,所以最終的輸出一定是count=200,代碼實現(xiàn)了線程安全。

但是Synchronized雖然確保了線程的安全,但是在性能上卻不是最優(yōu)的,Synchronized關(guān)鍵字會讓沒有得到鎖資源的線程進(jìn)入BLOCKED狀態(tài),而后在爭奪到鎖資源后恢復(fù)為RUNNABLE狀態(tài),這個過程中涉及到操作系統(tǒng)用戶模式和內(nèi)核模式的轉(zhuǎn)換,代價比較高。

盡管Java1.6為Synchronized做了優(yōu)化,增加了從偏向鎖到輕量級鎖再到重量級鎖的過度,但是在最終轉(zhuǎn)變?yōu)橹亓考夋i之后,性能仍然較低。

所謂原子操作類,指的是java.util.concurrent.atomic包下,一系列以Atomic開頭的包裝類。例如AtomicBoolean,AtomicIntegerAtomicLong。它們分別用于Boolean,Integer,Long類型的原子性操作。


    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) {
        for (int i = 0; i < 2; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(10);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                    //每個線程讓count自增100次
                    for (int i = 0; i < 100; i++) {
                        count.incrementAndGet();
                    }
                }
            }).start();
        }

        try{
            Thread.sleep(2000);
        }catch (Exception e){
            e.printStackTrace();
        }
        System.out.println(count);
    }

使用AtomicInteger之后,最終的輸出結(jié)果同樣可以保證是200。并且在某些情況下,代碼的性能會比Synchronized更好。

而Atomic操作的底層實現(xiàn)正是利用的CAS機(jī)制,好的,我們切入到這個博客的正點。

什么是CAS機(jī)制

CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較并替換。

CAS機(jī)制當(dāng)中使用了3個基本操作數(shù):內(nèi)存地址V,舊的預(yù)期值A(chǔ),要修改的新值B。

更新一個變量的時候,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實際值相同時,才會將內(nèi)存地址V對應(yīng)的值修改為B。

CAS是英文單詞Compare And Swap的縮寫,翻譯過來就是比較并替換。

CAS機(jī)制當(dāng)中使用了3個基本操作數(shù):內(nèi)存地址V,舊的預(yù)期值A(chǔ),要修改的新值B。

更新一個變量的時候,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V當(dāng)中的實際值相同時,才會將內(nèi)存地址V對應(yīng)的值修改為B。

這樣說或許有些抽象,我們來看一個例子:

1.在內(nèi)存地址V當(dāng)中,存儲著值為10的變量。

image

2.此時線程1想要把變量的值增加1。對線程1來說,舊的預(yù)期值A(chǔ)=10,要修改的新值B=11。

image

3.在線程1要提交更新之前,另一個線程2搶先一步,把內(nèi)存地址V中的變量值率先更新成了11。

image

4.線程1開始提交更新,首先進(jìn)行A和地址V的實際值比較(Compare),發(fā)現(xiàn)A不等于V的實際值,提交失敗。

image

5.線程1重新獲取內(nèi)存地址V的當(dāng)前值,并重新計算想要修改的新值。此時對線程1來說,A=11,B=12。這個重新嘗試的過程被稱為自旋。

image

6.這一次比較幸運(yùn),沒有其他線程改變地址V的值。線程1進(jìn)行Compare,發(fā)現(xiàn)A和地址V的實際值是相等的。

image

7.線程1進(jìn)行SWAP,把地址V的值替換為B,也就是12。

image

從思想上來說,Synchronized屬于悲觀鎖,悲觀地認(rèn)為程序中的并發(fā)情況嚴(yán)重,所以嚴(yán)防死守。CAS屬于樂觀鎖,樂觀地認(rèn)為程序中的并發(fā)情況不那么嚴(yán)重,所以讓線程不斷去嘗試更新。

看到上面的解釋是不是索然無味,查找了很多資料也沒完全弄明白,通過幾次驗證后,終于明白,最終可以理解成一個無阻塞多線程爭搶資源的模型。先上代碼

import java.util.concurrent.atomic.AtomicBoolean;

/**
 * @author hrabbit
 * 2018/07/16.
 */
public class AtomicBooleanTest implements Runnable {

    private static AtomicBoolean flag = new AtomicBoolean(true);

    public static void main(String[] args) {
        AtomicBooleanTest ast = new AtomicBooleanTest();
        Thread thread1 = new Thread(ast);
        Thread thread = new Thread(ast);
        thread1.start();
        thread.start();
    }
    @Override
    public void run() {
        System.out.println("thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
        if (flag.compareAndSet(true,false)){
            System.out.println(Thread.currentThread().getName()+""+flag.get());
            try {
                Thread.sleep(5000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag.set(true);
        }else{
            System.out.println("重試機(jī)制thread:"+Thread.currentThread().getName()+";flag:"+flag.get());
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            run();
        }

    }
}

輸出的結(jié)果:

thread:Thread-1;flag:true
thread:Thread-0;flag:true
Thread-1false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:false
重試機(jī)制thread:Thread-0;flag:false
thread:Thread-0;flag:true
Thread-0false

這里無論怎么運(yùn)行,Thread-1、Thread-0都會執(zhí)行if=true條件,而且還不會產(chǎn)生線程臟讀臟寫,這是如何做到的了,這就用到了我們的compareAndSet(boolean expect,boolean update)方法
我們看到當(dāng)Thread-1在進(jìn)行操作的時候,Thread一直在進(jìn)行重試機(jī)制,程序原理圖:

image

這個圖中重最要的是compareAndSet(true,false)方法要拆開成compare(true)方法和Set(false)方法理解,是compare(true)是等于true后,就馬上設(shè)置共享內(nèi)存為false,這個時候,其它線程無論怎么走都無法走到只有得到共享內(nèi)存為true時的程序隔離方法區(qū)。

看到這里,這種CAS機(jī)制就是完美的嗎?這個程序其實存在一個問題,不知道大家注意到?jīng)]有?

但是這種得不到狀態(tài)為true時使用遞歸算法是很耗cpu資源的,所以一般情況下,都會有線程sleep。

CAS的缺點:

1.CPU開銷較大
在并發(fā)量比較高的情況下,如果許多線程反復(fù)嘗試更新某一個變量,卻又一直更新不成功,循環(huán)往復(fù),會給CPU帶來很大的壓力。

2.不能保證代碼塊的原子性
CAS機(jī)制所保證的只是一個變量的原子性操作,而不能保證整個代碼塊的原子性。比如需要保證3個變量共同進(jìn)行原子性的更新,就不得不使用Synchronized了。

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

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

  • Java8張圖 11、字符串不變性 12、equals()方法、hashCode()方法的區(qū)別 13、...
    Miley_MOJIE閱讀 3,918評論 0 11
  • 一、線程狀態(tài)轉(zhuǎn)換新建(New)可運(yùn)行(Runnable)阻塞(Blocking)無限期等待(Waiting)限期等...
    達(dá)微閱讀 742評論 1 2
  • 第6章類文件結(jié)構(gòu) 6.1 概述 6.2 無關(guān)性基石 6.3 Class類文件的結(jié)構(gòu) java虛擬機(jī)不和包括java...
    kennethan閱讀 1,079評論 0 2
  • 本系列出于AWeiLoveAndroid的分享,在此感謝,再結(jié)合自身經(jīng)驗查漏補(bǔ)缺,完善答案。以成系統(tǒng)。 Java基...
    濟(jì)公大將閱讀 1,627評論 1 6
  • 過年啦!早上,我、媽媽、爸爸和妹妹一起去朋友家里拜年,和媽媽朋友家里的小朋友一起玩搭積木,我搭的可快了我搭出了一只...
    Happy天宇閱讀 183評論 0 0

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