并發(fā)編程之原子性

寫在前面

多線程訪問共享變量的時(shí)候卒茬,很容易出現(xiàn)并發(fā)問題船老。特別是多個(gè)線程對共享變量進(jìn)行寫入的時(shí)候,由于原子性的問題圃酵,很容易導(dǎo)致最后數(shù)據(jù)的錯(cuò)誤努隙。一般來講,我們可以進(jìn)行同步辜昵,同步的方式就是加鎖。另一方面咽斧,jdk也提供了變量的線程隔離方式——ThreadLocal堪置,盡管它的出現(xiàn)并不是為了解決上述的問題。

共享變量

何為共享變量张惹?說到這個(gè)舀锨,我們想一想什么變量不是共享的。在一個(gè)線程調(diào)用一個(gè)方法的時(shí)候宛逗,會(huì)在棧內(nèi)存上為局部變量和方法參數(shù)申請內(nèi)存坎匿,在方法調(diào)用結(jié)束的時(shí)候,這些內(nèi)存會(huì)被釋放雷激。不同的線程調(diào)用同一個(gè)方法都會(huì)為局部變量和方法參數(shù)copy一個(gè)副本替蔬,所以棧內(nèi)存是私有的,也就是說局部變量和方法參數(shù)不是線程共享的屎暇。而堆上的數(shù)組和對象是共享的承桥,堆內(nèi)存是所有線程可以訪問的,也就是說成員變量根悼,靜態(tài)變量和數(shù)組元素是可以共享的凶异。

原子性

即不可中斷的一個(gè)或一系列操作蜀撑,也就是說一個(gè)操作或者多個(gè)操作,要么全部執(zhí)行并且執(zhí)行的過程不會(huì)被任何因素打斷剩彬,要么就都不執(zhí)行酷麦。在線程級(jí)別,我們可以這樣說一個(gè)或幾個(gè)操作只能在一個(gè)線程執(zhí)行完之后喉恋,另一個(gè)線程才能開始執(zhí)行該操作沃饶,也就是說這些操作是不可分割的,線程不能在這些操作上交替執(zhí)行瀑晒。

舉個(gè)栗子:i++绍坝,這個(gè)操作的語義可以拆分成三步:

  • 讀取變量i的值
  • 將變量i值加1
  • 將計(jì)算結(jié)果寫入變量i

由于線程是基于處理器分配的時(shí)間片執(zhí)行的,這三個(gè)步驟可能讓多個(gè)線程交叉執(zhí)行苔悦。我們假設(shè)i

的初始值為0轩褐,如果兩個(gè)線程按照如下順序交替執(zhí)行的話:

01.png

我們看到,經(jīng)過了兩次i++的操作玖详,變量i最后的值是1把介,并不是想象中的2。這就是因?yàn)?code>i++并不是原子性操作所帶來的并發(fā)問題蟋座。

解決方案

從共享性解決

使用局部變量

方法中的方法參數(shù)和局部變量是線程私有的拗踢,自然不會(huì)存在并發(fā)問題。

使用ThreadLocal

ThreadLocal示例

ThreadLocal作為變量的線程隔離的類向臀,訪問此變量的每個(gè)線程都會(huì)copy一個(gè)此變量的副本巢墅。多個(gè)線程操作這個(gè)變量,實(shí)際上是操作的自己本地內(nèi)存的變量券膀,這樣就避免了多線程操作變量的安全問題君纫。

我們先來看一下ThreadLocal的簡單使用:

public class ThreadLocalDemo {
    static ThreadLocal<String> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        Thread thread1 = new Thread(()->{
            tl.set("thread 1 locals");
            System.out.println("thread 1 :"+ tl.get());
        });
        Thread thread2 = new Thread(()->{
            tl.set("thread 2 locals");
            System.out.println("thread 2 :"+ tl.get());
        });
        thread1.start();
        thread2.start();
    }
}

運(yùn)行結(jié)果:

02.png

