線程同步分析

參考 https://github.com/fanshanhong/note/blob/master/Android/%E5%B9%B6%E5%8F%91%E5%92%8C%E5%A4%9A%E7%BA%BF%E7%A8%8B/%E7%BA%BF%E7%A8%8B%E7%B3%BB%E5%88%97/%E7%BA%BF%E7%A8%8B%E5%90%8C%E6%AD%A5.md

線程同步是什么兜叨?

多個(gè)線程訪問同一份資源(共享資源)的時(shí)候,線程之間相同協(xié)調(diào)的過程成為線程同步衩侥。
在一般情況下国旷,創(chuàng)建一個(gè)線程是不能提高程序的執(zhí)行效率的,所以要?jiǎng)?chuàng)建多個(gè)線程茫死。但是多個(gè)線程同時(shí)運(yùn)行的時(shí)候可能調(diào)用線程函數(shù)跪但,在多個(gè)線程同時(shí)對同一個(gè)內(nèi)存地址進(jìn)行寫入,由于CPU時(shí)間調(diào)度上的問題峦萎,寫入數(shù)據(jù)會(huì)被多次的覆蓋屡久,所以就要使線程同步。

synchronized關(guān)鍵字

synchronized可以保證方法或者代碼快在運(yùn)行時(shí)骨杂,同一時(shí)刻只有一個(gè)方法可以進(jìn)入到臨界區(qū)涂身,同時(shí)它還可以保證共享變量的內(nèi)存可見性。
每個(gè)Java對象都有用作一個(gè)實(shí)現(xiàn)同步的鎖搓蚪,這些鎖成為java內(nèi)置鎖蛤售,可以理解為,在object對象中有個(gè)lock對象妒潭。
當(dāng)你使用synchronized(this)相當(dāng)于synchronized(this.lock)
當(dāng)你使用synchronized(obj)相當(dāng)于synchronized(obj.lock)悴能,因此我們說是持有某個(gè)對象的鎖。
sleep()方法雳灾,睡眠過程中漠酿,并不釋放當(dāng)前持有的鎖。

三種應(yīng)用方式:
1谎亩、普通同步方法(實(shí)例方法)炒嘲,鎖是當(dāng)前實(shí)例對象宇姚,進(jìn)入同步方法前要獲得當(dāng)前實(shí)例的鎖
2、靜態(tài)同步方法夫凸,鎖是當(dāng)前類的class對象浑劳,進(jìn)入同步代碼前要獲得當(dāng)前類對象的鎖
3、同步方法塊夭拌,鎖是括號里面的對象魔熏,對給定對象加鎖,進(jìn)入同步代碼庫前要獲得給定對象的鎖

保護(hù)共享資源

要保護(hù)好需要同步的對象鸽扁,需要對訪問共享資源的所有方法或代碼塊都要考慮是否需要加入鎖蒜绽。因?yàn)閯e的線程可以自由訪問非同步(即:未加鎖)的方法,這樣可能會(huì)對同步的方法產(chǎn)生影響桶现。

生產(chǎn)者和消費(fèi)者

下面使用最經(jīng)典的生產(chǎn)者消費(fèi)者的例子躲雅,講解線程同步。
生產(chǎn)者->做饅頭
消費(fèi)者->吃饅頭

/**
 * 饅頭封裝類
 */
class ManTou {
    // 給饅頭一個(gè)id
    int id;
    public ManTou(int id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "ManTou{" +
                "id=" + id +
                '}';
    }
}

使用一個(gè)數(shù)組(籃子)來存放饅頭骡和,然后維護(hù)一個(gè)棧頂指針


image.png
/**
 * 籃子
 */
class Basket {
    // 棧頂指針, 該裝第幾個(gè)了
    int index = 0;
    // 容量
    ManTou[] arrayManTou = new ManTou[6];
}

提供一個(gè)向籃子里扔饅頭(push)和從籃子里取饅頭(pop)的方法吏夯。

public void push(ManTou manTou) {
        arrayManTou[index] = woTou;
        index++;
    }
public synchronized ManTou pop() {
        index--;
        ManTou mt = arrayManTou[index];
        arrayManTou[index] = null;
        return mt;
    }

但是我們這樣會(huì)不會(huì)有問題呢?
對于每一個(gè)做饅頭的人和吃饅頭吃的人即横,都相當(dāng)于是一個(gè)線程噪生。
每個(gè)做饅頭的人都會(huì)調(diào)用push方法,向籃子里扔饅頭东囚。每個(gè)吃饅頭的人跺嗽,都會(huì)調(diào)用pop方法,從籃子里取出饅頭页藻。
當(dāng)做饅頭的人小A(A線程)調(diào)用push方法向筐里扔饅頭的過程中桨嫁,在 arrayManTou[index] = woTou;這一句之后,很不幸份帐,此時(shí)CPU時(shí)間片分給了做饅頭的小B(B線程執(zhí)行)璃吧。 那問題就來了:index 還沒來得及++。小B做好了饅頭也往籃子里面扔废境,就把剛才小A丟進(jìn)去的饅頭給覆蓋了畜挨。這個(gè)問題的關(guān)鍵就在于這兩條語句之間不能被打斷,因此要在push方法上加synchronized噩凹。
同樣的巴元,pop方法也有這樣的問題,因此也要在pop方法上添加synchronized關(guān)鍵字驮宴。

