第2章 并發(fā)編程的其他基礎(chǔ)知識(shí)

目錄

并行與并發(fā)區(qū)別

并發(fā)指同一時(shí)間段多個(gè)任務(wù)同時(shí)都在進(jìn)行桥嗤,并且都沒有執(zhí)行結(jié)束泛啸,而并行是說在單位時(shí)間內(nèi)多個(gè)任務(wù)在同時(shí)運(yùn)行。

并發(fā)任務(wù)強(qiáng)調(diào)在一個(gè)時(shí)間段內(nèi)同時(shí)進(jìn)行,而一個(gè)時(shí)間段有多個(gè)單位i時(shí)間構(gòu)成,所以說并發(fā)的多個(gè)任務(wù)在單位時(shí)間內(nèi)不一定同時(shí)在執(zhí)行。

一個(gè)CPU同時(shí)只能執(zhí)行一個(gè)任務(wù)熬北,所以單CPU時(shí)代多個(gè)任務(wù)都是并發(fā)執(zhí)行的。

注:在多線程時(shí)間中诚隙,線程的個(gè)數(shù)往往多于CPU個(gè)數(shù)讶隐,所以即使存在并行任務(wù),一般還是稱為多線程并發(fā)編程而非多線程并行編程久又。

Java中的線程安全問題

示例:計(jì)數(shù)器問題

t1 t2 t3 t4
線程A 從主內(nèi)存讀取count值到本線程 遞增本地線程count的值 寫回主內(nèi)存
線程B 從主內(nèi)存讀取count值到本線程 遞增本地線程count的值 寫回主內(nèi)存

假設(shè)count初始值為0巫延,線程A在t1和t2時(shí)間讀取了主內(nèi)存中的count并在本地將其遞增為1,t2時(shí)線程B從主內(nèi)存中讀取了count的值0并于t3時(shí)將其遞增為1地消,t3時(shí)線程A將count的新值1更新到主內(nèi)存烈评,t4時(shí)線程B進(jìn)行了同樣的操作,最終主內(nèi)存中count的值為1而非我們想要的2犯建。

Java中共享變量的內(nèi)存可見性問題

如圖是一個(gè)雙核CPU模型讲冠,每個(gè)核都有自己的一級緩存,有些架構(gòu)里還有一個(gè)所有CPU共享的二級緩存适瓦。

現(xiàn)假設(shè)線程A和線程B同時(shí)處理一個(gè)共享變量竿开,由于Cache的存在谱仪,將會(huì)出現(xiàn)內(nèi)存不可見問題,原因如下:

  • 線程A先獲取共享變量X的值(假設(shè)X=0)否彩,由于L1和L2緩存中都沒有X的值疯攒,線程A會(huì)直接從主存中去取X的值并將其緩存到L1和L2中。然后A將X的值遞增為1列荔,將其寫入緩存L1和L2敬尺,并刷新到主存。
  • 線程B也要獲取X的值贴浙,由于Core2中1級緩存沒有X的值砂吞,B會(huì)從L2緩存取到X的值1。然后B修改X為2崎溃,將其更新至L1蜻直、L2和主存。
  • 線程A又需要獲取X的值袁串,發(fā)現(xiàn)L1中已經(jīng)有了概而,但此時(shí)A獲得的X=1,與主存中X=2不同了囱修!

synchronized關(guān)鍵字

synchronized關(guān)鍵字是一種原子性內(nèi)置鎖赎瑰,線程進(jìn)入synchronized代碼塊前會(huì)自動(dòng)獲取監(jiān)視器鎖,這時(shí)其他線程再訪問該同步代碼塊是會(huì)被阻塞掛起破镰。

前面的共享變量內(nèi)存可見性問題主要是線程的工作內(nèi)存導(dǎo)致的乡范,而synchronized的內(nèi)存語義可以解決此問題。

synchronized的內(nèi)存語義:

  • 進(jìn)入synchronized塊:把synchronized塊中使用到的變量從線程的工作內(nèi)存中清除啤咽,這樣當(dāng)synchronized塊中要使用該變量時(shí)晋辆,就會(huì)從主存中去取。
  • 離開synchronized塊:把synchronized塊中對共享變量的修改刷新到主內(nèi)存宇整。

示例

public class ThreadSafeInteger {

    private int value;

    public synchronized int get() {
        return value;
    }

    public synchronized void set(int value) {
        this.value = value;
    }
}

注1:get()方法雖然只是讀操作瓶佳,但仍要加上synchronized來實(shí)現(xiàn)value的內(nèi)存可見性。

