天天看點

多線程六 線程間的通信

線程間的通信

    • 1. 線程間的通信
    • 2. 等待喚醒機制
      • 等待喚醒中的方法
    • 3. ThreadLocal
    • 4. 生産消費者模型

1. 線程間的通信

什麼是線程間的通信

多個線程協同處理同一個資源,線程的任務不相同

為什麼需要線程間的通信

多個線程并發執行時,在預設情況下CPU是随機切換線程的,當我們需要多個線程來共同完成一件任務,并且我們希望他們有規律的執行,那麼多線程之間需要協調通信,以此來幫我們達到多線程共同操作一份資料的目的。

如何保證線程間的通信

多個線程在操作同一份資料時,避免對同一共享變量的争奪,通過等待喚醒機制來保證線程能合理有有效的利用資源。

2. 等待喚醒機制

就像我們前面買票的例子,如果不加鎖線程在通路共享資源的時候就會競争,都去等待CPU的排程來争奪資源。但是,通過

synchronized

和Lock來對資源上鎖,使資源同時隻能被一個線程通路到,避免了并發帶來的不安全問題。這屬于線程間的競争。

就像人一樣,有競争就會有合作

線程間的合作就是一個線程進行了規定操作後,就進入等待狀态

wait

,等待其他線程執行完他們的指定代碼過後再将其喚酲

notify

。在有多個線程進行等待時,如果需要,可以使用

notifyAll

來喚酲所有的等待線程

wait / notify就是線程間的一種協作機制。

等待喚醒中的方法

wait、notify必須搭配synchronized使用

使用wait、notify()的前提:

必須在同步方法或同步代碼塊中使用(拿到相應對象的鎖),如果沒有

synchronized

會抛出

java.lang.IllegalMonitorStateException

(非法螢幕狀态異常)

wait----癡漢方法

持有鎖的線程調用

wait()

後會一直阻塞,直到有線程調用

notify()

将其喚醒

wait的重載方法:

等待一段時間,若還未被喚醒,繼續執行,預設機關為ms

public class WaitTest {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        synchronized (obj) {
            System.out.println("wait開始...");
            obj.wait(3000);
            System.out.println("wait結束...");
        }
    }
}
           

不加喚醒時間,沒有notify就會一直處于等待中

多線程六 線程間的通信

添加喚醒時間,在規定的時間内沒有被喚醒就會自動結束等待

多線程六 線程間的通信

notify

喚醒任意一個處于等待狀态的線程(notify方法就是使等待的線程繼續運作)

多線程六 線程間的通信
多線程六 線程間的通信

等待喚酲機制就是用于解決線程間通信的問題的,使用到的3個方法的含義如下:

  1. wait: 線程不再參與競争鎖,不再等待CPU排程,進入wait set中,此時線程的狀态是WAITING。它要等待别的線程執喚醒它(notify),通知它從wait set中釋放出來,重新進入到排程隊列 ready queue中
  2. notify: 喚醒等待的線程,通知等待的線程從wait set中釋放,重新進入到排程隊列 ready queue中
  3. notifyAll: 喚醒所有等待的線程

注意:

wait會釋放鎖,notify僅僅隻是通知,不釋放鎖。

哪怕隻通知了—個等待的線程,被通知線程也不能立即恢複執行,因為它當國中斷的地方是在同步塊内,而此刻它已經不持有鎖(wait方法會釋放鎖來等待),是以它需要再次嘗試去擷取鎖(很可能面臨其它線程的競争),成功後才能在當初調用wait方法之後的地方恢複執行。

  1. wait方法與notify方法必須要由同一個鎖對象調用
  2. wait方法與notify方法是屬于Object類的方法的
  3. wait方法與notify方法必須要在同步代碼塊或者是同步方法中使用

總結:

  • 如果能擷取鎖,線程就從WAITING狀态變為就緒狀态
  • 如果沒有擷取到鎖,就從wait set中出來然後進入到entry set,線程從WAITING變為阻塞狀态

notify()喚醒等待的線程:

class Sync implements Runnable{
    //标志位來喚醒等待的線程
    private boolean flag;
    private Object obj;

    public Sync(Object obj,boolean flag) {
        this.obj = obj;
        this.flag = flag;
    }

