Java ReentrantLock中condition通信的好處

之前看到Java Concurrent包中有個Condition接口。這個接口如今已經(jīng)普遍用于線程通信, 使用方法主要依靠condition的await方法和signal方法,但這一對方法和Java經(jīng)典的wait鹿霸,notify方法對頗為相似。但這個新的方法對有什么好處呢蔑水,思考過后得出一句結(jié)論:減少無謂的喚醒早芭。
于是寫下這篇文章做個簡單的筆記,文章首先簡要介紹一下預(yù)備知識恬叹,但不打算詳細(xì)說,畢竟重點僅放在condition上同眯。介紹完畢后就是Condition的使用方法以及舉例說明Condition好處在哪绽昼。

Java線程間的通信

Java 線程通信最常用的就是經(jīng)典的三種:

  1. volatile 共享變量輪詢
  2. synchronized 下使用object的wait,notify方法對
  3. ReentrantLock下使用condition的await须蜗,signal方法對

volatile 共享變量輪詢, 核心代碼如下

public class Main{
    volatile boolean shouldStop = false;
    Thread thread_1 = new Thread(){
           @Override
           public void run() {
               while(!shouldStop){
                   //do something
               }
           }
       };
       Thread thread_2 = new Thread(){
           @Override
           public void run() {
               try{
                   sleep(1000);
                   shouldStop = true;
               }catch (InterruptedException e){
                   e.printStackTrace();
               }
           }
        };
}

線程1和線程2通過共享shouldStop來決定是否停止工作硅确,至于為什么要用volatile關(guān)鍵字目溉,主要有兩點:

  1. 強制共享變量修改時flush回主存
  2. 禁止cpu優(yōu)化代碼時的指令重排
    具體的可以看這里 http://www.importnew.com/23535.html

synchronized中使用wait,notify方法對
雖然這個方法估計各位大佬都已經(jīng)熟爛了菱农,但為了和await缭付,signal機制做對比,請允許我寫一個生產(chǎn)者/消費者 模型來做說明循未。

public interface Buffer {
    void put(Integer integer) throws InterruptedException;
    Integer take() throws InterruptedException;
}
import java.util.ArrayList;

public class ClassicBuffer implements Buffer{
    private Object lock = new Object();
    private final static int CAPACITY = 1;
    private int count = 0;
    private ArrayList<Integer> list = new ArrayList<>(CAPACITY);
    public ArrayList<Integer> getList(){
        return list;
    }
    public  void put(Integer e) throws InterruptedException{
        if(e == null){
            return;
        }
        synchronized (lock) {
    
            try{
                while(count == CAPACITY){
                    lock.wait();
                    System.out.println("Classic_Put: "+Thread.currentThread());
                }
                list.add(e);
                count++;
                lock.notifyAll();
            }catch (InterruptedException exception) {
                // TODO: handle exception
                exception.printStackTrace();
            }
        }
    }
    public synchronized Integer take() throws InterruptedException{
    
        synchronized (lock) {
            
            Integer e = -1;
            try{
                while(count == 0){
                    lock.wait();
                    System.out.println("Classic_Take: "+Thread.currentThread() );
                }
                 e = list.get(count % CAPACITY);
                count --;
                lock.notifyAll();
                return e;
            }catch (InterruptedException exception) {
                // TODO: handle exception
                return e;
            }
        }
    }
}

這是用object的notify和wait來實現(xiàn)阻塞隊列的核心代碼陷猫,稍微解釋一下代碼含義。
阻塞隊列實現(xiàn)Buffer接口的妖,這個接口只有put和take兩個方法, 容量大小為定義好的常量CAPACITY绣檬,這里是1,當(dāng)前容量用count變量來統(tǒng)計羔味。

生產(chǎn)者(put):
put的時候如果滿足當(dāng)前容量count 等于容量CAPACITY河咽,那說明隊列已經(jīng)滿了,不能再投放數(shù)據(jù)了赋元,因此要用wait()來阻塞自己忘蟹。如果容量未滿,那么可以投放數(shù)據(jù)搁凸,一旦投放數(shù)據(jù)媚值,隊列就不為空,此時很有可能有一些消費者在阻塞等待隊列不為空护糖,因此這時候要喚醒這些等待的消費者褥芒。這里用的是notifyAll來做喚醒(個人覺得不應(yīng)該使用notify,因為notify只會隨機喚醒一條線程嫡良,如果有多條生產(chǎn)者線程會出現(xiàn)麻煩锰扶,后面會細(xì)細(xì)道來)。