注2:使用synchronized雖然解決了共享變量value的內(nèi)存可見性問題鳞青,但由于synchronized是獨(dú)占鎖霸饲,同時(shí)只能有一個(gè)線程調(diào)用get()方法,其他調(diào)用線程則會(huì)被阻塞臂拓,同時(shí)存在線程切換厚脉、調(diào)度的開銷,效率并不高胶惰。

volatile關(guān)鍵字

當(dāng)一個(gè)變量聲明為volatile時(shí)傻工,線程在寫入變量時(shí)就不會(huì)把值緩存,而是直接把值刷新到主存中;當(dāng)其他線程讀取該變量時(shí)中捆,會(huì)從主存中重新獲得最新值鸯匹,而不是使用當(dāng)前工作內(nèi)存中的值。

示例

public class ThreadSafeInteger {

    private volatile int value;

    public int get() {
        return value;
    }

    public void set(int value) {
        this.value = value;
    }
}

注:volatile雖然保證了可見性泄伪,但并不保證操作的原子性殴蓬。

volatile不保證原子性示例

public class Test {
    
    private static volatile long _longVal = 0;

    public static void main(String[] args) {

        Thread t1 = new Thread(new LoopVolatile1());
        t1.start();

        Thread t2 = new Thread(new LoopVolatile2());
        t2.start();

        try{
            t1.join();
            t2.join();
        }catch(Exception e){
            e.printStackTrace();
        }


        System.out.println("final val is: " + _longVal);
    }

    private static class LoopVolatile1 implements Runnable {
        public void run() {
            long val = 0;
            while (val < 100000) {
                _longVal++;
                val++;
            }
        }
    }


    private static class LoopVolatile2 implements Runnable {
        public void run() {
            long val = 0;
            while (val < 100000) {
                _longVal++;
                val++;
            }
        }
    }
}

運(yùn)行上述代碼,發(fā)現(xiàn)每次輸出不同蟋滴。

使用場景

  • 寫入變量值不依賴當(dāng)前值染厅。如果依賴當(dāng)前值,將是獲取-計(jì)算-寫入三步津函,這三步并非原子性肖粮,volatile也不保證原子性。
  • 讀寫變量值時(shí)沒有加鎖球散。因?yàn)榧渔i本身已經(jīng)保證了內(nèi)存可見性,再使用volatile就是畫蛇添足散庶。

Java中的CAS操作

CAS即Compare And Swap蕉堰,JDK中Unsafe類提供了一系列的compareAndSwap*方法。

示例

下面以compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法為例進(jìn)行介紹

boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)方法:如果obj對象中內(nèi)存偏移值為valueOffset的變量值為expect悲龟,則使用新的值update替換之屋讶。這是處理器提供的一個(gè)原子性指令。

ABA問題

問題描述

假如線程A要去通過CAS修改變量X须教,要先判斷X當(dāng)前值是否改變過皿渗,如果“未改變”,則更新之轻腺。但這并不能保證X沒有被改變過:假如A修改X前乐疆,線程B修改了X的值,然后又修改回來贬养,A的CAS操作仍能成功挤土,但X實(shí)際上發(fā)生過改變。

解決方案

JDK中的AtomicStampedReference類給每個(gè)變量都配備了一個(gè)時(shí)間戳误算,從而避免了ABA問題的產(chǎn)生仰美。

Unsafe類

JDK的rt.jar 包中的UnSafe類提供了硬件級別的原子性操作,Unsafe類中的方法都是native方法儿礼,使用JNI的方式訪問本地C++實(shí)現(xiàn)庫咖杂。

Unsafe類可以直接操作內(nèi)存,這是不安全的蚊夫,如果是普通的調(diào)用的話诉字,它會(huì)拋出一個(gè)SecurityException異常;只有由主類加載器加載的類才能調(diào)用這個(gè)方法。

見下:

public static Unsafe getUnsafe() {
    Class localClass = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(localClass.getClassLoader())) {
        throw new SecurityException("Unsafe");
    } else {
        return theUnsafe;
    }
}

但通過萬能的反射奏窑,還是可以使用到Unsafe類的:

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

Java指令重排序

Java內(nèi)存模型允許編譯器和處理器對不存在數(shù)據(jù)依賴性的指令進(jìn)行重排序以提高性能导披。

如下:

int a = 1; //(1)
int b = 2; //(2)
int c = a + b; //(3)

如果有必要,JVM完全可以將(2)放在(1)前執(zhí)行埃唯,但這并不會(huì)影響最終結(jié)果撩匕。

