当读操作远远高于写操作时,这时候使用读写锁让读——读可以并发,提高性能。
类似于数据库中的 select ., from .. lock in share mode
提供一个数据容器类内部分别使用读锁保护数据的 read ()方法,写锁保护数据的 write ()方法
简单来说,就是读锁可以共享,但是写锁必须独占
一、 ReentrantReadWriteLock
JUC为我们提供了一个可用于读和写的读写锁。
简单看下使用方法:
ReentrantReadWriteLock reentrantReadWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = reentrantReadWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = reentrantReadWriteLock.writeLock();
可以看到,读写锁使用的是一个Sync同步器(使用一个对象),可以分别创建。
注意事项
1、读锁不支持条件变量
2、重入时支持升级,即当前线程如果持有读锁,则在重入获得锁时不能在获得写锁
3、冲入支持降级,即当前线程如果持有写锁,则在重入时可以获得读锁
二、读写锁原理
加锁
读写锁用的是同一个Sync同步器,所以有相同的阻塞队列和state
一、如图,此时线程t1占有锁流程与之前讲的ReentrantLock相似,不同之处时写锁状态占了state的低16位,而读锁占了state的高16位。
同ReentrantLock类似,t1同时也修改了state位的值,表示该锁已被占有。
二、此时出现t2竞争,执行 r.lock() ,这时进入读锁的 sync . acquireShared (1)流程
首先会进入 tryAcquireShared 流程。如果有写锁占据,那么 tryAcquireShared
1、返回-1表 示失败
2、0表示成功,但后继节点不会继续唤醒
3、正数表示成功,而且数值是还有几个后继节点需要唤醒,读写锁返回1
此处由于t1占据锁,所以此处t2的尝试是失败的,于是返回-1。
注意
此时如果占据写锁的是t2线程,则获取读锁将会成功(锁降级原理)
三、这时会进入 sync . doAcquireShared (1)流程,首先也是调用 addWaiter 添加节点,不同之处在于节点被设置为 Node . SHARED 模式(此处我们假设t2要获取的是读锁)而非 Node . EXCLUSIVE(如果尝试获取的是写锁) 模式,注意此时t2仍处于活跃状态
四、口会看看自己的节点是不是老二,如果是,还会再次调用 tryAcquireShared (1)来尝
试获取锁
五、如果没有成功,在 doAcquireShared 内 for (;;)循环一次,把前驱节点的 waitStatus 改为-1(同ReentrantLock,用来表示前一节点有义务唤醒该线程)。
六、此时如果依然有线程来争夺锁资源,则重复t2竞争时的策略,加入到队列中
解锁
当t1线程结束对锁的占用时,会释放锁对象,此时会触发解锁流程。
这时会走到写锁的 sync . release (1)流程,调用 sync . tryRelease (1)成功接下来执行唤醒流程 sync . unparkSuccessor ,即让老二恢复运行,这t2 doAcquireShared 内 parkAndCheckInterrupt )处恢复运行这回再来一次 for (; ;)执行 tryAcquireShared 成功则让读锁计数加一
这时12已经恢复运行,接下来1调用 setHeadAndPropagate ( node ,1),它原本所在节点
被置为头节点
事情还没完,在 setHeadAndPropagate 方法内还会检查下一个节点是否是 shared ,如果是则调用 doReleaseShared )将 head 的状态从-1改为0并唤醒老二,这时3在 doAcquireShared 内parkAndCheckInterupt0处恢复运行。同时使读锁计数加一。直到遇到Ex节点为止。
简单来说,当一个线程释放锁时,回在队列中的线程就会开始抢占锁资源,如果排在第二位(第一位是占位节点)的节点是Shared,由于读锁是共享锁,所以第二位的线程获得读锁以后,会继续检查其他线程的状态,如果还是Shared,则继续使线程获得锁,直到遇到Ex线程为止。但此时的exclusiveOwner处还是空的(因为此时的Sync是读锁)。
如果第二个节点为Ex(即需要获得的是写锁,则仅仅使该线程获得锁,exclusiveOwner指向该线程。)
总结
综上我们可以看出,ReentrantReadWriteLock是一个读写双用锁,但是除非是同一个线程的重入,ReentrantReadWriteLock不可能同时有读锁和写锁的状态(某一时刻只能是读锁或写锁)。
成为读锁时,只增加state计数,而exclucsiveOwner处依然指向空。
成为写锁时,exclucsiveOwner指向该线程。