什么是ReentrantLock
ReentrantLock是concurrent包下的一个处理并发同步的类,实现了Lock接口,是一个可重入且独占式的锁,基本功能作用和synchronized关键字类似。
但ReentrantLock更灵活、强大,增加了轮询、超时、中断、公平锁和非公平锁等高级功能。可以用来代替synchronized关键字。
举例
如果用synchronized
关键字,代码如下:
java
public class Counter{
private int count;
public void add(int n){
synchronized(this) {
count += n;
}
}
}
如果用ReentrantLock
替代,代码如下:
java
public class Counter{
private final Lock lock = new ReentrantLock();
private int count;
private void add(int n) {
lock.lock();
try {
count += n;
} finally {
lock.unlock();
}
}
}
公平锁和非公平锁
- 公平锁:锁被释放之后,先申请的线程先得到锁。为了保证时间上的绝对顺序、公平,上下文切换更频繁,因此性能较差一些
- 非公平锁:锁被释放之后,后申请的线程可能会先得到锁,是随机或者按照其他优先级进行排序的。性能更好,但可能会导致某些线程永远无法获取到锁。
可中断锁和不可中断锁有什么区别?
- 可中断锁 :获取锁的过程中可以被中断,不需要一直等到获取锁之后 才能进行其他逻辑处理。
ReentrantLock
就属于是可中断锁。 - 不可中断锁 :一旦线程申请了锁,就只能等到拿到锁以后才能进行其他的逻辑处理。
synchronized
就属于是不可中断锁。
独占模式和共享模式
-
排他锁也叫独占锁,是指该锁一次只能被一个线程所持有。如果线程T对数据A加上排它锁后,则其他线程不能再对A加任何类型的锁。获得排它锁的线程既能读数据又能修改数据。
-
共享锁是指该锁可被多个线程所持有。如果线程T对数据A加上共享锁后,则其他线程只能对A再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
ReentrantLock和synchronized对比
两者都是可重入锁
可重入锁 也叫递归锁,指的是线程可以再次获取自己的内部锁。比如一个线程获得了某个对象的锁,此时这个对象锁还没有释放,当该线程再次想要获取这个对象锁的时候还是可以获取的。如果是不可重入锁的话,会造成死锁。 JDK提供的所有Lock实现类,以及synchronized关键字,实现的锁都是可重入的。
重入场景举例
java
// synchronized关键字修饰
public class SynchronizedDemo{
public synchronized void method1(){
System.out.println("方法1");
method2();
}
public synchronized void method2(){
System.out.println("方法2")
}
}
//使用ReentrantLock
class LockReentrant implements Runnable{
private final Lock lock = new ReentrantLock();
public void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "method1()");
method2();
} finally {
lock.unlock();
}
}
public void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + "method2()");
} finally {
lock.unlock();
}
}
@Override
public void run() {
//线程启动 执行方法1
method1();
}
}
由于synchronized
和ReentrantLock
都是可重入锁,同一个线程在调用method1()
时获取当前对象锁之后,再执行method2()
的时候,可以再次获取这个对象的锁,不会产生死锁问题。假如两个都不是可重入锁,由于该对象的锁已经被当前线程所持有且无法释放,这会导致线程在执行method2()
时获取锁失败,会出现死锁问题。
synchronized依赖于JVM,ReentrantLock依赖于API
- synchronized实现并发同步的功能,是依赖于JVM实现的,通过字节码分析,可以看到这个是通过获取monitor对象来获取锁的。它的加锁与释放是自动的,无需我们关心,不需要在代码里进行处理。
- ReentrantLock是在JDK层面实现的,需要使用lock、unlock方法,配合try、finally来实现。
ReentrantLock相比synchronized增加了一些高级功能
- 等待可中断 :
ReentrantLock
提供了一种能够中断等待锁的线程的机制,通过lock.lockInterruptible
来实现。也就是说正在等待的线程可以选择放弃等待,改为处理其他事情。 - 可实现公平锁 :
ReentrantLock
可以指定是公平锁还是非公平锁,而synchronized
只能是非公平锁。ReentrantLock
默认是非公平锁,可以通过ReentrantLock(boolen fair)
构造方法来指定是否公平锁。 - 可实现选择性通知 :
synchronized
关键字与wait()
和notify()
/notifyAll()
方法相结合可以实现等待/通知机制。ReentrantLock
也可以实现,但是需要借助于Condition
接口与newCondition()
方法。(具体使用方式待补充)
实际项目中的应用
暂时没在业务代码中看到过,目前业务代码并发相关的处理,直接使用hutool的ThreadUtil。
使用注意事项
- 默认情况下 ReentrantLock 为非公平锁而非公平锁;
- 加锁次数和释放锁次数一定要保持一致,否则会导致线程阻塞或程序异常;
- 加锁操作一定要放在 try 代码之前,这样可以避免未加锁成功又释放锁的异常;
- 释放锁一定要放在 finally 中,否则会导致线程阻塞。 反例
java
import java.util.concurrent.locks.ReentrantLock;
public class LockExample {
// 创建锁对象
private static final ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
try {
// 此处异常
int num = 1 / 0;
// 加锁操作
lock.lock();
} finally {
// 释放锁
lock.unlock();
System.out.println("锁释锁");
}
System.out.println("程序执行完成.");
}
}
ReentrantLock源码分析
类结构
ReentrantLock
类内部共存在Sync
、NonfairSync
、FairSync
三个内部类。
NonfairSync
与FairSync
类继承自Sync类Sync
类继承自AbstractQueuedSynchronizer
抽象类
加锁
java
// 代码中使用ReentrantLock的方式
public class ReEntrantLockDemo {
public static void main(String[] args) {
new Thread(new LockReentrant()).start();
}
}
class LockReentrant implements Runnable{
private final Lock lock = new ReentrantLock();
public void method1(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " method1()");
method2();
}finally {
lock.unlock();
}
}
public void method2(){
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " method2()");
}finally {
lock.unlock();
}
}
@Override
public void run() {
//线程启动 执行方法1
method1();
}
}
上面demo中的lock.lock
实际调用的是ReentrantLock
中的下面方法
java
public void lock() {
sync.lock();
}
上述lock方法实际又调用了sync对象的lock方法,该方法是1个抽象方法,需要实现类:NonfairSync
与FairSync
进行具体的实现.
java
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
/**
* Performs {@link Lock#lock}. The main reason for subclassing
* is to allow fast path for nonfair version.
*/
abstract void lock();
NonfairSync
类
java
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
/**
* Performs lock. Try immediate barge, backing up to normal
* acquire on failure.
*/
final void lock() {
//以cas方式尝试将AQS中的state从0更新为1
//更新成功则表明获取锁成功,并设为独占模式,其他线程不可再获取该锁。
//state在ReentrantLock的语境下等同于锁被线程重入的次数,
//state为0意味着只有当前锁未被任何线程持有时该动作才会返回成功
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
// 如果锁被占用,或者set失败,则执行该方法
else
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
上述acquire(1)
方法,就是AbstractQueuedSynchronizer
类中的下面方法
java
//AbstractQueuedSynchronizer类中的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 获取锁失败的线程要通过addWaiter方法加入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
//tryAcquire方法,又调用NonfairSync类的下述方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
//nonfairTryAcquire方法,是Sync类下面的方法
final boolean nonfairTryAcquire(int acquires) {
// 获取当前线程实例
final Thread current = Thread.currentThread();
// 获取state变量的值,即当前锁被重入的次数
int c = getState();
// 如果锁被重入的次数为0,即当前没有线程持有该锁
if (c == 0) {
// 以cas方式更新state,更新成功,则说明获取锁成功
if (compareAndSetState(0, acquires)) {
// 获取锁成功,则将当前线程标记为持有锁的线程
setExclusiveOwnerThread(current);
// 返回获取锁成功,非重入
return true;
}
}
// 如果锁被重入的次数不为0,而且当前线程就是持有锁的线程
else if (current == getExclusiveOwnerThread()) {
// 计算锁被重入的次数,用于更新state值
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
// 非同步方式更新state值
setState(nextc);
// 返回获取锁成功,重入
return true;
}
return false;
}
获取锁(加锁)失败后的处理
java
//下面都是AbstractQueuedSynchronizer类中的方法
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 获取锁失败的线程要通过addWaiter方法加入同步队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
//首先创建一个新节点,并将当前线程实例封装在内部,mode这里为null
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
// 下面这部分逻辑,在enq方法里都有
// 之所以加上这部分"重复代码"和尝试获取锁时的"重复代码"一样,对某些特殊情况 进行提前处理,牺牲一定的代码可读性换取性能提升。
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node); //入队的逻辑
return node;
}
private Node enq(final Node node) {
for (;;) {
//t指向当前队列的最后一个节点,队列为空则为null
Node t = tail;
// 如果队列为空,则需要初始化
if (t == null) { // Must initialize
// 构造一个空节点,cas方式设置为队首元素,当head=null的时候更新成功(仅当原值为null时更新成功)
if (compareAndSetHead(new Node()))
// 尾指针指向首结点
tail = head;
} else { // 队列不为空
node.prev = t;
// cas方式将尾指针指向当前节点,当t(原来的尾指针)==tail(当前真实的尾指针)时执行成功
//(仅当原值为t时更新成功)
if (compareAndSetTail(t, node)) {
// 原尾节点的next指针指向当前结点
t.next = node;
return t;
}
}
}
}
加锁流程图总结
ReentrantLock是如何实现公平锁和非公平锁得
-
FairSync 在 tryAquire 方法中,当判断到锁状态字段state == 0 时,不会立马将当前线程设置为该锁的占用线程,而是去判断是在此线程之前是否有其他线程在等待这个锁(执行hasQueuedPredecessors() 方法),如果是的话,则该线程会加入到等待队列中,进行排队(FIFO,先进先出的排队形式)。这也就是为什么 FairSync 可以让线程之间公平获得该锁。
-
NoFairSync的tryAquire 方法中,没有判断是否有在此之前的排队线程,而是直接进行获锁操作,因此多个线程之间同时争用一把锁的时候,谁先获取到就变得随机了,很有可能线程A比线程B更早等待这把锁,但是B却获取到了锁,A继续等待(这种现象叫做:线程饥饿)
如何实现可重入
加锁操作会对 state字段进行 +1 操作
这里需要注意到 AQS 中很多内部变量的修饰符都是采用的 volital,然后配合 CAS 操作来保证 AQS 本身的线程安全(因为 AQS 自己线程安全,基于它的衍生类才能更好地保证线程安全),这里的 state 字段就是 AQS 类中的一个用 volitale 修饰的 int 变量
state 字段初始化时,值为 0。表示目前没有任何线程持有该锁。当一个线程每次获得该锁时,值就会在原来的基础上加 1,多次获锁就会多次加 1(指同一个线程),这里就是可重入。因为可以同一个线程多次获锁,只是对这个字段的值在原来基础上加1; 相反 unlock 操作也就是解锁操作,实际上是调用 AQS 的 release 操作,而每执行一次这个操作,就会对 state 字段在原来的基础上减1,当 state==0 的 时候就表示当前线程已经完全释放了该锁。