爲什麼Synchronized不可中斷?首先中斷操作是Thread
類調用interrupt
方法實現的。基本上所有人都說Synchronized
後線程不可中斷,百度後的大部分文章都是這樣解釋說道:
不可中斷的意思是等待獲取鎖的時候不可中斷,拿到鎖之後可中斷,沒獲取到鎖的情況下,中斷操作一直不會生效。
驗證真僞
以下爲測試理論是否成立的Demo代碼示例:
public class Uninterruptible {
private static final Object o1 = new Object();
public static void main(String[] args) throws InterruptedException {
Thread thread1 = new Thread(() -> {
System.out.println("t1 enter");
synchronized (o1) {
try {
System.out.println("start lock t1");
Thread.sleep(20000);
System.out.println("end lock t1");
} catch (InterruptedException e) {
System.out.println("t1 interruptedException");
e.printStackTrace();
}
}
});
Thread thread2 = new Thread(() -> {
System.out.println("t2 enter");
synchronized (o1) {
try {
System.out.println("start lock t2");
Thread.sleep(1000);
System.out.println("end lock t2");
} catch (InterruptedException e) {
System.out.println("t2 interruptedException");
e.printStackTrace();
}
}
});
thread1.start();
thread2.start();
// 主線程休眠一下,讓t1,t2線程百分百已經啓動,避免線程交替導致測試結果混淆
Thread.sleep(1000);
// 中斷t2線程的執行
thread2.interrupt();
System.out.println("t2 interrupt...");
}
}
複製代碼
運行結果:
t1 enter
start lock t1
t2 enter
t2 interrupt... // 此處等待了好久好久,一直卡住
end lock t1
start lock t2 // 直到t1執行完釋放鎖後,t2拿到鎖準備執行時,interruptedException異常拋出
t2 interruptedException
java.lang.InterruptedException: sleep interrupted
at java.lang.Thread.sleep(Native Method)
at concurrent.Uninterruptible.lambda$main$1(Uninterruptible.java:48)
at concurrent.Uninterruptible$$Lambda$2/1134517053.run(Unknown Source)
at java.lang.Thread.run(Thread.java:745)
Process finished with exit code 0
複製代碼
結果正好印證了Synchronized不可中斷的說法:只有獲取到鎖之後才能中斷,等待鎖時不可中斷。
深入分析Synchronized
爲什麼Synchronized
要設計成這樣,ReentrantLock
都允許馬上中斷呀,是Synchronized
設計者有意爲之還是另有苦衷?
感覺如果設計成這樣有點蠢吧,爲什麼要拿到鎖纔去中斷,毫無理由啊。肯定有陰謀!
後來看了Thread.interrupt()
源碼發現,這裏面的操作只是做了修改一箇中斷狀態值爲true,並沒有顯式聲明拋出InterruptedException
異常。
/**
* <p> If this thread is blocked in an invocation of the {@link
* Object#wait() wait()}, {@link Object#wait(long) wait(long)}, or {@link
* Object#wait(long, int) wait(long, int)} methods of the {@link Object}
* class, or of the {@link #join()}, {@link #join(long)}, {@link
* #join(long, int)}, {@link #sleep(long)}, or {@link #sleep(long, int)},
* methods of this class, then its interrupt status will be cleared and
* it will receive an {@link InterruptedException}.
* 翻譯:如果此線程被以下命令(wait、join、sleep)阻塞,他的中斷狀態會被
* 清除並且會拋出InterruptedException異常
*
* <p> If none of the previous conditions hold then this thread's
* interrupt status will be set. </p>
* 翻譯:如果前面的條件都不滿足那麼將設置它的中斷狀態
*/
public void interrupt() {
if (this != Thread.currentThread())
checkAccess(); // 檢查權限
synchronized (blockerLock) {
Interruptible b = blocker;
if (b != null) {
interrupt0(); // 它是一個native方法, Just to set the interrupt flag
b.interrupt(this);
return;
}
}
interrupt0();
}
複製代碼
得到一個解釋說,中斷操作只是給線程的一個建議,最終怎麼執行看線程本身的狀態,那麼什麼狀態做什麼事情呢?
- 若線程被中斷前,如果該線程處於非阻塞狀態(未調用過
wait
,sleep
,join
方法),那麼該線程的中斷狀態將被設爲true, 除此之外,不會發生任何事。 - 若線程被中斷前,該線程處於阻塞狀態(調用了
wait
,sleep
,join
方法),那麼該線程將會立即從阻塞狀態中退出,並拋出一個InterruptedException
異常,同時,該線程的中斷狀態被設爲false, 除此之外,不會發生任何事。
查看wait
, sleep
, join
方法源碼,驗證上面的第2點:
/** @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public static native void sleep(long millis) throws InterruptedException;
/** @throws InterruptedException if any thread interrupted the
* current thread before or while the current thread
* was waiting for a notification. The <i>interrupted
* status</i> of the current thread is cleared when
* this exception is thrown.
* @see java.lang.Object#notify()
* @see java.lang.Object#notifyAll()
*/
public final native void wait(long timeout) throws InterruptedException;
/** @throws InterruptedException
* if any thread has interrupted the current thread. The
* <i>interrupted status</i> of the current thread is
* cleared when this exception is thrown.
*/
public final void join() throws InterruptedException {
join(0);
}
複製代碼
通過註釋可以看到這三個方法都會去檢查中斷狀態,隨時拋出中斷異常。native method屬於本地方法了,如果想看內部的實現細節,請各位同爲結合hotspot
源碼對比閱讀,這裏就不細說了。
所以說,Synchronized
鎖此時爲輕量級鎖或重量級鎖,此時等待線程是在自旋運行或者已經是重量級鎖導致的阻塞狀態了(非調用了wait
,sleep
,join
等方法的阻塞),只把中斷狀態設爲true,沒有拋出異常真正中斷。
對比ReentrantLock
那爲什麼ReentrantLock
可中斷呢(未獲取到鎖也可中斷),但是必須使用ReentrantLock.lockInterruptibly()
來獲取鎖,使用ReentrantLock.lock()
方法不可中斷。
來看看ReentrantLock.lockInterruptibly()
源碼:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1); // 調用可中斷的獲取鎖方法
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted()) // 獲取鎖時檢查中斷狀態
// 顯式拋中斷異常
throw new InterruptedException();
if (!tryAcquire(arg)) // 獲取不到鎖,執行doAcquireInterruptibly
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
// 把線程放進等待隊列
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
// 自旋
for (;;) {
// 獲取前置節點
final Node p = node.predecessor();
// 前置節點爲頭節點 && 當前節點獲取到鎖
if (p == head && tryAcquire(arg)) {
// 當前節點設爲頭節點
setHead(node);
p.next = null; // 應用置null,便於GC
failed = false;
// 結束自旋
return;
}
// 檢查是否阻塞線程 && 檢查中斷狀態
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 顯式拋中斷異常
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
複製代碼
從源碼可以知道,ReentrantLock.lockInterruptibly()
首次嘗試獲取鎖之前就會判斷是否應該中斷,如果沒有獲取到鎖,在自旋等待的時候也會繼續判斷中斷狀態。這時lockInterruptibly
底層再顯式拋錯,而不是像Synchronized
那樣交由線程自己決定是否拋錯。當然lockInterruptibly
獲取到鎖之後,也是得交由線程自己決定。
擴展閱讀
轉自作者:蔣老溼
鏈接:https://juejin.im/post/5ea25cb9518825737733e28a