單線程下這樣做是ok的,但多線程下就會(huì)出現(xiàn)問題:

    private static int num = 0;
    private static boolean ready = false;

    public static void main(String[] args){

        Thread t1 = new Thread(new ReadTask());
        Thread t2 = new Thread(new WriteTask());

        t1.start();
        t2.start();

        try{
            Thread.sleep(100);
        }catch(InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("main exit!");
    }



    public static class ReadTask implements Runnable {

        @Override
        public void run() {
            while(true) {
                if(ready) {
                    System.out.println(num);
                    return;
                }
            }
        }
    }
    
    public static class WriteTask implements Runnable {
    
        @Override
        public void run() {
            num = 1; //(1)
            ready = true; //(2)
        }
    }

理論上這段代碼并不一定輸出1墨叛,因?yàn)檫M(jìn)行指令重排序后止毕,WriteTask中的(2)語句有可能會(huì)先于(1)執(zhí)行,導(dǎo)致輸出為0漠趁。

樂觀鎖與悲觀鎖

  • 樂觀鎖:認(rèn)為數(shù)據(jù)在一般情況下不會(huì)造成沖突扁凛,所以在訪問記錄前不會(huì)加排他鎖,而是在進(jìn)行數(shù)據(jù)提交更新時(shí)才會(huì)對數(shù)據(jù)沖突進(jìn)行檢測并返回一定狀態(tài)闯传。用戶需根據(jù)狀態(tài)判斷是否更新成功并進(jìn)行相應(yīng)操作谨朝。
  • 悲觀鎖:對數(shù)據(jù)被外界修改持保守態(tài)度,認(rèn)為數(shù)據(jù)很容易被其他線程更改甥绿,所以在數(shù)據(jù)被處理前進(jìn)行加鎖字币。

公平鎖與非公平鎖

  • 公平鎖:線程獲取鎖的順序是按照請求鎖的時(shí)間順序決定的,先請求先得共缕。
  • 非公平鎖:不保證先請求的線程先獲得鎖洗出。

注:在沒有公平性需求的前提下盡量使用非公平鎖,因?yàn)楣芥i會(huì)帶來額外性能開銷图谷。

獨(dú)占鎖與共享鎖

  • 獨(dú)占鎖:任何時(shí)候都只有一個(gè)線程能得到鎖翩活,如ReentrantLock。
  • 共享鎖:可以多個(gè)線程持有便贵,如ReadWriteLock讀寫鎖菠镇。

可重入鎖

一個(gè)線程要再次獲取自己已經(jīng)獲取了的鎖時(shí)如果不被阻塞,則該鎖為可重入鎖承璃。

public class Test{

    public synchronized void f1() {
        System.out.println("f1...");
    }

    public synchronized void f2() {
        System.out.println("f2...");
        f1();
    }
}

上述代碼中辟犀,調(diào)用f2()并不會(huì)造成阻塞,說明synchronized內(nèi)部鎖是可重入鎖绸硕。

自旋鎖

當(dāng)前線程獲取鎖時(shí)堂竟,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有,并不會(huì)馬上阻塞自己玻佩,而是在不放棄CPU使用權(quán)的情況下出嘹,多次嘗試獲取,到一定次數(shù)后才放棄咬崔。目的是使用CPU時(shí)間換取線程阻塞與調(diào)度的開銷税稼。

更多

相關(guān)筆記:《Java并發(fā)編程之美》閱讀筆記

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末烦秩,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子郎仆,更是在濱河造成了極大的恐慌只祠,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件扰肌,死亡現(xiàn)場離奇詭異抛寝,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)曙旭,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進(jìn)店門盗舰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人桂躏,你說我怎么就攤上這事钻趋。” “怎么了剂习?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵蛮位,是天一觀的道長。 經(jīng)常有香客問我鳞绕,道長失仁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任猾昆,我火速辦了婚禮陶因,結(jié)果婚禮上骡苞,老公的妹妹穿的比我還像新娘垂蜗。我一直安慰自己,他們只是感情好解幽,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布贴见。 她就那樣靜靜地躺著,像睡著了一般躲株。 火紅的嫁衣襯著肌膚如雪片部。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天霜定,我揣著相機(jī)與錄音档悠,去河邊找鬼。 笑死望浩,一個(gè)胖子當(dāng)著我的面吹牛辖所,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播磨德,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼缘回,長吁一口氣:“原來是場噩夢啊……” “哼吆视!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起酥宴,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤啦吧,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后拙寡,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體授滓,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年倒庵,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了褒墨。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 38,654評論 1 340
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡擎宝,死狀恐怖郁妈,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情绍申,我是刑警寧澤噩咪,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站极阅,受9級特大地震影響胃碾,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜筋搏,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一仆百、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧奔脐,春花似錦俄周、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至排龄,卻和暖如春波势,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背橄维。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工尺铣, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人争舞。 一個(gè)月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓凛忿,卻偏偏與公主長得像,于是被迫代替她去往敵國和親兑障。 傳聞我的和親對象是個(gè)殘疾皇子侄非,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,543評論 2 349

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