一、前置知識
執行緒間通訊三要素:
多執行緒+判斷+操作+通知+資源類。
上面的五個要素,其他三個要素就是普通的多執行緒程式問題,那麼通訊就需要執行緒間的互相通知,往往伴隨著何時通訊的判斷邏輯。
在 java 的 Object 類裡就提供了對應的方法來進行通知,同樣的,保證安全的判斷採用隱式的物件鎖,也就是 synchronized 關鍵字實現。這塊內容在:
已經寫過。
二、使用 Lock 實現執行緒間通訊
那麼,我們知道 juc 包裡提供了顯式的鎖,即 Lock 介面的各種實現類,如果想用顯式的鎖來實現執行緒間通訊問題,喚醒方法就要使用對應的 Conditon 類的 await 和 signalAll 方法。(這兩個方法名字也能看得出來,對應 Object 類的 wait 和 notifyAll)
Condition 物件的獲取可以通過具體的 Lock 實現類的物件的 newCondition 方法獲得。
public class Communication2 {
public static void main(String[] args) {
Container container = new Container();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生產者1").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消費者1").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生產者2").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消費者2").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.increment();
}
},"生產者3").start();
new Thread(()->{
for (int i = 0; i < 10; i++){
container.decrenment();
}
},"消費者3").start();
}
}
class Container{
private int count = 0;
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
public void increment(){
lock.lock();
try {
while (count != 0){
condition.await();
}
count++;
System.out.println(Thread.currentThread().getName() + " "+count);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
public void decrenment(){
lock.lock();
try{
while (count == 0){
condition.await();
}
count--;
System.out.println(Thread.currentThread().getName()+ " " + count);
condition.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}
}
輸出也沒有任何問題。
這裡面我們模擬了 3 個生產者和 3 個消費者,進行對一個資源類,其實就是一個數字 count 的操作,並使用 Condition 類來進行喚醒操作。
從目前程式碼的用法來看, juc 包的 Lock 介面實現類和之前使用 synchronized + Object 類的執行緒通訊方法是一樣的,但是併發包的開發工具給了他更多的靈活性。靈活在哪?
三、喚醒特定執行緒
新技術解決了舊問題,這個方法解決的問題就是,在生產者消費者問題裡:有時候我們並不想喚醒所有的對面夥伴,而只想要喚醒特定的一部分,這時候該怎麼辦呢?
如果沒有顯式的 lock,我們的思路可能是:
- 採用一個標誌物件,可以是一個數值或者別的;
- 當通訊呼叫 signalAll 的時候,其他執行緒都去判斷這個標誌,從而決定自己應不應該工作。
這種實現是可行的,但是本質上其他執行緒都被喚醒,然後一直阻塞+判斷,其實還是在競爭。那麼 Condition 類其實就已經提供了對應的方法,來完成這樣的操作:
我們看這樣一個需求:
- 同樣是多執行緒操作、需要通訊。但是我們要指定各個執行緒交替的順序,以及指定喚醒的時候是指定哪個具體執行緒,這樣就不會存在喚醒所有執行緒然後他們之間互相競爭了。
- 具體說是:AA 列印 5 次,BB 列印 10 次, CC 列印 15次,然後接著從 AA 開始一輪三個交替。
程式碼如下:
public class TakeTurnPrint {
public static void main(String[] args) {
ShareResource resource = new ShareResource();
new Thread(()->{resource.printA();},"A").start();
new Thread(()->{resource.printB();},"B").start();
new Thread(()->{resource.printC();},"C").start();
}
}
/**
* 資源
*/
class ShareResource{
private int signal = 0;//0-A,1-B,2-C
private Lock lock = new ReentrantLock();
private Condition condition = lock.newCondition();
private Condition condition1 = lock.newCondition();
private Condition condition2 = lock.newCondition();
public void printA(){
lock.lock();
try{
while (signal != 0){
condition.await();//精準
}
for (int i = 0; i < 5; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 1;//精準
condition1.signal();//精準指定下一個
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printB(){
lock.lock();
try{
while (signal != 1){
condition1.await();//精準
}
for (int i = 0; i < 10; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 2;//精準
condition2.signal();//精準指定下一個
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
public void printC(){
lock.lock();
try{
while (signal != 2){
condition2.await();//精準
}
for (int i = 0; i < 15; i++){
System.out.println(Thread.currentThread().getName() + " " + i);
}
signal = 0;//精準
condition.signal();//精準指定下一個
}catch (InterruptedException e) {
e.printStackTrace();
}finally {
lock.unlock();
}
}
}
其中,使用三個 Condition 物件,用一個 signal 的不同值,來通知不同的執行緒。