Java并發(fā)編程之CAS

在Java并發(fā)編程的世界里冰抢,synchronized 和 Lock 是控制多線程并發(fā)環(huán)境下對共享資源同步訪問的兩大手段。其中 Lock 是 JDK 層面的鎖機制艘狭,是輕量級鎖挎扰,底層使用大量的自旋+CAS操作實現(xiàn)的。

學習并發(fā)推薦《Java并發(fā)編程的藝術》

那什么是CAS呢巢音?CAS遵倦,compare and swap,即比較并交換官撼,什么是比較并交換呢梧躺?在Lock鎖的理念中,采用的是一種樂觀鎖的形式傲绣,即多線程去修改共享資源時掠哥,不是在修改之前就加鎖,而是樂觀的認為沒有別的線程和自己爭鎖秃诵,就是通過CAS的理念去保障共享資源的安全性的续搀。CAS的基本思想是,拿變量的原值和內存中的值進行比較菠净,如果相同禁舷,則原值沒有被修改過,那么就將原值修改為新值嗤练,這兩步是原子的榛了,能夠保證同一時間只有一個線程修改成功。這就是CAS的理念煞抬。

Java中要想使用CAS原子的修改某值霜大,怎么做呢?幸運的是Java提供了這樣的API革答,就是在sun.misc.Unsafe.java類中战坤。Unsafe曙强,中文名不安全的,也被稱為魔術類途茫,魔法類碟嘴。

Unsafe類介紹

Unsafe類使Java擁有了像C語言的指針一樣操作內存空間的能力,一旦能夠直接操作內存囊卜,這也就意味著
(1)不受JVM管理娜扇,意思就是使用Unsafe操作內存無法被JVM GC,需要我們手動GC栅组,稍有不慎就會出現(xiàn)內存泄漏雀瓢。
(2)Unsafe的不少方法中必須提供原始地址(內存地址)和被替換對象的地址,并且偏移量要自己計算(其提供的有計算偏移量的方法)玉掸,所以一旦出現(xiàn)問題就是JVM崩潰級別的異常刃麸,會導致整個JVM實例崩潰,表現(xiàn)為應用程序直接crash掉司浪。
(3)直接操作內存泊业,所以速度更快,在高并發(fā)的條件之下能夠很好地提高效率啊易。

因此吁伺,從上面三個角度來看,雖然在一定程度上提升了效率但是也帶來了指針的不安全性认罩。這也是它被取名為Unsafe的原因吧箱蝠。

下面我們深入到源碼中看看,提供了什么方法直接操作內存垦垂。

打開Unsafe這個類宦搬,我們會發(fā)現(xiàn)里面有大量的被native關鍵字修飾的方法,這意味著這些方法是C語言提供的實現(xiàn)劫拗,底層調的是C語言的庫函數(shù)间校,我們無法直接看到他的源碼實現(xiàn),需要去從OpenJDK去看了页慷。另外還有一些基于native方法封裝的其他方法憔足,整個Unsafe中的方法大致可以歸結為以下幾類:
(1)初始化操作
(2)操作對象屬性
(3)操作數(shù)組元素
(4)線程掛起和恢復
(5)CAS機制

CAS的使用

如果你學過java并發(fā)編程的話,稍微閱讀過JUC并發(fā)包里面的源碼的話酒繁,對這個Unsafe類一定不陌生滓彰,因為整個java并發(fā)包底層實現(xiàn)的核心就是靠它。JUC并發(fā)包中主要使用它提供的CAS(compare and swap州袒,比較并交換)操作揭绑,原子的修改鎖的狀態(tài)和一些隊列元素。

沒看過JUC源碼的讀者也不用擔心,今天我們就是簡單介紹Unsafe類中的CAS操作他匪,那么我們接下來就會通過一個簡單的例子來看看Unsafe的CAS是怎么使用的菇存。

