學習阻塞隊列之前需要掌握線程的wait(),notify()以及notifyAll()的相關知識!
①wait()爲什麼要放置在while循環中,而不是使用if進行判斷
②notify()和notifyAll()有什麼區別?爲什麼有時候認爲該使用notify()的地方卻使用了notifyAll()?
上面這兩個問題是我在學習多線程時產生的疑問,我相信也會是大多數人的疑問
下面是相關介紹:
JVM會爲一個使用內部鎖的對象維護兩個集合EntrySet(鎖池)和WaitSet(等待池)
- ①EntrySet:線程A持有對象鎖,其它線程若想獲得該對象鎖,只能進入EntrySet,並且處於線程的BLOCKED狀態
- ②WaitSet:線程A調用了wait()方法後會釋放該對象的鎖,進入WaitSet,並且處於線程的WAITING狀態
線程B獲得對象鎖有兩個條件:
- ①對象鎖已經被釋放(持有鎖的線程A執行完了synchronized代碼塊,或者線程A調用了wait方法)
- ②線程B處於RUNNABLE狀態
兩個集合中的線程在什麼條件下可以轉變爲RUNNABLE狀態呢?
- ①EntrySet:當對象鎖被釋放後,JVM會喚醒EntrySet中某一線程,此線程的狀態就由BLOCKED轉爲RUNNABLE
- ②WaitSet:
- 1>當對象的notify()方法被調用時,JVM會喚醒WaitSet中的某一線程,此線程狀態由WAITING轉爲RUNNABLE
- 2>當對象的notifyAll()方法被調用時,WaitSet中的全部線程都被喚醒,這些線程狀態由WAITING轉爲RUNNABLE
- 3>喚醒後的線程全部進入EntrySet中
當對象鎖被釋放後,所有處於RUNNABLE狀態的線程會共同去競爭該對象鎖,競爭失敗的線程會在EntrySet中等待下一次機會
疑問解惑:
①wait()爲什麼要放置在while循環中,而不是使用if進行判斷?
那生產者消費者爲例,當前無可消費時,調用消費者的wait(),使其進入等待狀態,但是消費者線程無法確定其它線程什麼時候會被notify.多生產者多消費者的情況下這是無法預知並掌控的,極有可能發生的是喚醒生產者的是另一個生產者,喚醒消費者的是另一個消費者,如果將wait()放在if判斷中就會造成過度生產過度消費的情況,典型如IndexOutOfBoundsException的異常。所以所有的java書籍都會建議開發者永遠都要把wait()放到循環語句裏面
②notify()和notifyAll()有什麼區別?爲什麼有時候認爲該使用notify()的地方卻使用了notifyAll()?
我們應該儘量使用notifyAll()的原因就是,notify()非常容易導致死鎖。當然notifyAll並不一定都是優點,畢竟一次性將Wait Set中的線程都喚醒是一筆不菲的開銷,如果你能handle你的線程調度,那麼使用notify()也是有好處的
阻塞隊列與普通隊列的區別:
①當隊列爲空時,從隊列中獲取元素的操作將被阻塞
②當隊列爲滿時,向隊列中添加元素的操作將被阻塞
一.阻塞隊列的wait()和notify()方式實現
package blockqueue;
import java.util.LinkedList;
import java.util.List;
/**
* 第一種實現阻塞隊列的方式是使用wait()和notify()
*/
public class BlockingQueueModeFir {
//使用list實現阻塞隊列
private List queue = new LinkedList();
//有界隊列上限
private int limit = 10;
//阻塞隊列構造
public BlockingQueueModeFir(int limit) {
this.limit = limit;//指定隊列上限
}
/**
* 向阻塞隊列中添加元素
* @param item
* @throws InterruptedException
*/
public synchronized void enqueue(Object item) throws InterruptedException {
//阻塞隊列中元素數已達上限,則執行添加元素的線程阻塞
while (this.queue.size() == this.limit){
wait();
}
//阻塞隊列已被清空時,喚醒所有線程
if (this.queue.size() == 0){
notifyAll();
}
//阻塞隊列存在元素且元素數量未達上限,向隊列中添加元素
this.queue.add(item);
}
public synchronized Object dequeue() throws InterruptedException {
//當阻塞隊列爲空時,無法取出元素,執行等待
while (this.queue.size() == 0){
wait();
}
//當阻塞隊列已滿時,喚醒所有線程
if (this.queue.size() == this.limit){
notifyAll();
}
//阻塞隊列存在元素且元素數量未達上限,取出隊列中的元素
return this.queue.remove(0);
}
}
二.阻塞隊列的併發式實現
package blockqueue;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* 第二種實現阻塞隊列的方式是併發式
*/
public class BlockingQueueModeSec {
public static void main(String[] args) {
//緩衝區允許存放3個元素
final BlockingQueue<Integer> queue = new ArrayBlockingQueue<Integer>(3);
//開啓兩個線程向隊列中存入數據
for (int i = 0; i < 2; i++) {
new Thread(() -> {
while(true) {
try {
Thread.sleep((long) (Math.random()*1000));
System.out.println(Thread.currentThread().getName() + "準備放數據"
+ (queue.size() == 3?"..隊列已滿,正在等待":"..."));
queue.put(1);
System.out.println(Thread.currentThread().getName() + "存入數據,"
+ "隊列目前有" + queue.size() + "個數據");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}).start();
}
new Thread(new Runnable() {
@Override
public void run() {
while(true) {
try {
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName() + "準備取數據"
+ (queue.size() == 0?"..隊列已空,正在等待":"..."));
queue.take();
System.out.println(Thread.currentThread().getName() + "取出數據,"
+ "隊列目前有" + queue.size() + "個數據");
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
}).start();
}
}