5.1 等待與通知:wait/notify
wait()的作用是使其執行線程被暫停,該方法可以用來實現等待;notify()的作用是喚醒一個被暫停的線程,調用該方法可實現通知。
由於一個線程只有在持有一個對象的內部鎖的情況下才能調用該對象wait方法,因此wait()調用總是放在響應對象所引導的臨界區之中。
執行someObject.wait()而被暫停的線程稱爲對象someObject上等待的線程。由於同一個對象的該方法可以被多個線程執行,因此一個對象可能存在多個等待線程。someObject上的等待線程可以通過其他線程執行someObject.notify()來喚醒。someObject..wait()會以原子操作的方法使其執行線程(當前線程)暫停並使該線程釋放其持有的someObject對應的內部鎖。當前線程被暫停的時候其對someObject.wait()的調用並未返回。其他線程在該線程所需的保護條件成立的時候執行響應的notify方法,即someObject..notify()可以喚醒someObject上的一個等待線程。被喚醒的等待線程在其佔用處理器繼續運行的時候,需要再次申請someObject對應的內部鎖。被喚醒的線程在其再次持有someObject對應的內部鎖的情況下繼續執行someObject.wait()中剩餘的指令,直到wait方法返回。
//wait實現等待的模板方法
synchronized(someObject){
while(保護條件不成立){
//調用Object.wait()暫停當前線程
someObject.wait();
}
//代碼執行到這裏說明保護條件已經滿足
doAction();
}
//notify實現通知的模板方法
synchronized(someObject){
//更新等待線程的保護條件涉及的共享變量
updateSharedState();
//喚醒其他線程
someObject.notify();
}
等待線程對保護條件的判斷、Object.wait()的調用總是應該方法響應對象所引導的臨界區中的一個循環語句之中。
等待線程對保護條件的判斷、Object.wait()的執行以及目標動作的執行必須放在同一個對象(內部鎖)所引導的臨界區之中。
Object.wait()暫停當前線程時釋放的鎖只是與該wait方法所屬對象的內部鎖。當前線程所持有的其他內部所、顯示鎖並不會因此而被釋放。
由於一個線程只有在持有一個對象的內部鎖的情況下才能夠執行該對象的notify方法,因此Object.notify()調用總是放在相應對象內部鎖所引導的臨界區之中。也正是由於Object.notify()要求其執行線程必須持有該方法所屬對象的內部鎖,因此Object.wait()在暫停其執行線程的同時必須釋放相應的內部鎖;否則通知線程無法獲得相應的內部鎖,也就是無法執行相應對象的notify方法來通知等待線程!Object.notify()的執行線程持有的響應對象的內部鎖只有在Object.notify()調用所在的臨界區代碼執行結束後纔會被釋放,而Object.notify()本身並不會將這個內部鎖釋放。因此,爲了使等待線程在其被喚醒之後能夠儘快獲得相應的內部鎖,我們要儘可能的將Object.notify()調用放在靠近臨界區結束的地方。等待線程被喚醒之後佔用處理器繼續運行時,如果有其他線程持有了相應對象的內部鎖,那麼這個等待線程可能又會再次被暫停,以等待再次獲得相應內部鎖的機會,而這會導致上下文切換。
調用Object.notify()所喚醒的線程僅是響應對象上的一個任意等待線程,所以這個被喚醒的線程可能不是我們真正想要喚醒的那個線程。因此,有時候我們需要藉助Object.notifyAll(),它可以喚醒響應對象上的所有等待線程。由於等待線程和通知線程在其實現等待和通知的時候必須是調用同一個對象的wait方法、notify方法,而這兩個方法都要求其執行線程必須持有該方法所屬對象的內部鎖,因此等待線程和通知線程是同步在同一對象之上的兩種線程。
此處個人理解:notify和wait方法都是針對對象的,所以加鎖的地方需要注意!!
java虛擬機會爲每一個對象維護一個入口集用於存儲申請該對象內部鎖的線程。此外,java虛擬機還會爲每個對象維護一個被稱爲等待集的隊列,該隊列用戶存儲該對象上的等待線程。Object.wait()將當前線程暫停並釋放相應內部鎖的同時會將該線程存入該方法所屬對象的等待集中。執行一個對象的notify方法會使該對象的等待集中的一個任意線程被喚醒。被喚醒的線程仍然會停留在相應對象的等待集中,直到該線程再次持有相應內部鎖的時候Object.wait()會使當前線程從其所在的等待集移除,接着Object.wait()調用就返回了。
package JavaCoreThreadPatten.capter05;
import java.util.Random;
/**
* 告警代理
*/
public class AlarmAgent {
private static volatile AlarmAgent INSTANCE = null;
//是否已經連接上服務器
private volatile boolean connectToServer = false;
private AlarmAgent(){}
public static AlarmAgent getINSTANCE(){
if(null==INSTANCE){
synchronized (AlarmAgent.class){
if(null==INSTANCE){
INSTANCE = new AlarmAgent();
}
}
}
return INSTANCE;
}
/**
* 初始化操作
*/
public void init(){
doConnectionToServer();
Thread thread = new Thread(new HeartbeartThread());
thread.setDaemon(true);
thread.setName("心跳檢測後臺守護線程");
thread.start();
}
private void doConnectionToServer() {
//連接操作
synchronized (this){
//連接建立成功
connectToServer = true;
//喚醒所有在等待的線程
notifyAll();
}
}
public void sendAlarm(String message){
synchronized (this){
while (!connectToServer){
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//發送消息
doSendMessage(message);
}
}
public void doSendMessage(String message){
//發送消息邏輯
}
/**
* 心跳檢測線程
*/
class HeartbeartThread implements Runnable{
@Override
public void run() {
try {
Thread.sleep(3000);
while (true){
if(checkConnection()){
connectToServer = true;
}else{
connectToServer = false;
}
}
} catch (InterruptedException e) {
}
}
public boolean checkConnection(){
//檢測連接是否正常
return new Random().nextBoolean();
}
}
}
package JavaCoreThreadPatten.capter05;
import java.util.Objects;
import java.util.Random;
import java.util.concurrent.TimeUnit;
public class TimeOutWaitExample {
private static final Object lock = new Object();
private static boolean ready = false;
public static void main(String[] args) {
Thread t = new Thread(()->{
for(;;){
synchronized (lock){
ready = new Random().nextInt(100)>50 ? true : false;
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if(ready){
lock.notify();
}
System.out.println(ready);
}
}
});
t.setDaemon(true);
t.start();
try {
waiter(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void waiter(long timeout) throws InterruptedException {
if (timeout<0){
throw new IllegalArgumentException();
}
long start = System.currentTimeMillis();
long waitTime;
long now;
synchronized (lock){
while (!ready){
now = System.currentTimeMillis();
//計算剩餘等待時間
waitTime = timeout-(now-start);
if(waitTime<=0){
break;
}
//此處標識當前線程在lock這個對象的入口等待
lock.wait(waitTime);
}
if(ready){
//做正常的業務流程
System.out.println("做正常的業務邏輯");
}else {
System.out.println("wait time out");
}
}
}
}
wait/notify的問題:
5.2 java條件變量
Condition接口可以作爲wait/notify的替代品來實現等待/通知,它爲解決過早喚醒問題提供了支持,並解決了Object.wait(long)不能區分其返回是否是由等待超時而導致的問題。
package JavaCoreThreadPatten.capter05;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class ConditionUsage {
private static final Lock LOCK = new ReentrantLock();
private static final Condition CONDITION = LOCK.newCondition();
public void aGuaredMethod() {
try {
LOCK.lock();
boolean condition = true;
while (condition) {
try {
CONDITION.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//做一些事情
} finally {
LOCK.unlock();
}
}
public void anNotificationMethod(){
LOCK.lock();
CONDITION.signal();
}
}
package JavaCoreThreadPatten.capter05;
import java.util.Date;
import java.util.Random;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class TimeOutWaitExampleWithCondition {
private static final Lock LOCK = new ReentrantLock();
private static final Condition CONDITION = LOCK.newCondition();
private static boolean ready = false;
public static void main(String[] args) {
Thread t = new Thread(() -> {
for (; ; ) {
try {
LOCK.lock();
ready = new Random().nextInt(100) > 50 ? true : false;
try {
TimeUnit.MILLISECONDS.sleep(50);
} catch (InterruptedException e) {
e.printStackTrace();
}
if (ready) {
CONDITION.signal();
}
System.out.println(ready);
} finally {
LOCK.unlock();
}
}
});
t.setDaemon(true);
t.start();
try {
waiter(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public static void waiter(long timeout) throws InterruptedException {
if (timeout < 0) {
throw new IllegalArgumentException();
}
long start = System.currentTimeMillis();
long waitTime;
long now;
LOCK.lock();
while (!ready) {
now = System.currentTimeMillis();
//計算剩餘等待時間
waitTime = timeout - (now - start);
if (waitTime <= 0) {
break;
}
//此處標識當前線程在lock這個對象的入口等待
CONDITION.awaitUntil(new Date(System.currentTimeMillis()+timeout));
}
if (ready) {
//做正常的業務流程
System.out.println("做正常的業務邏輯");
} else {
System.out.println("wait time out");
}
}
}
5.3 倒計時協調器:CountDownLatch
CountDownLatch可以用來實現一個或多個線程等待其他線程完成一組特定操作之後才繼續運行。
CountDownLatch內部會維護一個用於表示未完成的先決操作數量計數器。CountDownLatch.countDown()每被執行一次就會使相應實例的計數器值減少1,當計數器值不爲0時CountDownLatch.await()的執行線程會被暫停,CountDownLatch.countDown()相當於一個通知方法,它會在計數器達到0的時候喚醒響應實例上的所有等待線程。
package JavaCoreThreadPatten.capter05;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* 結果:
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 一個線程執行完畢
* 十個任務已經執行完畢,開始執行下一階段任務
*/
public class CountdownLatchExample {
/**
* 十個線程的任務執行完畢開始執行接下來的業務
*/
private static final CountDownLatch COUNT_DOWN_LATCH = new CountDownLatch(10);
public static void main(String[] args){
try {
for(int i=0;i<10;i++){
new Thread(new Handler()).start();
TimeUnit.SECONDS.sleep(1);
}
COUNT_DOWN_LATCH.await();
System.out.println("十個任務已經執行完畢,開始執行下一階段任務");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
static class Handler implements Runnable{
@Override
public void run() {
try {
Thread.sleep(5000);
System.out.println("一個線程執行完畢");
} catch (InterruptedException e) {
}finally {
//注意,此處爲了保證計數器能夠釋放,應當放在finally中釋放
COUNT_DOWN_LATCH.countDown();
}
}
}
}
如果CountDownLatch內部計數器由於程序的錯誤而無法達到0,那麼相應實例上的線程會一直處於WAITING狀態,因此我們首先應當將countDown()方法的調用放在finally語句塊中,確保能夠被調用到;其次可以調用await(long,TimeUnit)允許指定一個超時時間,如果超過指定時間計數器的值仍未達到0,那麼所有執行該實例的await()方法的ixancheng都會被喚醒。
5.4 柵欄(CyclicBarrier)
有時候多個線程可能需要相互等待對方執行到代碼中的某個地方(集合點),這時這些線程才能夠繼續執行。
CyclicBarrier實例是可以重複使用的。
使用CyclicBarrier實現等待的縣城被稱爲參與方。參與方只需要執行CyclicBarrier.await()就可以實現等待。儘管從應用代碼的角度來看,參與方是併發執行CyclicBarrier.await()的,但是,CyclicBarrier內部維護了一個顯式鎖,這使得其總是可以在所有參與方中分出一個最後執行CyclicBarrier.await()的線程,該線程被稱爲最後一個線程。除最後一個線程外的任何參與方執行CyclicBarrier.await()都會導致該線程被暫停。最後一個線程執行CyclicBarrier.await()會使得使用相應CyclicBarrier實例的其他所有參與方被喚醒。
package JavaCoreThreadPatten.capter05;
import java.util.Random;
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
/**
* 柵欄功能:模擬100個人爬山,爬山一共分爲兩段,第一段結束等人全到開始爬第二段,第二段結束全部到達後宣佈勝利,團建結束
*/
public class CyclicBarrierExample {
private static final CyclicBarrier CYCLIC_BARRIER = new CyclicBarrier(100);
private static final CyclicBarrier CYCLIC_BARRIER2 = new CyclicBarrier(100);
private static final AtomicInteger ATOMIC_INTEGER = new AtomicInteger(1);
public static void main(String[] args) throws BrokenBarrierException, InterruptedException {
for (int i = 0; i < 100; i++) {
new Thread(new Person(i + "帥哥", CYCLIC_BARRIER)).start();
}
}
static class Person implements Runnable {
private final String name;
private final CyclicBarrier cyclicBarrier;
public Person(String name, CyclicBarrier cyclicBarrier) {
this.name = name;
this.cyclicBarrier = cyclicBarrier;
}
@Override
public void run() {
try {
//第一階段爬山
climb();
//等別人都到達第一階段終點
//這裏使用存在問題
CYCLIC_BARRIER.await();
//第二階段爬山開始
ATOMIC_INTEGER.set(2);
climb();
//等別人都到達第二階段終點
CYCLIC_BARRIER2.await();
//所有人都到達終點,慶祝開始
System.out.println("遊戲結束," + name + "打開了香檳!!!");
} catch (InterruptedException e) {
} catch (BrokenBarrierException e) {
}
}
public void climb() {
try {
TimeUnit.MILLISECONDS.sleep(new Random().nextInt(1000));
int s = new Random().nextInt(10);
TimeUnit.SECONDS.sleep(s);
System.out.println(name + "第" + ATOMIC_INTEGER.get() + "階段爬山結束了");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
CyclicBarrier內部實現是基於條件變量的,因此CyclicBarrier的開銷與條件變量的開銷相似,其主要開銷在可能產生的上下文切換。
CyclicBarrier內部使用了一個條件變量trip來實現等待/通知。CyclicBarrier內部實現使用了分代的概念用於表示CyclicBarrier實例是可以重複使用的。除最後一個線程以外的任何一個參與方都相當於一個等待線程,這些線程所使用的保護條件是當前分代內,尚未執行await方法的參與方個數(parties)爲0。當前分代的初始狀態是parties等於參與方總數。CyclicBarrier.await()每被執行一次會使相應實例的parties值減少1.最後一個線程相當於通知線程,它執行CyclicBarrier.await()會使相應實例的parties值變爲0,此時該線程會限制性barrierAction.run(),然後再執行trip.signalAll()來喚醒所有等待線程。接着,開始下一個分代,即使得CyclicBarrier的parties值又重新恢復爲其初始值。
CyclicBarrier的應用場景:1.使迭代算法併發化;2.在測試代碼中模擬高併發。
5.5 生產者-消費者模式
由於線程之間無法像函數調用那樣通過參數直接傳遞數據,因此生產者和消費者之間需要一個用於傳遞產品的傳輸通道。
JDK1.5中引入的接口java.util.concurrent.BlockingQueue定義線程安全的阻塞隊列,隊列按照其存儲容量是否受限制分爲有界隊列和無界隊列。
有界隊列可以使用ArrayBlockingQueue或者LinkedBlockingQueue來實現。ArrayBlockingQueue內部使用一個數組作爲其存儲空間,而數組的存儲空間是預先分配的,因此ArrayBlockingQueue的put操作、take操作本身並不會增加垃圾回收的負擔。ArrayBlockingQueue的缺點是其內部在實現put、take操作的時候使用的是同一個鎖(顯式鎖),從而可能導致鎖的高爭用,進而導致較多的上下文切換。
LinkedBlockingQueue即能實現無界隊列,也能實現有界隊列。LinkedBlockingQueue的其中一個構造器允許我們創建隊列得時候指定隊列容量。LinkedBlockingQueue的優點是其內部在實現put、take操作的時候分別使用了兩個顯式鎖(putLock和takeLock),這降低了鎖爭用的可能性。LinkedBlockingQueue的內部存儲空間是一個鏈表,而鏈表節點(對象)所需的存儲空間是動態分配的,put操作、take操作都會導致鏈表節點的動態創建和移除,因此LinkedBlockingQueue的缺點是它可能增加垃圾回收的負擔。另外,由於LinkedBlockingQueue的put、take操作使用的是兩個鎖,因此LinkedBlockingQueue維護其隊列的當前長度(size)時無法使用一個普通int型變量而是使用了一個原子變量(AtomicInteger)。這個原子變量可能會被生產者和消費者線程爭用,因此它可能導致額外的開銷。
SynchronousQueue是一個特殊的有界隊列,生產者執行SynchronousQueue.put(E)時如果沒有消費者線程執行SynchronousQueue.take(),那麼該生產者線程會被暫停,直到有消費者線程執行了SynchronousQueue.take();類似的,消費者線程執行SynchronousQueue.take()時如果沒有生產者執行了SynchronousQueue.put(E),那麼該消費者線程會被暫停,直到有生產者線程執行了SynchronousQueue.put(E)。SynchronousQueue適合於在消費者處理能力與生產者處理能力相差不大的情況下使用。否則,由於生產者線程執行執行put操作時沒有消費者線程執行take操作,或者消費者線程執行take操作的時候沒有生產者線程執行put操作的概率比較大,從而可能導致較多的等待。(意味着上下文切換)
ArrayBlockingQueue和SynchronousQueue都既支持非公平調度也支持公平調度,而LinkedBlockingQueue僅支持非公平調度
如果生產者線程和消費者線程之間的併發程度比較大,那麼這些線程對傳輸通道內部所使用的鎖的爭用可能性也隨之增加。這時,有界隊列的實現適合選用LinkedBlockingQueue,否則我們可以考慮使用ArrayBlockingQueue。
LinkedBlockingQueue適合在生產者線程和消費者線程之間併發程度比較大的情況下使用。
ArrayBlockingQueue適合在生產者線程和消費者線程之間的併發程度比較低的情況下使用。
SynchronousQueue適合在消費者處理能力與生產者處理能力相差不大的情況下使用。
信號量:Semaphore
java.util.concurrent.Semaphore可以用來實現流量控制。
Semaphore.acquire()/release()分別用於申請配額和返還配額。Semaphore.acquire()在成功獲得一個配額後會立即返回。如果當前的可用額度不足,那麼Semaphore.acquire()會使其執行線程暫停。Semaphore內部會維護一個等待隊列用於存儲這些被暫停的線程。Semaphore.acquire()在其返回之前總是會將當前的可用配額減少1。Semaphore.release()會使當前可用配額增加1,並喚醒響應Semaphore實例的等待隊列中的一個任意等待線程。
package JavaCoreThreadPatten.capter05;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.Semaphore;
/**
* 基於Semaphore的支持流量控制的傳輸通道實現
* @param <P>
*/
public class SemaphoreBasedChannel<P> {
private final BlockingQueue<P> queue;
private final Semaphore semaphore;
/**
*
* @param queue 無界隊列
* @param flowlimit 流量數限制
* @param fair 是否是公平
*/
public SemaphoreBasedChannel(BlockingQueue<P> queue, int flowlimit,boolean fair) {
this.queue = queue;
this.semaphore = new Semaphore(flowlimit,fair);
}
public P take(){
try {
return queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
public void put(P product){
//獲取憑證
try {
semaphore.acquire();
queue.put(product);
} catch (InterruptedException e) {
}finally {
//釋放憑證
semaphore.release();
}
}
}
Semaphore.acquire()和Semaphore.release()總是配對使用。
Semaphore.release()調用總是應該放在一個finally塊中,以避免虛擬資源訪問出現異常情況下當前線程所獲得的配額無法返回。
在問題規模一定的情況下,產品的粒度過細會導致產品再傳輸通道上的移動次數增大;產品的粒度稍微大些可以減少產品再傳輸通道上的移動次數,但是產品所佔用的資源也會隨之增加。
5.6 線程中斷機制
java平臺會爲每個線程維護一個被稱爲中斷標記的布爾型狀態變量用於表示相應線程是否接收到了中斷,中斷標記值爲true表示響應線程收到了中斷。目標線程可以通過Thread.currentThread().isInterrupted()調用來獲取該線程的中斷標記值,也可以通過Thread.interrupted()來獲取並重置中斷標記值,即Thread.interrupted()會返回當前線程的中斷標記值並將當前線程中斷標記重置爲false。調用一個線程的interrupt()相當於將該線程的中斷標記爲true;線程被中斷後會拋出InterruptedException異常。
優雅的中斷線程:光使用專門的實例變量來作爲線程停止標記仍然不夠,這是由於當線程停止標記爲true的時候,目標線程可能因爲執行了一些阻塞方法而被暫停,因此,這時線程停止標記壓根不會對目標線程產生任何影響!由此可見,爲了使線程停止標記的設置能夠起作用,我們可能還需要給目標線程發送中斷以將其喚醒,使之得以判斷線程停止標記。
package JavaCoreThreadPatten.capter05;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.atomic.AtomicLong;
/**
* 可優雅中斷的任務處理器
*/
public class TerminatableTaskRunnable<T extends Runnable> {
protected final BlockingQueue<T> queue;
private Thread workThread;
//表示是否需要中斷,true表示不需要中斷
private volatile boolean isUse = true;
//表示當前需要進行的任務數量
private final AtomicLong taskNum = new AtomicLong(0);
public TerminatableTaskRunnable(BlockingQueue<T> queue) {
this.queue = queue;
this.workThread = new WorkThred();
}
public void init(){
workThread.start();
}
/**
* 當沒有中斷的情況下,繼續接受任務
* @param task
* @throws InterruptedException
*/
public void submit(T task) throws InterruptedException {
if(isUse){
queue.put(task);
taskNum.incrementAndGet();
}else {
System.err.println("拒絕接受任務");
}
}
/**
* 中斷線程
*/
public void shutdown(){
isUse = false;
//喚醒線程,檢查中斷標識
workThread.interrupt();
}
class WorkThred extends Thread{
@Override
public void run() {
for(;;){
//中斷且當前待執行的任務書爲0
if(!isUse && taskNum.get()<=0){
break;
}
try {
queue.take().run();
} catch (InterruptedException e) {
}
taskNum.decrementAndGet();
}
}
}
}
在生產者-消費者模式中,生產者線程需要先於消費者線程停止,否則生產者所生產的產品會無法被處理。在單生產者-單消費者模式中,停止生產者、消費者線程有一種簡單的額方法;生產者線程在其中指前往傳輸通道中存入一個特殊產品作爲消費者線程的線程停止標誌,消費者線程取出這個產品之後就可以退出run方法而終止了。