ReentrantReadWriteLock即可重入读写锁,其同时应用了共享锁和排斥锁,写锁使用排斥锁,读锁使用共享锁,从而实现读读共享,读写互斥,写写互斥。
当读操作远远高于写操作时,这时候使用读写锁让读——读可以并发,提高性能。
读写锁使用的是一个Sync同步器(使用一个对象),可以分别创建。有公平锁和非公平锁两种子类进行实现。
一个32位的二进制数,分成两部分,高16位用于共享锁,表示持有锁的线程数;低16位用于排斥锁,表示锁重入次数。
ReadLock
和WriteLock
是ReentrantReadWriteLock
的内部类,都实现了Lock接口,分别表示读锁和写锁,用老控制读和写操作,
使用中,需要通过调用public ReentrantReadWriteLock.ReadLock readLock()
和public ReentrantReadWriteLock.WriteLock writeLock()
获取到对应的读锁和写锁,然后进行lock
和unlock
进行锁的获取和释放
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 读锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
// 写锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
当state等于0 --> 写状态和读状态都为0,表示未获得锁
当state不等于0,当写状态等于0时 --> 读状态大于0,表示获得读锁
当state不等于0,当读状态等于0时 --> 写状态大于0,表示获得写锁
当state不等于0,当读状态和写状态都不等于0时 --> 发生锁的降级
锁的降级: 获取写锁 -> 获取写锁 -> 释放写锁 -> 释放读锁
public static void main(String[] args) {
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
// 读锁
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
// 写锁
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
writeLock.lock();
System.out.println("1");
readLock.lock();
System.out.println("2");
writeLock.unlock();
System.out.println("3");
readLock.unlock();
}
这里如何使得读状态=2, 写状态=3的, 一个线程获取到写锁, 然后重入2次, 然后发生锁降级, 在释放写锁的时候, 获取到读锁, 并且重入一次。
读写锁用的是同一个Sync同步器,所以有相同的阻塞队列和state
这里通过分析, 得出一个32位的二进制数,分成两部分,高16位用于共享锁,表示持有锁的线程数;低16位用于排斥锁,表示锁重入次数。
ReentrantReadWriteLock是一个读写双用锁,但是除非是同一个线程的重入,ReentrantReadWriteLock不可能同时有读锁和写锁的状态(某一时刻只能是读锁或写锁)。
成为读锁时,只增加state计数,而exclucsiveOwner处依然指向空。
成为写锁时,exclucsiveOwner指向该线程。
t1为写锁, t2为读锁
此时线程t1占有锁流程与之前讲的ReentrantLock相似,不同之处时写锁状态占了state的低16位,而读锁占了state的高16位。
同ReentrantLock类似,t1同时也修改了state位的值,表示该锁已被占有。
此时出现t2竞争,执行 r.lock() ,这时进入读锁的 sync . acquireShared (1)流程
首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared
返回-1表示失败, 0表示成功, 但是后继节点不会唤醒, 正数也成功, 而且后面可以唤醒节点, 读写锁返回1, 此处由于t1占据锁,所以此处t2的尝试是失败的,于是返回-1。
private void doAcquireShared(int arg) {
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head) {
int r = tryAcquireShared(arg);
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
先调用 addWaiter 添加节点,不同之处在于节点被设置为 Node . SHARED 模式(此处我们假设t2要获取的是读锁)而非 Node . EXCLUSIVE(如果尝试获取的是写锁) 模式,注意此时t2仍处于活跃状态。
调用 tryAcquireShared (1)来尝试获取锁, 没成功的话, for (;;)循环一次, 把前驱节点的waitStatus改为-1。
此时如果依然有线程来争夺锁资源,则重复t2竞争时的策略,加入到队列中
写锁解锁, 调用unlock() -> sync.release(1)
先调用tryRelease(), 成功接下来执行唤醒流程unparkSuccessor, 让t1恢复运行
读锁解锁, unlock() -> releaseShared(1)
当一个线程释放锁时,回在队列中的线程就会开始抢占锁资源,如果排在第二位(第一位是占位节点)的节点是Shared,由于读锁是共享锁,所以第二位的线程获得读锁以后,会继续检查其他线程的状态,如果还是Shared,则继续使线程获得锁,直到遇到Ex线程为止。但此时的exclusiveOwner处还是空的(因为此时的Sync是读锁)。
如果第二个节点为Ex(即需要获得的是写锁,则仅仅使该线程获得锁,exclusiveOwner指向该线程。
成为读锁时,只增加state计数,而exclucsiveOwner处依然指向空。
成为写锁时,exclucsiveOwner指向该线程。