ReentrantLock 基本概述
ReentrantLock 是一把可重入锁
,也是一把互斥锁
,它具有与 synchronized
相同的方法和监视器锁的语义,但是它比 synchronized 有更多可扩展的功能。
ReentrantLock 的可重入性是指它可以由上次成功锁定但还未解锁的线程拥有。当只有一个线程尝试加锁时,该线程调用 lock()
方法会立刻返回成功并直接获取锁。如果当前线程已经拥有这把锁,这个方法会立刻返回。可以使用 isHeldByCurrentThread
和 getHoldCount
进行检查。
这个类的构造函数接受可选择的 fairness 参数,当 fairness 设置为 true 时,在多线程争夺尝试加锁时,锁倾向于对等待时间最长的线程访问,这也是公平性的一种体现。否则,锁不能保证每个线程的访问顺序,也就是非公平锁。与使用默认设置的程序相比,使用许多线程访问的公平锁的程序可能会显示较低
的总体吞吐量(即较慢;通常要慢得多)。但是获取锁并保证线程不会饥饿的次数比较小。无论如何请注意:锁的公平性不能保证线程调度的公平性。因此,使用公平锁的多线程之一可能会连续多次获得它,而其他活动线程没有进行且当前未持有该锁。这也是互斥性
的一种体现。
也要注意的 tryLock()
方法不支持公平性。如果锁是可以获取的,那么即使其他线程等待,它仍然能够返回成功。
推荐使用下面的代码来进行加锁和解锁
class MyFairLock { private final ReentrantLock lock = new ReentrantLock(); public void m() { lock.lock(); try { // ... } finally { lock.unlock() } } }
ReentrantLock 锁通过同一线程最多支持2147483647个递归锁。尝试超过此限制会导致锁定方法引发错误。
ReentrantLock 如何实现锁公平性
我们在上面的简述中提到,ReentrantLock 是可以实现锁的公平性的,那么原理是什么呢?下面我们通过其源码来了解一下 ReentrantLock 是如何实现锁的公平性的
跟踪其源码发现,调用 Lock.lock()
方法其实是调用了 sync
的内部的方法
abstract void lock();
而 sync 是最基础的同步控制 Lock 的类,它有公平锁和非公平锁的实现。它继承 AbstractQueuedSynchronizer
即 使用 AQS 状态代表锁持有的数量。
lock 是抽象方法是需要被子类实现的,而继承了 AQS 的类主要有
我们可以看到,所有实现了 AQS 的类都位于 JUC 包下,主要有五类:ReentrantLock
、ReentrantReadWriteLock
、Semaphore
、CountDownLatch
和 ThreadPoolExecutor
,其中 ReentrantLock、ReentrantReadWriteLock、Semaphore 都可以实现公平锁和非公平锁。
下面是公平锁 FairSync
的继承关系
非公平锁的NonFairSync
的继承关系
由继承图可以看到,两个类的继承关系都是相同的,我们从源码发现,公平锁和非公平锁的实现就是下面这段代码的区别(下一篇文章我们会从原理角度分析一下公平锁和非公平锁的实现)
通过上图中的源代码对比,我们可以明显的看出公平锁与非公平锁的lock()方法唯一的区别就在于公平锁在获取同步状态时多了一个限制条件:hasQueuedPredecessors()
。
hasQueuedPredecessors() 也是 AQS 中的方法,它主要是用来 查询是否有任何线程在等待获取锁的时间比当前线程长,也就是说每个等待线程都是在一个队列中,此方法就是判断队列中在当前线程获取锁时,是否有等待锁时间比自己还长的队列,如果当前线程之前有排队的线程,返回 true,如果当前线程位于队列的开头或队列为空,返回 false。
综上,公平锁就是通过同步队列来实现多个线程按照申请锁的顺序来获取锁,从而实现公平的特性。非公平锁加锁时不考虑排队等待问题,直接尝试获取锁,所以存在后申请却先获得锁的情况。
根据锁是否可重入进行区分
可重入锁
可重入锁又称为递归锁,是指在同一个线程在外层方法获取锁的时候,再进入该线程的内层方法会自动获取锁(前提锁对象得是同一个对象或者class),不会因为之前已经获取过还没释放而阻塞。Java 中 ReentrantLock
和synchronized
都是可重入锁,可重入锁的一个优点是在一定程度上可以避免死锁。
我们先来看一段代码来说明一下 synchronized 的可重入性
private synchronized void doSomething(){ System.out.println("doSomething..."); doSomethingElse(); } private synchronized void doSomethingElse(){ System.out.println("doSomethingElse..."); }
在上面这段代码中,我们对 doSomething()
和 doSomethingElse()
分别使用了 synchronized
进行锁定,doSomething() 方法中调用了 doSomethingElse() 方法,因为 synchronized 是可重入锁,所以同一个线程在调用 doSomething() 方法时,也能够进入 doSomethingElse() 方法中。
不可重入锁
如果 synchronized 是不可重入锁的话,那么在调用 doSomethingElse() 方法的时候,必须把 doSomething() 的锁丢掉,实际上该对象锁已被当前线程所持有,且无法释放。所以此时会出现死锁。
也就是说,不可重入锁会造成死锁
多个线程能够共享同一把锁
独占锁和共享锁
独占多和共享锁一般对应 JDK 源码的 ReentrantLock 和 ReentrantReadWriteLock 源码来介绍独占锁和共享锁。
独占锁又叫做排他锁,是指锁在同一时刻只能被一个线程拥有,其他线程想要访问资源,就会被阻塞。JDK 中 synchronized和 JUC 中 Lock 的实现类就是互斥锁。
共享锁指的是锁能够被多个线程所拥有,如果某个线程对资源加上共享锁后,则其他线程只能对资源再加共享锁,不能加排它锁。获得共享锁的线程只能读数据,不能修改数据。
我们看到 ReentrantReadWriteLock 有两把锁:ReadLock
和 WriteLock
,也就是一个读锁一个写锁,合在一起叫做读写锁。再进一步观察可以发现 ReadLock 和 WriteLock 是靠内部类 Sync 实现的锁。Sync 是继承于 AQS 子类的,AQS 是并发的根本,这种结构在CountDownLatch、ReentrantLock、Semaphore里面也都存在。
在 ReentrantReadWriteLock 里面,读锁和写锁的锁主体都是 Sync,但读锁和写锁的加锁方式不一样。读锁是共享锁,写锁是独享锁。读锁的共享锁可保证并发读非常高效,而读写、写读、写写的过程互斥,因为读锁和写锁是分离的。所以ReentrantReadWriteLock的并发性相比一般的互斥锁有了很大提升。