1、ReentrantReadWriteLock介绍
(1)ReentrantReadWriteLock与ReentrantLock
ReentrantLock
实现了标准的互斥操作, 也就是一次只能有一个线程持有锁, 也即所谓独占锁的概念. 显然这个特点在一定程度上面降低了吞吐量, 实际上独占锁是一种保守的锁策略, 在这种情况下任何”读/读”, “写/读”, “写/写”操作都不能同时发生.但是同样需要强调的一个概念是, 锁是有一定的开销的, 当并发比较大的时候, 锁的开销就比较客观了。 所以如果可能的话就尽量少用锁, 非要用锁的话就尝试看能否改造为读写锁.
ReadWriteLock描述的是: 一个资源能够被多个读线程访问, 或者被一个写线程访问,但是不能同时存在读写线程。
(2)ReentrantReadWriteLock中的state
1、传统的独占锁、共享锁
在ReentrantLock中该字段用来描述有多少线程获持有锁;
在独占锁的时代这个值通常是0或者1;
在共享锁的时代就是持有锁的数量;
2、ReadWriteLock的独占锁、共享锁
ReadWriteLock的读、写锁是相关但是又不一致的, 所以需要两个数来描述读锁(共享锁)和写锁(独占锁)的数量;
显然现在一个state就不够用了. 于是在ReentrantReadWrilteLock里面将这个字段一分为二, 高位16位表示共享锁的数量, 低位16位表示独占锁的数量;
共享锁和独占锁的数量最大只能是2^(16-1)=65536;
定义:
static final int SHARED_SHIFT = 16; //表示读锁占用的位数,常量16
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
(3)ReentrantReadWriteLock的内部结构
ReentrantReadWriteLock-->ReadWriteLock
NonfairSync/FairSync-->Sync-->AbstractQueuedSynchronizer-->AbstractOwnableSynchronizer
ReadLock/WriteLock-->Lock
注:NonfairSync,FairSync,Sync,ReadLock和WriteLock是ReentrantReadWriteLock的内部类。
2、ReentrantReadWriteLock的创建
//ReentrantReadWriteLock构造方法:无参、有参
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
//可以看到与ReentrantLock一样, 锁的主体部分依然是Sync(FairSync/NonFaireSync), 但是增加了readerLock和writerLock两个实例。
//ReadLock、WriteLock构造方法
protected ReadLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
}
//看ReadLock和WriterLock的构造函数可知两个实例共享一个sync实例。
3、写入锁和读取锁
(1)写入锁的获取
public void lock() {
sync.acquire(1);
}
//与ReentrantLock.lock()一致,分为公平锁和非公平锁(sync默认为非公平锁)
(2)读取锁的获取
public void lock() {
sync.acquireShared(1);
}
//AQS的acquireShared方法
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
流程:
1. 如果写线程持有锁(也就是独占锁数量不为0), 并且独占线程不是当前线程, 那么就返回失败,因为不允许写入线程获取锁的同时获取读取锁。否则进行2;
2. 如果读线程请求锁数量达到了65535(包括重入锁), 那么就跑出一个错误Error, 否则进行3;
3. 如果读线程不用等待, 并且增加读取锁状态数成功, 那么就返回成功, 否则进行4;
4. fullTryAcquireShared(内部包含一个无限循环, 实际上是过程3的不断尝试直到CAS计数成功或者被写入线程占有锁);
(3)读取锁的释放
读取锁的释放其实就是一个不断尝试的CAS操作, 直到修改状态成功。
(4)HoldCounter
a. HoldCounter保存了当前线程持有共享锁(读取锁)的数量, 包括重入的数量. 那么这个数量就必须和线程绑定在一起.
b. 在Java里面将一个对象和线程绑定在一起, 就只有ThreadLocal才能实现了. 所以毫无疑问HoldCounter就应该是绑定到线程上的一个计数器。
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention
final long tid = Thread.currentThread().getId();
}
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
(5)读写锁的应用
使用ReentrantReadWriteLock可以推广到大部分读, 少量写的场景, 因为读线程之间没有竞争, 所以比起sychronzied, 性能好很多。