線程之間通信 等待(wait)和通知(notify)

線程通信概念:

    線程是操作系統中獨立的個體,但這些個體如果不經過特殊的處理就不能成爲一個整體,線程之間的通信就成爲整體的必用方式之一。當線程存在通信指揮,系統間的交互性會更強大,在提高CPU利用率的同時還會對線程任務在處理過程中進行有效的把控與監督。

爲了支持多線程之間的協作,JDK提供了兩個非常重要的接口線程等待wait()方法和通知notify()方法。這兩個方法並不是在Thread類中的,而是輸出Object類。這也意味着任何對象都可以調用這2個方法。

我們先看一個簡單的例子:

 1 public class ListAdd1 {
 2     private volatile static List list = new ArrayList();
 3     public void add(){
 4         list.add("jianzh5");
 5     }
 6     public int size(){
 7         return list.size();
 8     }
 9 
10     public static void main(String[] args) {
11         final ListAdd1 list1 = new ListAdd1();
12         Thread t1 = new Thread(new Runnable() {
13             @Override
14             public void run() {
15                 try {
16                     for(int i = 0; i <10; i++){
17                         list1.add();
18                         System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了一個元素..");
19                         Thread.sleep(500);
20                     }
21                 } catch (InterruptedException e) {
22                     e.printStackTrace();
23                 }
24             }
25         }, "t1");
26 
27         Thread t2 = new Thread(new Runnable() {
28             @Override
29             public void run() {
30                 while(true){
31                     if(list1.size() == 5){
32                         System.out.println("當前線程收到通知:" + Thread.currentThread().getName() + " list size = 5 線程停止..");
33                         throw new RuntimeException();
34                     }
35                 }
36             }
37         }, "t2");
38         t1.start();
39         t2.start();
40     }
41 }

代碼很簡單,這是在沒使用JDK線程協作時的做法。線程t2一直在死循環,當list的size等於5時退出t2,t1則繼續運行。

這樣其實也可以是說線程之間的協作,但是問題就是t2會一直循環運行,浪費了CPU資源(PS:list必須使用關鍵字volatile修飾)。

我們再看使用wait和notify時的代碼:

 1 public class ListAdd2 {
 2     private volatile static List list = new ArrayList();
 3 
 4     public void add(){
 5         list.add("jianzh5");
 6     }
 7     public int size(){
 8         return list.size();
 9     }
10 
11     public static void main(String[] args) {
12 
13         final ListAdd2 list2 = new ListAdd2();
14         final byte[] lock = new byte[0];
15         Thread t1 = new Thread(new Runnable() {
16             @Override
17             public void run() {
18                 try {
19                     synchronized (lock) {
20                         System.out.println("t1啓動..");
21                         for(int i = 0; i <10; i++){
22                             list2.add();
23                             System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了一個元素..");
24                             Thread.sleep(500);
25                             if(list2.size() == 5){
26                                 System.out.println("已經發出通知..");
27                                 lock.notify();
28                             }
29                         }
30                     }
31                 } catch (InterruptedException e) {
32                     e.printStackTrace();
33                 }
34 
35             }
36         }, "t1");
37 
38         Thread t2 = new Thread(new Runnable() {
39             @Override
40             public void run() {
41                 synchronized (lock) {
42                     System.out.println("t2啓動..");
43                     if(list2.size() != 5){
44                         try {
45                             lock.wait();
46                         } catch (InterruptedException e) {
47                             e.printStackTrace();
48                         }
49                     }
50                     System.out.println("當前線程:" + Thread.currentThread().getName() + "收到通知線程停止..");
51                     throw new RuntimeException();
52                 }
53             }
54         }, "t2");
55         t2.start();
56         t1.start();
57     }
58 }

這裏首先創建了一個的byte[]對象lock,然後線程t1,t2使用synchronzied關鍵字同步lock對象。線程t1一直往list添加元素,當元素大小等於5的時候調用lock.notify()方法通知lock對象。線程t2在size不等於5的時候一直處於等待狀態。

