读写锁ReentrantReadWriteLock

一、简介

读写锁是一种特殊的自旋锁,它把对共享资源对访问者划分成了读者和写者,读者只对共享资源进行访问,写者则是对共享资源进行写操作。读写锁在ReentrantLock上进行了拓展使得该锁更适合读操作远远大于写操作对场景。一个读写锁同时只能存在一个写锁但是可以存在多个读锁,但不能同时存在写锁和读锁。

ReentrantLock和ReentrantReadWriteLock 比较

ReadWriteLock是Jdk5中提供读写分离锁,读写分离锁可以有效地帮助减少锁竞争,提升系统性能。如果使用重用锁或内部锁,理论上所有读之间、读写之间、写和写之间都是串行操作。然而读写所允许多个线程同时读,读写操作或者写写操作仍然需要相互等待和持有锁。在系统中如果读操作次数远远大于写操作次数,读写锁就可以发挥最大功效,提升系统性能。

如果读写锁当前没有读者,也没有写者,那么写者可以立刻获的读写锁,否则必须自旋,直到没有任何的写锁或者读锁存在。如果读写锁没有写锁,那么读锁可以立马获取,否则必须等待写锁释放。(但是有一个例外,就是读写锁中的锁降级操作,当同一个线程获取写锁后,在写锁没有释放的情况下可以获取读锁再释放读锁这就是锁降级的一个过程)

