Java多线程(二)——ReentrantLock源码解析(补充2——可打断的锁)

ReentrantLock源码解析(补充2——可打断的锁)

上一章 ReentrantLock源码解析 仅介绍了 ReentrantLock 的常用方法以及公平锁、非公平锁的实现。这里对上一章做一些补充。主要是:

1. 可打断锁的使用

打断锁的使用场景:线程 A 持有锁,并且临界区执行时间很长,线程 B 不想等待那么久,可以被中断,并放弃锁的争抢(退出 AQS 的等待队列)。

场景模拟如下:

  1. 线程 t2 中临界区执行时间很长。
  2. 线程 t1 如果等待超过5秒还是等不到锁资源,就中断t1的等待。
public class ReentrantLockDemo1 {
    ReentrantLock lock = new ReentrantLock();

    Runnable r_locked = new Runnable() {
        @Override
        public void run() {
            lock.lock();
            try{
                //模拟临界区的执行时间很长
                TimeUnit.SECONDS.sleep(20);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                lock.unlock();
            }
        }
    };

    Runnable r = new Runnable() {
        @Override
        public void run() {
            try {
                System.out.println(Thread.currentThread().getName()+" 尝试获取锁");
                lock.lockInterruptibly();
            } catch (InterruptedException e) {
                e.printStackTrace();
                System.out.println(Thread.currentThread().getName()+" 还没获取到锁,被中断了");
                return;
            }
            try{
                //临界区代码
                System.out.println(Thread.currentThread().getName()+" 获取到锁,并执行临界区代码");
            }finally {
                System.out.println(Thread.currentThread().getName()+" 释放锁");
                lock.unlock();
            }
        }
    };


    public static void main(String[] args) throws InterruptedException {
        ReentrantLockDemo1 demo = new ReentrantLockDemo1();
        //t1等待锁
        Thread t1 = new Thread(demo.r,"t1");
        //t2中临界区执行时间很长
        Thread t2 = new Thread(demo.r_locked,"t2");
        t2.start();
        t1.start();

        //我希望,如果五秒后,t1 还是等不到锁的释放,那么中断t1,让它退出锁的等待
        TimeUnit.SECONDS.sleep(3);
        System.out.println("打断t1的等待");
        t1.interrupt();

    }

}

运行结果如下:

t1 尝试获取锁
打断t1的等待
t1 还没获取到锁,被中断了
java.lang.InterruptedException
	at java.util.concurrent.locks.AbstractQueuedSynchronizer.doAcquireInterruptibly(AbstractQueuedSynchronizer.java:898)...

Process finished with exit code 0

可以发现,线程 t2 还在持有锁资源, 我们不希望线程 t1 继续等待锁资源,中断线程 t1, t1 响应中断,并抛出异常。

如果 t1 使用的是 lock.lock() 则 t1 在等待过程中,不会响应中断。

这里所谓的响应中断: 如果由于 interrupt() 导致线程从 等待状态(WAITING) 回到运行态 (RUNNABLE),该线程的中断标志为 true, 可以通过判断中断标志,并抛出异常,来响应中断。这部分原理在下面源码分析有解释。

2. 可打断原理

通过 lock.lockInterruptibly() 来进入可打断的锁获取。

@Override
public void run(){
    try{
    	lock.lockInterruptibly();
    }catch(InterruptedException e){
        //处理等待锁时,线程被中断后的处理
        return;
    }
    //如果没被中断,且等到并获取到了锁,进入临界区
    try{
        //临界区代码
    }finally{
        //释放锁
        lock.unlock();
    }
}

ReentrantLock 调用 sync.acquireInterruptibly(1) 来启动可打断锁,它和 AQS#acquire() 一样是一个模板方法。

//sync 调用的是父类 AQS 的acquireInterruptibly()方法
public final void acquireInterruptibly(int arg)
            throws InterruptedException {
    //如果当前线程本来就被中断,就响应中断,不进行锁的获取
    if (Thread.interrupted())
        throw new InterruptedException();
    //尝试锁的获取——具体实现分公平锁与非公平锁
    //公平锁:先看队列有没有等待节点,如果没有才cas一次尝试获取锁
    //非公平锁:先cas一次尝试获取锁
    if (!tryAcquire(arg))
        //如果没获取到锁,准备进入等待队列
        //doAcquireInterruptibly()实现内容 与 lock()方法调用链中的 accquireQueued(addWaiter(),1) 几乎一样
        doAcquireInterruptibly(arg);
}

doAcquireInterruptibly() 就是在原先 acquireQueued() 的 for( ; ; ) 基础上更改为可响应中断(可以抛出中断异常):

private void doAcquireInterruptibly(int arg)
        throws InterruptedException {
    //把 addWaiter() 方法放到里面来实现
    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; // help GC
                failed = false;
                return;
            }
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //在 park 过程中如果被 interrupt 会进入此
                // 这时候抛出异常,而不会再次进入 for(;;)
                throw new InterruptedException();
        }
    } finally {
        if (failed)
            cancelAcquire(node);
    }
}

private final boolean parkAndCheckInterrupt() {
    LockSupport.park(this);
    //LockSupport.park()使Java线程从运行态(RUNNABLE)进入到 等待状态(WAITING)
    //LockSupport.unpark() 或者 thread.interrupt()方法都可以让Java线程从 等待状态回到运行态。后者多了一步:将线程的中断标记置为 true
    return Thread.interrupted();
}

对比看一下 acquireQueued() 方法,只有下面展示的一处不同:

final boolean acquireQueued(final Node node, int arg) {
    try {
        for (;;) {
            。。。
            if (shouldParkAfterFailedAcquire(p, node) &&
                parkAndCheckInterrupt())
                //只有这里不同!!!
                interrupted = true;
        }
    } finally {
        。。。
    }
}

显然,两个方法都是死循环。

不同的是,doAcquireInterruptibly() 在死循环中,如果发现线程被 interrupt() 将会抛出异常,抛出异常后不再进入 for( ; ; )

parkAndCheckInterrupt() 由于 interrupt() 方法,继续执行 return方法,返回了线程的中断标记,由于 interrupt() 将线程的中断标记设为 true,所以在死循环中,会进入if方法体,执行 InterruptedException 异常的抛出。抛出异常后不再进入 for ( ; ; ) 。

而 acquireQueued() 则不会因为线程被 interrupt() 而抛出异常,而是继续进入for( ; ; )

acquireQueued() 中,线程如果被 interrupt() ,parkAndCheckInterrupt() 确实会返回 true, 但仅仅将 interrupted 局部变量设为 true,然后直接再次进入循环。由于此时锁资源还在被使用,该线程还会进入等待状态: parkAndCheckInterrupt() 方法并通过 LockSupport.park() 使该线程又进入AQS的等待队列并阻塞等待。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值