Java并发包的locks包里的锁基本上已经介绍得差不多了,ReentrantLock重入锁是个关键,在清楚的了解了同步器AQS的运行机制后,实际上再分析这些锁就会显得容易得多,这章节主讲另外一个重要的锁——ReentrantReadWriteLock读写锁。

  ReentrantLock是一个独占锁,也就是说只能由一个线程获取锁,但如果场景是线程只做读的操作呢?这样ReentrantLock就不是很合适,读的线程并不需要保证其线程的安全性,任何一个线程都能去获取锁,只有这样才能尽可能地保证性能和效率。ReentrantReadWriteLock就是这样的一个锁,在其内部分为读锁和写锁,可以有N个读操作线程获取到写锁,但是只能有1个写操作线程获取到写锁,那么可以预见的是写锁是共享锁(AQS中的共享模式),读锁是独占锁(AQS中的独占模式)。首先来看读写锁的接口类:

 public interface ReadWriteLock {
Lock readLock(); //获取读锁
Lock writeLock(); //获取写锁
}

  可以看到ReadWriteLock接口只定义了两个方法,获取读锁和获取写锁的方法。下面是ReadWriteLock的实现类——ReentrantReadWriteLock。  

  和ReentrantLock类似,ReentrantReadWriteLock在其内部也是通过一个内部类Sync实现同步器AQS,同样也是通过实现Sync实现公平锁和非公平锁,这一点的思路和ReentrantLock类似。在ReadWriteLock接口中获取的读锁和写锁是怎么实现的呢?

//ReentrantReadWriteLock
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
final Sync sync;
public ReentrantReadWriteLock(){
this(false); //默认非公平锁
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync(); //锁类型(公平/非公平)
readerLock = new ReadLock(this); //构造读锁
writerLock = new WriteLock(this); //构造写锁
}
……
public ReentrantReadWriteLock.WriteLock writeLock0{return writerLock;}
public ReentrantReadWriteLock.ReadLock readLock0{return ReaderLock;}
//ReentrantReadWriteLock$ReadLock
public static class ReadLock implements Lock {
protected ReadLock(ReentrantReadwritLock lock) {
sync = lock.sync; //最后还是通过Sync内部类实现锁
  }
…… //它实现的是Lock接口,其余的实现可以和ReentrantLock作对比,获取锁、释放锁等等
}
//ReentrantReadWriteLock$WriteLock
public static class WriteLock implemnts Lock {
protected WriteLock(ReentrantReadWriteLock lock) {
sync = lock.sync;
  }
…… //它实现的是Lock接口,其余的实现可以和ReentrantLock作对比,获取锁、释放锁等等
}

  上面是对ReentrantReadWriteLock做了一个大致的介绍,可以看到在其内部有好几个内部类,实际上读写锁内有两个锁——ReadLock、WriteLock,这两个锁都是实现自Lock接口,可以和ReentrantLock对比,而这两个锁的内部实现则是通过Sync,也就是同步器AQS实现的,这也可以和ReentrantLock中的Sync对比。
  回顾一下AQS,其内部有两个重要的数据结构——一个是同步队列、一个则是同步状态,这个同步状态应用到读写锁中也就是读写状态,但AQS中只有一个state整型来表示同步状态,读写锁中则有读、写两个同步状态需要记录。所以,读写锁将AQS中的state整型做了一下处理,它是一个int型变量一共4个字节32位,那么可以读写状态就可以各占16位——高16位表示读,低16位表示写。

  

  现在有一个疑问如果state的值位5,二进制为(00000000000000000000000000000101),如何快速确定读和写各自的状态呢?这就要用到位移运算了。计算方式为:写状态state & 0x0000FFFF,读状态state >>> 16。写状态增加1等于state + 1,读状态增加1等于state + (1 << 16)。有关移位运算可以参考《<<、>>、>>>移位操作》

