Java中atomic包中的原子操作類總結(jié)

原創(chuàng)文章&經(jīng)驗(yàn)總結(jié)&從校招到A廠一路陽光一路滄桑

詳情請戳www.codercc.com

image

1. 原子操作類介紹

在并發(fā)編程中很容易出現(xiàn)并發(fā)安全的問題亡笑,有一個很簡單的例子就是多線程更新變量i=1,比如多個線程執(zhí)行i++操作,就有可能獲取不到正確的值,而這個問題靶瘸,最常用的方法是通過Synchronized進(jìn)行控制來達(dá)到線程安全的目的(關(guān)于synchronized可以看這篇文章)咬腋。但是由于synchronized是采用的是悲觀鎖策略吸申,并不是特別高效的一種解決方案两嘴。實(shí)際上,在J.U.C下的atomic包提供了一系列的操作簡單届慈,性能高效徒溪,并能保證線程安全的類去更新基本類型變量忿偷,數(shù)組元素,引用類型以及更新對象中的字段類型臊泌。atomic包下的這些類都是采用的是樂觀鎖策略去原子更新數(shù)據(jù)鲤桥,在java中則是使用CAS操作具體實(shí)現(xiàn)。

2. 預(yù)備知識--CAS操作

能夠弄懂a(chǎn)tomic包下這些原子操作類的實(shí)現(xiàn)原理缺虐,就要先明白什么是CAS操作。

什么是CAS?

使用鎖時礁凡,線程獲取鎖是一種悲觀鎖策略高氮,即假設(shè)每一次執(zhí)行臨界區(qū)代碼都會產(chǎn)生沖突,所以當(dāng)前線程獲取到鎖的時候同時也會阻塞其他線程獲取該鎖顷牌。而CAS操作(又稱為無鎖操作)是一種樂觀鎖策略剪芍,它假設(shè)所有線程訪問共享資源的時候不會出現(xiàn)沖突,既然不會出現(xiàn)沖突自然而然就不會阻塞其他線程的操作窟蓝。因此罪裹,線程就不會出現(xiàn)阻塞停頓的狀態(tài)。那么运挫,如果出現(xiàn)沖突了怎么辦状共?無鎖操作是使用CAS(compare and swap)又叫做比較交換來鑒別線程是否出現(xiàn)沖突,出現(xiàn)沖突就重試當(dāng)前操作直到?jīng)]有沖突為止谁帕。

CAS的操作過程

CAS比較交換的過程可以通俗的理解為CAS(V,O,N)峡继,包含三個值分別為:V 內(nèi)存地址存放的實(shí)際值;O 預(yù)期的值(舊值)匈挖;N 更新的新值碾牌。當(dāng)V和O相同時,也就是說舊值和內(nèi)存中實(shí)際的值相同表明該值沒有被其他線程更改過儡循,即該舊值O就是目前來說最新的值了舶吗,自然而然可以將新值N賦值給V。反之择膝,V和O不相同誓琼,表明該值已經(jīng)被其他線程改過了則該舊值O不是最新版本的值了,所以不能將新值N賦給V肴捉,返回V即可踊赠。當(dāng)多個線程使用CAS操作一個變量是,只有一個線程會成功每庆,并成功更新筐带,其余會失敗。失敗的線程會重新嘗試缤灵,當(dāng)然也可以選擇掛起線程

CAS的實(shí)現(xiàn)需要硬件指令集的支撐伦籍,在JDK1.5后虛擬機(jī)才可以使用處理器提供的CMPXCHG指令實(shí)現(xiàn)蓝晒。

Synchronized VS CAS

元老級的Synchronized(未優(yōu)化前)最主要的問題是:在存在線程競爭的情況下會出現(xiàn)線程阻塞和喚醒鎖帶來的性能問題,因?yàn)檫@是一種互斥同步(阻塞同步)帖鸦。而CAS并不是武斷的間線程掛起斥滤,當(dāng)CAS操作失敗后會進(jìn)行一定的嘗試,而非進(jìn)行耗時的掛起喚醒的操作杆故,因此也叫做非阻塞同步舱沧。這是兩者主要的區(qū)別。

