Java線程安全如何進(jìn)行原子操作袄秩,一致性的最佳實(shí)踐(九)

上次主要說了一個(gè)結(jié)論就是volatile,線程安全可見性的問題逢并,大部分情況下可見性都不需要管理的之剧,但是多線程編程的代碼中,我們會使用到volatile關(guān)鍵字砍聊,通過volatile關(guān)鍵字解決可見性問題背稼,一個(gè)線程對共享變量的修改,能夠及時(shí)的被其他線程看到玻蝌。只要加了volatile關(guān)鍵字蟹肘,所有對變量的讀取立刻進(jìn)行同步。volatile關(guān)鍵字的用途:禁止緩存俯树;相關(guān)的變量不做重排序帘腹。

(一)線程安全

  • ① 介紹

線程安全是多線程編程時(shí)的計(jì)算機(jī)程序代碼中的一個(gè)概念。當(dāng)多個(gè)線程訪問某個(gè)方法時(shí)许饿,不管你通過怎樣的調(diào)用方式或者說這些線程如何交替的執(zhí)行阳欲,我們在主程序中不需要去做任何的同步,這個(gè)類的結(jié)果行為都是我們設(shè)想的正確行為,那么我們就可以說這個(gè)類是線程安全的胸完。

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

多線程訪問了相同的資源,向這些資源做了寫操作時(shí)翘贮,對執(zhí)行順序有要求赊窥。

臨界區(qū)

incr 方法內(nèi)部就是臨界區(qū)域,關(guān)鍵部分代碼的多線程并發(fā)執(zhí)行狸页,對會執(zhí)行結(jié)果產(chǎn)生影響锨能,下面的代碼就屬于臨界區(qū)。不見得就有一行代碼芍耘,只要對多線程并發(fā)有影響的都叫臨界區(qū)址遇。

int i = 0;
i =i +1;
x = i 

競態(tài)條件

可能發(fā)生在臨界區(qū)域內(nèi)的特殊條件。觸發(fā)線程安全的環(huán)境斋竞。 上邊的代碼 x = i 就是競態(tài)條件倔约。

  • ③ 問題代碼

多線程情況下,預(yù)期打印20000坝初,但是打印了13914浸剩。

public class LockDemo {

     volatile int i = 0;


    public void add() {
        // TODO xx00
         i++;// 三個(gè)步驟
    }

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

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

(二)共享資源

  1. 如果一段代碼是線程安全的,則它不包含競態(tài)條件鳄袍,只有當(dāng)多線程更新共享資源時(shí)绢要,才會發(fā)生競態(tài)條件。
  2. 棧封閉時(shí)拗小,不會在線程之間共享的變量重罪,都是線程安全的。
  3. 局部對象引用對象不共享哀九,但是引用了對象存儲在共享堆中剿配。如果方法內(nèi)創(chuàng)建的對象,只是在方法中傳遞勾栗,并且不對其他線程可用惨篱,那么也是線程安全的。

判定規(guī)則

如果創(chuàng)建围俘,使用和處理資源砸讳,永遠(yuǎn)不會逃脫單個(gè)線程的控制,該資源的使用線程安全的界牡。

(三)不可變對象

  • ① 實(shí)例
public class Demo{
  private int value = 0;
  public Demo(int value){
    this.value = value;
}
  public int getValue(){
    return this.value 
}
}

方法里面沒有setValue的方法簿寂,這就是不可變的對象。

  • ② 定義

創(chuàng)建不可變的共享對象來保證對象在線程共享時(shí)不會被修改宿亡,從而實(shí)現(xiàn)線程安全常遂。實(shí)例被創(chuàng)建,value變量就不能再被修改挽荠,這就是不可變性克胳。

不可變是相對的平绩,其實(shí)可以通過反射的方式進(jìn)行破壞。

(四)原子操作定義

  • ① 介紹

原子操作可以是一個(gè)步驟漠另,也可以是多個(gè)操作步驟捏雌,但是其順序不可以被打亂,也不可以被分割而只執(zhí)行其中的一部分(不可中斷性)笆搓。

將整個(gè)操作視作一個(gè)整體性湿,資源在該次操作中保持一致,這是原子性的核心特性满败。

