六、原子操作CAS

一罪佳、什么是原子操作逛漫?如何實現(xiàn)原子操作?

CAS:Compare And Swap,比較并且交換赘艳。隸屬于樂觀鎖機制酌毡。
什么是原子操作?
假設(shè)現(xiàn)在有A蕾管,B兩個操作枷踏,如果某個線程執(zhí)行A操作,當另外一個線程執(zhí)行B操作的時候掰曾,要么這個B全部執(zhí)行完旭蠕,要么這個B完全不執(zhí)行,那么對于A、B來講掏熬,他們彼此就是原子的佑稠。
在數(shù)據(jù)庫層面,這種操作就是事務(wù)操作旗芬,嚴格意義上來說事務(wù)操作也是屬于原子操作的一種舌胶。
如何實現(xiàn)原子操作
可以利用synchronize關(guān)鍵字,但是會引發(fā)一系列問題:

  • 1.synchronize是阻塞式的疮丛,一個線程擁有鎖后幔嫂,其他的線程必須等待
  • 2.等待中的線程優(yōu)先級很高,但是遲遲拿不到鎖怎么辦这刷?
  • 3.等待中的線程競爭很激烈,但是拿到鎖的線程遲遲不釋放鎖怎么辦娩井?
解決辦法CAS

CAS可以完美地解決上述的問題暇屋,進而更完美地實現(xiàn)原子操作,它利用了現(xiàn)代處理器都支持的CAS指令洞辣,這個指令是CPU級別的指令咐刨。

CAS包含的要素

1.內(nèi)存地址v:修改的對象或者變量的內(nèi)存地址
2.期望值A(chǔ):
3.新值B
當我去改這個內(nèi)存地址上所對應(yīng)的對象或者變量的時候,我期望在我改的時候扬霜,這個值是多少定鸟,如果是A,我就把他改成B著瓶,如果不是A联予,那我就不能改。將B值替換為A值材原。
比較---->交換沸久。

用java語言來講,這個操作需要兩個語句余蟹,一個是比較卷胯,一個是交換。
而在CPU層面威酒,只要你執(zhí)行了這個指令窑睁,我可以保證別的指令都被阻塞,只有這一個CAS指令操作完了才允許別的指令進行操作葵孤。

在JDK層面來講担钮,用到了循環(huán)(自旋、死循環(huán))尤仍,直到成功為止裳朋,原理如下:

原理

這種思想就是樂觀鎖

用一句話來概括CAS如何實現(xiàn)線程安全?

CAS在語言層面不作處理鲤嫡,我們把它交給了CPU和內(nèi)存送挑,利用CPU的能力實現(xiàn)硬件層面阻塞,進而實現(xiàn)CAS的線程安全暖眼。

二惕耕、CAS引起的問題

1.ABA問題

下面的兩種情況下會出現(xiàn)ABA問題。
1.A最開始的內(nèi)存地址是X诫肠,然后失效了司澎,又分配了B,恰好內(nèi)存地址是X栋豫,這時候通過CAS操作挤安,卻設(shè)置成功了
  這種情況在帶有GC的語言中,這種情況是不可能發(fā)生的丧鸯,為什么呢蛤铜?拿JAVA舉例,在執(zhí)行CAS操作時丛肢,A围肥,B對象肯定生命周期內(nèi),GC不可能將其釋放蜂怎,那么A指向的內(nèi)存是不會被釋放的穆刻,B也就不可能分配到與A相同的內(nèi)存地址,CAS失敗杠步。若在無GC的氢伟,A對象已經(jīng)被釋放了,那么B被分配了A的內(nèi)存幽歼,CAS成功腐芍。
2.線程1準備用CAS將變量的值由A替換為B,在此之前试躏,線程2將變量的值由A替換為C猪勇,又由C替換為A,然后線程1執(zhí)行CAS時發(fā)現(xiàn)變量的值仍然為A颠蕴,所以CAS成功泣刹。但實際上這時的現(xiàn)場已經(jīng)和最初不同了,盡管CAS成功犀被,但可能存在潛藏的問題椅您。比如:

現(xiàn)有一個用單向鏈表實現(xiàn)的堆棧,棧頂為A寡键,這時線程T1已經(jīng)知道A.next為B掀泳,然后希望用CAS將棧頂替換為B:head.compareAndSet(A,B);在T1執(zhí)行上面這條指令之前,線程T2介入,將A员舵、B出棧脑沿,再pushD、C马僻、A庄拇。而對象B此時處于游離狀態(tài):此時輪到線程T1執(zhí)行CAS操作,檢測發(fā)現(xiàn)棧頂仍為A韭邓,所以CAS成功措近,棧頂變?yōu)锽,但實際上B.next為null女淑,其中堆棧中只有B一個元素瞭郑,C和D組成的鏈表不再存在于堆棧中,平白無故就把C鸭你、D丟掉了屈张。

