9. 線程安全之原子操作

前言:上一節(jié)學(xué)習(xí)了JMM国裳、Happen Before鞠苟、可見性等等這種概念,基本都是來源于JDK的官方網(wǎng)站中熔酷,上面有所說明了孤紧,能夠追根溯源才能夠跟上技術(shù)演進(jìn)。

9.0 來自JDK官方的多線程描述

JDK官方對于多線程相關(guān)理論的說明:

https://docs.oracle.com/javase/tutorial/essential/concurrency/index.html

里面有介紹同步關(guān)鍵字拒秘、原子性号显、死鎖等等概念。(源于官方才是原汁原味)

9.1 原子性的引入

9.1.1 多線程引起的問題

下面跟上節(jié)一樣躺酒,我們先用一個簡單的程序來說明押蚤,并發(fā)產(chǎn)生的問題

package szu.vander.lock;

import java.util.concurrent.TimeUnit;

/**
 * @author : Vander
 * @date :   2019/08/7
 * @description :
 */
public class WrongLockDemo {

    volatile int i = 0;

    public void add() {
        i++;
    }

    public static void main(String[] args) throws InterruptedException {
        WrongLockDemo lockDemo = new WrongLockDemo();

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    lockDemo.add();
                }
            }).start();
        }
        // 讓主線程Sleep 2秒,保證有足夠的時間運行完
        TimeUnit.SECONDS.sleep(2);
        System.out.println(lockDemo.i);
    }

}

運行結(jié)果:發(fā)現(xiàn)并不是等于20000的羹应,而且遠(yuǎn)遠(yuǎn)不夠

我們先來簡單分析一下揽碘,首先i是加了volatile的,從上一節(jié)學(xué)習(xí)中知道了,加了此關(guān)鍵字能夠保證讀取的時候是主內(nèi)存的值雳刺,所以線程1對i進(jìn)行了加1操作肯定能被線程2發(fā)現(xiàn)的劫灶。第二個就是與i相關(guān)的操作不會進(jìn)行重排序。那么此處究竟是什么導(dǎo)致了沒加成功呢掖桦。

9.1.2 解析源碼

我們可以使用javap -v反編碼WrongLockDemo.class


我們發(fā)現(xiàn)i++其實是由好幾個步驟組成的本昏,首先是獲取到i的值,然后跟變量1相加枪汪,在把相加后的結(jié)果放回去涌穆。
說白了就是三個步驟:
1)加載i
2)執(zhí)行+1
3)賦值i
所以就會出現(xiàn)以下的情況,導(dǎo)致最后累加的結(jié)果不正確:

9.1.3 相關(guān)概念

線程安全
當(dāng)多個線程訪問某個類時雀久,這個類始終都能表現(xiàn)出正確的行為蒲犬,那么就稱這個類是線程安全的。(說白了就是在多線程的情況下岸啡,能得到你想要的原叮。)

競態(tài)條件與臨界區(qū)

多個線程訪問了相同的資源,向這些資源做了寫操作時巡蘸,對執(zhí)行順序有要求奋隶。
臨界區(qū):incr方法內(nèi)部就是臨界區(qū)域,關(guān)鍵部分代碼的多線程并發(fā)執(zhí)行悦荒,會對執(zhí)行結(jié)果產(chǎn)生影響唯欣。(簡單的說,就是某個方法在單線程運行沒問題搬味,多線程運行會有問題境氢,而這個方法就是臨界區(qū))
競態(tài)條件:可能發(fā)生在臨界區(qū)域內(nèi)的特殊條件。多線程執(zhí)行incr方法中的i++關(guān)鍵代碼時碰纬,產(chǎn)生了競態(tài)條件萍聊。(引發(fā)關(guān)鍵問題的關(guān)鍵代碼)

共享資源
如果一段代碼是線程安全的,則它不包含競態(tài)條件悦析。只有當(dāng)多個線程更新共享資源時寿桨,才會發(fā)生競態(tài)條件。
棧封閉時强戴,不會在線程之間共享的變量亭螟,都是線程安全的。
局部對象引用本身不共享骑歹,但是引用的對象存儲在共享堆中预烙。如果方法內(nèi)創(chuàng)建的對象,只是在方法中傳遞道媚,并且不對其他線程可用扁掸,那么也是線程安全的欢嘿。
局部變量只能由一個線程執(zhí)行,局部變量是存放在線程棧的棧幀里的也糊,不存在變量共享的問題炼蹦,所以不會有資源競爭的可能。