写锁的获取与释放

  根据我们之前的经验可以得知:AQS已经将获取锁的算法骨架搭好了,只需子类实现tryAcquire(独占锁),故我们只需查看tryAcquire。

 //ReentrantReadWriteLock$Sync
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread;
int c = getState(); //获取state状态
int w = exclusiveCount(c); //获取写状态,即 state & 0x00001111
if (c != 0) { //存在同步状态(读或写),作下一步判断
if (w == 0 || current != getExclusiveOwnerThread()) //写状态为0,但同步状态不为0表示有读状态,此时获取锁失败,或者当前已经有其他写线程获取了锁此时也获取锁失败
return false;
if (w + exclusiveCount(acquire) > MAX_COUNT) //锁重入是否超过限制
throw new Error(“Maxium lock count exceeded”);
setState(c + acquire); //记录锁状态
return true;
  }
  if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
  return false; //writerShouldBlock对于非公平锁总是返回false,对于公平锁则判断同步队列中是否有前驱节点
  setExclusiveOwnerThread(current);
  return true;
}

  上面是写锁的状态获取,不好理解的是writerShouldBlock方法,此方法上面有描述,非公平锁直接返回false,而对于公平锁则是调用hasQueuedPredecessors方法如下:

 //ReentrantReadWriteLock$FairSync
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}

  原因是为什么呢?这就要回到非公平锁和公平锁的区别上来了,简单回顾一下,详情可参考《5.Lock接口及其实现ReentrantLock》。对于非公平锁,每次线程获取锁时首先会强行进行锁获取操作而不管同步队列中是否有线程,当获取不到时才会将线程构造至队尾;对于公平锁来讲,只要同步队列中存在线程,就不会去获取锁,而是将线程构造添加至队尾。所以重新回到写状态的获取上,tryAcquire方法里,前面发现没有线程持有锁,但是此时会根据锁的不同做相应操作,对于非公平锁——抢锁,对公平锁——同步队列中有线程,不抢锁,添加至队尾排队。
  写锁的释放与ReentrantLock的释放过程基本类似,毕竟都是独占锁,每次释放减少写的状态,直到减小到0就表示写锁已经完全释放。

读锁的获取与释放

  同理,根据我们之前的经验可以得知:AQS已经将获取锁的算法骨架搭好了,只需子类实现tryAcquireShared(共享锁),故我们只需查看tryAcquireShared。我们知道对于共享模式下的锁,它能够被多个线程同时获取,现在问题来了,T1线程获取了锁,同步状态state=1,此时T2也获取了锁,state=2,接着T1线程重入state=3,也就是说读状态是所有线程读锁次数的总和,而每个线程各自获取读锁的次数只能选择保存在ThreadLock中,由线程自身维护,所以在这个地方要做一些复杂处理,源码有点长,但复杂就在于每个线程保存自身获取读锁的次数,具体参照源码的tryAcquireShared,仔细阅读并结合上面对写锁获取的分析不难读懂。
  读锁的释放值得注意的地方在于自身维护的获取锁的次数,以及通过移位操作减少状态state – (1 << 16)。

7.ReadWriteLock接口及其实现ReentrantReadWriteLock的更多相关文章

  1. Java并发ReadWriteLock接口

    java.util.concurrent.locks.ReadWriteLock接口允许一次读取多个线程,但一次只能写入一个线程. 读锁 - 如果没有线程锁定ReadWriteLock进行写入,则多线 ...

  2. ReadWriteLock 接口详解

    ReadWriteLock 接口详解 这是本人阅读ReadWriteLock接口源码的注释后,写出的一篇知识分享博客 读写锁的成分是什么? 读锁 Lock readLock(); 只要没有写锁,读锁可 ...

  3. Lock接口和ReadWriteLock接口

    Lock接口 Lock接口在java.util.concurrent.locks包中,在jdk1.5之后才有. Lock接口有6个方法: void lock(); void lockInterrupt ...

  4. 从源码来看ReentrantLock和ReentrantReadWriteLock

    上一篇花了点时间将同步器看了一下,心中对锁的概念更加明确了一点,知道我们所使用到的锁是怎么样获取同步状态的,我们也写了一个自定义同步组件Mutex,讲到了它其实就是一个简版的ReentrantLock ...

  5. 线程中的读写锁ReadWriteLock

    Lock锁还有两个非常强大的类 ReadWriteLock接口实现类ReentrantReadWriteLock(非常重要的锁) 想实现 读取的时候允许多线程并发访问,写入的时候不允许. 这种效果.. ...

  6. 源码分析:ReentrantReadWriteLock之读写锁

    简介 ReentrantReadWriteLock 从字面意思可以看出,是和重入.读写有关系的锁,实际上 ReentrantReadWriteLock 确实也是支持可重入的读写锁,并且支持公平和非公平 ...

  7. Synchronized和Lock接口

    关于synchronized字段,不管该关键字是修饰方法还是修饰同步代码块,synchronzed拿到的都是对象. 当synchronized修饰的是方法时,synchronized所拿到的是调用该方 ...

  8. 【转】Java并发编程:Lock

    阅读目录 一.synchronized的缺陷 二.java.util.concurrent.locks包下常用的类 三.锁的相关概念介绍 来自: http://www.importnew.com/18 ...

  9. Java并发编程:Lock

    原文出处: 海子 在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包 ...