  • ② 實(shí)例分析
public class Demo{
  public int i = 0;
   public void incr(){
    i++
  }
}

里面的i++ 底層運(yùn)行分為三步:加載i肤频,計(jì)算+1,賦值i 算墨,在底層被拆分了宵荒。

在多線程需要原子性操作,對修改米同,讀取骇扇,保持一致性。

(五)什么是CAS

  • ① 介紹

compare and swap的縮寫面粮,中文翻譯成比較并交換少孝。屬于硬件同步原語,處理器提供了基本內(nèi)功操作的原子性保證熬苍。CAS操作需要輸入兩個(gè)數(shù)值稍走,一個(gè)舊值A(chǔ)(操作前的值)和一個(gè)新值B,在操作期間先比較下舊值有沒有發(fā)生變化柴底,如果沒有發(fā)生變化婿脸,才交換新值,發(fā)生了變化則不交換柄驻。避免硬件底層出現(xiàn)并發(fā)的操作的可能狐树。

JAVA中的sun,misc.Unsafe類,提供了compareAndSwpInt() 和 compareAndSwpLong() 等幾個(gè)方法實(shí)現(xiàn)CAS鸿脓。

  • ② 演示

Unsafe 是操作c和c++底層來完成的抑钟。

import sun.misc.Unsafe;

import java.lang.reflect.Field;

public class LockDemo {
    volatile int value = 0;

    static Unsafe unsafe; // 直接操作內(nèi)存,修改對象野哭,數(shù)組內(nèi)存....強(qiáng)大的API
    private static long valueOffset;

    static {
        try {
            // 反射技術(shù)獲取unsafe值
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            unsafe = (Unsafe) field.get(null);

            // 獲取到 value 屬性偏移量(用于定于value屬性在內(nèi)存中的具體地址)
            valueOffset = unsafe.objectFieldOffset(LockDemo.class
                    .getDeclaredField("value"));

        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }

    public void add() {
        // TODO xx00
        // i++;// JAVA 層面三個(gè)步驟
        // CAS + 循環(huán) 重試
        int current;
        do {
            // 操作耗時(shí)的話在塔, 那么 線程就會占用大量的CPU執(zhí)行時(shí)間
            current = unsafe.getIntVolatile(this, valueOffset);
        } while (!unsafe.compareAndSwapInt(this, valueOffset, current, current + 1));
        // 可能會失敗
    }

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

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.value);
    }
}

上邊的代碼太高大上了,基本都看不懂吧拨黔,下面說一個(gè)簡單的方式蛔溃。

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

  • ① 介紹

java.util.concurrent(簡稱JUC)包,在此包中增加了在并發(fā)編程中很常用的工具類,用于定義類似于線程的自定義子系統(tǒng)贺待,包括線程池徽曲,異步 IO 和輕量級任務(wù)框架。還提供了設(shè)計(jì)用于多線程上下文中的 Collection 實(shí)現(xiàn)等麸塞。 rt.jar中的其實(shí)原子性疟位,jdk本身都考慮到了,定義了幾種類型喘垂。

  • ② 封裝類

JDK1.8新增的原子性

原有的 Atomic系列類通過CAS來保證并發(fā)時(shí)操作的原子性,但是高并發(fā)也就意味著CAS的失敗次數(shù)會增多绍撞,失敗次數(shù)的增多會引起更多線程的重試正勒,最后導(dǎo)致AtomicLong的效率降低。新的四個(gè)類通過減少并發(fā)傻铣,將單一value的更新壓力分擔(dān)到多個(gè)value中去章贞,降低單個(gè)value的“熱度”以提高高并發(fā)情況下的吞吐量。

DoubleAccumulator
DoubleAdder
LongAccumulator
LongAdder

  • ③ 實(shí)例分析
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @program: dispatch_system
 * @description: ${description}
 * @author: LiMing
 * @create: 2019-10-31 10:57
 **/
public class LockDemo {

