線程之間通信 等待(wait)和通知(notify)及Queue的模擬

線程通信概念:
線程是操作系統中獨立的個體,但這些個體如果不經過特殊的處理就不能成爲一個整體,線程之間的通信就成爲整體的必用方式之一。當線程存在通信指揮,系統間的交互性會更強大,在提高CPU利用率的同時還會對線程任務在處理過程中進行有效的把控與監督。
爲了支持多線程之間的協作,JDK提供了兩個非常重要的接口線程等待wait()方法和通知notify()方法。這兩個方法並不是在Thread類中的,而是輸出Object類。這也意味着任何對象都可以調用這2個方法。

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

public class ListAdd1 {
    private volatile static List list = new ArrayList();
    public void add(){
        list.add("jianzh5");
    }
    public int size(){
        return list.size();
    }

    public static void main(String[] args) {
        final ListAdd1 list1 = new ListAdd1();
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    for(int i = 0; i <10; i++){
                        list1.add();
                        System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了一個元素..");
                        Thread.sleep(500);
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                while(true){
                    if(list1.size() == 5){
                        System.out.println("當前線程收到通知:" + Thread.currentThread().getName() + " list size = 5 線程停止..");
                        throw new RuntimeException();
                    }
                }
            }
        }, "t2");
        t1.start();
        t2.start();
    }
}
代碼很簡單,這是在沒使用JDK線程協作時的做法。線程t2一直在死循環,當list的size等於5時退出t2,t1則繼續運行。
這樣其實也可以是說線程之間的協作,但是問題就是t2會一直循環運行,浪費了CPU資源(PS:list必須使用關鍵字volatile修飾)。

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

public class ListAdd2 {
    private volatile static List list = new ArrayList();

    public void add(){
        list.add("jianzh5");
    }
    public int size(){
        return list.size();
    }

    public static void main(String[] args) {

        final ListAdd2 list2 = new ListAdd2();
        final byte[] lock = new byte[0];
        Thread t1 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    synchronized (lock) {
                        System.out.println("t1啓動..");
                        for(int i = 0; i <10; i++){
                            list2.add();
                            System.out.println("當前線程:" + Thread.currentThread().getName() + "添加了一個元素..");
                            Thread.sleep(500);
                            if(list2.size() == 5){
                                System.out.println("已經發出通知..");
                                lock.notify();
                            }
                        }
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

            }
        }, "t1");

        Thread t2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock) {
                    System.out.println("t2啓動..");
                    if(list2.size() != 5){
                        try {
                            lock.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
                    System.out.println("當前線程:" + Thread.currentThread().getName() + "收到通知線程停止..");
                    throw new RuntimeException();
                }
            }
        }, "t2");
        t2.start();
        t1.start();
    }
}
這裏首先創建了一個的byte[]對象lock,然後線程t1,t2使用synchronzied關鍵字同步lock對象。線程t1一直往list添加元素,當元素大小等於5的時候調用lock.notify()方法通知lock對象。線程t2在size不等於5的時候一直處於等待狀態。
這裏使用byte[0]數組是因爲JVM創建byte[0]所佔用的空間比普通的object對象小,而花費的代價也最小。
運行結果如下:
t2啓動..
t1啓動..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
已經發出通知..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t1添加了一個元素..
當前線程:t2收到通知線程停止..
Exception in thread "t2" java.lang.RuntimeException
	at com.bjsxt.base.conn008.Abc$2.run(Abc.java:68)
	at java.lang.Thread.run(Thread.java:745)
看到這裏可能會有疑問,爲什麼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、將此代碼運行可以看到相應的效果。


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