初读ReentrantReadWriteLock
- 位于 java.util.concurrent.locks 包下,它实现了 ReadWriteLock 接口和 Serializable 接口
- ReentrantReadWriteLock读写锁在ReentrantLock上进行了拓展
- 读写锁是“读写互斥,写写互斥,读读共享” 的锁
- 读锁和写锁共用同一个 原子state 和 On Sync Queue 来进行资源控制,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作
- 在没有写操作的时候,允许多个线程同时读取共享资源,同一个线程读可以重入(但是同一个线程不可以获取写锁)
- 如果一个线程想去写这些共享资源,不允许其他线程对该资源进行读和写的操作了(但是同一个线程读写可以重入)
适合场景
该锁更适合读操作远远大于写操作对场景
读写锁有以下4个重要的特性
- 公平选择性:支持非公平(默认)和公平的锁获取方式,吞吐量还是非公平优于公平。
- 重进入:读锁和写锁都支持线程重进入。
- 锁降级:遵循获取写锁、获取读锁再释放写锁的次序,写锁能够降级成为读锁。
- 锁升级:读取锁是不能直接升级为写入锁的。因为获取一个写入锁需要释放所有读取锁,所以如果有两个读取锁视图获取写入锁而都不释放读取锁时就会发生死锁。
获取读写锁的条件
读写锁是“读写互斥,写写互斥,读读共享” 的锁
线程进入读锁的前提条件:
- 没有任何锁,那么读锁可以立马获取
- 有读锁,那么读锁可以立马获取
- 没有其他线程的写锁,那么读锁可以立马获取
- 有同一线程的写锁,获取读锁会产生锁降级(必须是先获得写锁再获取读锁,不然会造成死锁)
线程进入写锁的前提条件:
- 没有任何锁,那么可以立刻获的写锁
- 没有读锁,没其他线程的写锁,那么可以立刻获的写锁
读写锁使用
我们只需要保证读锁和写锁来自同一个 ReentrantReadWriteLock 即可
一个读写锁的例子
import java.util.Random;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.locks.ReadWriteLock;
/**
* 读写锁Demo
*/
public class ReentrantReadWriteLockDemo {
class MyObject {
private Object object;
private ReadWriteLock lock = new java.util.concurrent.locks.ReentrantReadWriteLock();
public void get() throws InterruptedException {
lock.readLock().lock();//上读锁
try {
System.out.println(Thread.currentThread().getName() + "准备读取数据");
Thread.sleep(new Random().nextInt(1000));
System.out.println(Thread.currentThread().getName() + "读数据为:" + this.object);
} finally {
lock.readLock().unlock();
}
}
public void put(Object object) throws InterruptedException {
lock.writeLock().lock();
try {
System.out.println(Thread.currentThread().getName() + "准备写数据");
Thread.sleep(new Random().nextInt(1000));
this.object = object;
System.out.println(Thread.currentThread().getName() + "写数据为" + this.object);
} finally {
lock.writeLock().unlock();
}
}
}
public static void main(String[] args) {
xMyObject myObject = new ReentrantReadWriteLockDemo().new MyObject();
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 3; j++) {
try {
myObject.put(new Random().nextInt(1000));//写操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
for (int i = 0; i < 3; i++) {
executorService.execute(new Runnable() {
@Override
public void run() {
for (int j = 0; j < 3; j++) {
try {
myObject.get();//多个线程读取操作
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
}
executorService.shutdown();
}
}
运行结果:
pool-1-thread-1准备写数据
pool-1-thread-1写数据为513
pool-1-thread-1准备写数据
pool-1-thread-1写数据为173
pool-1-thread-1准备写数据
pool-1-thread-1写数据为487
pool-1-thread-2准备写数据
pool-1-thread-2写数据为89
pool-1-thread-2准备写数据
pool-1-thread-2写数据为814
pool-1-thread-2准备写数据
pool-1-thread-2写数据为1
pool-1-thread-3准备写数据
pool-1-thread-3写数据为701
pool-1-thread-3准备写数据
pool-1-thread-3写数据为503
pool-1-thread-3准备写数据
pool-1-thread-3写数据为694
pool-1-thread-4准备读取数据
pool-1-thread-5准备读取数据
pool-1-thread-6准备读取数据
pool-1-thread-4读数据为:694
pool-1-thread-4准备读取数据
pool-1-thread-4读数据为:694
pool-1-thread-4准备读取数据
pool-1-thread-6读数据为:694
pool-1-thread-6准备读取数据
pool-1-thread-5读数据为:694
pool-1-thread-5准备读取数据
pool-1-thread-6读数据为:694
pool-1-thread-6准备读取数据
pool-1-thread-4读数据为:694
pool-1-thread-5读数据为:694
pool-1-thread-5准备读取数据
pool-1-thread-6读数据为:694
pool-1-thread-5读数据为:694
读写锁的源码解读
ReentrantReadWriteLock位于 java.util.concurrent.locks 包下,它实现了 ReadWriteLock 接口和 Serializable 接口
ReentrantReadWriteLock内部存在五个内部类
说明:如上图所示,FairSync 和 NonfairSync 都是 ReentrantLock 的内部类,都继承自Sync类、FairSync继承自Sync类,Sync继承自AQS(通过构造函数传入的布尔值决定要构造哪一种Sync实例);ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
ReentrantReadWriteLock内部的方法
ReentrantReadWriteLock的成员变量
/** 内部类读锁 */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** 内部类写锁 */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** 执行同步机制,很多方法内部实际调用的是sync的方法 */
final Sync sync;
构造函数
ReentrantReadWriteLock支持公平模式和非公平模式,无参的构造函数默认创建的是非公平锁
/**
* 创建ReentrantReadWriteLock,默认使用非公平机制。想要使用公平机制构造函数传入true
*/
public ReentrantReadWriteLock() {
//默认创建的是非公平锁
this(false);
}
/**
* 根据传入参数判断使用公平模式还是非公平模式
* FairSync,NonfairSync都继承了Sync
* 这里初始化了静态类ReadLock,WriteLock的sync
*/
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
Sync类的源码
- Sync是ReentrantReadWriteLock内部实现的一个内部类
- Sync继承了AbstractQueuedSynchronizer(AQS)
- Sync将Aqs的state分为高16位,低16位
- 低16位表示写锁计数(可重入次数),
写锁重入个数加1,其实是status状态值加 1 - 高16位表示持有读锁的线程数(多线程读),
读锁个数加1,其实是status状态值加 2^16 - 写锁用高位部分(即EXCLUSIVE_MASK:65535)&status状态值来计算写锁的重入计数(实际上是将高位的二进制置为0)
Sync成员变量
//支持65535个写锁和65535个读锁;
//低16位表示写锁计数,高16位表示持有读锁的线程数
static final int SHARED_SHIFT = 16;
//读锁用高位部分,读锁个数加1,其实是状态值加 2^16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//最多支持读锁,写锁个数65536-1
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
//写锁用高位部分,计算写锁的重入计数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
//当前线程持有的可重入读锁的数量,仅在构造方法和反序列化时被初始化
//当持有锁的数量为0时,移除此对象。
private transient ThreadLocalHoldCounter readHolds;
//最后一个获取readLock成功的线程的计数
private transient HoldCounter cachedHoldCounter;
//第一次获取读锁的线程
private transient Thread firstReader = null;
//firstReader的锁重入计数
private transient int firstReaderHoldCount;
//读锁计数,当前持有读锁的线程数,c的高16位
//读状态为c >>> 16(无符号位补0右移16位)
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
//写锁计数,也就是它的重入次数,c的低16位
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
Sync内部类
分别为HoldCounter和ThreadLocalHoldCounter
其中HoldCounter主要与读锁配套使用,其中,HoldCounter源码如下。
// 计数器
static final class HoldCounter {
// 计数
int count = 0;
// Use id, not reference, to avoid garbage retention
// 获取当前线程的TID属性的值
final long tid = getThreadId(Thread.currentThread());
}
说明:HoldCounter主要有两个属性,count和tid,其中count表示某个读线程重入的次数,tid表示该线程的tid字段的值,该字段可以用来唯一标识一个线程。ThreadLocalHoldCounter的源码如下
// 本地线程计数器
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
// 重写初始化方法,在没有进行set的情况下,获取的都是该HoldCounter值
public HoldCounter initialValue() {
return new HoldCounter();
}
}
说明:ThreadLocalHoldCounter重写了ThreadLocal的initialValue方法,ThreadLocal类可以将线程与对象相关联。在没有进行set的情况下,get到的均是initialValue方法里面生成的那个HolderCounter对象。
Sync类的构造函数
// 构造函数
Sync() {
// 本地线程计数器
readHolds = new ThreadLocalHoldCounter();
// 设置AQS的状态
setState(getState()); // ensures visibility of readHolds
}
读锁的获取与释放
读锁的lock和unlock的实际实现对应Sync的 tryAcquireShared ,tryReleaseShared方法。
lock():获取读锁方法
获取读锁步骤:(图示1:tryAcquireShared )
- step1:如果写锁持锁,直接获取资源acquire失败(返回 -1),持有写锁的是本线程除外。
- step2:如果不持有写锁,则首先根据 queue policy(公平锁或非公平锁) 判断一下要不要阻塞。不需要阻塞且并且读锁数量是否小于最大值,通过修改 原子state 来尝试获取资源,成功则要修改一下重入计数(步骤3)。
- step3:若当前没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若当前线程线程为第一个读线程,则增加firstReaderHoldCount;否则,将设置当前线程对应的HoldCounter对象的值(负责重入的计数)
- step4:上面的都失败了,则进入到fullTryAcquireShared中。
原子state 操作之 step1 解析:判断写锁是否已经持锁
Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取state值,锁状态
//如果独占锁不存在,一定会去获取锁
//如果独占式锁(写锁)的存在,并且获取锁的不是当前线程,则退出(-1)
//如果独占式锁(写锁)的存在,并且获取锁的是当前线程,则继续获取读锁,发生锁降级,写锁降级为读锁
// exclusiveOwnerThread 就是是否为独占线程拥有者,也就是判断当前线程是否持有写锁
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
- exclusiveCount( c )的方法实现是->c & EXCLUSIVE_MASK != 0,c 是我们的 原子state,如果 c 和 EXCLUSIVE_MASK 按位与后不为零,即表示已经有线程持有了写锁。
- getExclusiveOwnerThread() != current, exclusiveOwnerThread 如字面意思,就是是否为独占线程拥有者,也就是判断当前线程是否持有写锁。如果独占锁存在,且不是当前线程拥有则退出;如果独占锁存在,且是当前线程拥有则会继续获取读锁;如果独占锁不存在,一定会去获取锁
原子state 操作之 step2 解析:公平策略与尝试 CAS 操作
//获取当前共享资源的数量
int r = sharedCount(c);
if (!readerShouldBlock()
&& r < MAX_COUNT
&&compareAndSetState(c, c + SHARED_UNIT)) {// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功,代表可以获取读锁
- 老生常谈。我们知道,重入锁的重入每次使得 原子state++。读锁也是如此,读锁被设计为,当有新的线程共享了资源,则 原子state++,当然,是高位进行 ++。比如 3.1 那张图,高位为 3 (0000000000000011),代表有三个线程共享了该锁。compareAndSetState(c, c + SHARED_UNIT) 这个操作就很好理解了。SHARED_UNIT 就是高位的 1 ( 1 << 16 ),CAS 操作尝试将 原子state 由原值修改为原值高位++。
-
sharedCount(int c) -> c >>> SHARED_SHIFT计算读锁的个数 r < MAX_COUNT 避免原子 state 溢出
(前面我们说到,读锁的高位用于记录当前有多少个线程共享了此读锁,但是读锁只有 16 位,所以说,我们最多只能表示 65535 ( 16 个 1 ) 个线程共享此读锁) -
!readerShouldBlock() 公平锁与非公平锁策略
公平锁和非公平锁相信大家都知道,这里不多说。最大的区别就是,公平锁保证一定 “先到先得”,非公平锁则不保证。
我们这里只简单说说 非公平锁的 readerShouldBlock() 是怎么实现的。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
满足如上的逻辑,则直接进入 step4,跳过 step2。
看到 head,就应该联想起 On Sync Queue,四个判断连在一起其实就是汇成一句话,当前 On Sync Queue 没有正在等待的线程,或者正在等待的第一个 Node 是 SHARED 模式,代表可进入 step3。
step3 中做了什么
它的核心就是使用一个 HoldCounter 来保存重入状态,以便知道这个线程加了几层锁,道理和重入锁的 原子state++ 是一样的。
if (r == 0) {//如果当前没有线程获取读锁
firstReader = current;//当前线程是第一个读锁获取者
firstReaderHoldCount = 1;// 读线程重入锁占用的资源数为1
} else if (firstReader == current) { //当前线程为第一个读线程,表示第一个读锁线程重入
firstReaderHoldCount++;//代表重入锁计数器累加1
} else {// 读锁数量不为0并且不为当前线程
//内部定义的线程记录缓存
HoldCounter rh = cachedHoldCounter;//HoldCounter主要是一个类用来记录线程以及线程获取锁的数量
// 计数器为空或者不是当前线程
if (rh == null || rh.tid != current.getId())//如果不是当前线程
//从每个线程的本地变量ThreadLocal中获取, 运用ThreadLocal线程本地对象,将每个线程获取锁的次数保存到每个线程内部,这样释放锁的时候就不会影响到其它的线程。
cachedHoldCounter = rh = readHolds.get();// 获取当前线程对应的计数器
else if (rh.count == 0)//如果记录为0初始值设置
readHolds.set(rh);//设置记录
rh.count++;//递增当前线程的锁重入计数
}
return 1;//返回1代表获取到了同步状态
至于为什么第一个持有读锁的线程使用本地变量 firstReaderHoldCount ++,而其他线程使用 HoldCounter ++?而不是直接使用else那段代码?这是为了一个效率问题,避免共享读很少的情况(比如大多数情况就只有一个线程持有读锁),反复创建 HoldCounter 对象,firstReader是不会放入到readHolds中的,如果读锁仅有一个的情况下就会避免查找readHolds。HoldCounter
firstReader、firstReaderHoldCount的定义
一个表示线程,当然该线程是一个特殊的线程,一个是firstReader的重入计数
private transient Thread firstReader = null;
private transient int firstReaderHoldCount;
HoldCounter的定义
static final class HoldCounter {
int count = 0;
final long tid = Thread.currentThread().getId();
}
在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。但是如果要将一个对象和线程绑定起来仅记录tid肯定不够的,而且HoldCounter根本不能起到绑定对象的作用,只是记录线程tid而已。
诚然,在java中,我们知道如果要将一个线程和对象绑定在一起只有ThreadLocal才能实现。所以如下:
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
ThreadLocalHoldCounter继承ThreadLocal,并且重写了initialValue方法。
HoldCounter应该就是绑定线程上的一个计数器,ThradLocalHoldCounter则是线程绑定的ThreadLocal。
从上面我们可以看到ThreadLocal将HoldCounter绑定到当前线程上,同时HoldCounter也持有线程Id,这样在释放锁的时候才能知道ReadWriteLock里面缓存的上一个读取线程(cachedHoldCounter)是否是当前线程。
这样做的好处是可以减少ThreadLocal.get()的次数,因为这也是一个耗时操作。需要说明的是HoldCounter绑定线程id而不绑定线程对象的原因是避免HoldCounter和ThreadLocal互相绑定而GC难以释放它们(尽管GC能够智能的发现这种引用而回收它们,但是这需要一定的代价),所以其实这样做只是为了帮助GC快速回收对象而已。
更新成功后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程副本中记录当前线程重入数,这是为了实现jdk1.6中加入的getReadHoldCount()方法的,这个方法能获取当前线程重入共享锁的次数(state中记录的是多个线程的总重入次数)
原子state 操作之 step4 解析:fullTryAcquireShared
fullTryAcquireShared 它是一个死循环,这里就是 step 1 + step 2 的一个自旋版。
- 判断是否有写锁持锁
- 判断 onSyncQueue 有无等待者之类
- 高位不能超过 65535
- 进行 CAS 操作
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
//获取当前状态
int c = getState();
//判断是否非当前线程持有独占锁
if (exclusiveCount(c) != 0) {
//如果持有独占锁,不等于当前线程则获取锁失败
if (getExclusiveOwnerThread() != current)
return -1;
//同样的判断当前获取读锁操作是否需要阻塞
} else if (readerShouldBlock()) {
//防止重复获取读锁
if (firstReader == current) {
} else {
//这里逻辑跟上面讲的一样,如不懂可以看上面
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
//的共享锁重入计数为0,则移除锁重入计数对象
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//如果获取共享锁次数等于最大锁次数抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//此处跟tryAcquireShared()方法基本一致
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
详细代码注释如下:
ReadWriteLock lock = new ReentrantReadWriteLock();
lock.readLock().lock();//上读锁
public ReentrantReadWriteLock.ReadLock readLock() {
return readerLock;
}
public void lock() {
//自定义实现的获取锁方式
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();//获取当前线程
int c = getState();//获取state值,锁状态
//如果独占锁不存在,一定会去获取锁
//如果独占式锁(写锁)的存在,并且获取锁的不是当前线程,则退出(-1)
//如果独占式锁(写锁)的存在,并且获取锁的是当前线程,则继续获取读锁,发生锁降级,写锁降级为读锁
// exclusiveOwnerThread 就是是否为独占线程拥有者,也就是判断当前线程是否持有写锁
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//获取当前共享资源的数量
int r = sharedCount(c);
/** readerShouldBlock()判断读是否需要阻塞;
*1:当前没有线程占用写锁,这种情况readerShouldBlock方法会返回false。
*2:当前有线程占用写锁,并且占用写锁的线程就是当前线程,这时就会发生锁降级,判断head的下一个节点是否申请写锁,如果申请写锁返回true,阻塞。
*/
//公平锁机制执行hasQueuedPredecessors(),判断队列是否有排在前面的线程在等待锁;
//非公平锁执行apparentlyFirstQueuedIsExclusive(),这个方法判断是第一个排队的线程是否以独占模式等待
//判断获取写锁的次数是否超过最大次数
//cas修改线程持有共享锁的数量
if (!readerShouldBlock() && r < MAX_COUNT &&compareAndSetState(c, c + SHARED_UNIT)) {// 读线程是否应该被阻塞、并且小于最大值、并且比较设置成功,代表可以获取读锁
if (r == 0) {//如果当前没有线程获取读锁
firstReader = current;//当前线程是第一个读锁获取者
firstReaderHoldCount = 1;// 读线程重入锁占用的资源数为1
} else if (firstReader == current) { //当前线程为第一个读线程,表示第一个读锁线程重入
firstReaderHoldCount++;//代表重入锁计数器累加1
} else {// 读锁数量不为0并且不为当前线程
//内部定义的线程记录缓存
HoldCounter rh = cachedHoldCounter;//HoldCounter主要是一个类用来记录线程以及线程获取锁的数量
// 计数器为空或者不是当前线程
if (rh == null || rh.tid != current.getId())//如果不是当前线程
//从每个线程的本地变量ThreadLocal中获取, 运用ThreadLocal线程本地对象,将每个线程获取锁的次数保存到每个线程内部,这样释放锁的时候就不会影响到其它的线程。
cachedHoldCounter = rh = readHolds.get();// 获取当前线程对应的计数器
else if (rh.count == 0)//如果记录为0初始值设置
readHolds.set(rh);//设置记录
rh.count++;//递增当前线程的锁重入计数
}
return 1;//返回1代表获取到了同步状态
}
//用来处理CAS设置状态失败的和tryAcquireShared非阻塞获取读锁失败的,放到循环里重试
return fullTryAcquireShared(current);
}
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
//获取当前状态
int c = getState();
//判断是否非当前线程持有独占锁
if (exclusiveCount(c) != 0) {
//如果持有独占锁,不等于当前线程则获取锁失败
if (getExclusiveOwnerThread() != current)
return -1;
//同样的判断当前获取读锁操作是否需要阻塞
} else if (readerShouldBlock()) {
//防止重复获取读锁
if (firstReader == current) {
} else {
//这里逻辑跟上面讲的一样,如不懂可以看上面
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
//的共享锁重入计数为0,则移除锁重入计数对象
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//如果获取共享锁次数等于最大锁次数抛出异常
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//此处跟tryAcquireShared()方法基本一致
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}
流程图如下
读锁中对于 onSyncQueue 的操作
doAcquireShared 与 acquireQueued 无比的类似!
/**
* Acquires in shared uninterruptible mode.
* @param arg the acquire argument
*/
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean interrupted = false;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
return;
}
}
if (shouldParkAfterFailedAcquire(p, node))
interrupted |= parkAndCheckInterrupt();
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
} finally {
if (interrupted)
selfInterrupt();
}
}
unlock():读锁释放
public void unlock() {
sync.releaseShared(1);//AQS释放共享锁操作
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
//获取当前线程
Thread current = Thread.currentThread();
//判断当前线程是否是获取读锁的第一个线程
if (firstReader == current) {
//如果锁重入次数为1,将第一次获取锁的线程置为null
if (firstReaderHoldCount == 1)
firstReader = null;
else
//不等于1则减少锁重入次数
firstReaderHoldCount--;
} else {
//最后一个获取读锁成功的计数
HoldCounter rh = cachedHoldCounter;
//如果没有初始化,或者不等于当前线程
if (rh == null || rh.tid != getThreadId(current))
//获取线程持有的可重入读锁的数量
rh = readHolds.get();
int count = rh.count;
//判断是否小于等于1
if (count <= 1) {
//移除
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
//不小于1重入锁数量减1
--rh.count;
}
//cas自旋操作递减共享锁持有的线程数量
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
读锁线程释放锁的过程
- 首先判断当前线程是否为第一个读线程firstReader
- 若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1
- 若是,则设置第一个读线程firstReader为空
- 否则,将第一个读线程占有的资源数firstReaderHoldCount减1;
- 若当前线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 )
- 若计数器为空或者tid不等于当前线程的tid值,则获取当前线程的计数器,如果计数器的计数count小于等于1,则移除当前线程对应的计数器
- 如果计数器的计数count小于等于0,则抛出异常,之后再减少计数即可
- 无论何种情况,都会进入无限循环,该循环可以确保成功设置状态state
流程图
读锁有两个状态来维护锁的层级和持有共享锁的线程数量的,一个是 HoldCounter 负责重入计数,一个是 原子state高位,负责记录有多少个线程持有了共享锁。也就是说,我们释放资源release时,需要首先使得 HoldCounter–,直到其为零,再去操作原子state高位。
这些其实猜都能猜的出来,直到 原子state高位归零,就会去唤醒下一个等待的线程
写锁的获取和释放
写锁
获取写锁步骤如下:
(1)首先获取c、w。c表示当前锁状态;w表示写线程数量。然后判断同步状态state是否为0。如果state!=0,说明已经有其他线程获取了读锁或写锁,执行(2);否则执行(5)。
(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),说明读锁此时被其他线程占用,所以当前线程不能获取写锁,自然返回false。或者锁状态不为零,而写锁的状态也不为0,但是获取写锁的线程不是当前线程,则当前线程也不能获取写锁。
(3)判断当前线程获取写锁是否超过最大次数,若超过,抛异常,反之更新同步状态(此时当前线程已获取写锁,更新是线程安全的),返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否需要阻塞(公平和非公平方式实现不同),在非公平策略下总是不会被阻塞,在公平策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则需要被阻塞,否则,无需阻塞),如果不需要阻塞,则CAS更新同步状态,若CAS成功则返回true,失败则说明锁被别的线程抢去了,返回false。如果需要阻塞则也返回false。
(5)成功获取写锁后,将当前线程设置为占有写锁的线程,返回true。
lock(int arg);
public void lock() {
sync.acquire(1);//AQS独占式获取锁
}
获取锁的关键是tryAcquire()
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
//获取当前线程
Thread current = Thread.currentThread();
//获取同步状态值
int c = getState();
//获取写锁的重入次数,exclusiveCount(c)计算
int w = exclusiveCount(c);
//c不等于0说明存在锁,可能是写锁,也可能是读锁
if (c != 0) {//已经有线程获取了锁
// w != 0 && current != getExclusiveOwnerThread() 表示其他线程获取了写锁,获取失败
//w == 0 ,那么读锁存在,获取失败(读锁存在的时候不管是不是同一线程都不能获取写锁)
//w != 0,这时候可能会存在读锁(和独占锁同一线程),如果当前线程不是获取到独占锁的线程,获取失败
if (w == 0 || current != getExclusiveOwnerThread())
return false;//获取失败
//acquires一般是1,所以w + exclusiveCount(acquires)=w+1, 获取写锁的次数超过最大次数,抛出error
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
//cas修改state值
setState(c + acquires);
return true;
}
//如果当前没有获得任何锁,表示现在是第一次获取写锁
//WriterShouldBlock子类实现,
//非公平机制直接返回false
//公平锁在于hasQueuedPredecessors()判断队列是否有排在前面的线程在等待锁
//cas修改获取state,修改失败则获取锁失败,需要进去CLH同步队列
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
//cas修改成功,设置当前线程为独占线程,说明获取锁成功
setExclusiveOwnerThread(current);
return true;
}
写锁和之前的重入锁基本一致,所以这里不花太多篇幅去写写锁,写锁只有一个地方和重入锁有区别,那就是重入锁整个 原子state 都是为锁服务的,而对于写锁来说,只有低位服务于自己,且如果高位不为 0(代表有读锁已经持有了资源),也无法 acquire 成功,需要入队列中进行等待。
流程图如下
unlock():释放写锁
public void unlock() {
sync.release(1);//释放独占式同步状态
}
public final boolean release(int arg) {
//tryRelease(arg)返回true,则唤醒等待的线程
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
//判断当前线程是否是独占线程
if (!isHeldExclusively())
//如果不是独占线程抛出异常
throw new IllegalMonitorStateException();
//锁被获取次数-释放的锁
int nextc = getState() - releases;
//exclusiveCount(nextc)得到写锁获取的次数
//exclusiveCount(nextc)如果等于0,独占线程设置为null,修改state
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
写锁的释放过程还是相对而言比较简单的:首先查看当前线程是否为写锁的持有者,如果不是抛出异常。然后检查释放后写锁的线程数是否为0,如果为0则表示写锁空闲了,释放锁资源将锁的持有线程设置为null,否则释放仅仅只是一次重入锁而已,并不能将写锁的线程清空。
说明:此方法用于释放写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异常,否则,计算释放资源后的写锁的数量,若为0,表示成功释放,资源不将被占用,否则,表示资源还被占用。
流程图如下
读写锁之锁降级操作
什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:
public class CacheDemo {
private Map<String, Object> cache = new HashMap<String, Object>();
private ReadWriteLock rwl = new ReentrantReadWriteLock();
public ReadLock rdl = rwl.readLock();
public WriteLock wl = rwl.writeLock();
public volatile boolean update = false;
public void processData(){
rdl.lock();//获取读锁
if(!update){
rdl.unlock();//释放读锁
wl.lock();//获取写锁
try{
if(!update){
update =true;
}
rdl.lock();//获取读锁
finally{
wl.unlock();//释放写锁
}
}
try{
}finally{
rdl.unlock();//释放读锁
}
}
为什么会死锁?
- 在同一线程中先获取读锁,再获取写锁会死锁。但是先获取写锁再获取读锁不会死锁。所以ReentrantReadWriteLock不支持锁升级。
//如果获取了读锁,c不等于0,但是w等于0,那么此时获得了读锁,返回false
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
if (w == 0 || current != getExclusiveOwnerThread())
return false;
- 一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁可以“降级”为读锁;读锁不能“升级”为写锁
- 看下面代码
如果要实现读锁套写锁,其实十分简单,只需要在入写锁 acquire 时判断本线程是否已经有 HoldCounter 即可。但是这样会带来一个问题:
假设有十个线程正在共享读锁,此时其中一个读线程重入了写锁,这将导致写锁的 独享形同虚设,正常应该是此线程进入等待队列,等待所有线程都释放了读锁后,才能实现获得独占锁。
然而这个持有读锁的线程已经在执行逻辑了,它无法进行阻塞,如果进入阻塞状态,便会有一个问题:那就是这个阻塞的读线程将 永远无法释放读锁!
这就是读锁套写锁会死锁的原因,但我个人认为这种情况,JDK应该将异常抛出才对,之前的项目中便是碰到了这个情况,找了很久才发现是这个问题,这也算是个小 tips 吧。请大家要注意这个问题 ~ 项目中尽量避免读写锁互相套。
小结
在线程持有读锁的情况下,该线程不能取得写锁(因为获取写锁的时候,如果发现当前的读锁被占用,就马上获取失败,不管读锁是不是被当前线程持有)。
在线程持有写锁的情况下,该线程可以继续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被当前线程占用的情况才会获取失败)
仔细想想,这个设计是合理的:因为当线程获取读锁的时候,可能有其他线程同时也在持有读锁,因此不能把获取读锁的线程“升级”为写锁;而对于获得写锁的线程,它一定独占了读写锁,因此可以继续让它获取读锁,当它同时获取了写锁和读锁后,还可以先释放写锁继续持有读锁,这样一个写锁就“降级”为了读锁。
存在的问题
读写锁是在重入锁ReentrantLock基础上的一大改造,其通过在重入锁上维护一个读锁一个写锁实现的。对于ReentrantLock和ReentrantreadWriteLock的使用需要在开发者自己根据实际项目的情况而定。对于读写锁当读的操作远远大于写操作的时候会增加程序很高的并发量和吞吐量。虽说在高并发的情况下,读写锁的效率很高,但是同时又会存在一些问题,比如当读并发很高时读操作长时间占有锁,导致写锁长时间无法被获取而导致的线程饥饿问题,因此在JDK1.8中又在ReentrantReadWriteLock的基础上新增了一个读写并发锁StampLock。
关注公众号,获取更多源码分析文章,在阅读源码的路上,大师兄陪你越走越远!