    // volatile int i = 0;
    AtomicInteger i = new AtomicInteger(0);


    public void add() {
        // TODO xx00
        // i++;// 三個(gè)步驟
        i.incrementAndGet();
    }

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

        for (int i = 0; i < 2; i++) {
            new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    ld.add();
                }
            }).start();
        }
        Thread.sleep(2000L);
        System.out.println(ld.i);
    }
}

  • ② LongAdder

就是嘗試使用分段CAS以及自動分段遷移的方式來大幅度提升多線程高并發(fā)執(zhí)行CAS操作的性能非洲!

在LongAdder的底層實(shí)現(xiàn)中鸭限,首先有一個(gè)base值,剛開始多線程來不停的累加數(shù)值两踏,都是對base進(jìn)行累加的败京,比如剛開始累加成了base = 5。接著如果發(fā)現(xiàn)并發(fā)更新的線程數(shù)量過多梦染,就會開始施行分段CAS的機(jī)制赡麦,也就是內(nèi)部會搞一個(gè)Cell數(shù)組,每個(gè)數(shù)組是一個(gè)數(shù)值分段帕识。這時(shí)泛粹,讓大量的線程分別去對不同Cell內(nèi)部的value值進(jìn)行CAS累加操作,這樣就把CAS計(jì)算壓力分散到了不同的Cell分段數(shù)值中了肮疗!這樣就可以大幅度的降低多線程并發(fā)更新同一個(gè)數(shù)值時(shí)出現(xiàn)的無限循環(huán)的問題晶姊,大幅度提升了多線程并發(fā)更新數(shù)值的性能和效率!而且內(nèi)部實(shí)現(xiàn)了自動分段遷移的機(jī)制伪货,也就是如果某個(gè)Cell的value執(zhí)行CAS失敗了们衙,那么就會自動去找另外一個(gè)Cell分段內(nèi)的value值進(jìn)行CAS操作。這樣也解決了線程空旋轉(zhuǎn)超歌、自旋不停等待執(zhí)行CAS操作的問題砍艾,讓一個(gè)線程過來執(zhí)行CAS時(shí)可以盡快的完成這個(gè)操作。會把base值和所有Cell分段數(shù)值加起來返回給你巍举。

計(jì)算的時(shí)候很快脆荷,取結(jié)果的是比較慢。這個(gè)思路就類似現(xiàn)在的互聯(lián)網(wǎng)分而治之的思路,量比較大蜓谋,就接很多小的管道梦皮,小管道里面慢慢的去處理,如果直接處理比較的大的比較慢桃焕,就讓小管道慢慢處理剑肯。分而治之的思路。

  • ③ 示例
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.LongAdder;

// 測試用例: 同時(shí)運(yùn)行2秒观堂,檢查誰的次數(shù)最多
public class LongAdderDemo {
    private long count = 0;

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

    // Atomic方式
    private AtomicLong acount = new AtomicLong(0L);

    public void testAtomic() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 運(yùn)行兩秒
                    acount.incrementAndGet(); // acount++;
                }
                long endtime = System.currentTimeMillis();
                System.out.println("AtomicThread spend:" + (endtime - starttime) + "ms" + " v-" + acount.incrementAndGet());
            }).start();
        }
    }

    // LongAdder 方式
    private LongAdder lacount = new LongAdder();
    public void testLongAdder() throws InterruptedException {
        for (int i = 0; i < 3; i++) {
            new Thread(() -> {
                long starttime = System.currentTimeMillis();
                while (System.currentTimeMillis() - starttime < 2000) { // 運(yùn)行兩秒
                    lacount.increment();
                }
                long endtime = System.currentTimeMillis();
                System.out.println("LongAdderThread spend:" + (endtime - starttime) + "ms" + " v-" + lacount.sum());
            }).start();
        }
    }

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

(七)CAS三大問題

  • ① ABA問題

