接著上一篇文章今天我們來(lái)介紹下LockSupport和Java中線程的中斷(interrupt)。
其實(shí)除了LockSupport,Java之初就有Object對(duì)象的wait和notify方法可以實(shí)現(xiàn)線程的阻塞和喚醒。那么它們的區(qū)別 是什么呢?
主要的區(qū)別應(yīng)該說(shuō)是它們面向的對(duì)象不同。阻塞和喚醒是對(duì)于線程來(lái)說(shuō)的,LockSupport的park/unpark更符合這個(gè)語(yǔ)義,以“線程”作為方法的參數(shù), 語(yǔ)義更清晰,使用起來(lái)也更方便。而wait/notify的實(shí)現(xiàn)使得“線程”的阻塞/喚醒對(duì)線程本身來(lái)說(shuō)是被動(dòng)的,要準(zhǔn)確的控制哪個(gè)線程、什么時(shí)候阻塞/喚醒很困難, 要不隨機(jī)喚醒一個(gè)線程(notify)要不喚醒所有的(notifyAll)。

wait/notify最典型的例子應(yīng)該就是生產(chǎn)者/消費(fèi)者了:

class BoundedBuffer1 { PRivate int contents; final Object[] items = new Object[100]; int putptr, takeptr, count; public synchronized void put(Object x) { while (count == items.length) { try { wait(); } catch (InterruptedException e) { } } items[putptr] = x; if (++putptr == items.length) putptr = 0; ++count; notifyAll(); } public synchronized Object take() { while (count == 0) { try { wait(); } catch (InterruptedException e) { } } Object x = items[takeptr]; if (++takeptr == items.length) takeptr = 0; --count; notifyAll(); return x; } public static class Producer implements Runnable { private BoundedBuffer1 q; Producer(BoundedBuffer1 q) { this.q = q; new Thread(this, "Producer").start(); } int i = 0; public void run() { int i = 0; while (true) { q.put(i++); } } } public static class Consumer implements Runnable { private BoundedBuffer1 q; Consumer(BoundedBuffer1 q) { this.q = q; new Thread(this, "Consumer").start(); } public void run() { while (true) { System.out.println(q.take()); } } } public static void main(String[] args) throws InterruptedException { final BoundedBuffer1 buffer = new BoundedBuffer1(); new Thread(new Producer(buffer)).start(); new Thread(new Consumer(buffer)).start(); }}View Code上面的例子中有一點(diǎn)需要知道,在調(diào)用對(duì)象的wait之前當(dāng)前線程必須先獲得該對(duì)象的監(jiān)視器(synchronized),被喚醒之后需要重新獲取到監(jiān)視器才能繼續(xù)執(zhí)行。
//wait會(huì)先釋放當(dāng)前線程擁有的監(jiān)視器obj.wait();//會(huì)re-acquire監(jiān)視器
而LockSupport并不需要獲取對(duì)象的監(jiān)視器。LockSupport機(jī)制是每次unpark給線程1個(gè)“許可”——最多只能是1,而park則相反,如果當(dāng)前 線程有許可,那么park方法會(huì)消耗1個(gè)并返回,否則會(huì)阻塞線程直到線程重新獲得許可,在線程啟動(dòng)之前調(diào)用park/unpark方法沒(méi)有任何效果。
// 1次unpark給線程1個(gè)許可LockSupport.unpark(Thread.currentThread());// 如果線程非阻塞重復(fù)調(diào)用沒(méi)有任何效果LockSupport.unpark(Thread.currentThread());// 消耗1個(gè)許可LockSupport.park(Thread.currentThread());// 阻塞LockSupport.park(Thread.currentThread());
因?yàn)樗鼈儽旧淼膶?shí)現(xiàn)機(jī)制不一樣,所以它們之間沒(méi)有交集,也就是說(shuō)LockSupport阻塞的線程,notify/notifyAll沒(méi)法喚醒。
實(shí)際上現(xiàn)在很少能看到直接用wait/notify的代碼了,即使生產(chǎn)者/消費(fèi)者也基本都會(huì)用Lock和Condition來(lái)實(shí)現(xiàn),我會(huì)在后面《Java并發(fā)包源碼學(xué)習(xí)之AQS框架(五)ConditionObject源碼分析》 文章中再回頭看這個(gè)例子。
總結(jié)下LockSupport的park/unpark和Object的wait/notify:
雖然兩者用法不同,但是有一點(diǎn),LockSupport的park和Object的wait一樣也能響應(yīng)中斷。
public static void main(String[] args) throws InterruptedException { final Thread t = new Thread(new Runnable() { @Override public void run() { LockSupport.park(); System.out.println("thread " + Thread.currentThread().getId() + " awake!"); } }); t.start(); Thread.sleep(3000); // 2. 中斷 t.interrupt();}thread 9 awake!
在我之前的一篇博客“如何正確停止一個(gè)線程”有介紹過(guò)Thread.interrupt()
Thread.interrupt()方法不會(huì)中斷一個(gè)正在運(yùn)行的線程。這一方法實(shí)際上完成的是,在線程受到阻塞時(shí)拋出一個(gè)中斷信號(hào),這樣線程就得以退出阻塞的狀態(tài)。更確切的說(shuō),如果線程被Object.wait, Thread.join和Thread.sleep三種方法之一阻塞,那么,它將接收到一個(gè)中斷異常(InterruptedException),從而提早地終結(jié)被阻塞狀態(tài)。
LockSupport.park()也能響應(yīng)中斷信號(hào),但是跟Thread.sleep()不同的是它不會(huì)拋出InterruptedException, 那怎么知道線程是被unpark還是被中斷的呢,這就依賴(lài)線程的interrupted status,如果線程是被中斷退出阻塞的那么該值被設(shè)置為true, 通過(guò)Thread的interrupted和isInterrupted方法都能獲取該值,兩個(gè)方法的區(qū)別是interrupted獲取后會(huì)Clear,也就是將interrupted status重新置為false。
AQS和Java線程池中都大量用到了中斷,主要的作用是喚醒線程、取消任務(wù)和清理(如ThreadPoolExecutor的shutdown方法),AQS中的acquire方法也有中斷和不可中斷兩種。 其中對(duì)于InterruptedException如何處理最重要的一個(gè)原則就是Don't swallow interrupts,一般兩種方法:
try { ………} catch (InterruptedException e) { // Restore the interrupted status Thread.currentThread().interrupt(); // or thow a new //throw new InterruptedException();}AQS的acquire就用到了第一種方法。
關(guān)于InterruptedException處理的最佳實(shí)踐可以看IBM的這篇文章。
最后按照慣例做下引申。上面BoundedBuffer1類(lèi)的put和take方法中的wait為什么要放在一個(gè)while循環(huán)里呢? 你如果去看Object.wait()方法的Javadoc的話會(huì)發(fā)現(xiàn)官方也是建議下面這樣的用法:
synchronized (obj) { while (<condition does not hold>) …… obj.wait(); ……}StackOverflow上有一個(gè)問(wèn)題里一個(gè)叫xagyg的回答解釋的比較清楚,有興趣的可以看下。 簡(jiǎn)單來(lái)說(shuō)因?yàn)椋?/p>
wait前會(huì)釋放監(jiān)視器,被喚醒后又要重新獲取,這瞬間可能有其他線程剛好先獲取到了監(jiān)視器,從而導(dǎo)致?tīng)顟B(tài)發(fā)生了變化, 這時(shí)候用while循環(huán)來(lái)再判斷一下條件(比如隊(duì)列是否為空)來(lái)避免不必要或有問(wèn)題的操作。 這種機(jī)制還可以用來(lái)處理偽喚醒(spurious wakeup),所謂偽喚醒就是no reason wakeup,對(duì)于LockSupport.park()來(lái)說(shuō)就是除了unpark和interrupt之外的原因。
LockSupport也會(huì)有同樣的問(wèn)題,所以看AQS的源碼會(huì)發(fā)現(xiàn)很多地方都有這種re-check的思路,我們下一篇文就來(lái)看下AbstractQueuedSynchronizer類(lèi)的源碼。
新聞熱點(diǎn)
疑難解答
圖片精選
網(wǎng)友關(guān)注