這裏使用byte[0]數組是因爲JVM創建byte[0]所佔用的空間比普通的object對象小,而花費的代價也最小。

運行結果如下:

看到這裏可能會有疑問,爲什麼t1通知了t2線程運行而結果卻是t1先運行完後t2再運行。

說明如下:

1、wait() 和 notify()必須配合synchrozied關鍵字使用,無論是wait()還是notify()都需要首先獲取目標對象的一個監聽器。

2、wait()釋放鎖,而notify()不釋放鎖。

 線程t2一開始處於wait狀態,這時候釋放了鎖所以t1可以一直執行,而t1在notify的時候並不會釋放鎖,所以t1還會繼續運行。 

 

知識拓展

現在我們來探討一下有界阻塞隊列的實現原理並模擬一下它的實現 :

1、有界隊列顧名思義是有容器大小限制的

2、當調用put()方法時,如果此時容器的長度等於限定的最大長度,那麼該方法需要阻塞直到隊列可以有空間容納下添加的元素

3、當調用take()方法時,如果此時容器的長度等於最小長度0,那麼該方法需要阻塞直到隊列中有了元素能夠取出

4、put() 和 take()方法是需要協作的,能夠及時通知狀態進行插入和移除操作

根據以上阻塞隊列的幾個屬性,我們可以使用wait 和notify實現以下它的實現原理:

 

/**
 * 自定義大小的阻塞容器
 */
public class MyQueue {
    //1、初始化容器
    private final LinkedList<Object> list = new LinkedList<>();
    //2、定義計數器
    private AtomicInteger count = new AtomicInteger(0);
    //3、設定容器的上限和下限
    private final int minSize = 0;
    private final int maxSize;

    //4、構造器
    public MyQueue(int size) {
        this.maxSize = size;
    }

    //5、定義鎖對象
    private final Object lock = new Object();

    //6、阻塞增加方法
    public void put(Object obj) {
        synchronized (lock) {
            while (count.get() == this.maxSize) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //加入元素 計數器累加 喚醒取數線程可以取數
            list.add(obj);
            count.incrementAndGet();
            lock.notify();
            System.out.println("新增的元素:" + obj);
        }
    }

    public Object take() {
        Object result = null;
        synchronized (lock) {
            while (count.get() == this.minSize) {
                try {
                    lock.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //移除元素 計數器遞減 喚醒添加的線程可以添加元素
            result = list.removeFirst();
            count.decrementAndGet();
            lock.notify();
        }
        return result;
    }

    public int getSize() {
        return this.count.get();
    }

    public static void main(String[] args) {
        final MyQueue myQueue = new MyQueue(5);
        myQueue.put("a");
        myQueue.put("b");
        myQueue.put("c");
        myQueue.put("d");
        myQueue.put("e");

        System.out.println("當前隊列長度:" + myQueue.getSize());
        Thread t1 = new Thread(new Runnable() {
            @Override public void run() {
                myQueue.put("f");
                myQueue.put("g");
            }
        }, "t1");

        t1.start();

        Thread t2 = new Thread(new Runnable() {
            @Override public void run() {
                Object obj = myQueue.take();
                System.out.println("移除的元素爲:"+obj);
                Object obj2 = myQueue.take();
                System.out.println("移除的元素爲:"+obj2);
            }
        },"t2");

        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        t2.start();
    }
}

 

實現過程如下:

1、通過構造器初始化指定容器的大小

2、程序內部有一個AtomicInteger的計數器,當調用put()操作時此計數器加1;當調用take()方法時此計數器減1

3、在進行相應的take()和put()方法時會使用while判斷進行阻塞,會一直處於wait狀態,並在可以進行操作的時候喚醒另外一個線程可以進行相應的操作。

4、將此代碼運行可以看到相應的效果。

 

 

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章