1. 简介
可重入锁 ReentrantLock 自 JDK 1.5 被引入,功能上与 synchronized 关键字类似,但是功能上比 synchronized 更强大,除可重入之外,ReentrantLock 还具有 4 个特性:等待可中断、可实现公平锁、可设置超时、以及锁可以绑定多个条件
。在 synchronized 不能满足的场景下,如公平锁、允许中断、需要设置超时、需要多个条件变量的情况下,需要考虑使用 ReentrantLock。
2. 用法
ReenTrantLock 继承了 Lock 接口,Lock 接口声明有如下方法:
2.1 可重入锁
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者 class),不会因为之前已经获取过还没释放而阻塞。
void m1() {
lock.lock();
try {
// 调用 m2,因为可重入,所以并不会被阻塞
m2();} finally {lock.unlock()
}
}
void m2() {
lock.lock();
try {// do something} finally {lock.unlock()
}
}
注:ReentrantLock 的 方法需要置于 try-finally 块中,需要在 finally 中释放锁
,防止因方法异常锁无法释放。
2.2 可中断锁
在等待获取锁过程中可中断。注意是在等待锁过程中才可以中断,如果已经获取了锁,中断就无效
。调用锁的 lockInterruptibly 方法即可实现可中断锁,当通过这个方法去获取锁时,如果其他线程正在等待获取锁,则这个线程能够响应中断,即 中断线程的等待状态
。也就是说,当两个线程同时通过 lock.lockInterruptibly() 想获取某个锁时,假若此时线程 A 获取到了锁,而线程 B 只有等待,那么对线程 B 调用 threadB.interrupt()方法能够中断线程 B 的等待过程。示例如下:
public class ReentrantLockTest {
private static int account = 0;
private static ReentrantLock lock = new ReentrantLock();
public static void main (String [] args) {Thread t1 = new Thread(()->{
try {lock.lockInterruptibly();
System.out.println("线程 t1 输出:"+account++);
} catch (InterruptedException e) {System.out.println("线程 t1 被中断了");
}finally {lock.unlock();
}
},"t1");
Thread t2 = new Thread(()->{
try {lock.lockInterruptibly();
System.out.println("线程 t2 输出:"+account++);
// 调用 interrupt 方法中断线程 t1
t1.interrupt();
Thread.sleep(2000);
} catch (InterruptedException e) {e.printStackTrace();
System.out.println("线程 t2 被中断了");
}finally {lock.unlock();
}
},"t2");
t2.start();
t1.start();}
}
可能的运行结果为:
注:上面结果只是其中一种可能,在实际运行中可能还有其他结果
通常,中断的使用场景有以下几个:
- 点击某个桌面应用中的取消按钮时;
- 某个操作超过了一定的执行时间限制需要中止时;
- 多个线程做相同的事情,只要一个线程成功其它线程都可以取消时;
- 一组线程中的一个或多个出现错误导致整组都无法继续时;
- 当一个应用或服务需要停止时。
2.3 公平锁
锁的获取顺序符合请求的绝对时间顺序,即 FIFO,先到的线程优先获取锁。
形象的说:
张三、李四、王二去超市购物,只有一个收银台,3 人都买完了东西,准备去结账
公平锁:张三发现收银台没有人,立马跑去了收银台结账,李四和王二看见张三在前面,只好乖乖的排队结账了
非公平锁:张三第一个到收银台结账,李四次之,但李四发现张三还没有结完,所以排在了张三后面,此时王二也来了,发现张三结完了,就马上抢着去结账,留下了李四仰天长叹“不公平啊”
public class FairTest {
// 传入 true 表示 ReentrantLock 的公平锁,false 为非公平锁,默认是 false 非公平锁
private ReentrantLock lock = new ReentrantLock(true);
public void testFair() {lock.lock();
try {System.out.println(Thread.currentThread().getName() +"获得了锁");
}finally {lock.unlock();
}
}
public static void main(String[] args) {FairTest fairLock = new FairTest();
Runnable runnable = () -> {System.out.println(Thread.currentThread().getName()+"启动");
fairLock.testFair();};
Thread[] threadArray = new Thread[5];
for (int i=0; i<5; i++) {threadArray[i] = new Thread(runnable);
threadArray[i].start();}
}
}
可能的运行结果:
可以看出:锁的获取顺序与线程的请求锁的顺序一致
2.4 设置超时
tryLock() 方法尝试获取一次锁,在成功获得锁后返回 true,否则,立即返回 false, 而且线程可以立即离开去做其他的事情;
tryLock(long timeout, TimeUnit unit) 是一个具有超时参数的尝试申请锁的方法,阻塞时间不会超过给定的值,如果在给定时间内成功获取到锁则返回 true,否则阻塞直到超时,然后返回 flase。
public class TimeoutTest
{
private ReentrantLock lock = new ReentrantLock();
public void testTryLock() {if (lock.tryLock()) {
try {System.out.println(Thread.currentThread().getName() + "获取到锁");
Thread.sleep(1000);
}catch(Exception e){e.printStackTrace();
}finally {lock.unlock();
}
}else {System.out.println(Thread.currentThread().getName() + "没有获取到锁");
}
}
public void testTryLockWithTimeout() {
try {if (lock.tryLock(1, TimeUnit.SECONDS)){
try {System.out.println(Thread.currentThread().getName() + "在 1s 内获取到锁");
Thread.sleep(1000);
}finally {lock.unlock();
}
}else {System.out.println(Thread.currentThread().getName() + "在 1s 内没有获取到锁");
}
}catch (InterruptedException e) {System.out.println(Thread.currentThread().getName() + "was interrupted");
}
}
public static void main(String [] args) {TimeoutTest timeouttest = new TimeoutTest();
Runnable runnable = () -> {System.out.println(Thread.currentThread().getName()+"启动");
timeouttest.testTryLock();
//timeouttest.testTryLockWithTimeout();};
Thread[] threadArray = new Thread[5];
for (int i=0; i<5; i++) {threadArray[i] = new Thread(runnable);
threadArray[i].start();}
}
}
执行 testTryLock()可能的运行结果:
执行 testTryLockWithTimeout()可能的运行结果:
2.5 可绑定多个条件
这是与 synchronize 最主要的一个区别,synchronize 相当于只有一个条件,而 ReentrantLock 可以绑定多个条件,这也就是在需要多个条件变量的场景下,只能考虑 ReentrantLock,比如阻塞队列。
阻塞队列的要求:当队列中为空时,从队列中获取元素的操作将被阻塞,当队列满时,向队列中添加元素的操作将被阻塞
。
下面代码摘抄自 JDK1.8 中 ArrayBlockingQueue 的源码,有所简略,如需了解更多,可阅读 ArrayBlockingQueue 的源码,或者参考博客《阻塞队列和 ArrayBlockingQueue 源码解析(JDK1.8)》
public class ArrayBlockingQueue<E> {
final Object[] items; // 用数组模拟队列
int takeIndex; // 下一次读取或移除的位置
int putIndex; // 下一次存放元素的位置
int count; // 队列中元素的总数
final ReentrantLock lock; // 所有访问的保护锁
private final Condition notEmpty; // 队列不空的条件
private final Condition notFull; // 队列未满的条件
public ArrayBlockingQueue(int capacity) {this(capacity, false);
}
public ArrayBlockingQueue(int capacity, boolean fair) {if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();}
// 入队操作
private void enqueue(E x) {final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();}
// 出队操作
private E dequeue() {final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
notFull.signal();
return x;
}
// 队列满时,向队列中添加元素的操作将被阻塞
public void put(E e) throws InterruptedException {checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 使用 while 循环来判断队列是否已满,防止假唤醒
while (count == items.length)
notFull.await();
enqueue(e);
} finally {lock.unlock();
}
}
// 当队列中为空时,从队列中获取元素的操作将被阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
// 使用 while 循环来判断队列是否已满,防止假唤醒
while (count == 0)
notEmpty.await();
return dequeue();} finally {lock.unlock();
}
}
private static void checkNotNull(Object v) {if (v == null)
throw new NullPointerException();}
}
3. 原理
原理请参考博客《Java 重入锁 ReentrantLock 原理分析》,这篇博客写得非常详细,也很有深度
4. 总结
本文从 ReentrantLock 的 4 个特性 等待可中断、可实现公平锁、可设置超时、以及锁可以绑定多个条件
入手,总结了 ReentrantLock 的基本用法,为接下来深入学习 ReentrantLock 的原理以及在日常开发中熟练使用 ReentrantLock 打下基础。