    public void waitMethod() {
        synchronized (obj) {
            while (true) {
                try {
                    System.out.println("wait方法開始..." + Thread.currentThread().getName());
                    obj.wait();
                    System.err.println("wait方法結束..." + Thread.currentThread().getName());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public void notifyMethod() {
        synchronized (obj) {
            System.out.println("notify方法開始,喚醒等待的線程" + Thread.currentThread().getName());
            obj.notify();
            System.err.println("notify方法結束!!!" + Thread.currentThread().getName());
        }
    }


    @Override
    public void run() {
        if (flag) {
            this.waitMethod();
        }else {
            this.notifyMethod();
        }
    }
}

public class SyncWaitNotify {
    public static void main(String[] args) throws InterruptedException {
        Object obj = new Object();
        Sync wait = new Sync(obj,true);
        Sync notify = new Sync(obj,false);
        new Thread(wait,"wait線程").start();
        Thread.sleep(2000);
        new Thread(notify,"notify線程").start();
    }
}
           
多線程六 線程間的通信

運作結果分析:

從結果上來看第一個線程執行的是一個waitMethod方法,該方法裡面有個死循環并且使用了wait方法進入等待狀态将鎖釋放,如果這個線程不被喚醒的話将會一直等待下去,這個時候第二個線程執行的是notifyMethod方法,該方法裡面執行了一個喚醒線程的操作,并且一直将notify的同步代碼塊執行完畢之後才會釋放鎖然後繼續執行wait結束列印語句。

多線程六 線程間的通信
多線程六 線程間的通信

任意一個Object及其子類對象都有兩個隊列:

  • 同步隊列:所有嘗試擷取該對象

    Monitor

    失敗的線程,都加入同步隊列,排隊擷取
  • 等待隊列:已經拿到了鎖的線程在等待其他資源時,主動釋放鎖,置入該對象等待隊列中,等待其被喚醒;當調用

    notify()

    會在等待隊列中任意喚醒一個線程,将其置入到同步隊列尾部,排隊擷取鎖

notifyAll

将等待隊列中的所有線程喚醒,并且加入到同步隊列

敲黑闆:

既然學習了

wait()

,它使目前線程處于等待中,需要用

notify()

或者

notifyAll()

來喚醒,那麼它和

sleep()

有何差別呢?

  1. sleep()是Thread類中定義的方法,到了一定的時間後該線程自動喚醒,不會釋放對象鎖。
  2. wait()是Object類中定義的方法,要想喚醒必須使用notify()、notifyAll()方法才能喚醒,會釋放對象鎖

總結一下就是:所在的部門不同,喚醒的方法也不同

3. ThreadLocal

ThreadLocal----線程本地變量(屬于線程私有資源,不與其他線程共享)

set()

設定線程私有屬性值

get()

取得線程私有屬性值

  • 在使用 ThreadLocal 類型變量進行相關操作時,都會通過目前線程擷取到 ThreadLocalMap 來完成操作。
  • 每個線程的

    ThreadLocalMap

    (存放元素)是屬于線程自己的,

    ThreadLocalMap

    中維護的值也是屬于線程自己的。這就保證了

    ThreadLocal

    類型的變量在每個線程中是獨立的,在多線程環境下不會互相影響

4. 生産消費者模型

生産者消費者模式是通過一個容器來解決生産者和消費者的強耦合問題。生産者和消費者彼此之間不直接通訊,而通過阻塞隊列來進行通訊,是以生産者生産完資料之後不用等待消費者處理,直接扔給阻塞隊列,消費者不找生産者要資料,而是直接從阻塞隊列裡取,阻塞隊列就相當于一個緩沖區,平衡了生産者和消費者的處理能力。

這個阻塞隊列就是用來給生産者和消費者解耦的。大多數設計模式,都會找一個第三者出來進行解耦,如工廠模式的第三者是工廠類,模闆模式的第三者是模闆類。在學習一些設計模式的過程中,如果先找到這個模式的第三者,能幫助我們快速熟悉一個設計模式

舉一個生活中的例子來說明:

多線程六 線程間的通信

作為一個即将邁入社會并被生活毒打的卑微青年,畢業後到“工地搬磚”一定會遇到租房子的問題。對于房東來說,我是一個消費者,她是房子的生産者。我要租房子,一定得和房東協商,那麼,這個效率就比較低了,如果這個房子我覺得不合适,隻能再去看房子,再和其它的房東進行協商;而房東呐,她也隻能等着房客來看房子。

但是,有一個機構它聰明呀,他可能偷偷看了“生産者消費者模型”,了解到了其中的真谛,于是,他作為“中介”的角色出現了…現在,他到各個房東手上收集房源,然後整理出來給租客們選擇,然後悶聲發大财。

那麼,中介這個角色是不是就相當于“容器”來解決生産者(房東)和消費者(租客)的強耦合問題。房客住的房子有問題了,找中介;房東想漲房租,找中介;中介來調和房東與房客之間的問題,不在需要房東與房客之間有聯系。

代碼試着來實作上述的邏輯:

class Goods {
    private String rooms;  //貨物名稱
    private int count;       //貨物庫存

    //生産商品
    public synchronized void set(String rooms) {
        this.rooms = rooms;
        this.count = count + 1;
        System.out.println(Thread.currentThread().getName()+ " 生産" + this);
    }

    //消費商品
    public synchronized void get() {
        this.count = this.count - 1;
        System.out.println(Thread.currentThread().getName() + " 消費" + this);
    }

    @Override
    public String toString() {
        return "Goods { " +
                "rooms='" + rooms + '\'' +
                ", count=" + count +
                '}';
    }
}

class Producer implements Runnable {
    private Goods goods;
    public Producer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        this.goods.set("海景别墅一套,房租減半,水電全免...");
    }
}

class Consumer implements Runnable {
    private Goods goods;
    public Consumer(Goods goods) {
        this.goods = goods;
    }

    @Override
    public void run() {
        this.goods.get();
    }
}

public class ProducerConsumer {
    public static void main(String[] args) throws InterruptedException {
        Goods goods = new Goods();
        new Thread(new Producer(goods),"生産者線程").start();
        Thread.sleep(1000);
        new Thread(new Consumer(goods),"消費者線程").start();
    }
}
           
多線程六 線程間的通信

ATTENTION:

那麼問題來了,将生産者線程啟動和消費者線程啟動的代碼換個位置。此時問題産生了,生産者還沒生産商品,消費者就要消費商品,導緻數量不正确。

多線程六 線程間的通信