系列傳送門:
- Java併發包源碼學習系列:AbstractQueuedSynchronizer
- Java併發包源碼學習系列:CLH同步隊列及同步資源獲取與釋放
- Java併發包源碼學習系列:AQS共享式與獨佔式獲取與釋放資源的區別
- Java併發包源碼學習系列:ReentrantLock可重入獨佔鎖詳解
- Java併發包源碼學習系列:ReentrantReadWriteLock讀寫鎖解析
- Java併發包源碼學習系列:詳解Condition條件隊列、signal和await
LockSupport概述
LockSupport工具類定義了一組公共的靜態方法,提供了最基本的線程阻塞和喚醒功能,是創建鎖和其他同步類的基礎,你會發現,AQS中阻塞線程和喚醒線程的地方,就是使用LockSupport提供的park和unpark方法,比如下面這段:
// 掛起線程
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
return Thread.interrupted();
}
// 喚醒線程
private void unparkSuccessor(Node node) {
//...
if (s != null)
LockSupport.unpark(s.thread);
}
park與unpark相關方法
LockSupport提供了一組park開頭的方法來阻塞當前線程【省略static】:
void park()
:阻塞當前線程,如果調用unpark(Thread thread)方法或者當前線程被中斷,才能從park()方法返回。void parkNanos(long nanos)
:阻塞當前線程,最長不超過nanos納秒,返回條件在park()的基礎上增加了超時返回。void parkUntil(long deadline)
:阻塞當前線程,直到deadline【從1970年開始到deadline時間的毫秒數】時間。void unpark(Thread thread)
:喚醒處於阻塞狀態的線程thread。
JDK1.6中,增加了帶有blocker參數的幾個方法,blocker參數用來標識當前線程在等待的對象,用於問題排查和系統監控。
下面演示park()方法和unpark()方法的使用:
在thread線程中調用park()方法,默認情況下該線程是不持有許可證的,因此將會被阻塞掛起。
unpark(thread)方法將會讓thread線程獲得許可證,才能從park()方法返回。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() ->{
String name = Thread.currentThread().getName();
System.out.println(name + " begin park");
LockSupport.park();// 如果調用park的線程已經獲得了關聯的許可證,就會立即返回
System.out.println(name + " end park");
},"A");
thread.start(); // 默認情況下,thread不持有許可證,會被阻塞掛起
Thread.sleep(1000);
System.out.println(thread.getName() + " begin unpark");
LockSupport.unpark(thread);//讓thread獲得許可證
}
// 結果如下
A begin park
A begin unpark
A end park
你需要理解,許可證在這裏的作用,我們也可以事先給線程一個許可證,接着在park的時候就不會被阻塞了。
public static void main(String[] args) {
System.out.println("begin park");
// 使當前線程獲得許可證
LockSupport.unpark(Thread.currentThread());
// 再次調用park方法,因爲已經有許可證了,不會被阻塞
LockSupport.park();
System.out.println("end park");
}
// 結果如下
begin park
end park
中斷演示
線程被中斷的時候,park方法不會拋出異常,因此需要park退出之後,對中斷狀態進行處理。
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(() -> {
String name = Thread.currentThread().getName();
System.out.println(name + " begin park");
// 一直掛起自己,只有被中斷,纔會推出循環
while (!Thread.currentThread().isInterrupted()) {
LockSupport.park();
}
System.out.println(name + " end park");
}, "A");
thread.start();
Thread.sleep(1000);
System.out.println("主線程準備中斷線程" + thread.getName());
// 中斷thread
thread.interrupt();
}
// 結果如下
A begin park
主線程準備中斷線程A
A end park
blocker的作用
JDK1.6開始,一系列park方法開始支持傳入blocker參數,標識當前線程在等待的對象,當線程在沒有持有許可證的情況下調用park方法而被阻塞掛起時,這個blocker對象會被記錄到該線程內部。
public static void park(Object blocker) {
Thread t = Thread.currentThread();
setBlocker(t, blocker); // 設置blocker
UNSAFE.park(false, 0L);
setBlocker(t, null); // 清除blocker
}
Thread類裏有個volatile Object parkBlocker
變量,用來存放park方法傳遞的blocker對象,也就是把blocker變量存放到了調用park方法的線程的成員變量中。
接下來我們通過兩個例子感受一下:
測試無blocker
public class TestParkWithoutBlocker {
public void park(){
LockSupport.park();
}
public static void main(String[] args) throws InterruptedException {
new TestParkWithoutBlocker().park();
Thread.sleep(3000);
}
}
使用jps命令,列出當前運行的進程4412 TestPark
,接着使用jstack 4412
命令查看線程堆棧:
測試帶blocker
public class TestBlockerPark {
public void park(){
LockSupport.park(this); // 傳入blocker = this
}
public static void main(String[] args) throws InterruptedException {
new TestBlockerPark().park();
Thread.sleep(3000);
}
}
明顯的差別就在於,使用帶blocker 參數的park方法,能夠通過jstack看到具體阻塞對象的信息:
- parking to wait for <0x000000076b77dff0> (a chapter6_1_LockSupport.TestBlockerPark)
診斷工具可以調用getBlocker(Thread)方法來獲取blocker對象,JDK推薦我們使用帶有blocker參數的park方法,並且設置blocker爲this,這樣當在打印線程堆棧排查問題的時候就能夠知道那個類被阻塞了。
JDK提供的demo
老傳統了,摘一段JavaDoc上的使用案例:
/**
* 先進先出的鎖,只有隊列的首元素可以獲取鎖
*/
class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);
private final Queue<Thread> waiters
= new ConcurrentLinkedQueue<Thread>();
public void lock() {
// 中斷標誌
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);
// 不是隊首線程 或 當前鎖已經被其他線程獲取,則調用park方法掛起自己
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
// 如果park方法是因爲被中斷而返回,則忽略中斷,並且重置中斷標誌
// 接着再次進入循環
if (Thread.interrupted()) // ignore interrupts while waiting
wasInterrupted = true;
}
waiters.remove();
// 如果標記爲true,則中斷線程
// [雖然我對中斷信號不感興趣,忽略它,但是不代表其他線程對該標誌不感興趣,因此恢復一下.]
if (wasInterrupted) // reassert interrupt status on exit
current.interrupt();
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
總結
- LockSupport提供了有關線程掛起park和喚醒unpark的靜態方法。
- JDK1.6之後允許傳入blocker阻塞對象,便於問題監控和排查。
- 如果park的線程被中斷,不會拋出異常,需要自行對中斷狀態進行處理。
參考閱讀
- 翟陸續 薛冰田 《Java併發編程之美》
- 方騰飛 《Java併發編程的藝術》
- 【J.U.C】LockSupport