并發(fā)編程之:Atomic

大家好锯茄,我是小黑厢塘,一個(gè)在互聯(lián)網(wǎng)茍且偷生的農(nóng)民工。

在開(kāi)始講今天的內(nèi)容之前肌幽,先問(wèn)一個(gè)問(wèn)題晚碾,使用int類(lèi)型做加減操作是不是線(xiàn)程安全的呢?比如 i++ ,++i喂急,i=i+1這樣的操作在并發(fā)情況下是否會(huì)有問(wèn)題格嘁?

我們通過(guò)運(yùn)行代碼來(lái)看一下。

public class AtomicDemo {
    public static void main(String[] args) throws InterruptedException {
        Data data = new Data();
        Thread a = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                System.out.println(Thread.currentThread().getName()+"_"+data.increment());
            }
        }, "A");
        Thread b = new Thread(() -> {
            for (int i = 0; i < 100000; i++) {
                System.out.println(Thread.currentThread().getName()+"_"+data.increment());
            }
        }, "B");
        a.start();
        b.start();
        // 等待A廊移,B線(xiàn)程執(zhí)行完畢
        a.join();
        b.join();
        System.out.println(data.getI());
    }
}

class Data {
    private volatile int i = 0;
    public int increment() {
        i++;
        return i;
    }
    public int getI() {
        return i;
    }
}

以上代碼比較簡(jiǎn)單糕簿,通過(guò)A,B兩個(gè)線(xiàn)程同時(shí)對(duì)Data對(duì)象中的i執(zhí)行++操作涣易,各自執(zhí)行100000次,最后輸出冶伞,如果說(shuō)i++操作時(shí)線(xiàn)程安全的新症,那么最后輸出的結(jié)果應(yīng)該是200000,但是我們運(yùn)行代碼會(huì)看到如下結(jié)果:

我們發(fā)現(xiàn)最后輸出的并不是200000响禽,而是199982徒爹,如果多執(zhí)行幾次的話(huà),這個(gè)結(jié)果會(huì)發(fā)生變化芋类,并且大多數(shù)情況下不會(huì)是200000隆嗅。這主要是因?yàn)閕nt類(lèi)型的++操作不是原子的,i++同等于i=i+1,也就是加1這一步和對(duì)i重新賦值這一步不是同時(shí)完成的侯繁,不具備原子性胖喳,所以我們得出結(jié)論int類(lèi)型的操作不是線(xiàn)程安全的。

在很多實(shí)際場(chǎng)景中都需要對(duì)一個(gè)數(shù)據(jù)進(jìn)行并發(fā)操作贮竟,比如電商的秒殺活動(dòng)中丽焊,對(duì)一個(gè)商品數(shù)量的扣減,那么我們想保證安全性應(yīng)該怎么做呢咕别?

首先我們可以想到的就是使用synchronized關(guān)鍵字對(duì)increment()這個(gè)方法加鎖技健,這樣就能保證每次只有一個(gè)線(xiàn)程能訪(fǎng)問(wèn)。

但是之前的文章中我們有講到synchronized是一個(gè)重量級(jí)的悲觀(guān)鎖惰拱,我們的業(yè)務(wù)場(chǎng)景的并發(fā)可能是一段時(shí)間內(nèi)的雌贱,多數(shù)情況下可能并不會(huì)有很多競(jìng)爭(zhēng),所以有沒(méi)有更好的處理方式呢偿短,答案就是通過(guò)AtomicInteger欣孤。

AtomicInteger

AtomicInteger是java.util.concurrent.atomic包中的一個(gè)類(lèi)。我們看官方文檔對(duì)于這個(gè)包的描述昔逗,說(shuō)它是支持單個(gè)變量上的無(wú)鎖線(xiàn)程安全編程的工具包降传,好像和我們期望的一樣,在不加鎖的情況下達(dá)到線(xiàn)程安全纤子。

我們來(lái)修改一下上面例子的代碼搬瑰。

class Data {
    private volatile AtomicInteger i = new AtomicInteger(0);

    public int increment() {
        return i.incrementAndGet();
    }

    public int getI() {
        return i.get();
    }
}

很簡(jiǎn)單,將原來(lái)的int修改為AtomicInteger控硼,在執(zhí)行increment()方法進(jìn)行增加操作時(shí),調(diào)用incrementAndGet()方法就可以了艾少。同樣我們運(yùn)行代碼卡乾,會(huì)發(fā)現(xiàn),不管運(yùn)行多少次缚够,代碼最后執(zhí)行的結(jié)果都是一樣的幔妨,200000鹦赎。所以我們說(shuō)AtomicInteger是線(xiàn)程安全的。除了incrementAndGet()方法以外误堡,還有很多其他的操作古话,比如decrementAndGet(),getAndIncrement()锁施,getAndDecrement()陪踩,getAndAdd(int delta),addAndGet(int delta)等等悉抵,實(shí)際上就是對(duì)i++肩狂,++i,i=i+n姥饰,i+=n這些操作的原子實(shí)現(xiàn)傻谁。

除了AtomicInteger以外,java.util.concurrent.atomic包中還有一些其他類(lèi)型列粪,比如AtomicBoolean审磁,AtomicLong等。


