四满俗、Java線程間通信

摘自《Java并發(fā)編程的藝術(shù)》

1 volatile和synchronized關(guān)鍵字

關(guān)鍵字volatile可以用來修飾字段(成員變量)腻菇,就是告知程序任何對該變量的訪問均需要從共享內(nèi)存中獲取,而對它的改變必須同步刷新回共享內(nèi)存惦积,它能保證所有線程對變量訪問的可見性接校。

關(guān)鍵字synchronized可以修飾方法或者以同步塊的形式來進行使用,它主要確保多個線程在同一個時刻狮崩,只能有一個線程處于方法或者同步塊中蛛勉,它保證了線程對變量訪問的可見性和排他性

通過使用javap工具查看生成的class文件信息來分析synchronized關(guān)鍵字的實現(xiàn)細節(jié)厉亏,代碼如下

public class Synchronized {

    public static void main(String[] args) {
        synchronized (Synchronized.class){
            m();
        }
    }
    public static synchronized void m(){
    }
}

執(zhí)行javap -v Synchronized.class董习,部分相關(guān)輸出如下所示:

對于同步塊的實現(xiàn)使用了 monitorentermonitorexit 指令,而同步方法則是依賴方法修飾符上的ACC_SYNCHRONIZED來完成爱只。無論采用哪種方式皿淋,其本質(zhì)是對一個對象的監(jiān)視器進行獲取招刹,而這個獲取過程是排他的,也就是同一時刻只能有一個線程獲取到由synchronized所保護對象的監(jiān)視器窝趣。

任意一個對象都擁有自己的監(jiān)視器疯暑,當這個對象由同步塊或者這個對象的同步方法調(diào)用時,執(zhí)行方法的線程必須先獲取到該對象的監(jiān)視器才能進入同步塊或者同步方法哑舒,而沒有獲取到監(jiān)視器(執(zhí)行該方法)的線程將會被阻塞在同步塊和同步方法的入口處妇拯,進入BLOCKED狀態(tài)。

下圖描述了對象洗鸵、對象的監(jiān)視器越锈、同步隊列和執(zhí)行線程之間的關(guān)系:

從圖中可以看到,任意線程對Object(Object由synchronized保護)的訪問膘滨,首先要獲得Object的監(jiān)視器甘凭。如果獲取失敗,線程進入同步隊列火邓,線程狀態(tài)變?yōu)锽LOCKED丹弱。當訪問Object的前驅(qū)(獲得了鎖的線程)釋放了鎖,則該釋放操作喚醒阻塞在同步隊列中的線程铲咨,使其重新嘗試對監(jiān)視器的獲取躲胳。

2 等待 / 通知機制

等待/通知機制是指一個線程A調(diào)用了對象O的wait()方法進入等待狀態(tài),而另一個線程B調(diào)用了對象O的notify()或notifyAll()方法纤勒,線程A收到通知后從對象O的wait()方法返回坯苹,進而執(zhí)行后續(xù)操作。上述兩個線程對象O來完成交互踊东,而對象上的wait()和notify/notifyAll()的關(guān)系就如同開關(guān)信號一樣北滥,用來完成等待方通知方之間的交互工作。

下面所示的例子中闸翅,創(chuàng)建了兩個線程——WaitThread和NotifyThread,前者檢查flag值是否為false菊霜,如果符合要求坚冀,進行后續(xù)操作,否則在lock上等待鉴逞,后者在睡眠了一段時間后對lock進行通知记某,示例如下所示。

public class WaitNotify {
    static boolean flag = true;
    static Object lock = new Object();
 
    public static void main(String[] args) throws Exception {
        Thread waitThread = new Thread(new Wait(), "WaitThread");
        waitThread.start();
        TimeUnit.SECONDS.sleep(1);
        Thread notifyThread = new Thread(new Notify(), "NotifyThread");
        notifyThread.start();
    }
 
    static class Wait implements Runnable {
        public void run() {
            // 加鎖构捡,擁有l(wèi)ock的Monitor
            synchronized (lock) {
                // 當條件不滿足時液南,繼續(xù)wait,同時釋放了lock的鎖
                while (flag) {
                    try {
                        System.out.println(Thread.currentThread()
                                + " flag is true. wait@ "
                                + new SimpleDateFormat("HH:mm:ss")
                                        .format(new Date()));
                        lock.wait();
                    } catch (InterruptedException e) {
                    }
                }
                // 條件滿足時勾徽,完成工作
                System.out.println(Thread.currentThread()
                        + " flag is false. running@ "
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
            }
        }
    }
 