其實(shí)無論運(yùn)行多少次,無論幾個(gè)線程一起跑芹彬,最后打印出來的都是各個(gè)線程自己維護(hù)在內(nèi)存里的本地變量蓄髓,而不會(huì)出現(xiàn)線程1設(shè)置的變量被線程2修改這種情況。

ThreadLocal源碼解讀
結(jié)構(gòu)
03.png
04.png

由ThreadLocal和Thread的類結(jié)構(gòu)可知舒帮,Thread里面有兩個(gè)成員變量threadLocalsthreadLocals会喝,他們都是ThreadLocalMap類型的,而ThreadLocalMapThreadLocal的一個(gè)靜態(tài)內(nèi)部類玩郊,這是一個(gè)定制化的HashMap肢执。默認(rèn)每個(gè)線程一開始的時(shí)候,這兩個(gè)變量都是null瓦宜。

05.png

類結(jié)構(gòu)圖如下:

06.png
Set方法

ThreadLocal#set:

public void set(T value) {
        Thread t = Thread.currentThread(); //當(dāng)前線程
        ThreadLocalMap map = getMap(t); //獲取當(dāng)前線程的threadlocals
        if (map != null)
            map.set(this, value); //(k,v)存入ThreadLocalMap
        else
            createMap(t, value); //初始化當(dāng)前線程的threadlocals蔚万,創(chuàng)建ThreadLocalMap
    }

代碼的字面意思:如果當(dāng)前線程的threadLocals變量不為null,則將當(dāng)前的ThreadLocal實(shí)例為key临庇,傳入的value值為value反璃,放入ThreadLocalMap對象里面昵慌;如果當(dāng)前線程的threadLocals為null,則初始化當(dāng)前線程的threadLocals變量淮蜈。

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals; //返回當(dāng)前線程的threadLocals
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue); //初始化當(dāng)前線程的threadLocals斋攀,k為當(dāng)前的ThreadLocal對象,v為設(shè)置的值梧田。
    }

看到這里淳蔼,說一下為什么為什么會(huì)是個(gè)map。那是因?yàn)橐粋€(gè)線程可以綁定多個(gè)ThreadLocal實(shí)例裁眯。例如:

static ThreadLocal<String> tl1 = new ThreadLocal<>();
static ThreadLocal<Integer> tl2 = new ThreadLocal<>();
Get方法
public T get() {
        Thread t = Thread.currentThread(); //當(dāng)前線程
        ThreadLocalMap map = getMap(t); //當(dāng)前線程的threadlocals變量
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this); //取出Entry
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }
private T setInitialValue() {
        T value = initialValue(); //初始化為null
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); //獲取當(dāng)前線程的thredLocals是變量
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

這段代碼的意思是:我先判斷當(dāng)前線程的本地變量threadLocals變量是否為null鹉梨,不為null則以當(dāng)前的ThreadLocal實(shí)例為key從map中取出Entry,能取出Entry則返回對應(yīng)的value值穿稳;若當(dāng)前線程的本地變量threadLocals變量為null存皂,則初始化threadLocals變量,初始化的工作和set差不多逢艘,只不過set設(shè)置的值為傳入的參數(shù)旦袋,初始化設(shè)置的value是null(在當(dāng)前線程的threadLocals變量不為null的時(shí)候)。

Remove 方法
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

就是如果當(dāng)前線程的 本地變量threadLocals不為null它改,則刪除當(dāng)前線程中指定 ThreadLocal 實(shí)例的本地變量疤孕。

總結(jié)一下:每個(gè)線程都有一個(gè)成員變量叫threadLocals,它是ThreadLocalMap類型的。其中的key為每一個(gè)ThreadLocal的實(shí)例央拖,value為傳入的參數(shù)值祭阀。