随机推荐

  1. 第一个 lua 程序

    第一个 lua 程序 lua 提供一个交互式编程模式, 直接在命令行输入 lua 开启 $ lua > -- 此处可以输入 lua 程序 lua 脚本执行时的 2 种方式 lua + lua 脚 ...

  2. stm32通过电调带动电机(可按键调速)

    这几天在做32通过电调带动电机的实验,上网一查,发现这方面的资料很少,经过自己的亲自实践,总结出以下经验,供大家参考. 论坛上也有很多人说自己在做,但是都遇到了同样的瓶颈.我想他们大多是pwm的频率和 ...

  3. MySQL中binlog参数:binlog_rows_query_log_events

    在使用RBR也就是行格式的时候,去解析binlog,需要逆向才能分析出对应的原始SQL是什么,而且,里面对应的是每一条具体行变更的内容.当然,你可以开启general log,但如果我们需要的只是记录 ...

  4. python库安装(numpy+scipy+matplotlib+scikit_learn)

    python安装好后,库安装走了很多弯路,查了很多资料,终于安装成功,并且保存了该文章的地址,分享给大家 本人电脑windows 7,64位系统,安装的Python是3.5的,因此下载的库也是对应版本 ...

  5. BackgroundWorker的DoWork方法中发生异常无法传递到RunWorkedCompleted方法

    在使用C#的BackgroundWorker时需要在UI界面上显示DoWork中发生的异常,但怎么调试都无法跳转到界面上,异常也不会传递到RunWorkerCompleted方法中(e.Error为空 ...

  6. FrameBuffer系列 之 介绍

    1.     来由 FrameBuffer是出现在2.2.xx内核当中的一种驱动程序接口.Linux工作在保护模式下,所以用户态进程是无法象 DOS 那样使用显卡 BIOS里提供的中断调用来实现直接写 ...

  7. Vue2.x中的父子组件相互通信

    业务场景:(这里指的是直接父子级关系的通信) 美女(子组件)将消息发送给大群(父组件) 大群(父组件)收到美女发送的消息后再回个信息给美女(子组件) 父组件 template <template ...

  8. openlayers3应用一:显示百度地图

    在项目中使用百度地图,最直接的方式是使用百度api,但是使用百度api需要申请key,并且某些功能调用有次数限制. 本文讲述在openlayers3中使用百度地图的方法.调用百度地图,也是经过了几番周 ...

  9. Win10或Win8下ObjectARX2015 Wizard向导创建项目失败解决方法

    [原创]objectARX 2015 Wizard安装向导在Win8/win10下无法创建项目的解决方法总结by edata @2017-5-1objectARX 2015 Wizard安装向导在Wi ...

  10. IOS中的绘图Quartz2D

    drawRect 方法的使用 常见图形的绘制:线条.多边形.圆 绘图状态的设置:文字颜色.线宽等 图形上下文状态的保存与恢复 图形上下文栈 Quartz 2D是一个二维绘图引擎,同时支持IOS和MAC ...