以上就是由于ABA問題帶來的隱患,各種樂觀鎖的實現(xiàn)中通常都會用版本戳version來對記錄或?qū)ο髽擞浳荆苊獠l(fā)操作帶來的問題袜茧,在Java中菜拓,AtomicStampedReference<E>也實現(xiàn)了這個作用瓣窄,它通過包裝[E,Integer]的元組來對對象標記版本戳stamp,從而避免ABA問題纳鼎,例如下面的代碼分別用AtomicInteger和AtomicStampedReference來對初始值為100的原子整型變量進行更新俺夕,AtomicInteger會成功執(zhí)行CAS操作,而加上版本戳的AtomicStampedReference對于ABA問題會執(zhí)行CAS失敗贱鄙。

package concur.lock;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class ABA {
    
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference<Integer> atomicStampedRef = 
            new AtomicStampedReference<Integer>(100, 0);
    
    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3);        //true
            }
        });
        
        intT1.start();
        intT2.start();
        intT1.join();
        intT2.join();
        
        Thread refT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                atomicStampedRef.compareAndSet(100, 101, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
                atomicStampedRef.compareAndSet(101, 100, 
                        atomicStampedRef.getStamp(), atomicStampedRef.getStamp()+1);
            }
        });
        
        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                System.out.println("before sleep : stamp = " + stamp);    // stamp = 0
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
                System.out.println(c3);        //false
            }
        });
        
        refT1.start();
        refT2.start();
    }

}

如何解決劝贸?
增加版本號,也就是說在每個變量前面都要加一個版本號逗宁,每次修改的時候都對其版本+1映九。其實在大多數(shù)開發(fā)過程中,我們是不關(guān)心ABA問題的瞎颗。但是ABA問題在一線互聯(lián)網(wǎng)公司的面試中是經(jīng)常問到的件甥。

  • 1.ABA問題的解決思路是使用版本號,每次變量更新的時候版本號加1哼拔,那么A->B->A就會變成1A->2B->3A
  • 2.從jdk1.5開始引有,jdk的Atomic包里就提供了兩個類來解決ABA問題,一個是AtomicStampedReference,另一個是AtomicMarkableReference倦逐,AtomicStampedReference這個類中的compareAndSet方法的作用就是首先檢查當前引用是否等于預期引用譬正,并且檢查當前標志是否等于預期標志,如果全部相等,則以原子方式將該引用和該標志的值更新為指定的新值曾我。
    AtomicStampedReferenceAtomicMarkableReference的區(qū)別
    AtomicStampedReference帶了版本號粉怕,關(guān)心被修改過幾次,AtomicMarkableReference只關(guān)心有沒有人修改過您单。

2.開銷問題

自旋CAS如果長時間不成功斋荞,會給CPU帶來非常大的執(zhí)行開銷。如果jvm能支持處理器提供的pause指令虐秦,那么效率會有一定的提升平酿。pause指令有兩個作用:

第一,它可以延遲流水線執(zhí)行指令(de-pipeline)悦陋,使CPU不會消耗過多的執(zhí)行資源蜈彼,延遲的時間取決于具體實現(xiàn)的版本,在一些處理器上延遲時間是零俺驶。

第二幸逆,它可以避免在退出循環(huán)的時候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率暮现。

3.只能保證一個變量的原子操作

當對一個共享變量執(zhí)行操作時还绘,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個共享變量操作時栖袋,循環(huán)CAS就無法保證操作的原子性拍顷,這個時候就可以用鎖。還有一個方法塘幅,就是把多個共享變量合并成一個共享變量來操作昔案。比如,有兩個共享變量i=2,j=a合并一下ij=2a电媳,然后用CAS來操作ij踏揣。從java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性匾乓,就可以把多個變量放在一個對象里來進行CAS操作捞稿。

三、原子操作類的使用

jdk中相關(guān)原子操作類的使用

  • 更新基本類型類:AtomicBoolean,AtomicInteger拼缝,AtomicLong
  • 更新數(shù)組類:AtomicIntegerArray娱局,AtomicLongArray,AtomicReferenceArray
  • 更新引用類:AtomicReference珍促,AtomicMarkableReference铃辖,AtomicStampeReference
  • 原子更新字段類:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater

舉例:

import java.util.concurrent.atomic.AtomicInteger;

/**
 *類說明:演示基本類型的原子操作類
 */
public class UseAtomicInt {
    static AtomicInteger ai = new AtomicInteger(10);

    public static void main(String[] args) {
        //返回的是我自增以前的值
        int i =  ai.getAndIncrement(); // i++
        //返回自增以后的值
        int b = ai.incrementAndGet();// ++i
        System.out.println(i +"------"+ b);
        //ai.compareAndSet();
        int fianl = ai.addAndGet(24);
        System.out.println("加了24之后的值為:"+fianl);
    }
}

運行結(jié)果:


原子操作類的使用
import java.util.concurrent.atomic.AtomicIntegerArray;


/**
 *類說明: 演示原子操作數(shù)組
 */
public class AtomicArray {
    static int[] value = new int[] { 1, 2 };
    static AtomicIntegerArray ai = new AtomicIntegerArray(value);
    public static void main(String[] args) {
        ai.getAndSet(0, 3);
        System.out.println(ai.get(0));
        System.out.println(value[0]);//原數(shù)組不會變化
        }
}

運行結(jié)果:


原子操作數(shù)組
注意:

原子操作只會操作原子類的值,不會操作原數(shù)組猪叙,原子操作類的值再怎么變也不會影響原數(shù)組的值

運用原子操作類修改兩個變量的值
import java.util.concurrent.atomic.AtomicReference;

/**
 *類說明:演示引用類型的原子操作類
 */
public class UseAtomicReference {
    static AtomicReference<UserInfo> atomicUserRef;
    public static void main(String[] args) {
        UserInfo user = new UserInfo("Mark", 15);//要修改的實體的實例
        atomicUserRef = new AtomicReference(user);
        UserInfo updateUser = new UserInfo("Bill",17);
        atomicUserRef.compareAndSet(user,updateUser);

        System.out.println(atomicUserRef.get());
        System.out.println(user);
    }
    
    //定義一個實體類
    static class UserInfo {
        private volatile String name;
        private int age;
        public UserInfo(String name, int age) {
            this.name = name;
            this.age = age;
        }
        public String getName() {
            return name;
        }
        public int getAge() {
            return age;
        }

        @Override
        public String toString() {
            return "UserInfo{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }

}

運行結(jié)果:


AtomicReference

這是運用AtomicReference修改兩個變量的值娇斩,本質(zhì)上是包裝成一個變量仁卷,對這一個變量進行修改。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末犬第,一起剝皮案震驚了整個濱河市锦积,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌歉嗓,老刑警劉巖丰介,帶你破解...
    沈念sama閱讀 218,451評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異鉴分,居然都是意外死亡哮幢,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,172評論 3 394
  • 文/潘曉璐 我一進店門志珍,熙熙樓的掌柜王于貴愁眉苦臉地迎上來橙垢,“玉大人,你說我怎么就攤上這事伦糯」衲常” “怎么了?”我有些...
    開封第一講書人閱讀 164,782評論 0 354
  • 文/不壞的土叔 我叫張陵敛纲,是天一觀的道長喂击。 經(jīng)常有香客問我,道長淤翔,這世上最難降的妖魔是什么翰绊? 我笑而不...
    開封第一講書人閱讀 58,709評論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮办铡,結(jié)果婚禮上辞做,老公的妹妹穿的比我還像新娘琳要。我一直安慰自己寡具,他們只是感情好,可當我...
    茶點故事閱讀 67,733評論 6 392
  • 文/花漫 我一把揭開白布稚补。 她就那樣靜靜地躺著童叠,像睡著了一般。 火紅的嫁衣襯著肌膚如雪课幕。 梳的紋絲不亂的頭發(fā)上厦坛,一...
    開封第一講書人閱讀 51,578評論 1 305
  • 那天,我揣著相機與錄音乍惊,去河邊找鬼杜秸。 笑死,一個胖子當著我的面吹牛润绎,可吹牛的內(nèi)容都是我干的撬碟。 我是一名探鬼主播诞挨,決...
    沈念sama閱讀 40,320評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼呢蛤!你這毒婦竟也來了惶傻?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,241評論 0 276
  • 序言:老撾萬榮一對情侶失蹤其障,失蹤者是張志新(化名)和其女友劉穎银室,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體励翼,經(jīng)...
    沈念sama閱讀 45,686評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡蜈敢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,878評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了汽抚。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片扶认。...
    茶點故事閱讀 39,992評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖殊橙,靈堂內(nèi)的尸體忽然破棺而出辐宾,到底是詐尸還是另有隱情,我是刑警寧澤膨蛮,帶...
    沈念sama閱讀 35,715評論 5 346
  • 正文 年R本政府宣布叠纹,位于F島的核電站,受9級特大地震影響敞葛,放射性物質(zhì)發(fā)生泄漏誉察。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,336評論 3 330
  • 文/蒙蒙 一惹谐、第九天 我趴在偏房一處隱蔽的房頂上張望持偏。 院中可真熱鬧,春花似錦氨肌、人聲如沸鸿秆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,912評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽卿叽。三九已至,卻和暖如春恳守,著一層夾襖步出監(jiān)牢的瞬間考婴,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,040評論 1 270
  • 我被黑心中介騙來泰國打工催烘, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留沥阱,地道東北人。 一個月前我還...
    沈念sama閱讀 48,173評論 3 370
  • 正文 我出身青樓伊群,卻偏偏與公主長得像考杉,于是被迫代替她去往敵國和親屁使。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,947評論 2 355

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