目录
六、AbstractQueuedSynchronizer实现总结
ReentrantReadWriteLock是一个可重入的,支持读写分离的锁,本篇博客就详细探讨该类的实现。
一、定义
ReentrantReadWriteLock用于实现ReadWriteLock接口,该接口定义的方法如下:
该类包含多个内部类,如下:
ReadLock和WriteLock两个都是基于Sync实现的Lock接口,只是调用Sync的方法不同而已。另外两个类FairSync和NonfairSync都继承自Sync,分别表示公平锁和非公平锁,其类继承关系如下:
FairSync和NonfairSync都是只是实现了父类的两个抽象方法,writerShouldBlock和readerShouldBlock方法,这两方法用于判断写操作是否需要阻塞,读操作是否需要阻塞。
Sync内部还有两个内部类,其定义如下:
HoldCounter是一个数据结构用来记录每个线程的重入读锁次数和关联的线程ID, ThreadLocalHoldCounter表示一个存储HoldCounter实例的本地线程变量。多个线程可同时获取读锁,他们获取的读锁实际都是同一个Sync实例,Sync只有一个state属性用来保存重入读锁的总次数,当该次数变成0的时候才能真正释放读锁。因为获取读锁的线程都需要执行unlock方法释放锁,所以必须记录每个线程自身重入读锁的次数,使用ThreadLocalHoldCounter,即下面的readHolds属性来实现。
Sync中包含的属性如下:
//本地线程变量,保存当前线程累计重入读锁的次数
private transient ThreadLocalHoldCounter readHolds;
//缓存的上一次成功获取读锁的HoldCounter
private transient HoldCounter cachedHoldCounter;
//第一个获取读锁的线程
private transient Thread firstReader = null;
//第一个获取读锁的线程,累计重入读锁的次数
private transient int firstReaderHoldCount;
除此之外,Sync定义了几个常量,用来计算重入读锁或者写锁的累计次数,如下:
因为一个线程在获取写锁后可以再获取读锁,为了在state属性中同时记录重入这两种锁的次数,Sync只有这一个属性保存锁重入次数,就将state属性拆成两半,高16位的数值用于表示读锁的重入次数,低16位的值表示写锁的重入次数,因为有位数限制,所以重入的最大次数不能超过上面的MAX_COUNT。
ReentrantReadWriteLock本身的属性如下:
//读锁实例
private final ReentrantReadWriteLock.ReadLock readerLock;
//写锁实例
private final ReentrantReadWriteLock.WriteLock writerLock;
//完成实际同步动作的Sync实例
final Sync sync;
private static final sun.misc.Unsafe UNSAFE;
//Thread类的tid属性的偏移量,该属性实际是关联的JVM中的JavaThread实例的指针
private static final long TID_OFFSET;
两个static属性通过static代码块完成初始化,如下:
另外三个属性是在构造函数中初始化的,构造函数的实现如下:
public ReentrantReadWriteLock() {
this(false);
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
下面就以读锁和写锁的具体实现为入口来分析内部类Sync的相关实现,因为其底层都是基于AbstractQueuedSynchronizer,涉及的相关方法可以参考上一篇《Java8 ReentrantLock 源码解析》。
二、使用
ReentrantReadWriteLock的使用,核心还是基于其writeLock和readLock返回的写锁和读锁,跟ReentrantLock的使用是一样,如下:
@Test
public void test() throws Exception {
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
Lock readLock=lock.readLock();
CyclicBarrier cyclicBarrier=new CyclicBarrier(6);
Runnable a=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" start,time->"+System.currentTimeMillis());
try {
readLock.lock();
Thread.sleep(1000);
System.out.println("do something end for read,time->"+System.currentTimeMillis());
//读锁时因为读锁是共享锁,其他线程都能成功获取锁,所以不会出现死锁问题
cyclicBarrier.await();
}catch (Exception e){
e.printStackTrace();
}finally {
readLock.unlock();
}
}
};
for(int i=0;i<5;i++){
new Thread(a).start();
}
long start=System.currentTimeMillis();
cyclicBarrier.await();
System.out.println("read end,time->"+(System.currentTimeMillis()-start));
Lock writeLock=lock.writeLock();
Runnable b=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" start,time->"+System.currentTimeMillis());
try {
writeLock.lock();
Thread.sleep(1000);
System.out.println("do something end for write,time->"+System.currentTimeMillis());
}catch (Exception e){
e.printStackTrace();
}finally {
//先unlock解锁,在await等待,避免形成死锁
writeLock.unlock();
try {
cyclicBarrier.await();
} catch (Exception e) {
e.printStackTrace();
}
}
}
};
for(int i=0;i<5;i++){
new Thread(b).start();
}
start=System.currentTimeMillis();
cyclicBarrier.await();
System.out.println("write end,time->"+(System.currentTimeMillis()-start));
}
上述用例的输出如下:
//5个读操作线程依次启动
Thread-0 start,time->1585987927568
Thread-1 start,time->1585987927568
Thread-3 start,time->1585987927568
Thread-2 start,time->1585987927568
Thread-4 start,time->1585987927569
//几乎是同一时间获取读锁,然后完成读操作
do something end for read,time->1585987928568
do something end for read,time->1585987928569
do something end for read,time->1585987928569
do something end for read,time->1585987928569
do something end for read,time->1585987928569
//读锁是共享锁,整体耗时基本等于单个线程耗时,即sleep的耗时
read end,time->1018
//5个写操作线程依次启动
Thread-5 start,time->1585987928571
Thread-6 start,time->1585987928571
Thread-7 start,time->1585987928571
Thread-8 start,time->1585987928571
Thread-9 start,time->1585987928571
//写锁是排他锁,5个线程依次获取锁
do something end for write,time->1585987929571
do something end for write,time->1585987930571
do something end for write,time->1585987931571
do something end for write,time->1585987932571
do something end for write,time->1585987933571
//总耗时是5个写操作线程的累计耗时
write end,time->5000
上述用例中读锁似乎没啥价值,因为不加锁的效果跟加读锁的一样。读锁的价值不在于共享,而在于可以排斥写锁,即获取了读锁以后就不能再获取写锁了,必须等读锁释放了才能获取读锁,测试用例如下:
@Test
public void test2() throws Exception {
ReentrantReadWriteLock lock=new ReentrantReadWriteLock();
Lock readLock=lock.readLock();
Lock writeLock=lock.writeLock();
CountDownLatch writeCount=new CountDownLatch(5);
Runnable a=new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+" start,time->"+System.currentTimeMillis());
try {
readLock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" do something end for read,time->"+System.currentTimeMillis());
try{
//写锁必须等待所有的读锁释放才能获取锁,此处是读锁获取完成,等待获取写锁
//就形成了死锁
writeLock.lock();
Thread.sleep(1000);
System.out.println(Thread.currentThread().getName()+" do something end for write,time->"+System.currentTimeMillis());
}finally {
writeLock.unlock();
}
}catch (Exception e){
e.printStackTrace();
}finally {
readLock.unlock();
writeCount.countDown();
}
}
};
for(int i=