CAS的問題

  1. ABA問題
    因?yàn)镃AS會檢查舊值有沒有變化攻锰,這里存在這樣一個有意思的問題晾嘶。比如一個舊值A(chǔ)變?yōu)榱顺葿,然后再變成A娶吞,剛好在做CAS時檢查發(fā)現(xiàn)舊值并沒有變化依然為A垒迂,但是實(shí)際上的確發(fā)生了變化。解決方案可以沿襲數(shù)據(jù)庫中常用的樂觀鎖方式妒蛇,添加一個版本號可以解決机断。原來的變化路徑A->B->A就變成了1A->2B->3C。

  2. 自旋時間過長

使用CAS時非阻塞同步绣夺,也就是說不會將線程掛起吏奸,會自旋(無非就是一個死循環(huán))進(jìn)行下一次嘗試,如果這里自旋時間過長對性能是很大的消耗陶耍。如果JVM能支持處理器提供的pause指令苦丁,那么在效率上會有一定的提升。

3. 原子更新基本類型

atomic包提高原子更新基本類型的工具類物臂,主要有這些:

  1. AtomicBoolean:以原子更新的方式更新boolean旺拉;
  2. AtomicInteger:以原子更新的方式更新Integer;
  3. AtomicLong:以原子更新的方式更新Long;

這幾個類的用法基本一致棵磷,這里以AtomicInteger為例總結(jié)常用的方法

  1. addAndGet(int delta) :以原子方式將輸入的數(shù)值與實(shí)例中原本的值相加蛾狗,并返回最后的結(jié)果;
  2. incrementAndGet() :以原子的方式將實(shí)例中的原值進(jìn)行加1操作仪媒,并返回最終相加后的結(jié)果沉桌;
  3. getAndSet(int newValue):將實(shí)例中的值更新為新值,并返回舊值算吩;
  4. getAndIncrement():以原子的方式將實(shí)例中的原值加1留凭,返回的是自增前的舊值;

還有一些方法偎巢,可以查看API蔼夜,不再贅述。為了能夠弄懂AtomicInteger的實(shí)現(xiàn)原理压昼,以getAndIncrement方法為例求冷,來看下源碼:

public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

可以看出瘤运,該方法實(shí)際上是調(diào)用了unsafe實(shí)例的getAndAddInt方法,unsafe實(shí)例的獲取時通過UnSafe類的靜態(tài)方法getUnsafe獲冉程狻:

private static final Unsafe unsafe = Unsafe.getUnsafe();

Unsafe類在sun.misc包下拯坟,Unsafer類提供了一些底層操作,atomic包下的原子操作類的也主要是通過Unsafe類提供的compareAndSwapInt韭山,compareAndSwapLong等一系列提供CAS操作的方法來進(jìn)行實(shí)現(xiàn)郁季。下面用一個簡單的例子來說明AtomicInteger的用法:

public class AtomicDemo {
    private static AtomicInteger atomicInteger = new AtomicInteger(1);

    public static void main(String[] args) {
        System.out.println(atomicInteger.getAndIncrement());
        System.out.println(atomicInteger.get());
    }
}
輸出結(jié)果:
1
2

例子很簡單,就是新建了一個atomicInteger對象钱磅,而atomicInteger的構(gòu)造方法也就是傳入一個基本類型數(shù)據(jù)即可梦裂,對其進(jìn)行了封裝。對基本變量的操作比如自增续搀,自減塞琼,相加菠净,更新等操作禁舷,atomicInteger也提供了相應(yīng)的方法進(jìn)行這些操作。但是毅往,因?yàn)閍tomicInteger借助了UnSafe提供的CAS操作能夠保證數(shù)據(jù)更新的時候是線程安全的牵咙,并且由于CAS是采用樂觀鎖策略,因此攀唯,這種數(shù)據(jù)更新的方法也具有高效性洁桌。

AtomicLong的實(shí)現(xiàn)原理和AtomicInteger一致,只不過一個針對的是long變量侯嘀,一個針對的是int變量另凌。而boolean變量的更新類AtomicBoolean類是怎樣實(shí)現(xiàn)更新的呢?核心方法是compareAndSett方法,其源碼如下:

public final boolean compareAndSet(boolean expect, boolean update) {
    int e = expect ? 1 : 0;
    int u = update ? 1 : 0;
    return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}

可以看出戒幔,compareAndSet方法的實(shí)際上也是先轉(zhuǎn)換成0,1的整型變量吠谢,然后是通過針對int型變量的原子更新方法compareAndSwapInt來實(shí)現(xiàn)的∈ィ可以看出atomic包中只提供了對boolean,int ,long這三種基本類型的原子更新的方法工坊,參考對boolean更新的方式,原子更新char,doule,float也可以采用類似的思路進(jìn)行實(shí)現(xiàn)敢订。

4. 原子更新數(shù)組類型

atomic包下提供能原子更新數(shù)組中元素的類有:

  1. AtomicIntegerArray:原子更新整型數(shù)組中的元素王污;
  2. AtomicLongArray:原子更新長整型數(shù)組中的元素;
  3. AtomicReferenceArray:原子更新引用類型數(shù)組中的元素

這幾個類的用法一致楚午,就以AtomicIntegerArray來總結(jié)下常用的方法:

  1. addAndGet(int i, int delta):以原子更新的方式將數(shù)組中索引為i的元素與輸入值相加昭齐;
  2. getAndIncrement(int i):以原子更新的方式將數(shù)組中索引為i的元素自增加1;
  3. compareAndSet(int i, int expect, int update):將數(shù)組中索引為i的位置的元素進(jìn)行更新

可以看出矾柜,AtomicIntegerArray與AtomicInteger的方法基本一致司浪,只不過在AtomicIntegerArray的方法中會多一個指定數(shù)組索引位i泊业。下面舉一個簡單的例子:

public class AtomicDemo {
    //    private static AtomicInteger atomicInteger = new AtomicInteger(1);
    private static int[] value = new int[]{1, 2, 3};
    private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);

    public static void main(String[] args) {
        //對數(shù)組中索引為1的位置的元素加5
        int result = integerArray.getAndAdd(1, 5);
        System.out.println(integerArray.get(1));
        System.out.println(result);
    }
}
輸出結(jié)果:
7
2

通過getAndAdd方法將位置為1的元素加5,從結(jié)果可以看出索引為1的元素變成了7啊易,該方法返回的也是相加之前的數(shù)為2吁伺。

5. 原子更新引用類型

如果需要原子更新引用類型變量的話,為了保證線程安全租谈,atomic也提供了相關(guān)的類:

  1. AtomicReference:原子更新引用類型篮奄;
  2. AtomicReferenceFieldUpdater:原子更新引用類型里的字段;
  3. AtomicMarkableReference:原子更新帶有標(biāo)記位的引用類型割去;

這幾個類的使用方法也是基本一樣的窟却,以AtomicReference為例,來說明這些類的基本用法呻逆。下面是一個demo

public class AtomicDemo {

    private static AtomicReference<User> reference = new AtomicReference<>();

    public static void main(String[] args) {
        User user1 = new User("a", 1);
        reference.set(user1);
        User user2 = new User("b",2);
        User user = reference.getAndSet(user2);
        System.out.println(user);
        System.out.println(reference.get());
    }

    static class User {
        private String userName;
        private int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

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

輸出結(jié)果:
User{userName='a', age=1}
User{userName='b', age=2}

首先將對象User1用AtomicReference進(jìn)行封裝夸赫,然后調(diào)用getAndSet方法,從結(jié)果可以看出咖城,該方法會原子更新引用的user對象茬腿,變?yōu)?code>User{userName='b', age=2},返回的是原來的user對象User{userName='a', age=1}宜雀。

6. 原子更新字段類型

如果需要更新對象的某個字段切平,并在多線程的情況下,能夠保證線程安全辐董,atomic同樣也提供了相應(yīng)的原子操作類:

  1. AtomicIntegeFieldUpdater:原子更新整型字段類悴品;
  2. AtomicLongFieldUpdater:原子更新長整型字段類;
  3. AtomicStampedReference:原子更新引用類型简烘,這種更新方式會帶有版本號苔严。而為什么在更新的時候會帶有版本號,是為了解決CAS的ABA問題孤澎;

要想使用原子更新字段需要兩步操作:

