線程通信概念:
線程是操作系統中獨立的個體,但這些個體如果不經過特殊的處理就不能成爲一個整體,線程之間的通信就成爲整體的必用方式之一。當線程存在通信指揮,系統間的交互性會更強大,在提高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、將此代碼運行可以看到相應的效果。