實(shí)現(xiàn)原理

那么AtomicInteger是如何實(shí)現(xiàn)在不使用synchronized的情況下保證原子性的呢岂座?我們來(lái)看一下源碼力图。

public class AtomicInteger extends Number implements java.io.Serializable {
    
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    // value在內(nèi)存中的地址偏移值
    private static final long valueOffset;
    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    // value為volatile的保證內(nèi)存可見(jiàn)性
    private volatile int value;

    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
   
}
public final class Unsafe {
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 獲取volatile的Int,保證拿到的值是最新的
            var5 = this.getIntVolatile(var1, var2);
            // compareAndSwapInt 比較并交換 native方法
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
        return var5;
    }
}

通過(guò)源碼我們看到在incrementAndGet()方法中調(diào)用了Unsafe類(lèi)的getAndAddInt方法掺逼,在這個(gè)方法內(nèi)部對(duì)value進(jìn)行compareAndSwapInt操作吃媒。通過(guò)這個(gè)方法名我們就可以看出是比較并交換,也就是我們之前提到過(guò)的CAS吕喘。也就是在執(zhí)行賦值操作時(shí)赘那,先看一下當(dāng)前值是不是我加之前的值,如果不是氯质,那我就重新加一次之后再進(jìn)行比較募舟,是一個(gè)循環(huán)的過(guò)程,這個(gè)過(guò)程也稱(chēng)作自旋闻察。

CAS這種處理方式雖然很高效的解決了原子操作拱礁,但是它仍然存在三個(gè)問(wèn)題,在實(shí)際開(kāi)發(fā)中一定要注意辕漂,結(jié)合自己的實(shí)際業(yè)務(wù)場(chǎng)景使用呢灶。

ABA問(wèn)題

什么是ABA問(wèn)題呢,通俗理解钉嘹,就是你大爺還是你大爺鸯乃,你大媽已經(jīng)不是你大媽了~

ABA問(wèn)題

什么意思呢?就是當(dāng)線(xiàn)程1取到A之后跋涣,有另一個(gè)線(xiàn)程2把A變成了B缨睡,又變成了A鸟悴,當(dāng)線(xiàn)程1再修改完值進(jìn)行CAS比較時(shí),發(fā)現(xiàn)值還是A奖年,和自己取到的一樣细诸,就直接更新了,但是在這個(gè)過(guò)程中陋守,這個(gè)A中間是發(fā)生過(guò)變化的震贵。就好比一個(gè)小偷,偷了別人家錢(qián)然后再還回來(lái)嗅义,還是原來(lái)的錢(qián)嗎屏歹?雖然你的錢(qián)沒(méi)變,但是這個(gè)小偷已經(jīng)觸犯了法律之碗,而你自己還不知道蝙眶。

為了解決這個(gè)問(wèn)題,atomic包中提供了一個(gè)類(lèi)褪那,我們看下是如何解決的幽纷。