/** 
 * 像以下代碼也是線程安全的
 */
public vold someMethod() {
      LocalObject localObject = new LocalObject();
      localOblect.callMethod();
      method2(localObject);
}
public void method2(LocalObject localObject){
    localObject.setValue("value");
}

判定資源是否線程安全的規(guī)則:如果創(chuàng)建狸剃、使用和處理資源掐隐,永遠(yuǎn)不會逃脫單個線程的控制,該資源的使用時線程安全的钞馁。

不可變對象

public class Demo {
    private int value = 0;
    
    public Demo(int value){
        this.value = value;
    }
    
    public int getValue(){
        return this.value;
    }
    
}

以上代碼沒有提供set方法虑省,一旦構(gòu)造完成,該對象中的value屬性就不會再改變僧凰,這種變量稱為不可變對象探颈。
創(chuàng)建不可變的共享對象來保證對象在線程間共享時不會被修改,從而實現(xiàn)線程安全训措。實例被創(chuàng)建伪节,value變量就不能再被修改,這就是不可變性绩鸣。

原子操作的定義
原子操作可以是一個步驟怀大,也可以是多個操作步驟,但是其順序不可以被打亂呀闻,也不可以被切割而只執(zhí)行其中的一部分(不可中斷性)
將整個操作視為一個整體化借,資源在該次操作中保持一致,這是原子性的核心特征捡多。
上述的incr()方法中的i++蓖康,實際上執(zhí)行的是三個步驟:1)加載 2)計算 3)賦值
也就是說,這三個步驟是不可中斷的垒手,否則原子操作就不成立了蒜焊。

9.2 原子性的實現(xiàn)方式

9.2.1 硬件同步原語—Unsafe類

CAS機(jī)制
Compare and swap比較和替換,屬于硬件同步原語淫奔,處理器提供了基本內(nèi)存操作的原子性保證山涡。CAS操作需要輸入兩個數(shù)值堤结,一個舊值A(chǔ)(期望操作前的值)和一個新值B唆迁,在操作
期間先比較舊值有沒有發(fā)生變化,如果沒有發(fā)生變化竞穷,才交換成新值唐责,發(fā)生了變化則不交換。
sun.mise.Unsafe
Java中的sun.mise.Unsafe類瘾带,提供了compareAndSwapInt()和compareAndSwapLong()等幾個方法實現(xiàn)CAS鼠哥。

在硬件底層中,對同一個內(nèi)存地址同一時刻只能有一個線程去修改,假設(shè)線程1,2都先讀取到了A=1朴恳,然后線程1先去修改這個內(nèi)存的值抄罕,改成功了,然后線程B也來改成2于颖,結(jié)果發(fā)現(xiàn)原來的值已經(jīng)改變了呆贿,所以不進(jìn)行+1操作了。

示例:使用Unsafe硬件原語實現(xiàn)自增的原子性

package szu.vander.atomicity;

import sun.misc.Unsafe;

import java.lang.reflect.Field;
import java.util.concurrent.TimeUnit;

/**
 * @author : Vander
 * @date :   2019/11/20
 * @description : Unsafe中的方法都是本地方法森渐,均由C實現(xiàn)
 */
public class UnsafeLockDemo {

    volatile int num;

    private static Unsafe unsafe;

    private static long valueOffset;// 屬性偏移量做入,用于JVM去定位屬性在內(nèi)存中的地址

    static {
        try {
            Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
            theUnsafe.setAccessible(true);
            unsafe = (Unsafe) theUnsafe.get(null);

            // CAS 硬件原語 ---java語言無法直接改內(nèi)存,曲線通過對象及屬性的定位方式
            valueOffset = unsafe.objectFieldOffset(UnsafeLockDemo.class.getDeclaredField("num"));
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public void add() {
        boolean result;
        do {
            // 1)獲取當(dāng)前值
            int currentNum = unsafe.getIntVolatile(this, valueOffset);
            // 2)計算值
            int nextNum = currentNum + 1;
            // 3)寫入值同衣,若num的值被其它線程修改了竟块,則操作不成功
            result = unsafe.compareAndSwapInt(this, valueOffset, currentNum, nextNum);
        } while (!result);
    }

    public static void main(String[] args) throws InterruptedException {
        UnsafeLockDemo unsafeLockDemo = new UnsafeLockDemo();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                // 每個線程循環(huán)加1w次
                for (int temp = 0; temp < 10000; temp++) {
                    unsafeLockDemo.add();
                }
            }).start();
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println("累加后的結(jié)果:" + unsafeLockDemo.num);
    }

}

