为什么Synchronized不可中断?

2020-04-30 15:53:08 浏览数 (3)

为什么Synchronized不可中断?首先中断操作是Thread类调用interrupt方法实现的。基本上所有人都说Synchronized后线程不可中断,百度后的大部分文章都是这样解释说道:

不可中断的意思是等待获取锁的时候不可中断,拿到锁之后可中断,没获取到锁的情况下,中断操作一直不会生效。

验证真伪

以下为测试理论是否成立的Demo代码示例:

代码语言:javascript复制
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...");

    }
}
复制代码

运行结果:

代码语言:javascript复制
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异常。

代码语言:javascript复制
/**
 * <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点:

代码语言:javascript复制
 /** @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()源码:

代码语言:javascript复制
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获取到锁之后,也是得交由线程自己决定。

0 人点赞