    static class Notify implements Runnable {
        public void run() {
            // 加鎖滑凉,擁有l(wèi)ock的Monitor
            synchronized (lock) {
                // 獲取lock的鎖,然后進行通知,通知時不會釋放lock的鎖畅姊,
                // 直到當前線程釋放了lock后咒钟,WaitThread才能從wait方法中返回
                System.out.println(Thread.currentThread()
                        + " hold lock. notify @ "
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                lock.notifyAll();
                flag = false;
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 再次加鎖
            synchronized (lock) {
                System.out.println(Thread.currentThread()
                        + " hold lock again. sleep@ "
                        + new SimpleDateFormat("HH:mm:ss").format(new Date()));
                try {
                    TimeUnit.SECONDS.sleep(5);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
}

輸出如下(輸出內(nèi)容可能不同,主要區(qū)別在時間上)

Thread[WaitThread,5,main] flag is true. wait@ 16:15:44
Thread[NotifyThread,5,main] hold lock. notify @ 16:15:45
Thread[NotifyThread,5,main] hold lock again. sleep@ 16:15:50
Thread[WaitThread,5,main] flag is false. running@ 16:15:55

上述第3行和第4行輸出的順序可能會互換若未,而上述例子主要說明了調(diào)用wait()朱嘴、notify()以及notifyAll()時需要注意的細節(jié),如下粗合。

  1. 使用wait()萍嬉、notify()和notifyAll()時需要先對調(diào)用對象加鎖
  2. 調(diào)用wait()方法后隙疚,線程狀態(tài)由RUNNING變?yōu)閃AITING壤追,并將當前線程放置到對象的等待隊列。
  3. notify()或notifyAll()方法調(diào)用后甚淡,等待線程依舊不會從wait()返回大诸,需要調(diào)用notify()或notifAll()的線程釋放鎖之后,等待線程才有機會從wait()返回贯卦。
  4. notify()方法將等待隊列中的一個等待線程從等待隊列中移到同步隊列中资柔,而notifyAll()方法則是將等待隊列中所有的線程全部移到同步隊列,被移動的線程狀態(tài)由WAITING變?yōu)锽LOCKED撵割。
  5. 從wait()方法返回的前提是獲得了調(diào)用對象的鎖贿堰。

從上述細節(jié)中可以看到,等待/通知機制依托于同步機制啡彬,其目的就是確保等待線程從wait()方法返回時能夠感知到通知線程對變量做出的修改羹与。下圖描述了上述示例的過程。

在圖中庶灿,WaitThread首先獲取了對象的鎖纵搁,然后調(diào)用對象的wait()方法,從而放棄了鎖并進入了對象的等待隊列WaitQueue中往踢,進入等待狀態(tài)腾誉。由于WaitThread釋放了對象的鎖,NotifyThread隨后獲取了對象的鎖峻呕,并調(diào)用對象的notify()方法利职,將WaitThread從WaitQueue移到SynchronizedQueue中,此時WaitThread的狀態(tài)變?yōu)樽枞麪顟B(tài)瘦癌。NotifyThread釋放了鎖之后猪贪,WaitThread再次獲取到鎖并從wait()方法返回繼續(xù)執(zhí)行。

2.1 生產(chǎn)者/消費者模式

Consumer.java

public class Consumer extends Thread {
    // 每次消費的產(chǎn)品數(shù)量
    private int num;
 
    // 所在放置的倉庫
    private Storage storage;
 
    // 構(gòu)造函數(shù)讯私,設(shè)置倉庫
    public Consumer(Storage storage) {
        this.storage = storage;
    }
 
    // 線程run函數(shù)
    public void run() {
        consume(num);
    }
 
    // 調(diào)用倉庫Storage的生產(chǎn)函數(shù)
    public void consume(int num) {
        storage.consume(num);
    }
 
    // get/set方法
    public int getNum() {
        return num;
    }
 
    public void setNum(int num) {
        this.num = num;
    }
 
    public Storage getStorage() {
        return storage;
    }
 
    public void setStorage(Storage storage) {
        this.storage = storage;
    }
}

Producer.java

public class Producer extends Thread {
    // 每次生產(chǎn)的產(chǎn)品數(shù)量
    private int num;
 
    // 所在放置的倉庫
    private Storage storage;
 
    // 構(gòu)造函數(shù)热押,設(shè)置倉庫
    public Producer(Storage storage) {
        this.storage = storage;
    }
 
    // 線程run函數(shù)
    public void run() {
        produce(num);
    }
 
    // 調(diào)用倉庫Storage的生產(chǎn)函數(shù)
    public void produce(int num) {
        storage.produce(num);
    }
 
    // get/set方法
    public int getNum() {
        return num;
    }
 
    public void setNum(int num) {
        this.num = num;
    }
 
    public Storage getStorage() {
        return storage;
    }
 
    public void setStorage(Storage storage) {
        this.storage = storage;
    }
}

Storage.java

public class Storage {
    // 倉庫最大存儲量
    private final int MAX_SIZE = 100;
 
    // 倉庫存儲的載體
    private LinkedList<Object> list = new LinkedList<Object>();
 
    // 生產(chǎn)num個產(chǎn)品
    public void produce(int num) {
        // 同步代碼段
        synchronized (list) {
            // 如果倉庫剩余容量不足
            while (list.size() + num > MAX_SIZE) {
                System.out.println("【要生產(chǎn)的產(chǎn)品數(shù)量】:" + num + "\t【庫可以存放存量】:"
                        + list.size() + "\t暫時不能執(zhí)行生產(chǎn)任務(wù)!");
                try {
                    // 由于條件不滿足西傀,生產(chǎn)阻塞
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
 
            // 生產(chǎn)條件滿足情況下,生產(chǎn)num個產(chǎn)品
            for (int i = 1; i <= num; ++i) {
                list.add(new Object());
            }
 
            System.out.println("【已經(jīng)生產(chǎn)產(chǎn)品數(shù)】:" + num + "\t【現(xiàn)倉儲量為】:" + list.size());
            // 通知消費者來消費
            list.notifyAll();
        }
    }
 
    // 消費num個產(chǎn)品
    public void consume(int num) {
        // 同步代碼段
        synchronized (list) {
            // 如果倉庫存儲量不足
            while (list.size() < num) {
                System.out.println("【要消費的產(chǎn)品數(shù)量】:" + num + "\t【庫存量】:"
                        + list.size() + "\t暫時不能執(zhí)行生產(chǎn)任務(wù)!");
                try {
                    // 由于條件不滿足楞黄,消費阻塞
                    list.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
 
            // 消費條件滿足情況下池凄,消費num個產(chǎn)品
            for (int i = 1; i <= num; ++i) {
                list.remove();
            }
 
            System.out.println("【已經(jīng)消費產(chǎn)品數(shù)】:" + num + "\t【現(xiàn)倉儲量為】:" + list.size());
            // 通知生產(chǎn)者生產(chǎn)
            list.notifyAll();
        }
    }
 
    // get/set方法
    public LinkedList<Object> getList() {
        return list;
    }
 
    public void setList(LinkedList<Object> list) {
        this.list = list;
    }
 
    public int getMAX_SIZE() {
        return MAX_SIZE;
    }
}

Test.java

public class Test {
    public static void main(String[] args) {
        // 倉庫對象
        Storage storage = new Storage();
 
        // 生產(chǎn)者對象
        Producer p1 = new Producer(storage);
        Producer p2 = new Producer(storage);
        Producer p3 = new Producer(storage);
        Producer p4 = new Producer(storage);
        Producer p5 = new Producer(storage);
        Producer p6 = new Producer(storage);
        Producer p7 = new Producer(storage);
 
        // 消費者對象
        Consumer c1 = new Consumer(storage);
        Consumer c2 = new Consumer(storage);
        Consumer c3 = new Consumer(storage);
 
        // 設(shè)置生產(chǎn)者產(chǎn)品生產(chǎn)數(shù)量
        p1.setNum(10);
        p2.setNum(10);
        p3.setNum(10);
        p4.setNum(10);
        p5.setNum(10);
        p6.setNum(10);
        p7.setNum(80);
 
        // 設(shè)置消費者產(chǎn)品消費數(shù)量
        c1.setNum(50);
        c2.setNum(20);
        c3.setNum(30);
 
        // 線程開始執(zhí)行
        c1.start();
        c2.start();
        c3.start();
        p1.start();
        p2.start();
        p3.start();
        p4.start();
        p5.start();
        p6.start();
        p7.start();
    }
}

3 Thread.join()的使用

如果一個線程A執(zhí)行了thread.join()語句,其含義是:當前線程A等待thread線程終止之后才 從thread.join()返回鬼廓。線程Thread除了提供join()方法之外肿仑,還提供了join(long millis)和join(long millis,int nanos)兩個具備超時特性的方法。這兩個超時方法表示碎税,如果線程thread在給定的超時時間里沒有終止尤慰,那么將會從該超時方法中返回。

4 ThreadLocal的使用

ThreadLocal雷蹂,即線程變量伟端,是一個以ThreadLocal對象為鍵、任意對象為值的存儲結(jié)構(gòu)匪煌。這 個結(jié)構(gòu)被附帶在線程上责蝠,也就是說一個線程可以根據(jù)一個ThreadLocal對象查詢到綁定在這個線程上的一個值。

可以通過set(T)方法來設(shè)置一個值萎庭,在當前線程下再通過get()方法獲取到原先設(shè)置的值霜医。

在代碼清單4-15所示的例子中,構(gòu)建了一個常用的Profiler類驳规,它具有begin()和end()兩個方法肴敛,而end()方法返回從begin()方法調(diào)用開始到end()方法被調(diào)用時的時間差,單位是毫秒吗购。

public class Profiler {
    // 第一次get()方法調(diào)用時會進行初始化(如果set方法沒有調(diào)用)医男,每個線程會調(diào)用一次
    private static final ThreadLocal<Long> TIME_THREADLOCAL = 
            new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return System.currentTimeMillis();
        }
    };

    public static void main(String[] args) throws InterruptedException {
        Profiler.begin();
        TimeUnit.SECONDS.sleep(1);
        System.out.println("Time cost is: " + Profiler.end() + " mills");
    }

    public static final void begin() {
        TIME_THREADLOCAL.set(System.currentTimeMillis());
    }

    public static final long end() {
        // 時間消耗
        return System.currentTimeMillis() - TIME_THREADLOCAL.get();
    }
}

輸出結(jié)果如下所示

Cost: 1001 mills
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市捻勉,隨后出現(xiàn)的幾起案子镀梭,更是在濱河造成了極大的恐慌,老刑警劉巖踱启,帶你破解...
    沈念sama閱讀 217,826評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件丰辣,死亡現(xiàn)場離奇詭異,居然都是意外死亡禽捆,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,968評論 3 395
  • 文/潘曉璐 我一進店門飘哨,熙熙樓的掌柜王于貴愁眉苦臉地迎上來胚想,“玉大人,你說我怎么就攤上這事芽隆∽欠” “怎么了统屈?”我有些...
    開封第一講書人閱讀 164,234評論 0 354
  • 文/不壞的土叔 我叫張陵,是天一觀的道長牙躺。 經(jīng)常有香客問我愁憔,道長,這世上最難降的妖魔是什么孽拷? 我笑而不...
    開封第一講書人閱讀 58,562評論 1 293
  • 正文 為了忘掉前任吨掌,我火速辦了婚禮,結(jié)果婚禮上脓恕,老公的妹妹穿的比我還像新娘膜宋。我一直安慰自己,他們只是感情好炼幔,可當我...
    茶點故事閱讀 67,611評論 6 392
  • 文/花漫 我一把揭開白布秋茫。 她就那樣靜靜地躺著,像睡著了一般乃秀。 火紅的嫁衣襯著肌膚如雪肛著。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,482評論 1 302
  • 那天跺讯,我揣著相機與錄音枢贿,去河邊找鬼。 笑死抬吟,一個胖子當著我的面吹牛萨咕,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播火本,決...
    沈念sama閱讀 40,271評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼危队,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了钙畔?” 一聲冷哼從身側(cè)響起茫陆,我...
    開封第一講書人閱讀 39,166評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎擎析,沒想到半個月后簿盅,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,608評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡揍魂,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,814評論 3 336
  • 正文 我和宋清朗相戀三年桨醋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片现斋。...
    茶點故事閱讀 39,926評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡喜最,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出庄蹋,到底是詐尸還是另有隱情瞬内,我是刑警寧澤迷雪,帶...
    沈念sama閱讀 35,644評論 5 346
  • 正文 年R本政府宣布,位于F島的核電站虫蝶,受9級特大地震影響章咧,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜能真,卻給世界環(huán)境...
    茶點故事閱讀 41,249評論 3 329
  • 文/蒙蒙 一赁严、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧舟陆,春花似錦误澳、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,866評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至踱承,卻和暖如春倡缠,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背茎活。 一陣腳步聲響...
    開封第一講書人閱讀 32,991評論 1 269
  • 我被黑心中介騙來泰國打工昙沦, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人载荔。 一個月前我還...
    沈念sama閱讀 48,063評論 3 370
  • 正文 我出身青樓盾饮,卻偏偏與公主長得像,于是被迫代替她去往敵國和親懒熙。 傳聞我的和親對象是個殘疾皇子丘损,可洞房花燭夜當晚...
    茶點故事閱讀 44,871評論 2 354

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