🔐 在 Java 中,常用的锁有两种:synchronized(内置锁)和 ReentrantLock(可重入锁,re+entrant)。
二者的功效都是相同的,但又有很多不同点,所以我们今天就来聊聊。
区别1:用法不同
synchronized
可用来修饰普通方法、静态方法和代码块,而 ReentrantLock
只能用在代码块上。
synchronized
基础使用
使用synchronized
修饰代码块:
1 | public void method() { |
ReentrantLock 基础使用
ReentrantLock
在使用之前需要先创建 ReentrantLock
对象,然后使用 lock
方法进行加锁,使用完之后再调用 unlock
方法释放锁,具体使用如下:
1 | public class LockExample { |
区别2:获取锁和释放锁方式不同
synchronized
会自动加锁和释放锁,当进入 synchronized
修饰的代码块之后会自动加锁,当离开 synchronized
的代码段之后会自动释放锁,如下图所示:
而 ReentrantLock 需要手动加锁和释放锁,如下图所示:
PS:在使用ReentrantLock
时要特别小心,unlock
释放锁的操作一定要放在finally
中,否者有可能会出现锁一直被占用,从而导致其他线程一直阻塞的问题。
区别3:锁类型不同
公平锁
多个线程按照申请锁的顺序去获得锁,线程会直接进入队列去排队,永远都是队列的第一位才能得到锁。
优点:所有的线程都能得到资源,不会饿死在队列中。
缺点:吞吐量会下降很多,队列里面除了第一个线程,其他的线程都会阻塞,cpu唤醒阻塞线程的开销会很大。
非公平锁
多个线程去获取锁的时候,会直接去尝试获取,获取不到,再去进入等待队列,如果能获取到,就直接获取到锁。
优点:可以减少CPU唤醒线程的开销,整体的吞吐效率会高点,CPU也不必取唤醒所有线程,会减少唤起线程的数量。
缺点:你们可能也发现了,这样可能导致队列中间的线程一直获取不到锁或者长时间获取不到锁,导致饿死。
synchronized
属于非公平锁,而ReentrantLock
既可以是公平锁也可以是非公平锁。默认情况下ReentrantLock
为非公平锁,这点查看源码可知:
使用new ReentrantLock(true)可以创建公平锁,查看源码可知:
区别4:响应中断不同
ReentrantLock
可以使用lockInterruptibly
获取锁并响应中断指令,而synchronized
不能响应中断,也就是如果发生了死锁,使用 synchronized
会一直等待下去,而使用ReentrantLock
可以响应中断并释放锁,从而解决死锁的问题,比如以下ReentrantLock
响应中断的示例:
1 | import java.util.concurrent.TimeUnit; |
以上程序的执行结果如下所示:
区别5:底层实现不同
synchronized
是 JVM 层面通过监视器(Monitor)实现的,而 ReentrantLock
是通过 AQS(AbstractQueuedSynchronizer)
程序级别的 API 实现。synchronized
通过监视器实现,可通过观察编译后的字节码得出结论,如下图所示:
1 | public class SynchronizedTest { |
其中 monitorenter
表示进入监视器,相当于加锁操作,而 monitorexit
表示退出监视器,相当于释放锁的操作。ReentrantLock
是通过 AQS 实现,可通过观察 ReentrantLock
的源码得出结论,核心实现源码如下:
小结
synchronized
和 ReentrantLock
都是 Java 中提供的可重入锁,二者的主要区别有以下 5 个:
- 用法不同:synchronized 可以用来修饰普通方法、静态方法和代码块,而 ReentrantLock 只能用于代码块。
- 获取锁和释放锁的机制不同:synchronized 是自动加锁和释放锁的,而 ReentrantLock 需要手动加锁和释放锁。
- 锁类型不同:synchronized 是非公平锁,而 ReentrantLock 默认为非公平锁,也可以手动指定为公平锁。
- 响应中断不同:ReentrantLock 可以响应中断,解决死锁的问题,而 synchronized 不能响应中断。
- 底层实现不同:synchronized 是 JVM 层面通过监视器实现的,而 ReentrantLock 是基于 AQS 实现的。