ReentrantLock
ReentrantLock,也被称为“可重入锁”,是一个同步工具类,在java.util.concurrent.locks包下。
这种锁的一个重要特点是,它允许一个线程多次获取同一个锁而不会产生死锁.
ReentrantLock的核心特性:
- 可重入性:
ReentrantLock
的一个主要特点是它的名字所表示的含义——“可重入”。简单来说,如果一个线程已经持有了某个锁,那么它可以再次调用lock()方法而不会被阻塞。这在某些需要递归锁定的场景中非常有用。锁的持有计数会在每次成功调用lock()方法时递增,并在每次unlock()方法被调用时递减。 - 公平性:与内置的
synchronized
关键字不同,ReentrantLock
提供了一个公平锁的选项。公平锁会按照线程请求锁的顺序来分配锁,而不是像非公平锁那样允许线程抢占已经等待的线程的锁。公平锁可以减少“饥饿”的情况,但也可能降低一些性能。 - 可中断性:
ReentrantLock
的获取锁操作(lockInterruptibly()
方法)可以被中断。这提供了另一个相对于synchronized
关键字的优势,因为synchronized
不支持响应中断。 - 条件变量:
ReentrantLock
类中还包含一个Condition
接口的实现,该接口允许线程在某些条件下等待或唤醒。这提供了一种比使用wait()
和notify()
更灵活和更安全的线程通信方式。
Common API
Lock,tryLock,lockInterruptibly区别
1)lock(), 拿不到lock就不罢休,不然线程就一直block。 比较无赖的做法。
2)tryLock(),马上返回,拿到lock就返回true,不然返回false。 比较潇洒的做法。 带时间限制的tryLock(),拿不到lock,就等一段时间,超时返回false。比较聪明的做法。
3)lockInterruptibly()就稍微难理解一些。线程在请求lock并被阻塞时,如果被interrupt了,则“此线程会被唤醒并被要求处理InterruptedException”。
并且如果线程已经被interrupt,再使用lockInterruptibly的时候,此线程也会被要求处理interruptedException
Usage
try finally结构
每个lock()的调用必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。
lock:获取锁
unlock:释放锁
lock.lock();
try {
xxx业务代码
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
Condition
在介绍方法的使用和分析源码之前,先来了解一下Condition是什么。
可以把Condition看作是Object监视器的替代品。众所周知,Object有wait()和notify()方法,用于线程间的通信。并且这两个方法只能在synchronized同步块内才可以调用,所有线程的等待和唤醒都需要关联到监视器对象的WaitSet集合。
Condition同样可以实现上面的线程通信。不同点在于,synchronized锁对象关联的监视器对象仅有一个,所以等待队列也只有一个。
而一个ReentrantLock可以有多个Condition,这样可以根据不同的业务需求,在使用同一个lock锁对象的基础上使用多个等待队列,让不同性质的线程加入到不同的等待队列当中。
也即,一个condition对应一个等待队列;ReentrantLock中有newCondition()的方法,来实例化一个Condition对象,因此可以调用多次newCondition()方法,来得到多个等待队列。
Common API
await & signal & signalAll
-
await()的作用是: 将当前线程挂起,暂停执行,并释放当前持有的锁。把当前线程的node放入等待condition的链表中,当前线程从运行状态进入等待状态,可以被中断。
-
signal的作用是:唤醒一个等待在 Condition 条件队列上的某一个线程。在condition.signal的时候,会从等待condition的链表中取出node,该node(That thread) must then re-acquire the lock before returning from await.
-
signalAll的作用是:唤醒阻塞,该condition对应的等待队列上的所有线程,上面的signal,是唤醒一个线程,而这里是唤醒等待队列里面的所有线程。If any threads are waiting on this condition then they are all woken up. Each thread must re-acquire the lock before it can return from await.
注意:await方法,会立即释放锁;但是signal方法不会释放持有的锁,要想释放持有的锁,还是依赖于unlock方法。
本质上,虽然ReentrantLock可以有多个Condition,对应了多个等待队列A/B/C,比如这里有3个condition等待队列,每个队列有5个线程,那么总共就有15个线程等待锁。
但是其实,所有等待队列上共15个线程,其实最终只有1个线程能拿到锁,不是说,每个condition等待队列,都分别有一个线程能拿到锁。
从这个角度来看,其实和synchronized区别不大。
但是,引入了condition后好处是,开发者,可以根据需要,通知某个等待队列上等待线程,比如,开发者希望通知conditionB上的等待线程,不想通知conditionA上的等待线程。那么就只有conditionB上的5个线程,来竞争锁,不会出现15个线程一起竞争锁,这样,锁的竞争效率也更高一点。
Usage
每个lock()的调用必须紧跟一个try-finally子句,用来保证在所有情况下都可以释放锁。任务在可以调用await(),signal(),signalAll()之前,必须拥有这个锁。
@Test
public void testCondition() throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
//创建新的条件变量
Condition condition = lock.newCondition();
Thread thread0 = new Thread(() -> {
lock.lock();
try {
System.out.println("线程0获取锁");
// sleep不会释放锁
Thread.sleep(500);
//进入休息室等待
System.out.println("线程0释放锁,进入等待");
condition.await();
System.out.println("线程0被唤醒了");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
});
thread0.start();
//叫醒
Thread thread1 = new Thread(() -> {
lock.lock();
try {
System.out.println("线程1获取锁");
//唤醒
condition.signal();
System.out.println("线程1唤醒线程0");
} finally {
lock.unlock();
System.out.println("线程1释放锁");
}
});
thread1.start();
thread0.join();
thread1.join();
}
运行结果:
线程0获取锁
线程0释放锁,进入等待
线程1获取锁
线程1唤醒线程0
线程1释放锁
线程0被唤醒了
图解实现原理
refer to : https://xie.infoq.cn/article/32c3b0ee8c70d4a19fdf15c33
await 过程
- 线程 0(Thread-0)一开始获取锁,exclusiveOwnerThread 字段是 Thread-0, 如下图中的深蓝色节点
- Thread-0 调用 await 方法,Thread-0 封装成 Node 进入 ConditionObject 的队列,因为此时只有一个节点,所有 firstWaiter 和 lastWaiter 都指向 Thread-0,会释放锁资源,NofairSync 中的 state 会变成 0,同时 exclusiveOwnerThread 设置为 null。如下图所示。
- 线程 1(Thread-1)被唤醒,重新获取锁,如下图的深蓝色节点所示。
- Thread-0 被 park 阻塞,如下图灰色节点所示:
signal 过程
- Thread-1 执行 signal 方法唤醒条件队列中的第一个节点,即 Thread-0,条件队列置空
- Thread-0 的节点的等待状态变更为 0, 重新加入到 AQS 队列尾部。
- 后续就是 Thread-1 释放锁,其他线程重新抢锁。