Java多線程 – 線程通信
一、傳統的線程通信
假設現在系統中有兩個線程,交替打印奇偶數。
爲了實現這個功能,可以藉助於Object
類的wait()
,notify()
,notifyAll()
三個方法,這三個方法並不屬於Thread
類,而是屬於Object
類。但是這三個方法必須由同步監視器對象來進行調用。
可以分爲兩種情況:
- 對於使用了
synchronized
修飾的同步方法,因爲該類的默認實例(this
)就是同步監視器,所以可以在同步方法之中直接調用三個方法。 - 對於使用了
synchronized
修飾的同步代碼塊,同步監視器是synchronized
後括號裏的對象,所以必須使用對象調用這三個方法。
這三種方法:
wait()
:導致當前線程等待,直到其他線程調用該同步監視器的notify()
方法或者notifyAll()
方法來喚醒該線程,其中wait()
方法有三種形式,無參數的(一直等待,直到其他線程通知),帶毫秒的,以及帶微妙的參數,(這兩個方法是等待指定時間後自動屬性),調用wait()
方法的當前線程會釋放對該同步監視器的鎖定。notify()
:喚醒在此同步監視器上等待的耽擱線程,如果說所有的線程都在此同步監視器上等待,則會喚醒其中一個線程,選擇是任意性的。只有當前線程放棄對該同步監視器的鎖定後(使用wait()
方法),纔可以執行被喚醒的進程。notifyAll()
:喚醒在此同步監視器上等待的所有線程。只有當前線程放棄對該同步監視器的鎖定後,纔可以執行被喚醒的進程。
public class Demo {
public int start = 1;//定義共享變量
public boolean flag = false;//定義標識
public static void main(String[] args) {
Demo d = new Demo();//新建一個對象實例
OddNum oddNum = new OddNum(d);
EvenNum evenNum = new EvenNum(d);
Thread oddNumThread = new Thread(oddNum);
Thread evenNumThread = new Thread(evenNum);
oddNumThread.start();
evenNumThread.start();
}
}
class OddNum implements Runnable{
private Demo demo;
OddNum(Demo demo){
this.demo = demo;
}
@Override
public void run(){
while (demo.start < 100){
synchronized (Demo.class){
if(!demo.flag){
System.out.println("奇數線程——" + demo.start);
demo.start++;
demo.flag = true;
Demo.class.notify();//讓另一線程甦醒
}else{
try {
Demo.class.wait();//本線程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
class EvenNum implements Runnable{
private Demo demo;
EvenNum(Demo demo){
this.demo = demo;
}
@Override
public void run(){
while(demo.start < 100){
synchronized (Demo.class){
if(demo.flag){
System.out.println("偶數線程——" + demo.start);
demo.start++;
demo.flag = false;
Demo.class.notify();
}else{
try{
Demo.class.wait();
}catch (InterruptedException e){
e.printStackTrace();
}
}
}
}
}
}
二、使用Condition
控制線程通信
如果程序不採用synchronized
關鍵字來保證同步,而是採用Lock
對象來保證同步,則系統中不存在隱式的同步監視器,也就不可以使用wait()
,notify()
,notifyAll()
方法進行線程通信了。
但是Java使用了Condition
類來保持協調,使用Condition
可以讓那些已經得到的Lock對象無法繼續執行線程數釋放Lock
對象,Condition
對象也可以喚醒其他在等待的線程、
Condition
將同步監視器方法(wait()
,notify()
,notifyAll()
)分解成截然不同的對象,以便通過將這些對象與Lock
對象組合使用,爲每個對象提供多個等待集(wait-set
),這種情況之下,Lock
替代了同步方法或同步代碼塊,Condition
替代了同步監視器的功能。
Condition
實例被綁定在一個Lock
對象上,要獲得特定Lock
實例的Condition
實例,調用Lock
對象的newCondition()
方法即可,Condition
類提供了三個方法:
await()
:類似隱式同步監視器上的wait()
方法,導致當前線程等待,指導其他線程調用該Condition
的signal()
或sianalAll()
方法來喚醒該線程。signal()
:喚醒在此Lock
對象上等待的單個線程。如果所有線程都在該Lock
對象上等待,則會選擇喚醒其中一個線程,選擇是任意性的,只有當前線程放棄對該Lock
對象的鎖定後(await()方法
),纔可以執行被喚醒的線程。signalAll()
:喚醒在此Lock
對象上等待的所有線程,只有當前線程放棄對該Lock
對象的鎖定後(await()方法
),纔可以執行被喚醒的線程。
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class Demo{
Lock lock = new ReentrantLock();
Condition oddCondition = lock.newCondition();
Condition evenCondition = lock.newCondition();
boolean flag = false;
int start = 1;
public static void main(String[] args) {
Demo demo = new Demo();
Thread odd = new Thread(new OddNum(demo));
Thread even = new Thread(new EvenNum(demo));
odd.start();
even.start();
}
}
class OddNum implements Runnable{
private Demo demo;
OddNum(Demo demo){
this.demo = demo;
}
@Override
public void run(){
while (demo.start < 100){
if(!demo.flag){
demo.lock.lock();
System.out.println(demo.start);
demo.start++;
demo.evenCondition.signal();
}else{
try {
demo.oddCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
}finally {
demo.lock.unlock();
}
}
}
}
}
class EvenNum implements Runnable{
private Demo demo;
EvenNum(Demo demo){
this.demo = demo;
}
@Override
public void run(){
while (demo.start < 100){
if(demo.flag){
demo.lock.lock();
System.out.println(demo.start);
demo.start++;
demo.oddCondition.signal();
}else{
try {
demo.evenCondition.await();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
demo.lock.unlock();
}
}
}
}
}
三、使用阻塞隊列(BlockingQueue
)控制線程通信
Java 5提供了一個BlockingQueue
藉口,雖然BlockingQueue
也是Queue
的子接口,但它主要用途不是容易,而是作爲線程同步的工具。BlockingQueue
具有一個特徵:當生產者試圖向其中放入元素的時候,如果該隊列已經滿了,則線程會阻塞,當消費者試圖從其中去除元素,當隊列爲空,則該線程被阻塞。
BlockingQueue
提供如下兩個支持阻塞的方法:
put(E e)
:將E元素放入,如果已滿,阻塞該線程。
take()
:嘗試從頭部去除元素,如果已空,則阻塞之。