消費者(take):
邏輯和生產(chǎn)者相似寝受,如果當(dāng)前容量count已經(jīng)等于0坷牛,那么說明隊列為空,沒有數(shù)據(jù)很澄,因此消費者需要wait自己來阻塞等待數(shù)據(jù)到來京闰。如果容量不為空,那么消費者會取走一個數(shù)據(jù)甩苛,容量減少蹂楣,因此隊列此時一定不滿,需要notifyAll來喚醒阻塞中的生產(chǎn)者讯蒲。

另外: 生產(chǎn)者消費者只需要在runnable中實現(xiàn)調(diào)用這個阻塞隊列的put/take就可以了痊土,這部分的代碼會在本文末章奉上。

ReentrantLock中使用condition 的await墨林,signal方法對

import java.util.ArrayList;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class ConditionBuffer implements Buffer{
    private Lock lock = new ReentrantLock();
    private Condition notEmpty = lock.newCondition();
    private Condition notFull = lock.newCondition();
    private final static int CAPACITY = 1;
    private int count = 0;
    private ArrayList<Integer> list = new ArrayList<>(CAPACITY);
    public ArrayList<Integer> getList(){
        return list;
    }
    public void put(Integer e) throws InterruptedException{
        if(e == null){
            return;
        }
        lock.lock();
        try{
            while(count == CAPACITY){
                notFull.await();
                System.out.println("Reentrant_put: "+Thread.currentThread());
            }
            list.add(e);
            count++;
            notEmpty.signal();
        }finally {
            lock.unlock();
        }
    }
    public Integer take() throws InterruptedException{
        lock.lock();
        while(count == 0){
            notEmpty.await();
            System.out.println("Reentrant_take: "+Thread.currentThread());
        }
        try{
            Integer e = list.get(count % CAPACITY);
            count --;
            notFull.signal();
            return e;
        }finally {
            lock.unlock();
        }
    }
}

這段代碼的邏輯和上一段是一樣的施戴,不同的地方是使用ReentrantLock代替synchronized來做同步, 用condition代替object來做線程通信反浓。
具體的使用方法跟object的wait萌丈,notify很相似赞哗,await和signal同樣要在同步區(qū)中調(diào)用,并且使用ReentrantLock要記得手動unlock辆雾。稍微提一提ReentrantLock肪笋。

ReentrantLock是 Java concurrent包里實現(xiàn)的可重入鎖機制。它和synchronized的主要區(qū)別是
ReentrantLock是在java層面上實現(xiàn)的度迂,基于AQS(AbstractQueuedSynchronized)框架下使用自旋CAS機制實現(xiàn)藤乙,另外ReentrantLock擴展了很多額外的同步方法,比如公平鎖惭墓,非公平鎖坛梁,可中斷鎖,非阻塞鎖腊凶。
而synchronized是基于JVM層面實現(xiàn)的划咐,使用計數(shù)監(jiān)視鎖來做同步。
具體可以到這里看 http://hanhailong.com/

Condition比object通信好在哪

扯了那么多钧萍,終于來到做筆記的地方啦褐缠。再次說一遍好處:condition減少無謂的喚醒。
咱們現(xiàn)在開始把生產(chǎn)消費搞起风瘦,做一次測試队魏。
生產(chǎn)者線程:

public class Producer implements Runnable{
    Buffer buffer;
    public Producer(Buffer buffer){
        this.buffer = buffer;
    }
    public void run(){
        try{
            while(true){
                buffer.put(1);
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
            // TODO: handle exception
        }
    }
}

消費者線程

public class Consumer implements Runnable {
    Buffer buffer;
    public Consumer(Buffer buffer){
        this.buffer = buffer;
    }
    public void run(){
        try{
            while(true){
                buffer.take();
            }
        }catch (InterruptedException e) {
            e.printStackTrace();
            // TODO: handle exception
        }
    }
}

很簡單對吧,僅僅是把實現(xiàn)好的阻塞隊列注入到線程中万搔。好胡桨,現(xiàn)在我們創(chuàng)建三條生產(chǎn)者線程,一條消費者線程瞬雹。走起

public class Main {
    public static void main(String[] args){
        ClassicBuffer classicBuffer = new ClassicBuffer();
        ConditionBuffer blockBuffer = new ConditionBuffer();
        Thread thread_1;
        Thread thread_2;
        Thread thread_3;
        Thread thread_4;
        Consumer consumer;
        Producer producer;
        if(args[0].contains("classic")){
            consumer = new Consumer(classicBuffer);
            producer = new Producer(classicBuffer);
        }
        else{
            consumer = new Consumer(blockBuffer);
            producer = new Producer(blockBuffer);
        }
        thread_1 = new Thread(consumer);
        thread_2 = new Thread(producer);
        thread_3 = new Thread(producer);
        thread_4 = new Thread(producer);
        
        thread_1.start();
        thread_2.start();
        thread_3.start();
        thread_4.start();
    }
}

0號是來看看結(jié)果吧昧谊,先上condition的結(jié)果

Condition_reentrant_result.png

因為隊列只有1容量,出現(xiàn)了與預(yù)想中一樣很均勻的線程切換: 一個生產(chǎn)者挖炬,一個消費者輪流切換揽浙,沒有任何多余的線程喚醒。

再看object wait/notify的結(jié)果

Condition_synchronized_result.png

是時候做分析了
我們先看回上面的object和condition實現(xiàn)的阻塞隊列代碼意敛。再次貼一些關(guān)鍵的部分, 以生產(chǎn)者為例馅巷,

// ConditionBuffer.Put()
lock.lock();
try{
    while(count == CAPACITY){
        notFull.await();
        System.out.println("Reentrant_put: "+Thread.currentThread());
    }
    list.add(e);
    count++;
    notEmpty.signal();
}finally {
    lock.unlock();
}

//ClassicBuffer.Put()
synchronized (lock) {   
try{
    while(count == CAPACITY){
        lock.wait();
        System.out.println("Classic_Put: "+Thread.currentThread());
    }
    list.add(e);
    count++;
    lock.notifyAll();
}catch (InterruptedException exception) {
// TODO: handle exception
    exception.printStackTrace();
    }
}

很明顯,對比兩個結(jié)果草姻,object實現(xiàn)的結(jié)果比condition實現(xiàn)的結(jié)果每次多了兩條無謂線程的切換钓猬,因為object每次是以notifyAll來喚醒的,所以所有等待中的線程撩独,無論是生產(chǎn)者和消費者都要被喚醒敞曹。
但考慮到隊列容量只有1账月,當(dāng)生產(chǎn)者線程1完成數(shù)據(jù)插入時,它會把生產(chǎn)者線程2澳迫,3以及消費者線程0給喚醒局齿,顯然,生產(chǎn)者線程此時被喚醒之后做的唯一一件事就是判斷容量是否等于1橄登,由于此時生產(chǎn)者線程1剛剛完成插入抓歼,因此,2拢锹,3生產(chǎn)者發(fā)現(xiàn)容量等于1谣妻,再次進入wait,相當(dāng)于他們這次醒來什么都沒干卒稳,造成線程切換的浪費蹋半。

然而聰明的你們可能已經(jīng)發(fā)現(xiàn)了"你這不公平!憑什么object要用notifyAll充坑,而condition用的是signal并非是signalAll!"
好减江,好,先把刀放下匪傍,signal能夠完成任務(wù)咱就不討論用signalAll了您市,因為有快的方法就沒有必要用慢的對吧。那我們討論能不能用notify役衡,把消費者所有的notifyAll改成notify茵休,代碼就不貼出來了,直接看結(jié)果手蝎。

Condition_notify_result.png

咦榕莺?程序卡住不動了。為什么棵介?
我們分析一下結(jié)果

  1. 程序剛進入钉鸯,0號消費者啟動: 隊列容量0, 發(fā)現(xiàn)容量為0,阻塞自己邮辽。
  2. 生產(chǎn)者2號啟動: 發(fā)現(xiàn)隊列容量為0唠雕,插入數(shù)據(jù),容量變?yōu)?吨述。notify喚醒別的線程岩睁,然而很不幸,它喚醒了1號生產(chǎn)者揣云。
  3. 生產(chǎn)者1號啟動:發(fā)現(xiàn)隊列容量為1捕儒,接著睡。
    就這樣結(jié)束了,再也沒有別的線程能喚醒整個系統(tǒng)刘莹,因此卡死了阎毅。