注意:

  • 由于ThreadLocalMap的key為WeakReference,在外部沒有強(qiáng)引用時(shí)鲜戒,發(fā)生GC會(huì)回收時(shí)柬讨,如果創(chuàng)建ThreadLocal一直運(yùn)行,將會(huì)導(dǎo)致這個(gè)key對應(yīng)的value將會(huì)一直在內(nèi)存中得不到回收袍啡,發(fā)生內(nèi)存泄露。所以在用完ThreadLocal的時(shí)候要注意手動(dòng)remove却桶。
  • 其實(shí)ThreadLocal會(huì)有個(gè)問題境输,那就是子線程通過獲取不了父線程中的ThreadLocal變量,這個(gè)其實(shí)java已經(jīng)給出了解決方案了颖系,就是Thread的另一個(gè)ThreadLocalMap類型的變量inheritableThreadLocals,我們通過這個(gè)變量嗅剖,從get方法中能獲取到本線程和父線程的ThreadLocal變量。

同步方法解決

話說回來嘁扼,剛剛我們從共享性角度解決并發(fā)編程的原子性問題信粮,提出了ThreadLocal,也就是每個(gè)線程獨(dú)占的趁啸,自然不會(huì)有并發(fā)問題强缘。下面從另一個(gè)角度來說督惰,也就是我們都知道的方式:加鎖。

鎖的概念

《并發(fā)編程的藝術(shù)》里是這么定義鎖的:"鎖是用來控制多個(gè)線程訪問共享資源的方式旅掂,一般來說赏胚,一個(gè)鎖能防止多個(gè)線程同時(shí)訪問共享資源(但是有些鎖可以允許多個(gè)線程并發(fā)的訪問共享資源,比如讀寫鎖)"商虐。

提到鎖觉阅,一大堆名詞就冒出來了:內(nèi)置鎖,顯示鎖秘车,可重入鎖典勇,讀寫鎖,以及祖師爺抽象隊(duì)列同步器(AQS)……

最大名鼎鼎的要屬于synchronized的了叮趴。

synchronized關(guān)鍵字

synchronized同步關(guān)鍵字割笙,可以修飾方法,使之成為同步方法疫向】任担可以修飾this或Class對象,使之成為同步代碼塊搔驼。

public 返回類型 方法名(參數(shù)列表) {
    synchronized (鎖對象) {
        需要保持原子性的一系列代碼
        }
}

public synchronized 返回類型 方法名(參數(shù)列表) {
    需要被同步執(zhí)行的代碼
}

public synchronized static 返回類型 方法名(參數(shù)列表) {
    需要被同步執(zhí)行的代碼
}

例如這個(gè)demo:

public class SynchronizedDemo {
    private Object lock = new Object();
    
    public synchronized void m1(){
    }
    public void m2(){
        synchronized (lock) {
        }
    }
}

通過javap反編譯出來:

07.png

可以看到谈火,同步代碼塊的的synchronized是用monitorentermoniteorexit實(shí)現(xiàn)的,同步方法看不出來(其實(shí)是jvm底層的的ACC_SYNCHRONIZED實(shí)現(xiàn)的)舌涨。

monitorenter指令對應(yīng)于同步代碼塊的開始位置糯耍,監(jiān)視器在這個(gè)位置進(jìn)入,獲取鎖囊嘉;moniteorexit指令對應(yīng)于同步代碼塊的結(jié)束位置温技,監(jiān)視器在這個(gè)位置退出,釋放鎖扭粱。

JVM需要保證每一個(gè)monitorenter都有一個(gè)monitorexit與之對應(yīng)舵鳞,任何對象都有個(gè)monitor與之關(guān)聯(lián)。一旦monitor被持有琢蛤,這個(gè)對象將被鎖定蜓堕。

Java對象頭

synchronized用的鎖是存在java對象頭里的,對象頭一般占有2字寬(1字寬為4字節(jié)博其,即32bit)套才,但是如果對象是數(shù)組類型,則需要3字寬慕淡。對象頭里的Mark Word默認(rèn)存儲(chǔ)對象的HashCode背伴,分代年齡和鎖標(biāo)記位。對象頭的存儲(chǔ)結(jié)構(gòu)如下:

08.png

(此圖來源于互聯(lián)網(wǎng),侵刪)

鎖的升級(jí)與優(yōu)化

