可重入锁ReentrantLock自 JDK 1.5 被引入,功能上与synchronized关键字类似,但是功能上比 synchronized 更强大,除可重入之外,ReentrantLock还具有4个特性: 等待可中断、可实现公平锁、可设置超时、以及锁可以绑定多个条件
。在synchronized不能满足的场景下,如公平锁、允许中断、需要设置超时、需要多个条件变量的情况下,需要考虑使用ReentrantLock。
ReenTrantLock继承了Lock接口,Lock接口声明有如下方法:
可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者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中释放锁
,防止因方法异常锁无法释放。
在等待获取锁过程中可中断。注意是在等待锁过程中才可以中断,如果已经获取了锁,中断就无效
。调用锁的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(); }
}
可能的运行结果为:
注:上面结果只是其中一种可能,在实际运行中可能还有其他结果
通常,中断的使用场景有以下几个:
锁的获取顺序符合请求的绝对时间顺序,即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(); } }
}
可能的运行结果:
可以看出:锁的获取顺序与线程的请求锁的顺序一致
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()可能的运行结果:
这是与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(); }
}
原理请参考博客《 Java 重入锁 ReentrantLock 原理分析 》,这篇博客写得非常详细,也很有深度
本文从ReentrantLock的4个特性 等待可中断、可实现公平锁、可设置超时、以及锁可以绑定多个条件
入手,总结了ReentrantLock的基本用法,为接下来深入学习ReentrantLock的原理以及在日常开发中熟练使用ReentrantLock打下基础。