那接下來還有其他的問題:
對于做饅頭的人而言逮刨,籃子滿了怎么辦?因?yàn)槲覀兓@子里面數(shù)組的容量只有6堵泽。
既然籃子只有這么大修己,那就等會(huì)在做饅頭吧恢总,等籃子里的饅頭被吃掉了,再往籃子里面扔饅頭睬愤。

public synchronized void push(ManTou manTou) {
        while (index == arrayManTou.length) { // 滿了
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        arrayManTou[index] = manTou;
        index++;
        notify();// 喚醒一個(gè)正在wait在當(dāng)前對象上的線程   notify無法喚醒自己
        // notifyAll();
    }

注意:這個(gè)wait是Object的的wait方法
this.wait()是啥意思? 是指當(dāng)前執(zhí)行這個(gè)代碼塊的線程wait离熏, 也就是已經(jīng)持有了synchronized(this)語句中的this對象的鎖的線程等待。等待在哪里戴涝?等待在this對象上。等待其他線程調(diào)用這個(gè)(this)對象的notify方法的時(shí)候喚醒自己钻蔑。
一個(gè)線程進(jìn)入push方法的時(shí)候啥刻, 已經(jīng)拿到了鎖了。在它執(zhí)行的過程中咪笑,遇到一個(gè)事件可帽,必須阻塞。也就是說做饅頭的人窗怒,在往籃子里扔的時(shí)候映跟,先檢查了一下籃子滿了,他就只能等著了扬虚,不能再往里扔了努隙,再扔就冒出來了。要等到有人吃了辜昵,才能再繼續(xù)往里扔荸镊。
調(diào)用wait()或者notify()之前,必須使用synchronized關(guān)鍵字持有被wait/notify的對象的鎖堪置。只有持有了鎖躬存,才有資格wait。如果你壓根拿不到鎖舀锨,就根本無法wait岭洲。
也就是你 synchronized(XX) 和 XX.wait 必須是同一個(gè)對象, 否則拋出 java.lang.IllegalMonitorStateException
那何時(shí)醒來坎匿?等籃子里的饅頭被別人吃了盾剩,讓吃饅頭的人把他叫醒就行啦, 即:等待別的線程調(diào)用同一個(gè)Basket對象的notify/nofityAll 方法的時(shí)候替蔬, 就會(huì)醒來啦彪腔。
對于吃饅頭的人而言,籃子空了咋整进栽?很簡單德挣,等著唄。等人家做好了咱再吃快毛。

public synchronized ManTou pop() {
        while (index == 0) { // 空了
            try {
                this.wait();
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        index--;
        ManTou mt = arrayManTou[index];
        notify();
        return mt;
    }

細(xì)心的小伙伴可能已經(jīng)發(fā)現(xiàn):我們在push方法和pop方法的最后都調(diào)用了notify方法格嗅。
notify() 喚醒一個(gè)正在wait在當(dāng)前對象上的線程 notify無法喚醒自己
notifyAll() 喚醒所有正在wait在當(dāng)前對象上的線程
顯然番挺,剛才已經(jīng)有線程wait在了Basket對象上。
在push方法中:如果籃子里沒有滿的話屯掖,我們還是向往常一樣往籃子里扔饅頭玄柏,但是扔完了,記得叫醒等著吃饅頭的人贴铜。因?yàn)榭赡苡腥嗽诘戎浴?br> 在pop方法中:如果籃子不是空的粪摘,取出了一個(gè)就趕緊通知做饅頭的人, 說:“現(xiàn)在籃子已經(jīng)不是滿的了绍坝,有空間了徘意,你們可以做起來啦⌒郑”
這個(gè)籃子椎咧,我們終于是封裝好了,現(xiàn)在我們把做饅頭的人和吃饅頭的人也封裝起來把介。

/**
 * 做饅頭的人
 */
class Producer implements Runnable {
    Basket basket;
    public Producer(Basket basket) {
        this.basket = basket;
    }

    @Override
    public void run() {
        for (int i=0; i<20; i++) {
            ManTou manTou = new ManTou(i);
            basket.push(manTou);
            System.out.println("生產(chǎn)了:" + manTou);
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

生產(chǎn)者(做饅頭的人)需要知道往哪個(gè)籃子里扔饅頭勤讽,所以要持有籃子的引用。
當(dāng)創(chuàng)建生產(chǎn)者的時(shí)候拗踢,就告訴他要往哪個(gè)籃子里扔脚牍。因此我們提供一個(gè)構(gòu)造方法,為Basket賦值
生產(chǎn)的過程巢墅,也就是我們的 run()方法啦莫矗。在run()方法中,不斷做饅頭砂缩,不斷往籃子里扔作谚。

/**
 * 消費(fèi)者(吃饅頭的人)
 */
class Consumer implements Runnable {
    Basket basket;
    public Consumer(Basket basket) {
        this.basket = basket;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            ManTou manTou = basket.pop();
            System.out.println("消費(fèi)了:" + manTou);
            try {
                Thread.sleep(1000);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

消費(fèi)者( 吃饅頭的人),需要知道從哪個(gè)籃子里拿饅頭吃庵芭。所以也要持有筐的引用妹懒。我們在構(gòu)造方法中,為Basket賦值双吆。
消費(fèi)的過程眨唬, 也就是run()方法。不斷從籃子里取出饅頭好乐。
有個(gè)細(xì)節(jié)匾竿,我們要注意。在push方法判斷籃子滿的時(shí)候蔚万, 以及在pop方法判斷籃子空的時(shí)候岭妖,我們都用了while,為什么用while 而不用if呢?
考慮下面這樣一種情況:在push方法中昵慌,如果在wait的時(shí)候被打斷假夺,將進(jìn)入catch 代碼塊去處理異常,異常處理之后斋攀,就跳出了if已卷, 繼續(xù)下面的執(zhí)行。如果此時(shí)籃子還是滿的呢淳蔼? 就有問題了侧蘸。
所以要用while。即便是發(fā)生了Exception鹉梨, 仍要要回頭先檢查是否已經(jīng)滿了讳癌, 如果滿了, 還要繼續(xù)wait俯画。如果不滿了,才能繼續(xù)向下執(zhí)行司草。

總結(jié):

存放饅頭的籃子艰垂,就是所謂的共享資源,對于共享資源的保護(hù)埋虹,就是需要對訪問共享資源的所有方法和代碼塊都要考慮加入鎖猜憎,也就是Baseket類中的push和pop方法。

wait()和sleep()的區(qū)別

1搔课、wait是Object的方法胰柑,sleep是Thread的方法。
2爬泥、wait的時(shí)候不再持有那個(gè)鎖柬讨,等notify醒來才重新獲取鎖,sleep是睡著袍啡,還是擁有鎖踩官,并不釋放
3、調(diào)用wait的時(shí)候必須鎖定對象境输,如果沒有進(jìn)入synchronized代碼塊蔗牡,則沒有wait的資格

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市嗅剖,隨后出現(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ī)與錄音温技,去河邊找鬼革为。 笑死,一個(gè)胖子當(dāng)著我的面吹牛舵鳞,可吹牛的內(nèi)容都是我干的篷角。 我是一名探鬼主播,決...
    沈念sama閱讀 39,136評論 3 410
  • 文/蒼蘭香墨 我猛地睜開眼系任,長吁一口氣:“原來是場噩夢啊……” “哼恳蹲!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起俩滥,我...
    開封第一講書人閱讀 37,882評論 0 268
  • 序言:老撾萬榮一對情侶失蹤嘉蕾,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后霜旧,有當(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
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了以清。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片儿普。...
    茶點(diǎn)故事閱讀 38,789評論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖掷倔,靈堂內(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. 我叫王不留,地道東北人题造。 一個(gè)月前我還...
    沈念sama閱讀 46,598評論 2 362
  • 正文 我出身青樓傍菇,卻偏偏與公主長得像,于是被迫代替她去往敵國和親界赔。 傳聞我的和親對象是個(gè)殘疾皇子丢习,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,697評論 2 351

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

  • 本文主要講了java中多線程的使用方法、線程同步淮悼、線程數(shù)據(jù)傳遞咐低、線程狀態(tài)及相應(yīng)的一些線程函數(shù)用法、概述等袜腥。 首先講...
    李欣陽閱讀 2,444評論 1 15
  • Java多線程學(xué)習(xí) [-] 一擴(kuò)展javalangThread類 二實(shí)現(xiàn)javalangRunnable接口 三T...
    影馳閱讀 2,952評論 1 18
  • 多線程三個(gè)特征:原子性损痰、可見性以及有序性. 同步鎖 /并發(fā)鎖/ 讀寫鎖,顯示鎖酒来, ReentrantLock與Co...
    架構(gòu)師springboot閱讀 1,915評論 0 5
  • 1.解決信號量丟失和假喚醒 public class MyWaitNotify3{ MonitorObject m...
    Q羅閱讀 873評論 0 1
  • 線程安全 當(dāng)多個(gè)線程訪問一個(gè)對象時(shí)卢未,如果不用考慮這些線程在運(yùn)行時(shí)環(huán)境下的調(diào)度和交替執(zhí)行,也不需要進(jìn)行額外的同步役首,或...
    閩越布衣閱讀 761評論 0 6