從jdk1.6開始傻寂,對鎖進(jìn)行了進(jìn)一步的升級(jí)和優(yōu)化息尺。鎖一共有4種狀態(tài),無鎖崎逃,偏向鎖掷倔,輕量級(jí)鎖,重量級(jí)鎖个绍,這幾種狀態(tài)會(huì)隨著競爭情況逐漸升級(jí)勒葱。鎖可以升級(jí)但不能降級(jí)。

偏向鎖

偏向鎖的引入背景:只在單線程訪問同步塊的場景巴柿。

當(dāng)鎖不存在多線程競爭的時(shí)候凛虽,為了讓線程獲得鎖的代價(jià)更低引入了偏向鎖。當(dāng)一個(gè)線程訪問同步快并獲取鎖時(shí)广恢,會(huì)在對象頭Mark Word上記錄偏向鎖狀態(tài)位1凯旋,此時(shí)的鎖標(biāo)識(shí)位是01。

當(dāng)一個(gè)線程獲取鎖的時(shí)候钉迷,會(huì)先檢查Mark Word上的可偏狀態(tài)至非,如果是1,則繼續(xù)檢查對象頭的線程Id糠聪。如果線程Id不是當(dāng)前線程荒椭,則通過CAS競爭獲取鎖,競爭成功將線程Id替換舰蟆,如果CAS競爭鎖失敗趣惠,證明存在多線程情況,此時(shí)偏向鎖被掛起身害,升級(jí)升輕量級(jí)鎖味悄。如果線程是當(dāng)先線程,則執(zhí)行同步代碼塊塌鸯。

偏向鎖的釋放使用了一種等到競爭出現(xiàn)才釋放鎖的機(jī)制侍瑟,所以當(dāng)其他線程去競爭鎖時(shí),持有偏向鎖的線程才會(huì)釋放鎖丙猬。此時(shí)將恢復(fù)到無鎖狀態(tài)或偏向于其他線程丢习。

輕量級(jí)鎖

輕量級(jí)鎖的引入背景:沒有多線程競爭的前提下,減少重量級(jí)鎖的互斥產(chǎn)生的性能消耗淮悼。

線程在執(zhí)行同步塊之前,JVM會(huì)首先在當(dāng)前線程的棧楨中創(chuàng)建用于存儲(chǔ)鎖記錄的空間揽思,并通過CAS將Mark Word替換為指向鎖記錄的指針袜腥。如果成功,則當(dāng)前線程獲得鎖,如果失敗羹令,則表示有其他線程競爭鎖鲤屡,此時(shí)會(huì)自旋等待,并會(huì)膨脹成重量級(jí)鎖福侈。

輕量級(jí)鎖的釋放也是通過CAS來執(zhí)行的酒来,如果成功,則表示沒有競爭發(fā)生肪凛,如果失敗堰汉,鎖會(huì)膨脹成重量級(jí)鎖。

我們發(fā)現(xiàn)伟墙,偏向鎖相比較輕量級(jí)鎖通過CAS以及自旋等方式獲取鎖翘鸭,性能更好一些,因?yàn)樗挥性谂袛鄬ο箢^中的線程Id不是當(dāng)前線程的時(shí)候才去CAS競爭鎖戳葵,而輕量級(jí)鎖一開始就CAS競爭鎖了就乓。

重量級(jí)鎖

重量級(jí)鎖通過對象內(nèi)部的monitor實(shí)現(xiàn),當(dāng)鎖處于整個(gè)狀態(tài)下拱烁,其他線程試圖獲取鎖時(shí)生蚁,都會(huì)被阻塞住,當(dāng)持有鎖的線程釋放鎖之后才會(huì)喚醒這些線程戏自,被喚醒的線程展開新的一輪鎖爭奪邦投。此時(shí)操作系統(tǒng)實(shí)現(xiàn)線程之間的切換需要從用戶態(tài)到內(nèi)核態(tài)的切換,線程切換的成本非常高浦妄。

參考資料