首先,使用這個類我們第一個要做的事情就是拿到這個類的實例邦蜜,下面我們自定義了一個Util類用來獲取Unsafe的實例

import sun.misc.Unsafe;
import java.lang.reflect.Field;

public class UnsafeUtil {
    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

這個工具類通過反射的方式拿到Unsafe類中的一個名為theUnsafe字段依鸥,該字段是Unsafe類型,并在static塊中new一個Unsafe對象初始化這個字段(單例模式)悼沈。

然后我們定義了一個AtomicState類贱迟,這個類很簡單,有一個int型的state字段絮供,還有一個Unsafe的常量关筒,以及int型的offsetState,用來記錄state字段在AtomicState對象中的偏移量杯缺。具體代碼如下:

import com.walking.juc.util.UnsafeUtil;
import sun.misc.Unsafe;
public class AtomicState {
    private volatile int state = 0;
    public int getState() {
        return state;
    }

    private static final Unsafe UNSAFE = UnsafeUtil.reflectGetUnsafe();
    private static final long offsetState;
    static {
        try {
            offsetState = UNSAFE.objectFieldOffset(AtomicState.class.getDeclaredField("state"));
        } catch (NoSuchFieldException e) {
            throw new Error(e);
        }
    }
    public final boolean compareAndSetState(int oldVal, int newVal) {
        return UNSAFE.compareAndSwapInt(this, offsetState, oldVal, newVal);
    }
}

我們定義了一個compareAndSetState方法,需要傳兩個參數(shù)睡榆,分別是state的舊值和新值萍肆,也就是讀到的state的之前的值,以及想要把它修改成什么值胀屿,該方法內部調用的是Unsafe類的compareAndSwapInt方法塘揣,它有四個參數(shù),分別是要修改的類實例對象宿崭、要修改的值的偏移量亲铡、舊值、新值葡兑。解釋一下偏移量奖蔓,剛才我們提到Unsafe提供給我們直接訪問內存的能力,那么訪問內存肯定是要知道內存的地址在哪才能去修改其相應的值吧讹堤,我們看吆鹤,第一個參數(shù)是對象實例引用,也就是說洲守,已經知道這個對象的地址了疑务,那么我們想修改這個對象里的state的值,就只需要計算出state在這個對象的偏移量就能找到state所在的內存地址梗醇,那就可以修改它了知允。

然后,我們通過一個測試類來驗證Unsafe的CAS操作叙谨。這個測試類我來解釋下大致的思想温鸽,我們弄5個線程,讓這個5個線程一個個啟動唉俗,我們無法保證線程同時開始啟動嗤朴,那么我們有辦法保證這個5個線程同時執(zhí)行我們的代碼配椭,就是使用JUC包里的CyclicBarrier工具來實現(xiàn)的,這個工具初始化時需要傳入一個int值n雹姊,我們在線程的run方法內部在業(yè)務代碼執(zhí)行之前調用CyclicBarrier的await方法股缸,當指定數(shù)量n的線程都調用了這個方法那么這n個線程將同時往下執(zhí)行,就像設置了一個屏障吱雏,所有人都達到這個屏障后敦姻,一起通過屏障,依次來模擬多線程并發(fā)

import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.CyclicBarrier;
@Slf4j
public class TestAtomicState {

    static int tNum = 5;//線程數(shù) 我們開10個線程模擬多線程并發(fā)
    static CyclicBarrier cyclicBarrier = new CyclicBarrier(tNum);//柵欄
    static CountDownLatch countDownLatch = new CountDownLatch(tNum);//計數(shù)器
    static AtomicState atomicState = new AtomicState();

    public static void main(String[] args) throws InterruptedException {
        for (int i = 1; i <= tNum; i++) {
            new Thread(new MyTask(),"t-"+i).start();
        }
        countDownLatch.await();//為的是讓主線程在這句阻塞住歧杏,等待所有線程執(zhí)行完畢(計數(shù)器減到0)再往下走
        log.info("state最后的值:" + atomicState.getState());
    }