執(zhí)行效果:

9.2.2 JDK提供的java.util.concurrent

針對原子類的實現(xiàn)i++的方式,同一時刻只有一個線程能加成功耐齐,其它的線程都失敗浪秘,這樣必定會造成CPU資源的損耗和浪費,JDK1.8又提供了LongAdder等專門用于計數(shù)的類埠况。

J.U.C包內(nèi)的原子操作封裝類




JDK1.8后又進(jìn)行了部分更新:
更新器:DoubleAccumulator秫逝、LongAccumulator
計數(shù)器:DoubleAdder、LongAdder
計數(shù)器增強(qiáng)版询枚,高井發(fā)下性能更好
基本原理:頻繁更新但不太頻繁讀取的匯總統(tǒng)計信息時违帆,使用分成多個操作單元,不同線程更新不同的單元金蜀。只有需要匯總的時候才計算所有單元的操作

使用原子類實現(xiàn)累加

package szu.vander.atomicity;

import java.sql.Time;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author : caiwj
 * @date :   2019/11/20
 * @description : 原子遞增類的使用
 */
public class AtomicAdder {

    AtomicInteger num = new AtomicInteger(0);

    public void add() {
        num.incrementAndGet();
    }

    public static void main(String[] args) throws InterruptedException {
        AtomicAdder atomicAdder = new AtomicAdder();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10_000; j++) {
                    atomicAdder.add();
                }
            }).start();
        }
        TimeUnit.SECONDS.sleep(1);
        System.out.println(atomicAdder.num.get());
    }

}

9.2.2 性能比較

下面是三種加的方式進(jìn)行比較:Synchronize刷后、AtomicLong、LongAdder進(jìn)行性能比較

package szu.vander.atomicity;

import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

/**
 * @author : Vander
 * @date :   2019/11/20
 * @description : 測試用例: 同時運行2秒渊抄,檢查誰的次數(shù)最多
 */
public class CompareAdder {
    private long syncCount = 0;

    /**
     * 同步代碼塊的方式
     */
    public void testSync() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
                    synchronized (this) {
                        ++syncCount;
                    }
                }
                long endTime = System.currentTimeMillis();
                System.out.println("SyncThread spend:" + (endTime - startTime) + "ms" + " count:" + syncCount);
            }).start();
        }
    }

    private AtomicLong atomicLongCount = new AtomicLong(0L);

    /**
     * Atomic方式
     */
    public void testAtomic() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
                    atomicLongCount.incrementAndGet();
                }
                long endTime = System.currentTimeMillis();
                System.out.println("AtomicThread spend:" + (endTime - startTime) + "ms" + " count:" + atomicLongCount.incrementAndGet());
            }).start();
        }
    }


    private LongAdder longAdderCount = new LongAdder();

    /**
     * LongAdder 方式
     */
    public void testLongAdder() {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long startTime = System.currentTimeMillis();
                while (System.currentTimeMillis() - startTime < 2000) { // 運行兩秒
                    longAdderCount.increment();
                }
                long endTime = System.currentTimeMillis();
                System.out.println("LongAdderThread spend:" + (endTime - startTime) + "ms" + " count:" + longAdderCount.sum());
            }).start();
        }
    }

    public static void main(String[] args) {
        CompareAdder demo = new CompareAdder();
        demo.testSync();
        demo.testAtomic();
        demo.testLongAdder();
    }
}

執(zhí)行結(jié)果:

可以發(fā)現(xiàn)JDK8新實現(xiàn)的累加器確實提高了接近一倍的性能尝胆,而原子類又會比同步關(guān)鍵字操作的累加性能提升五倍。

LongAdder實現(xiàn)思路:

思路就是不讓多個線程操作同一個變量护桦,作累加操作含衔,線程1加了X次,線程2加了y次二庵,線程3加了z次贪染,最后通過sum方法來讀取這些線程累加起來的值。
這種思路是分而治之的思路催享,不同的線程只Add屬于它自己的變量杭隙,最后通過sum累加起來。這就類似于高并發(fā)的時候因妙,使用集群來分擔(dān)壓力痰憎。