方騰飛:《Java并發(fā)編程的藝術(shù)》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末尼摹,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子剂娄,更是在濱河造成了極大的恐慌蠢涝,老刑警劉巖,帶你破解...
    沈念sama閱讀 218,036評(píng)論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件阅懦,死亡現(xiàn)場離奇詭異和二,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)耳胎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門惯吕,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人怕午,你說我怎么就攤上這事废登。” “怎么了郁惜?”我有些...
    開封第一講書人閱讀 164,411評(píng)論 0 354
  • 文/不壞的土叔 我叫張陵堡距,是天一觀的道長。 經(jīng)常有香客問我,道長羽戒,這世上最難降的妖魔是什么缤沦? 我笑而不...
    開封第一講書人閱讀 58,622評(píng)論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮易稠,結(jié)果婚禮上缸废,老公的妹妹穿的比我還像新娘。我一直安慰自己驶社,他們只是感情好企量,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,661評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著衬吆,像睡著了一般梁钾。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上逊抡,一...
    開封第一講書人閱讀 51,521評(píng)論 1 304
  • 那天姆泻,我揣著相機(jī)與錄音,去河邊找鬼冒嫡。 笑死拇勃,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的孝凌。 我是一名探鬼主播方咆,決...
    沈念sama閱讀 40,288評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼蟀架!你這毒婦竟也來了瓣赂?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,200評(píng)論 0 276
  • 序言:老撾萬榮一對情侶失蹤片拍,失蹤者是張志新(化名)和其女友劉穎煌集,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體捌省,經(jīng)...
    沈念sama閱讀 45,644評(píng)論 1 314
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡苫纤,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,837評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了纲缓。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片卷拘。...
    茶點(diǎn)故事閱讀 39,953評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖祝高,靈堂內(nèi)的尸體忽然破棺而出栗弟,到底是詐尸還是另有隱情,我是刑警寧澤工闺,帶...
    沈念sama閱讀 35,673評(píng)論 5 346
  • 正文 年R本政府宣布乍赫,位于F島的核電站颓屑,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏耿焊。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,281評(píng)論 3 329
  • 文/蒙蒙 一遍搞、第九天 我趴在偏房一處隱蔽的房頂上張望罗侯。 院中可真熱鬧,春花似錦溪猿、人聲如沸钩杰。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽讲弄。三九已至,卻和暖如春依痊,著一層夾襖步出監(jiān)牢的瞬間避除,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評(píng)論 1 269
  • 我被黑心中介騙來泰國打工胸嘁, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留瓶摆,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,119評(píng)論 3 370
  • 正文 我出身青樓性宏,卻偏偏與公主長得像群井,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個(gè)殘疾皇子毫胜,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,901評(píng)論 2 355

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

  • Java8張圖 11书斜、字符串不變性 12、equals()方法酵使、hashCode()方法的區(qū)別 13荐吉、...
    Miley_MOJIE閱讀 3,707評(píng)論 0 11
  • 一、線程狀態(tài)轉(zhuǎn)換新建(New)可運(yùn)行(Runnable)阻塞(Blocking)無限期等待(Waiting)限期等...
    達(dá)微閱讀 586評(píng)論 1 2
  • 進(jìn)程和線程 進(jìn)程 所有運(yùn)行中的任務(wù)通常對應(yīng)一個(gè)進(jìn)程,當(dāng)一個(gè)程序進(jìn)入內(nèi)存運(yùn)行時(shí),即變成一個(gè)進(jìn)程.進(jìn)程是處于運(yùn)行過程中...
    勝浩_ae28閱讀 5,108評(píng)論 0 23
  • 我的家里有兩個(gè)狗凝化,今天稍坯,可可和樂樂在吵架,它們“汪汪”的叫個(gè)不停搓劫,可我一句也聽不懂瞧哟,于是,我突發(fā)奇想枪向,那我不...
    金奕彤閱讀 1,254評(píng)論 0 2
  • 自昨晚收到某打車軟件下的司機(jī)師傅在未接單之前的騷擾短信后秘蛔,再也不敢晚上搭乘順風(fēng)車陨亡!用戶數(shù)-1 現(xiàn)在很多用戶產(chǎn)品追求...
    小妞變身superwoman閱讀 318評(píng)論 0 3