public static void main(String[] args) throws InterruptedException {
    AtomicStampedReference<String> ref = new AtomicStampedReference<>("A", 0);
    new Thread(() -> {
        try {
            int stamp = ref.getStamp();
            String reference = ref.getReference();
            System.out.println("線(xiàn)程1拿到的值:" + reference + " stamp:" + stamp);
            // sleep 2秒模擬線(xiàn)程切換到2
            TimeUnit.SECONDS.sleep(2);
            boolean success = ref.compareAndSet(reference, "C", stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName() + " " + success);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }, "線(xiàn)程1").start();

    new Thread(() -> {
        // 先改為B
        int stamp = ref.getStamp();
        String reference = ref.getReference();
        System.out.println("線(xiàn)程2拿到的值:" + reference + " stamp:" + stamp);
        ref.compareAndSet(reference, "B", stamp, stamp + 1);
        // 再改回A
        stamp = ref.getStamp();
        reference = ref.getReference();
        System.out.println("線(xiàn)程2拿到的值:" + reference + " stamp:" + stamp);
        ref.compareAndSet(reference, "A", ref.getStamp(), stamp + 1);
    }, "線(xiàn)程2").start();
}

我們可以看到AtomicStampedReference的compareAndSet()方法有4個(gè)參數(shù):

  1. expectedReference:表示期望的引用值
  2. newReference:表示要修改后的新引用值
  3. expectedStamp:表示期望的戳(版本號(hào))
  4. newStamp:表示修改后新的戳(版本號(hào))

什么意思呢?就是在修改時(shí)不光比較值是不是和獲取到的一樣博敬,還要比較版本號(hào)友浸。這樣的話(huà),每次操作時(shí)都對(duì)版本號(hào)加1偏窝,那么就算值從A改為B再改回A收恢,但是版本號(hào)從0改成了1又改成了2,并沒(méi)有變回0祭往,就可以避免ABA問(wèn)題的發(fā)生伦意。

循環(huán)時(shí)間變長(zhǎng)

在并發(fā)非常大的情況下,使用CAS可能會(huì)存在一些線(xiàn)程一直循環(huán)修改不成功硼补,導(dǎo)致循環(huán)時(shí)間變長(zhǎng)驮肉,會(huì)給CPU帶來(lái)很大的執(zhí)行開(kāi)銷(xiāo)。并且由于A(yíng)tomicReference中的引用是volatile的已骇,為了保證內(nèi)存可見(jiàn)性离钝,需要保證緩存一致性,通過(guò)總線(xiàn)傳輸數(shù)據(jù)褪储,當(dāng)有大量的CAS循環(huán)時(shí)卵渴,會(huì)產(chǎn)生總線(xiàn)風(fēng)暴

只能保證一個(gè)變量的原子操作

CAS的第三個(gè)問(wèn)題就是AtomicReference中只能存放一個(gè)變量乱豆,如果需要保證多個(gè)變量操作的原子性奖恰,是做不到的。對(duì)于這種情況只能使用synchronized或者juc包中的Lock工具宛裕。

小結(jié)

簡(jiǎn)單做個(gè)小結(jié)瑟啃,使用int類(lèi)型在并發(fā)場(chǎng)景下存在線(xiàn)程安全問(wèn)題,可以用AtomicInteger來(lái)保證原子性操作揩尸,Atomic是通過(guò)CAS做到無(wú)鎖線(xiàn)程安全的蛹屿。但是CAS有三個(gè)問(wèn)題,第一ABA問(wèn)題岩榆,可以通過(guò)AtomicStampedReference解決错负;第二競(jìng)爭(zhēng)激烈情況下循環(huán)時(shí)間會(huì)變長(zhǎng),會(huì)產(chǎn)生總線(xiàn)風(fēng)暴勇边;第三只能保證一個(gè)變量的原子操作犹撒。

具體業(yè)務(wù)場(chǎng)景中是使用synchronized,Lock等鎖工具還是使用Atomic的CAS無(wú)鎖操作粒褒,還是要結(jié)合場(chǎng)景考慮识颊。


好的,今天的內(nèi)容就到這里奕坟,我們下期見(jiàn)祥款。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市月杉,隨后出現(xiàn)的幾起案子刃跛,更是在濱河造成了極大的恐慌,老刑警劉巖苛萎,帶你破解...
    沈念sama閱讀 211,194評(píng)論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件桨昙,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡腌歉,警方通過(guò)查閱死者的電腦和手機(jī)蛙酪,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,058評(píng)論 2 385
  • 文/潘曉璐 我一進(jìn)店門(mén),熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)究履,“玉大人滤否,你說(shuō)我怎么就攤上這事∽盥兀” “怎么了藐俺?”我有些...
    開(kāi)封第一講書(shū)人閱讀 156,780評(píng)論 0 346
  • 文/不壞的土叔 我叫張陵,是天一觀(guān)的道長(zhǎng)泥彤。 經(jīng)常有香客問(wèn)我信不,道長(zhǎng)肿孵,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 56,388評(píng)論 1 283
  • 正文 為了忘掉前任,我火速辦了婚禮尘奏,結(jié)果婚禮上镊讼,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好官辽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,430評(píng)論 5 384
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著粟瞬,像睡著了一般同仆。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上裙品,一...
    開(kāi)封第一講書(shū)人閱讀 49,764評(píng)論 1 290
  • 那天俗批,我揣著相機(jī)與錄音,去河邊找鬼市怎。 笑死岁忘,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的区匠。 我是一名探鬼主播干像,決...
    沈念sama閱讀 38,907評(píng)論 3 406
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼辱志!你這毒婦竟也來(lái)了蝠筑?” 一聲冷哼從身側(cè)響起,我...
    開(kāi)封第一講書(shū)人閱讀 37,679評(píng)論 0 266
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤揩懒,失蹤者是張志新(化名)和其女友劉穎什乙,沒(méi)想到半個(gè)月后,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體已球,經(jīng)...
    沈念sama閱讀 44,122評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡臣镣,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,459評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了智亮。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片忆某。...
    茶點(diǎn)故事閱讀 38,605評(píng)論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖阔蛉,靈堂內(nèi)的尸體忽然破棺而出弃舒,到底是詐尸還是另有隱情,我是刑警寧澤状原,帶...
    沈念sama閱讀 34,270評(píng)論 4 329
  • 正文 年R本政府宣布聋呢,位于F島的核電站,受9級(jí)特大地震影響颠区,放射性物質(zhì)發(fā)生泄漏削锰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,867評(píng)論 3 312
  • 文/蒙蒙 一毕莱、第九天 我趴在偏房一處隱蔽的房頂上張望器贩。 院中可真熱鬧颅夺,春花似錦、人聲如沸蛹稍。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 30,734評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)稳摄。三九已至稚字,卻和暖如春饲宿,著一層夾襖步出監(jiān)牢的瞬間厦酬,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 31,961評(píng)論 1 265
  • 我被黑心中介騙來(lái)泰國(guó)打工瘫想, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留仗阅,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,297評(píng)論 2 360
  • 正文 我出身青樓国夜,卻偏偏與公主長(zhǎng)得像减噪,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子车吹,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,472評(píng)論 2 348

推薦閱讀更多精彩內(nèi)容