因?yàn)镃AS需要在操作值的時(shí)候让网,檢查值有沒有發(fā)生變化,如果沒有發(fā)生變化則更新师痕,但是如果一個(gè)值原來是A溃睹、變成了B、又變成了A胰坟,那么使用CAS進(jìn)行檢查時(shí)會發(fā)現(xiàn)它的值沒有發(fā)生變化因篇,但實(shí)際上卻變化了。

  • ② 循環(huán)開銷時(shí)間長

自旋CAS如果長時(shí)間不成功笔横,會給CPU帶來非常大的執(zhí)行開銷竞滓。如果jvm能支持處理器提供的pause指令,那么效率會有一定的提升吹缔。pause指令有兩個(gè)作用:

1.它可以延遲流水線執(zhí)行指令(de-pipeline)商佑,使CPU不會消耗過多的執(zhí)行資源,延遲的時(shí)間取決于具體實(shí)現(xiàn)的版本厢塘,在一些處理器上延遲時(shí)間是零莉御。
2.它可以避免在退出循環(huán)的時(shí)候因內(nèi)存順序沖突(Memory Order Violation)而引起CPU流水線被清空(CPU Pipeline Flush),從而提高CPU的執(zhí)行效率俗冻。

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

當(dāng)對一個(gè)共享變量執(zhí)行操作時(shí)礁叔,我們可以使用循環(huán)CAS的方式來保證原子操作,但是多個(gè)共享變量操作時(shí)迄薄,循環(huán)CAS就無法保證操作的原子性琅关,這個(gè)時(shí)候就可以用鎖。還有一個(gè)方法讥蔽,就是把多個(gè)共享變量合并成一個(gè)共享變量來操作涣易。比如,有兩個(gè)共享變量i=2,j=a合并一下ij=2a冶伞,然后用CAS來操作ij新症。從java1.5開始,JDK提供了AtomicReference類來保證引用對象之間的原子性响禽,就可以把多個(gè)變量放在一個(gè)對象里來進(jìn)行CAS操作徒爹。

PS:代碼都是最終的結(jié)果荚醒,這里面涉及的思路很多,JDK已經(jīng)到13了里面的工具越來越多隆嗅。本次主要引用了原子性界阁,數(shù)據(jù)變化,保證數(shù)據(jù)的一致性胖喳,這是個(gè)本質(zhì)泡躯,希望各位老鐵參與評論,大家多交流丽焊。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末较剃,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子技健,更是在濱河造成了極大的恐慌重付,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件凫乖,死亡現(xiàn)場離奇詭異,居然都是意外死亡弓颈,警方通過查閱死者的電腦和手機(jī)帽芽,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來翔冀,“玉大人导街,你說我怎么就攤上這事∠俗樱” “怎么了搬瑰?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵,是天一觀的道長控硼。 經(jīng)常有香客問我泽论,道長,這世上最難降的妖魔是什么卡乾? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任翼悴,我火速辦了婚禮,結(jié)果婚禮上幔妨,老公的妹妹穿的比我還像新娘鹦赎。我一直安慰自己,他們只是感情好误堡,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布古话。 她就那樣靜靜地躺著,像睡著了一般锁施。 火紅的嫁衣襯著肌膚如雪陪踩。 梳的紋絲不亂的頭發(fā)上杖们,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天,我揣著相機(jī)與錄音膊毁,去河邊找鬼胀莹。 笑死,一個(gè)胖子當(dāng)著我的面吹牛婚温,可吹牛的內(nèi)容都是我干的描焰。 我是一名探鬼主播,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼栅螟,長吁一口氣:“原來是場噩夢啊……” “哼荆秦!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起力图,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤步绸,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后吃媒,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體瓤介,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年赘那,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了刑桑。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 39,785評論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡募舟,死狀恐怖祠斧,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情拱礁,我是刑警寧澤琢锋,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布,位于F島的核電站呢灶,受9級特大地震影響吴超,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜鸯乃,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一烛芬、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧飒责,春花似錦赘娄、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至拾并,卻和暖如春揍堰,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背屏歹。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留蝙眶,地道東北人季希。 一個(gè)月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓式塌,卻偏偏與公主長得像,于是被迫代替她去往敵國和親友浸。 傳聞我的和親對象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,713評論 2 354

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