Java锁深入理解4——ReentrantLock VS synchronized

前言

本篇博客是《Java锁深入理解》系列博客的第四篇,建议依次阅读。
各篇博客链接如下:
Java锁深入理解1——概述及总结
Java锁深入理解2——ReentrantLock
Java锁深入理解3——synchronized
Java锁深入理解4——ReentrantLock VS synchronized
Java锁深入理解5——共享锁

怎么选择

一般情况下我们选择synchronized即可。

  • 虽说ReentrantLock更灵活。但synchronized首先写起来就很方便。
  • 而且synchronized通过优化的锁升级机制,性能更高。synchronized相当于无锁+偏向锁+轻量级锁+ReentrantLock。
  • 无论是开源框架还是JDK自己的代码,synchronized的应用都比ReentrantLock更普遍。
    如ConcurrentHashMap在1.8版就每个segment的分段锁ReentrantLock删掉了,改为使用CAS+synchronized。
    再比如,相比较用ReentrantLockd的logback。性能更好的log4j2也是用的synchronized(当然,log4j2性能比较好,还有很多其他优化的地方。比如用性能更强悍的环形队列)

中断

在说到ReentrantLock 和 synchronized区别时,会提到一点:ReentrantLock可以被中断,synchronized不可被中断
你是否想过这句话是准确的含义是什么。

怎么中断

是通过报错吗?并不是。这里的中断特指一种给线程发的信号(注意:这仅仅是一种信号。至于怎么处理这种信号,需要有其他逻辑去实现。有点类似Java“注解”的概念),让线程停下来。
怎么发信号呢

t.interrupt();//t是一个线程

这样线程就接收到了中断信号。但是线程本身是不会处理信号的,它只负责接收。处理信号,需要t线程内部去处理。比如sleep, wait, ReentrantLock。(如果对中断没有概念的,可以先看看我的另一篇博客,其中有中断的介绍,这里就不再重复了)
所以,前面那句话的意思就是:如果线程中使用了ReentrantLock,它会响应中断信号,并报一个异常。 而synchronized却会无视这种信号

说到这里,你也许对如何响应中断还是没个清晰的概念。那就做个实验。写个直观的demo看一下。
怎么写demo呢。你可能会想,写一个线程,里面套上ReentrantLock,然后在里面sleep几秒钟,启动后,在主线程执行中断信号t.interrupt();就行了。但如果你看过前面那篇博客,就会知道sleep本身就会响应中断信号而报错(wait也是)。这不就是在引入干扰因素了吗。
其实上面的想法,一开始就错了。因为ReentrantLock响应中断,不是在运行中,而是在等待中
而且要响应中断,我们不能使用普通的.lock()方法,而是使用lockInterruptibly()方法来抢锁。

所以完整的说法是:如果线程中使用了ReentrantLock的lockInterruptibly(),线程在等待过程中会响应中断信号,并报一个异常。 而synchronized在等待过程中却会无视这种信号

这样说的话,demo好像更难写了。我们怎么抓住稍纵即逝的“抢锁等待间隙”呢。换个思路,我们可以让一个线程“望眼欲穿的一直等”呀。
也就是让另一个线程走在它前面,通过ReentrantLock同步挡着他,不让它走(ReentrantLock不就是干这个事的吗)。

Demo

import java.util.concurrent.locks.LockSupport;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;

/**
 * 在这个示例中,我们创建了两个线程,都试图获取同一个ReentrantLock实例。
 * 我们使用`lockInterruptibly()`方法获取锁,这样当一个线程等待锁时,可以响应中断。
 *
 * `main()`方法中,我们启动两个线程,让它们尝试获取锁。
 * 然后,我们等待1秒,以确保两个线程都开始运行。接着,我们中断第二个线程,使用`thread2.interrupt()`方法。
 * 当第二个线程被中断时,`lockInterruptibly()`方法抛出InterruptedException异常,
 * 我们在异常处理代码中输出线程被中断的信息,并重新设置线程的中断状态。
 *
 * 最后,我们等待两个线程执行完毕,示例结束。
 */
public class ReentrantLockInterruptExample {
    private final ReentrantLock lock = new ReentrantLock();

    public static void main(String[] args) throws InterruptedException {
        ReentrantLockInterruptExample example = new ReentrantLockInterruptExample();

        // 创建并启动两个线程
        Thread thread1 = new Thread(() -> example.method(), "Thread 1");
        Thread thread2 = new Thread(() -> example.method(), "Thread 2");
        thread1.start();
        TimeUnit.MILLISECONDS.sleep(1);//保证线程1先抢到锁,然后把线程2堵住
        thread2.start();

        // 等待两个线程都开始运行
        TimeUnit.SECONDS.sleep(1);

        // 中断第二个线程
        thread2.interrupt();

        // 等待两个线程执行完毕
        thread1.join();
        thread2.join();
    }

    /**
     * 证明了:lockInterruptibly()可以在等待的时候被打断
     */
    public void method() {
        try {
            System.out.println(Thread.currentThread().getName() + " trying to acquire the lock...");
            lock.lockInterruptibly(); // 可以响应中断的锁获取方法(替换原来动lock.lock()方法)

            try {
                System.out.println(Thread.currentThread().getName() + " acquired the lock.");
                // 临界区代码
                TimeUnit.SECONDS.sleep(5);
                System.out.println(Thread.currentThread().getName() + "--------------2-");
            } finally {
                lock.unlock();
                System.out.println(Thread.currentThread().getName() + " released the lock.");
            }
        } catch (InterruptedException e) {
            // 处理中断逻辑
            System.out.println(Thread.currentThread().getName() + " was interrupted.");
            Thread.currentThread().interrupt(); // 重新设置中断状态
        }
    }
    
 }

