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