爲什麼Synchronized不可中斷?

爲什麼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獲取到鎖之後,也是得交由線程自己決定。

擴展閱讀

Thread類源碼解讀(3)——線程中斷interrupt


轉自作者:蔣老溼
鏈接:https://juejin.im/post/5ea25cb9518825737733e28a

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章