    static class MyTask implements Runnable{
        @Override
        public void run() {
            try {
                log.info(Thread.currentThread().getName() + "到達起跑線");
                String name = Thread.currentThread().getName();
                String substring = name.substring(name.indexOf("-") + 1);
                int i1 = Integer.parseInt(substring);

                cyclicBarrier.await();//設置一個屏障镰惦,所有線程達到這后開始一起往下執(zhí)行 模擬并發(fā)
                boolean b = atomicState.compareAndSetState(0, i1);
                if (b) {
                    log.info("修改成功,tName:{}" ,Thread.currentThread().getName());
                } else {
                    log.info("修改失敗犬绒,tName:{}" ,Thread.currentThread().getName());
                }
            } catch (InterruptedException | BrokenBarrierException e) {
                e.printStackTrace();
            } finally {
                countDownLatch.countDown();//線程執(zhí)行完畢計數(shù)器減一
            }
        }
    }
}

cyclicBarrier.await();之后我們調用AtomicStatecompareAndSetState方法傳入舊值0和新值旺入,新值就是線程名t-n中的n,哪個線程修改成功凯力,最后state值就是線程名中的數(shù)字茵瘾。
至于CountDownLatch使用它的目的是讓mian線程等到t-1到t-5的線程全部執(zhí)行完后打印state的值。我們的重點不是CyclicBarrierCountDownLatch咐鹤,知道它們是干什么的就行拗秘。

然后我們運行這個測試程序:

13:57:46.619 [t-2] INFO com.walking.castest.TestAtomicState - t-2到達起跑線
13:57:46.619 [t-3] INFO com.walking.castest.TestAtomicState - t-3到達起跑線
13:57:46.619 [t-5] INFO com.walking.castest.TestAtomicState - t-5到達起跑線
13:57:46.619 [t-1] INFO com.walking.castest.TestAtomicState - t-1到達起跑線
13:57:46.619 [t-4] INFO com.walking.castest.TestAtomicState - t-4到達起跑線
13:57:46.628 [t-1] INFO com.walking.castest.TestAtomicState - 修改失敗,tName:t-1
13:57:46.628 [t-4] INFO com.walking.castest.TestAtomicState - 修改成功祈惶,tName:t-4
13:57:46.628 [t-2] INFO com.walking.castest.TestAtomicState - 修改失敗雕旨,tName:t-2
13:57:46.628 [t-5] INFO com.walking.castest.TestAtomicState - 修改失敗,tName:t-5
13:57:46.628 [t-3] INFO com.walking.castest.TestAtomicState - 修改失敗捧请,tName:t-3
13:57:46.636 [main] INFO com.walking.castest.TestAtomicState - state最后的值:4

可以看到只有一個線程執(zhí)行成功凡涩,這就是CAS的基本使用。

CAS的ABA問題

何為ABA問題呢疹蛉?舉個例子突照,小明和小花合伙賣煎餅,不就后攢了10萬元氧吐,他們一起去銀行把錢存在他們公共的賬戶里讹蘑,但是小明聽說最近牛市來了,就偷偷的把錢轉移到了股票市場筑舅,公共賬戶余額是0座慰。1個月后股票賺了一筆錢,然后小明把之前轉移的10萬元又存到他們的公共賬戶翠拣。小明和小花一個月后又去存錢版仔,去查賬戶余額是10萬。這就是ABA問題,簡單來說就是一個值本來是A蛮粮,兩個線程同時都看到是A益缎,然后線程1把A改成B后又改成A,線程1結束了然想。然后線程2去修改時莺奔,看到的是A,無法感知到這個過程中值發(fā)生過變化变泄,對于線程2來說就發(fā)生了ABA的問題令哟。

模擬ABA問題:

import java.util.concurrent.atomic.AtomicInteger;
@Slf4j
public class CAS_ABA_Stampe {
    static AtomicInteger atomicInteger = new AtomicInteger(10);
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                log.info("{}拿到state的值為:{}", Thread.currentThread().getName(), atomicInteger.get());
                log.info("{}第一次修改", Thread.currentThread().getName());
                atomicInteger.getAndSet(0);
                Thread.sleep(2000);
                log.info("{}第二次修改", Thread.currentThread().getName());
                atomicInteger.getAndSet(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            try {
                log.info("{}第一次拿到state的值為:{}", Thread.currentThread().getName(), atomicInteger.get());
                Thread.sleep(2500);
                log.info("{}第二次拿到state的值為:{}", Thread.currentThread().getName(), atomicInteger.get());
                log.info("{}開始修改state的值為2", Thread.currentThread().getName());
                atomicInteger.getAndSet(20);
                log.info("{}修改成功", Thread.currentThread().getName());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");
        t2.start();
        t1.join();
        t2.join();
        log.info("最終state的值:{}", atomicInteger.get());
    }
}
//結果t2也能修改成功,并沒有發(fā)現(xiàn)這種變化

怎么解決CAS的ABA問題呢妨蛹?
那就是基于版本號去解決屏富,增加一個版本號的概念,每次被修改這個版本號就加1蛙卤,版本號是一直向前的狠半,版本號變了,就說明被修改過颤难。

JUC包中提供了解決ABA問題的工具:

import lombok.extern.slf4j.Slf4j;
import java.util.concurrent.atomic.AtomicStampedReference;

@Slf4j
public class CAS_ABA_Stampe {
    static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(10, 1);

    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            try {
                int stamp = stampedReference.getStamp();
                int intValue = stampedReference.getReference().intValue();
                log.info("{}私挪公款拿到stamp的值為:{}典予,余額:{}", Thread.currentThread().getName(), stamp,intValue);
                stampedReference.compareAndSet(10, 0, stamp, stamp + 1);
                Thread.sleep(2000);
                stamp = stampedReference.getStamp();
                intValue = stampedReference.getReference().intValue();
                log.info("{}還回公款拿到stamp的值為:{},余額:{}", Thread.currentThread().getName(), stamp,intValue);
                stampedReference.compareAndSet(0, 10, stamp, stamp + 1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t1");
        t1.start();

        Thread t2 = new Thread(() -> {
            try {
                int stamp = stampedReference.getStamp();
                int intValue = stampedReference.getReference().intValue();
                log.info("{}拿到stamp的值為:{}乐严,余額:{}", Thread.currentThread().getName(), stamp, intValue);
                Thread.sleep(3000);

                log.info("{}開始存款", Thread.currentThread().getName());
                if (stampedReference.compareAndSet(10, 20, stamp, stamp + 1)) {
                    log.info("{}款款成功", Thread.currentThread().getName());
                }else {
                    log.info("{}存款失敗,發(fā)現(xiàn)賬戶異常!!oldStamp:{},currentStamp:{}", Thread.currentThread().getName(),stamp,stampedReference.getStamp());
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "t2");
        t2.start();
        t1.join();
        t2.join();
        log.info("最終賬戶余額:{}W", stampedReference.getReference().intValue());
    }
}

運行結果:

15:32:37.488 [t1] INFO com.walking.castest.CAS_ABA_Stampe - t1私挪公款拿到stamp的值為:1衣摩,余額:10
15:32:37.476 [t2] INFO com.walking.castest.CAS_ABA_Stampe - t2拿到stamp的值為:1昂验,余額:10
15:32:39.500 [t1] INFO com.walking.castest.CAS_ABA_Stampe - t1還回公款拿到stamp的值為:2,余額:0
15:32:40.498 [t2] INFO com.walking.castest.CAS_ABA_Stampe - t2開始存款
15:32:40.498 [t2] INFO com.walking.castest.CAS_ABA_Stampe - t2存款失敗艾扮,發(fā)現(xiàn)賬戶異常<惹佟!oldStamp:1,currentStamp:3
15:32:40.498 [main] INFO com.walking.castest.CAS_ABA_Stampe - 最終賬戶余額:10W

t2存款時就發(fā)現(xiàn)賬戶異常泡嘴,因為版本號已經變成了3甫恩,和t2剛開始拿到的不一樣,說明已經被別人修改過酌予,從而解決ABA問題磺箕。

到這里CAS就完啦。別忘了點贊抛虫,轉發(fā)松靡。


往期熱文:




關注公眾號建椰,點擊右下角【技術書籍】免費領取


歡迎關注公眾號雕欺,謝謝支持。

?著作權歸作者所有,轉載或內容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市屠列,隨后出現(xiàn)的幾起案子啦逆,更是在濱河造成了極大的恐慌,老刑警劉巖笛洛,帶你破解...
    沈念sama閱讀 219,270評論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件夏志,死亡現(xiàn)場離奇詭異,居然都是意外死亡撞蜂,警方通過查閱死者的電腦和手機盲镶,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,489評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來蝌诡,“玉大人溉贿,你說我怎么就攤上這事∑趾担” “怎么了宇色?”我有些...
    開封第一講書人閱讀 165,630評論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長颁湖。 經常有香客問我宣蠕,道長,這世上最難降的妖魔是什么甥捺? 我笑而不...
    開封第一講書人閱讀 58,906評論 1 295
  • 正文 為了忘掉前任抢蚀,我火速辦了婚禮,結果婚禮上镰禾,老公的妹妹穿的比我還像新娘皿曲。我一直安慰自己,他們只是感情好吴侦,可當我...
    茶點故事閱讀 67,928評論 6 392
  • 文/花漫 我一把揭開白布屋休。 她就那樣靜靜地躺著,像睡著了一般备韧。 火紅的嫁衣襯著肌膚如雪劫樟。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,718評論 1 305
  • 那天织堂,我揣著相機與錄音叠艳,去河邊找鬼。 笑死易阳,一個胖子當著我的面吹牛虑绵,可吹牛的內容都是我干的。 我是一名探鬼主播闽烙,決...
    沈念sama閱讀 40,442評論 3 420
  • 文/蒼蘭香墨 我猛地睜開眼翅睛,長吁一口氣:“原來是場噩夢啊……” “哼声搁!你這毒婦竟也來了?” 一聲冷哼從身側響起捕发,我...
    開封第一講書人閱讀 39,345評論 0 276
  • 序言:老撾萬榮一對情侶失蹤疏旨,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后扎酷,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體檐涝,經...
    沈念sama閱讀 45,802評論 1 317
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 37,984評論 3 337
  • 正文 我和宋清朗相戀三年法挨,在試婚紗的時候發(fā)現(xiàn)自己被綠了谁榜。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 40,117評論 1 351
  • 序言:一個原本活蹦亂跳的男人離奇死亡凡纳,死狀恐怖窃植,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情荐糜,我是刑警寧澤巷怜,帶...
    沈念sama閱讀 35,810評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站暴氏,受9級特大地震影響延塑,放射性物質發(fā)生泄漏。R本人自食惡果不足惜答渔,卻給世界環(huán)境...
    茶點故事閱讀 41,462評論 3 331
  • 文/蒙蒙 一关带、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧沼撕,春花似錦宋雏、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,011評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽燃箭。三九已至冲呢,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間招狸,已是汗流浹背敬拓。 一陣腳步聲響...
    開封第一講書人閱讀 33,139評論 1 272
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留裙戏,地道東北人乘凸。 一個月前我還...
    沈念sama閱讀 48,377評論 3 373
  • 正文 我出身青樓,卻偏偏與公主長得像累榜,于是被迫代替她去往敵國和親营勤。 傳聞我的和親對象是個殘疾皇子灵嫌,可洞房花燭夜當晚...
    茶點故事閱讀 45,060評論 2 355