9.3 CAS機(jī)制的局限性

CAS的三個問題
1)循環(huán)+CAS票髓,自旋的實現(xiàn)讓所有線程都處于高頻運行,爭搶CPU執(zhí)行時間的狀態(tài)铣耘。如果操作長時間不成功洽沟,會帶來很大的CPU資源消耗。
2)僅針對單個變量的操作蜗细,不能用于多個變量來實現(xiàn)原子操作玲躯。
3)ABA問題。(無法體現(xiàn)出數(shù)據(jù)的變動)
針對第一點鳄乏,CAS操作適用于一些耗時較短的操作跷车,不然長時間的不成功會導(dǎo)致CPU壓力巨大,CAS實際上是使用自旋鎖來實現(xiàn)的橱野。

ABA問題

所謂的ABA問題朽缴,其實影響并不大,即線程一先修改了i的值水援,然后線程二又將值改回來密强,線程三來讀取的時候就發(fā)現(xiàn)值沒有變化,然后線程三繼續(xù)進(jìn)行操作蜗元。如果要避免這種情況或渤,只需要在每次修改都增加一個修改次數(shù)的標(biāo)識即可。

其它:
Unsafe類是沒有注釋的奕扣,要看到更詳細(xì)的需要看OpenJDK薪鹦。
OpenJDK官方網(wǎng)站:OpenJDK.java.net

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市惯豆,隨后出現(xiàn)的幾起案子池磁,更是在濱河造成了極大的恐慌,老刑警劉巖楷兽,帶你破解...
    沈念sama閱讀 222,104評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件地熄,死亡現(xiàn)場離奇詭異,居然都是意外死亡芯杀,警方通過查閱死者的電腦和手機(jī)端考,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,816評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來揭厚,“玉大人却特,你說我怎么就攤上這事∑迕郑” “怎么了核偿?”我有些...
    開封第一講書人閱讀 168,697評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長顽染。 經(jīng)常有香客問我漾岳,道長,這世上最難降的妖魔是什么粉寞? 我笑而不...
    開封第一講書人閱讀 59,836評論 1 298
  • 正文 為了忘掉前任尼荆,我火速辦了婚禮,結(jié)果婚禮上唧垦,老公的妹妹穿的比我還像新娘捅儒。我一直安慰自己,他們只是感情好振亮,可當(dāng)我...
    茶點故事閱讀 68,851評論 6 397
  • 文/花漫 我一把揭開白布巧还。 她就那樣靜靜地躺著,像睡著了一般坊秸。 火紅的嫁衣襯著肌膚如雪麸祷。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,441評論 1 310
  • 那天褒搔,我揣著相機(jī)與錄音阶牍,去河邊找鬼。 笑死星瘾,一個胖子當(dāng)著我的面吹牛走孽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播琳状,決...
    沈念sama閱讀 40,992評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼磕瓷,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了念逞?” 一聲冷哼從身側(cè)響起生宛,我...
    開封第一講書人閱讀 39,899評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎肮柜,沒想到半個月后陷舅,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,457評論 1 318
  • 正文 獨居荒郊野嶺守林人離奇死亡审洞,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 38,529評論 3 341
  • 正文 我和宋清朗相戀三年莱睁,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片芒澜。...
    茶點故事閱讀 40,664評論 1 352
  • 序言:一個原本活蹦亂跳的男人離奇死亡仰剿,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出痴晦,到底是詐尸還是另有隱情南吮,我是刑警寧澤,帶...
    沈念sama閱讀 36,346評論 5 350
  • 正文 年R本政府宣布誊酌,位于F島的核電站部凑,受9級特大地震影響露乏,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜涂邀,卻給世界環(huán)境...
    茶點故事閱讀 42,025評論 3 334
  • 文/蒙蒙 一瘟仿、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧比勉,春花似錦劳较、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,511評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至衣洁,卻和暖如春墓捻,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背闸与。 一陣腳步聲響...
    開封第一講書人閱讀 33,611評論 1 272
  • 我被黑心中介騙來泰國打工毙替, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人践樱。 一個月前我還...
    沈念sama閱讀 49,081評論 3 377
  • 正文 我出身青樓厂画,卻偏偏與公主長得像,于是被迫代替她去往敵國和親拷邢。 傳聞我的和親對象是個殘疾皇子袱院,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 45,675評論 2 359

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