  1. 原子更新字段類都是抽象類届氢,只能通過靜態(tài)方法newUpdater來創(chuàng)建一個更新器,并且需要設(shè)置想要更新的類和屬性亥至;
  2. 更新類的屬性必須使用public volatile進(jìn)行修飾悼沈;

這幾個類提供的方法基本一致,以AtomicIntegerFieldUpdater為例來看看具體的使用:

public class AtomicDemo {

    private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
    public static void main(String[] args) {
        User user = new User("a", 1);
        int oldValue = updater.getAndAdd(user, 5);
        System.out.println(oldValue);
        System.out.println(updater.get(user));
    }

    static class User {
        private String userName;
        public volatile int age;

        public User(String userName, int age) {
            this.userName = userName;
            this.age = age;
        }

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

輸出結(jié)果:
1
6

從示例中可以看出姐扮,創(chuàng)建AtomicIntegerFieldUpdater是通過它提供的靜態(tài)方法進(jìn)行創(chuàng)建絮供,getAndAdd方法會將指定的字段加上輸入的值,并且返回相加之前的值茶敏。user對象中age字段原值為1壤靶,加5之后,可以看出user對象中的age字段的值已經(jīng)變成了6惊搏。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末贮乳,一起剝皮案震驚了整個濱河市忧换,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌向拆,老刑警劉巖亚茬,帶你破解...
    沈念sama閱讀 212,718評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異浓恳,居然都是意外死亡刹缝,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,683評論 3 385
  • 文/潘曉璐 我一進(jìn)店門颈将,熙熙樓的掌柜王于貴愁眉苦臉地迎上來梢夯,“玉大人,你說我怎么就攤上這事晴圾∷淘遥” “怎么了?”我有些...
    開封第一講書人閱讀 158,207評論 0 348
  • 文/不壞的土叔 我叫張陵死姚,是天一觀的道長人乓。 經(jīng)常有香客問我,道長知允,這世上最難降的妖魔是什么撒蟀? 我笑而不...
    開封第一講書人閱讀 56,755評論 1 284
  • 正文 為了忘掉前任叙谨,我火速辦了婚禮温鸽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘手负。我一直安慰自己涤垫,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,862評論 6 386
  • 文/花漫 我一把揭開白布竟终。 她就那樣靜靜地躺著蝠猬,像睡著了一般。 火紅的嫁衣襯著肌膚如雪统捶。 梳的紋絲不亂的頭發(fā)上榆芦,一...
    開封第一講書人閱讀 50,050評論 1 291
  • 那天,我揣著相機(jī)與錄音喘鸟,去河邊找鬼匆绣。 笑死,一個胖子當(dāng)著我的面吹牛什黑,可吹牛的內(nèi)容都是我干的崎淳。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼愕把,長吁一口氣:“原來是場噩夢啊……” “哼拣凹!你這毒婦竟也來了森爽?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤嚣镜,失蹤者是張志新(化名)和其女友劉穎爬迟,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體菊匿,經(jīng)...
    沈念sama閱讀 44,330評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡雕旨,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,651評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了捧请。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片凡涩。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖疹蛉,靈堂內(nèi)的尸體忽然破棺而出活箕,到底是詐尸還是另有隱情,我是刑警寧澤可款,帶...
    沈念sama閱讀 34,477評論 4 333
  • 正文 年R本政府宣布育韩,位于F島的核電站,受9級特大地震影響闺鲸,放射性物質(zhì)發(fā)生泄漏筋讨。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,135評論 3 317
  • 文/蒙蒙 一摸恍、第九天 我趴在偏房一處隱蔽的房頂上張望悉罕。 院中可真熱鬧,春花似錦立镶、人聲如沸壁袄。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,864評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽嗜逻。三九已至,卻和暖如春缭召,著一層夾襖步出監(jiān)牢的瞬間栈顷,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 32,099評論 1 267
  • 我被黑心中介騙來泰國打工嵌巷, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留萄凤,地道東北人。 一個月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓晴竞,卻偏偏與公主長得像蛙卤,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

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