运行结果:

Thread 1 trying to acquire the lock...
Thread 1 acquired the lock.
Thread 2 trying to acquire the lock...
Thread 2 was interrupted.
Thread 1--------------2-
Thread 1 released the lock.

Thread 2没能抢到锁,接收到中断信号之后,直接报错进入catch。

注意:Thread 1是不会响应异常的,因为thread2.interrupt();是专门针对Thread 2发的中断信号。

同样,你可以把lockInterruptibly()换回原来的lock(),看看会不会报错,对照一下。
而如果把ReentrantLock换成synchronized,却发现synchronized根本没有这样的检查异常可以被捕获,所以还得把catch删掉。
最终我们验证了上面的结论:ReentrantLock可以响应异常,而synchronized不行

会在运行中被中断吗

lockInterruptibly()可以在等待的时候被打断。那么在运行时会被打断吗?

:不会。看过AQS源码,我们知道:锁根本不关心程序动运行过程。它只会在线程碰到lock.lock(), conditon.await()等节点时起作用。
线程运行过程中和锁没半毛钱关系,锁并不会去监视线程的运行
就像高速公路动收费站,只会在开始结尾时管你,中间并不会管你干什么。
而"等待中"之所以可以被中断,那是因为"等待中"并不是属于运行时。相当于你还堵在收费站门口。所以它才可以中断你。
那么synchronized也是一样,只不过它在等待过程中也不管你。

什么机制

直接原因就是lockInterruptibly()代码中有:
在这里插入图片描述
使用Thread.interrupted()判断接受到中断信号的时候,就抛出异常throw new InterruptedException();

我在之前那篇博客中介绍中断的用法,demo的写法就是这样:自己发信号,自己接受信号,自己处理。很常规的思路。

但是,在lockInterruptibly()有两处判断并抛异常的地方,一个是一开始抢锁的时候,就是上面所说的常规处理中断信号的方式。
还有一个是循环抢锁的过程中。
在这里插入图片描述
当这里返回true,就会执行if语句方法体内的抛中断异常语句了。
但这个方法为什么会返回true呢,之前我们在看普通lock()方法时也是这样的逻辑,为什么那个地方不会返回true呢。这就是parkAndCheckInterrupt这个方法的神奇之处了。

parkAndCheckInterrupt

    private final boolean parkAndCheckInterrupt() {
        LockSupport.park(this);
        return Thread.interrupted();
    }

这是AQS中的一个方法:阻塞并检查中断。
为什么这个方法要单独讲讲,因为我们前面一直说阻塞(park)。但从未见过庐山真面目。它就长这样。而且这个方法有它的神奇之处。
会停下来,我们已经见识过了。然后接收到unpark信号之后,会继续走,我们也知道。但它重新被唤醒,除了因为unpark,还有一种情况:收到t.interrupted();中断信号。

之前我们可能一直认为:中断(interrupted),它只是一个信号。
没想到在这里,它真的能直接对线程的动作起作用(当然,再往下看c++代码,肯定也仅仅是接收信号,然后让线程继续走。
只是说,站在Java代码层面,中断信号在这里的表现不仅仅是一个“信号”那么简单,它将和unpark属于一个级别)。

所以说:park让线程停下来,而unpark和interrupted会让线程继续运行
但看过代码我们就知道,前面我们在说await/signal,本质就是park/unpark在起作用(一个负责停,一个负责重新启动)
而这里我们说中断。park负责停,而interrupted负责重新启动。
关键的来了:上面这两种情况居然用的是同一套代码,就是上面我展示这个parkAndCheckInterrupt方法。
这不会出现混乱吗?都是唤醒线程,我们哪知道它是怎么被唤醒的。

就是方法中那个我们可能一直忽略的Thread.interrupted()在起作用。

Thread.interrupted()
这个方法在说中断的时候也讲过,意思就是判断:线程有没有接收到中断信号,有接收到返回true,没有就返回false(和t.interrupted()配合使用,里应外合对线程逻辑做小动作)。

其实还没完。信号相当于写在小黑板上的一个标记。当外部给线程发中断信号,就是往小黑板上写了一个中断标记。然后就可以被Thread.interrupted()读到,读完之后:它就把这个标记擦掉了。(有点像“阅后即焚”)。再去读(执行Thread.interrupted())就读不到了。
所以,相当于Thread.interrupted()有两个功能,先

接着讲parkAndCheckInterrupt方法。
有了Thread.interrupted(),我们就能弥补线程莫名其妙被唤醒的尴尬:当线程再次开始执行,就执行Thread.interrupted(),它的返回值将会告我们,它是被什么唤醒的。
现在就很完美了。这也是park的官方推荐的严谨使用方法。

小结

  • ReentrantLock可以响应异常,而synchronized不行
  • ReentrantLock响应中断是指 线程等待时中断,不会在运行中中断(计算机不存在什么无所不在的“上帝”,“超距感应”。其实底层原理都特别简单。)
  • 中断的本质还是中断的常规用法:t.interrupted()和Thread.interrupted()配合使用,里应外合传信号,手动处理逻辑。
  • 中断除了可以作为单纯的信号,还可以直接作用在阻塞park上,起到唤醒阻塞的作用。可以通过Thread.interrupted()来区分是不是因为中断而苏醒的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值