二、简单示例

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1 package cn.memedai; 2
3 import java.util.Random; 4 import java.util.concurrent.ExecutorService; 5 import java.util.concurrent.Executors; 6 import java.util.concurrent.locks.ReadWriteLock; 7
8 /**
9 * 读写锁Demo 10 */
11 public class ReentrantReadWriteLockDemo { 12
13 class MyObject { 14 private Object object; 15
16 private ReadWriteLock lock = new java.util.concurrent.locks.ReentrantReadWriteLock(); 17
18 public void get() throws InterruptedException { 19 lock.readLock().lock();//上读锁
20 try { 21 System.out.println(Thread.currentThread().getName() + "准备读取数据"); 22 Thread.sleep(new Random().nextInt(1000)); 23 System.out.println(Thread.currentThread().getName() + "读数据为:" + this.object); 24 } finally { 25 lock.readLock().unlock(); 26 } 27 } 28
29 public void put(Object object) throws InterruptedException { 30 lock.writeLock().lock(); 31 try { 32 System.out.println(Thread.currentThread().getName() + "准备写数据"); 33 Thread.sleep(new Random().nextInt(1000)); 34 this.object = object; 35 System.out.println(Thread.currentThread().getName() + "写数据为" + this.object); 36 } finally { 37 lock.writeLock().unlock(); 38 } 39 } 40 } 41
42 public static void main(String[] args) { 43 final MyObject myObject = new ReentrantReadWriteLockDemo().new MyObject(); 44 ExecutorService executorService = Executors.newCachedThreadPool(); 45 for (int i = 0; i < 3; i++) { 46 executorService.execute(new Runnable() { 47 @Override 48 public void run() { 49 for (int j = 0; j < 3; j++) { 50
51 try { 52 myObject.put(new Random().nextInt(1000));//写操作 53 } catch (InterruptedException e) { 54 e.printStackTrace(); 55 } 56 } 57 } 58 }); 59 } 60
61 for (int i = 0; i < 3; i++) { 62 executorService.execute(new Runnable() { 63 @Override 64 public void run() { 65 for (int j = 0; j < 3; j++) { 66 try { 67 myObject.get();//多个线程读取操作 68 } catch (InterruptedException e) { 69 e.printStackTrace(); 70 } 71 } 72 } 73 }); 74 } 75
76 executorService.shutdown(); 77 } 78 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

下面是代码运行结果的一种:

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;">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</pre>

[
复制代码

](javascript:void(0); "复制代码")

从数据中也可以发现一开始读取的数据可能不一样,但是你会发现下面的时候线程4和线程5、线程6之间的读取的数据都是一样的,这就是共享读的特性。

三、实现原理

ReentrantReadWriteLock的基本原理和ReentrantLock没有很大的区别,只不过在ReentantLock的基础上拓展了两个不同类型的锁,读锁和写锁。首先可以看一下ReentrantReadWriteLock的内部结构:

image

内部维护了一个ReadLock和一个WriteLock,整个类的附加功能也就是通过这两个内部类实现的。

那么内部又是怎么实现这个读锁和写锁的呢。由于一个类既要维护读锁又要维护写锁,那么这两个锁的状态又是如何区分的。在ReentrantReadWriteLock对象内部维护了一个读写状态:

image

读写锁依赖自定义同步器实现同步功能,读写状态也就是同步器的同步状态。读写锁将整形变量切分成两部分,高16位表示读,低16位表示写:

image

读写锁的状态低16位为写锁,高16位为读锁

读写锁通过位运算计算各自的同步状态。假设当前同步状态的值为c,写状态就为c&0x0000FFFF,读状态为c >>> 16(无符号位补0右移16位)。当写状态增加1状态变为c+1,当读状态增加1时,状态编码就是c+(1 <<< 16)。

怎么维护读写状态的已经了解了,那么就可以开始了解具体怎么样实现的多个线程可以读,一个线程写的情况。

首先介绍的是ReadLock获取锁的过程

lock():获取读锁方法

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;">1      public void lock() { 2 sync.acquireShared(1);//自定义实现的获取锁方式 3 }</pre>

acquireShared(int arg):这是一个获取共享锁的方法

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1 protected final int tryAcquireShared(int unused) {
17 Thread current = Thread.currentThread();//获取当前线程 18 int c = getState();//获取锁状态 19 if (exclusiveCount(c) != 0 &&
20 getExclusiveOwnerThread() != current)//如果获取锁的不是当前线程,并且由独占式锁的存在就不去获取,这里会发现必须同时满足两个条件才能判断其不能获取读锁这也会后面的锁降级做了准备 21 return -1; 22 int r = sharedCount(c);//获取当前共享资源的数量 23 if (!readerShouldBlock() &&
24 r < MAX_COUNT &&
25 compareAndSetState(c, c + SHARED_UNIT)) {//代表可以获取读锁 26 if (r == 0) {//如果当前没有线程获取读锁 27 firstReader = current;//当前线程是第一个读锁获取者 28 firstReaderHoldCount = 1;//在计数器上加1 29 } else if (firstReader == current) { 30 firstReaderHoldCount++;//代表重入锁计数器累加 31 } else {
              //内部定义的线程记录缓存 32 HoldCounter rh = cachedHoldCounter;//HoldCounter主要是一个类用来记录线程已经线程获取锁的数量 33 if (rh == null || rh.tid != current.getId())//如果不是当前线程 34 cachedHoldCounter = rh = readHolds.get();//从每个线程的本地变量ThreadLocal中获取 35 else if (rh.count == 0)//如果记录为0初始值设置 36 readHolds.set(rh);//设置记录 37 rh.count++;//自增 38 } 39 return 1;//返回1代表获取到了同步状态 40 } 41 return fullTryAcquireShared(current);//用来处理CAS设置状态失败的和tryAcquireShared非阻塞获取读锁失败的 42 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

内部运用到了ThreadLocal线程本地对象,将每个线程获取锁的次数保存到每个线程内部,这样释放锁的时候就不会影响到其它的线程。

fullTryAcquireShared(Thread current):此方法用于处理在获取读锁过程中CAS设置状态失败的和非阻塞获取读锁失败的线程

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1       final int fullTryAcquireShared(Thread current) { 2 //内部线程记录器
8 HoldCounter rh = null;
9 for (;;) { 10 int c = getState();//同步状态 11 if (exclusiveCount(c) != 0) {//代表存在独占锁 12 if (getExclusiveOwnerThread() != current)//获取独占锁的线程不是当前线程返回失败 13 return -1;
16 } else if (readerShouldBlock()) {//判断读锁是否应该被阻塞
18 if (firstReader == current) {
20 } else { 21 if (rh == null) {//为null 22 rh = cachedHoldCounter;//从缓存中进行获取 23 if (rh == null || rh.tid != current.getId()) { 24 rh = readHolds.get();//获取线程内部计数状态 25 if (rh.count == 0) 26 readHolds.remove();//移除 27 } 28 } 29 if (rh.count == 0)//如果内部计数为0代表获取失败 30 return -1; 31 } 32 } 33 if (sharedCount(c) == MAX_COUNT) 34 throw new Error("Maximum lock count exceeded"); 35 if (compareAndSetState(c, c + SHARED_UNIT)) {//CAS设置成功 36 if (sharedCount(c) == 0) { 37 firstReader = current;//代表为第一个获取读锁 38 firstReaderHoldCount = 1; 39 } else if (firstReader == current) { 40 firstReaderHoldCount++;//重入锁 41 } else { 42 if (rh == null) 43 rh = cachedHoldCounter; 44 if (rh == null || rh.tid != current.getId()) 45 rh = readHolds.get(); 46 else if (rh.count == 0) 47 readHolds.set(rh); 48 rh.count++; 49 cachedHoldCounter = rh; //将当前多少读锁记录下来
50 } 51 return 1;//返回获取同步状态成功 52 } 53 } 54 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

分析完上面的方法可以总结一下获取读锁的过程:首先读写锁中读状态为所有线程获取读锁的次数,由于是可重入锁,又因为每个锁获取的读锁的次数由每个锁的本地变量ThreadLocal对象去保存因此增加了读取获取的流程难度,在每次获取读锁之前都会进行一次判断是否存在独占式写锁,如果存在独占式写锁就直接返回获取失败,进入同步队列中。如果当前没有写锁被获取,则线程可以获取读锁,由于共享锁的存在,每次获取都会判断线程的类型,以便每个线程获取同步状态的时候都在其对应的本地变量上进行自增操作。

lock(int arg):写锁的获取

|

1

2

3

|

public void lock() {

sync.acquire(``1``);``//AQS独占式获取锁

}

|

tryAcquire(int arg):独占式的获取写锁

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1     protected final boolean tryAcquire(int acquires) {
13 Thread current = Thread.currentThread();//获取当前线程 14 int c = getState();//获取同步状态值 15 int w = exclusiveCount(c);//获取独占式资源值 16 if (c != 0) {//已经有线程获取了
           //代表已经存在读锁,或者当前线程不是获取到写锁的线程
18 if (w == 0 || current != getExclusiveOwnerThread()) 19 return false;//获取失败 20 if (w + exclusiveCount(acquires) > MAX_COUNT) 21 throw new Error("Maximum lock count exceeded"); 22 //设置同步状态
23 setState(c + acquires); 24 return true; 25 } 26 if (writerShouldBlock() ||
27 !compareAndSetState(c, c + acquires))//判断当前写锁线程是否应该阻塞,这里会有公平锁和非公平锁之间的区分 28 return false; 29 setExclusiveOwnerThread(current);//设置为当前线程 30 return true; 31 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

获取写锁相比获取读锁就简单了很多,在获取读锁之前只需要判断当前是否存在读锁,如果存在读锁那么获取失败,进而再判断获取写锁的线程是否为当前线程如果不是也就是失败否则就是重入锁在已有的状态值上进行自增

unlock():读锁释放

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;">     public void unlock() {
sync.releaseShared(1);//AQS释放共享锁操作
}</pre>

tryReleaseShared(int arg):释放共享锁

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1     protected final boolean tryReleaseShared(int unused) { 2 Thread current = Thread.currentThread();//获取当前线程 3 if (firstReader == current) {//如果当前线程就是获取读锁的线程
5 if (firstReaderHoldCount == 1)//如果此时获取资源为1
6 firstReader = null;//直接赋值null
7 else
8 firstReaderHoldCount--;//否则计数器自减
9 } else {
           //其他线程 10 HoldCounter rh = cachedHoldCounter;//获取本地计数器 11 if (rh == null || rh.tid != current.getId()) 12 rh = readHolds.get(); 13 int count = rh.count; 14 if (count <= 1) {//代表只获取了一次 15 readHolds.remove(); 16 if (count <= 0) 17 throw unmatchedUnlockException(); 18 } 19 --rh.count; 20 } 21 for (;;) { 22 int c = getState(); 23 int nextc = c - SHARED_UNIT; 24 if (compareAndSetState(c, nextc))
28 return nextc == 0;//代表已经全部释放 29 } 30 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

释放锁的过程不难,但是有一个注意点,并不是释放一次就已经代表可以获取独占式写锁了,只有当同步状态的值为0的时候也就是代表既没有读锁存在也没有写锁存在才代表完全释放了读锁。

unlock():释放写锁

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;">1      public void unlock() { 2 sync.release(1);//释放独占式同步状态 3 }</pre>

tryRelease(int arg):释放独占式写锁

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1      protected final boolean tryRelease(int releases) { 2 if (!isHeldExclusively())//判断是否
3 throw new IllegalMonitorStateException(); 4 int nextc = getState() - releases;//同步状态值自减 5 boolean free = exclusiveCount(nextc) == 0;//如果状态值为0代表全部释放
6 if (free) 7 setExclusiveOwnerThread(null);
8 setState(nextc);
9 return free; 10 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

写锁的释放相比读锁的释放简单很多,只需要判断当前的写锁是否全部释放完毕即可

四、读写锁之锁降级操作

什么是锁降级,锁降级就是从写锁降级成为读锁。在当前线程拥有写锁的情况下,再次获取到读锁,随后释放写锁的过程就是锁降级。这里可以举个例子:

[
复制代码

](javascript:void(0); "复制代码")

<pre style="margin: 0px; padding: 0px; white-space: pre-wrap; overflow-wrap: break-word; font-family: "Courier New" !important; font-size: 18px !important;"> 1 public class CacheDemo {
3 private Map<String, Object> cache = new HashMap<String, Object>();
4
5 private ReadWriteLock rwl = new ReentrantReadWriteLock(); 6    public ReadLock rdl = rwl.readLock(); 7    public WriteLock wl = rwl.writeLock(); 8
9 public volatile boolean update = false; 10 public void processData(){ 11 rdl.lock();//获取读锁 12 if(!update){ 13 rdl.unlock();//释放读锁 14 wl.lock();//获取写锁 15 try{ 16 if(!update){ 17 update =true; 18 } 19 rdl.lock();//获取读锁 20 finally{ 21 wl.unlock();//释放写锁 22 } 23 } 24 try{ 25 }finally{ 26 rdl.unlock();//释放读锁 27 }
29 }</pre>

[
复制代码

](javascript:void(0); "复制代码")

五、总结

读写锁是在重入锁ReentrantLock基础上的一大改造,其通过在重入锁上维护一个读锁一个写锁实现的。对于ReentrantLock和ReentrantreadWriteLock的使用需要在开发者自己根据实际项目的情况而定。对于读写锁当读的操作远远大于写操作的时候会增加程序很高的并发量和吞吐量。虽说在高并发的情况下,读写锁的效率很高,但是同时又会存在一些问题,比如当读并发很高时读操作长时间占有锁,导致写锁长时间无法被获取而导致的线程饥饿问题,因此在JDK1.8中又在ReentrantReadWriteLock的基础上新增了一个读写并发锁StampLock。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342

推荐阅读更多精彩内容