但是為什么condition只用signal就可以,而不需要用signalAll呢点弯?
因為condition只會喚醒獲得相同條件鎖的線程扇调。也就是生產(chǎn)者喚醒的永遠(yuǎn)是消費者, 反之亦然。參考上面代碼蒲拉,生產(chǎn)者使用notEmpty.signal()肃拜,而它本身是以notFull.await()來阻塞自己的,所以生產(chǎn)者并不會喚醒生產(chǎn)者雌团,消費者大家可以同樣去分析。
好了士聪,大概就是這樣锦援,如果有什么不滿意的,歡迎討論剥悟。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末灵寺,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子区岗,更是在濱河造成了極大的恐慌略板,老刑警劉巖,帶你破解...
    沈念sama閱讀 217,277評論 6 503
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件慈缔,死亡現(xiàn)場離奇詭異叮称,居然都是意外死亡,警方通過查閱死者的電腦和手機藐鹤,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,689評論 3 393
  • 文/潘曉璐 我一進店門瓤檐,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人娱节,你說我怎么就攤上這事挠蛉。” “怎么了肄满?”我有些...
    開封第一講書人閱讀 163,624評論 0 353
  • 文/不壞的土叔 我叫張陵谴古,是天一觀的道長。 經(jīng)常有香客問我稠歉,道長掰担,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 58,356評論 1 293
  • 正文 為了忘掉前任轧抗,我火速辦了婚禮恩敌,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘横媚。我一直安慰自己纠炮,他們只是感情好月趟,可當(dāng)我...
    茶點故事閱讀 67,402評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著恢口,像睡著了一般孝宗。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上耕肩,一...
    開封第一講書人閱讀 51,292評論 1 301
  • 那天因妇,我揣著相機與錄音,去河邊找鬼猿诸。 笑死婚被,一個胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的梳虽。 我是一名探鬼主播址芯,決...
    沈念sama閱讀 40,135評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼窜觉!你這毒婦竟也來了谷炸?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 38,992評論 0 275
  • 序言:老撾萬榮一對情侶失蹤禀挫,失蹤者是張志新(化名)和其女友劉穎旬陡,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體语婴,經(jīng)...
    沈念sama閱讀 45,429評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡描孟,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,636評論 3 334
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了腻格。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片画拾。...
    茶點故事閱讀 39,785評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖菜职,靈堂內(nèi)的尸體忽然破棺而出青抛,到底是詐尸還是另有隱情,我是刑警寧澤酬核,帶...
    沈念sama閱讀 35,492評論 5 345
  • 正文 年R本政府宣布蜜另,位于F島的核電站,受9級特大地震影響嫡意,放射性物質(zhì)發(fā)生泄漏举瑰。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,092評論 3 328
  • 文/蒙蒙 一蔬螟、第九天 我趴在偏房一處隱蔽的房頂上張望此迅。 院中可真熱鬧,春花似錦、人聲如沸耸序。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,723評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽坎怪。三九已至罢坝,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間搅窿,已是汗流浹背嘁酿。 一陣腳步聲響...
    開封第一講書人閱讀 32,858評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留男应,地道東北人闹司。 一個月前我還...
    沈念sama閱讀 47,891評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像殉了,于是被迫代替她去往敵國和親开仰。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,713評論 2 354

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

  • layout: posttitle: 《Java并發(fā)編程的藝術(shù)》筆記categories: Javaexcerpt...
    xiaogmail閱讀 5,819評論 1 19
  • Java-Review-Note——4.多線程 標(biāo)簽: JavaStudy PS:本來是分開三篇的薪铜,后來想想還是整...
    coder_pig閱讀 1,649評論 2 17
  • Java并發(fā)總結(jié) 1.多線程的優(yōu)點 資源利用率更好 程序在某些情況下更簡單 程序響應(yīng)更快 2.創(chuàng)建線程 1.實現(xiàn)R...
    不會上樹的猴子閱讀 1,027評論 0 5
  • 日落西山凹,人約黃昏后恩溅。 請君多珍重隔箍,他日在聚首。
    郝逗閱讀 169評論 0 0
  • 雷雁雄8月22日總結(jié):今天早上和合作伙伴在一起再次策劃周年店慶的活動脚乡,我發(fā)現(xiàn)我和合作伙伴都有拖延癥蜒滩,每件事都是逼不...
    雷雁雄閱讀 126評論 0 0