问题导读:
    1、Java高级特性Volatile如何理解?
    2、如何保证内存可见性?
    3、Java高级特性锁如何应用?
    4、如何进行重入性的实现原理?

    **

    Java高级特性增强-Volatile本部分网络上有大量的资源可以参考,在这里做了部分整理,感谢前辈的付出,每节文章末尾有引用列表,源码推荐看JDK1.8以后的版本,注意甄别~
    ####多线程###集合框架###NIO###Java并发容器

    volatile关键字volatile特性
    volatile就可以说是java虚拟机提供的最轻量级的同步机制。但它同时不容易被正确理解,也至于在并发编程中很多程序员遇到线程安全的问题就会使用synchronized。Java内存模型告诉我们,各个线程会将共享变量从主内存中拷贝到工作内存,然后执行引擎会基于工作内存中的数据进行操作处理。线程在工作内存进行操作后何时会写到主内存中?这个时机对普通变量是没有规定的,而针对volatile修饰的变量给java虚拟机特殊的约定,线程对volatile变量的修改会立刻被其他线程所感知,即不会出现数据脏读的现象,从而保证数据的“可见性”。通俗来说就是,线程A对一个volatile变量的修改,对于其它线程来说是可见的,即线程每次获取volatile变量的值都是最新的。
    volatile的实现原理
    在生成汇编代码时会在volatile修饰的共享变量进行写操作的时候会多出Lock前缀的指令。我们想这个Lock指令肯定有神奇的地方,那么Lock前缀的指令在多核处理器下会发现什么事情了?主要有这两个方面的影响:

    将当前处理器缓存行的数据写回系统内存;这个写回内存的操作会使得其他CPU里缓存了该内存地址的数据无效
    为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完不知道何时会写到内存。如果对声明了volatile的变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。因此,经过分析我们可以得出如下结论:

    Lock前缀的指令会引起处理器缓存写回内存;一个处理器的缓存回写到内存会导致其他处理器的缓存失效;当处理器发现本地缓存失效后,就会从内存中重读该变量数据,即可以获取当前最新值。
    这样针对volatile变量通过这样的机制就使得每个线程都能获得该变量的最新值。
    我们在项目中如何使用?
    1、状态标记量在高并发的场景中,通过一个boolean类型的变量isopen,控制代码是否走促销逻辑,该如何实现?

    1. public class ServerHandler {
    2. private volatile isopen;
    3. public void run() {
    4. if (isopen) {
    5. //isopen=true逻辑
    6. } else {
    7. //其他逻辑
    8. }
    9. }
    10. public void setIsopen(boolean isopen) {
    11. this.isopen = isopen
    12. }
    13. }

    场景细节无需过分纠结,这里只是举个例子说明volatile的使用方法,用户的请求线程执行run方法,如果需要开启促销活动,可以通过后台设置,具体实现可以发送一个请求,调用setIsopen方法并设置isopen为true,由于isopen是volatile修饰的,所以一经修改,其他线程都可以拿到isopen的最新值,用户请求就可以执行isopen=true的逻辑。

    2、double check单例模式的一种实现方式,但很多人会忽略volatile关键字,因为没有该关键字,程序也可以很好的运行,只不过代码的稳定性总不是100%,说不定在未来的某个时刻,隐藏的bug就出来了。

    1. class Singleton {
    2. private volatile static Singleton instance;
    3. public static Singleton getInstance() {
    4. if (instance == null) {
    5. syschronized(Singleton.class) {
    6. if (instance == null) {
    7. instance = new Singleton();
    8. }
    9. }
    10. }
    11. return instance;
    12. }
    13. }

    不过在众多单例模式的实现中,我比较推荐懒加载的优雅写法Initialization on Demand Holder(IODH)。

    1. public class Singleton {
    2. static class SingletonHolder {
    3. static Singleton instance = new Singleton();
    4. }
    5. public static Singleton getInstance(){
    6. return SingletonHolder.instance;
    7. }
    8. }

    如何保证内存可见性
    在java虚拟机的内存模型中,有主内存和工作内存的概念,每个线程对应一个工作内存,并共享主内存的数据,下面看看操作普通变量和volatile变量有什么不同:

    1、对于普通变量:读操作会优先读取工作内存的数据,如果工作内存中不存在,则从主内存中拷贝一份数据到工作内存中;写操作只会修改工作内存的副本数据,这种情况下,其它线程就无法读取变量的最新值。

    2、对于volatile变量,读操作时JMM会把工作内存中对应的值设为无效,要求线程从主内存中读取数据;写操作时JMM会把工作内存中对应的数据刷新到主内存中,这种情况下,其它线程就可以读取变量的最新值。volatile变量的内存可见性是基于内存屏障(Memory Barrier)实现的,什么是内存屏障?内存屏障,又称内存栅栏,是一个CPU指令。在程序运行时,为了提高执行性能,编译器和处理器会对指令进行重排序,JMM为了保证在不同的编译器和CPU上有相同的结果,通过插入特定类型的内存屏障来禁止特定类型的编译器重排序和处理器重排序,插入一条内存屏障会告诉编译器和CPU:不管什么指令都不能和这条Memory Barrier指令重排序。
    举例如下:

    1. class Singleton {
    2. private volatile static Singleton instance;
    3. private int a;
    4. private int b;
    5. private int b;
    6. public static Singleton getInstance() {
    7. if (instance == null) {
    8. syschronized(Singleton.class) {
    9. if (instance == null) {
    10. a = 1; // 1
    11. b = 2; // 2
    12. instance = new Singleton(); // 3
    13. c = a + b; // 4
    14. }
    15. }
    16. }
    17. return instance;
    18. }
    19. }

    1、如果变量instance没有volatile修饰,语句1、2、3可以随意的进行重排序执行,即指令执行过程可能是3214或1324。2、如果是volatile修饰的变量instance,会在语句3的前后各插入一个内存屏障。通过观察volatile变量和普通变量所生成的汇编代码可以发现,操作volatile变量会多出一个lock前缀指令:

    1. Java代码:
    2. instance = new Singleton();
    3. 汇编代码:
    4. 0x01a3de1d: movb $0x0,0x1104800(%esi);
    5. 0x01a3de24: **lock** addl $0x0,(%esp);

    这个lock前缀指令相当于上述的内存屏障,提供了以下保证:1、将当前CPU缓存行的数据写回到主内存;2、这个写回内存的操作会导致在其它CPU里缓存了该内存地址的数据无效。CPU为了提高处理性能,并不直接和内存进行通信,而是将内存的数据读取到内部缓存(L1,L2)再进行操作,但操作完并不能确定何时写回到内存,如果对volatile变量进行写操作,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,不过还是存在一个问题,就算内存的数据是最新的,其它CPU缓存的还是旧值,所以为了保证各个CPU的缓存一致性,每个CPU通过嗅探在总线上传播的数据来检查自己缓存的数据有效性,当发现自己缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会重新从内存中读取数据到缓存中。这也是我们之前讲的原理部分的解释~
    volatile的happens-before关系volatile变量可以通过缓存一致性协议保证每个线程都能获得最新值,即满足数据的“可见性”。我们继续延续上一篇分析问题的方式(我一直认为思考问题的方式是属于自己,也才是最重要的,也在不断培养这方面的能力),我一直将并发分析的切入点分为两个核心,三大性质。两大核心:JMM内存模型(主内存和工作内存)以及happens-before;三条性质:原子性,可见性,有序性(关于三大性质的总结在以后得文章会和大家共同探讨)。废话不多说,先来看两个核心之一:volatile的happens-before关系。在六条happens-before规则中有一条是:volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。下面我们结合具体的代码,我们利用这条规则推导下:

    1. public class VolatileExample {
    2. private int a = 0;
    3. private volatile boolean flag = false;
    4. public void writer(){
    5. a = 1; //1
    6. flag = true; //2
    7. }
    8. public void reader(){
    9. if(flag){ //3
    10. int i = a; //4
    11. }
    12. }
    13. }

    上面的实例代码对应的happens-before关系如下图所示:
    image.png

    加锁线程A先执行writer方法,然后线程B执行reader方法图中每一个箭头两个节点就代码一个happens-before关系,
    黑色的代表根据程序顺序规则推导出来,红色的是根据volatile变量的写happens-before于任意后续对volatile变量的读,
    而蓝色的就是根据传递性规则推导出来的。这里的2 happen-before 3,同样根据happens-before规则定义:
    如果A happens-before B,则A的执行结果对B可见,并且A的执行顺序先于B的执行顺序,
    我们可以知道操作2执行结果对操作3来说是可见的,也就是说当线程A将volatile变量flag更改为true后线程B就能够迅速感知。

    Java高级特性增强-锁本部分网络上有大量的资源可以参考,在这里做了部分整理,感谢前辈的付出,每节文章末尾有引用列表,源码推荐看JDK1.8以后的版本,注意甄别~####多线程###集合框架###NIO###Java并发容器

    锁Java中的锁分类在读很多并发文章中,会提及各种各样锁如公平锁,乐观锁等等,这篇文章介绍就是各种锁。介绍的内容如下:公平锁/非公平锁可重入锁独享锁/共享锁互斥锁/读写锁乐观锁/悲观锁分段锁偏向锁/轻量级锁/重量级锁自旋锁上面是很多锁的名词,这些分类并不是全是指锁的状态,有的指锁的特性,有的指锁的设计,下面总结的内容是对每个锁的名词进行一定的解释。

    公平锁/非公平锁

    公平锁是指多个线程按照申请锁的顺序来获取锁。非公平锁是指多个线程获取锁的顺序并不是按照申请锁的顺序,有可能后申请的线程比先申请的线程优先获取锁。有可能,会造成优先级反转或者饥饿现象。对于Java ReentrantLock而言,通过构造函数指定该锁是否是公平锁,默认是非公平锁。非公平锁的优点在于吞吐量比公平锁大。对于Synchronized而言,也是一种非公平锁。由于其并不像ReentrantLock是通过AQS的来实现线程调度,所以并没有任何办法使其变成公平锁。

    可重入锁

    可重入锁又名递归锁,是指在同一个线程在外层方法获取锁的时候,在进入内层方法会自动获取锁。说的有点抽象,下面会有一个代码的示例。对于Java ReentrantLock而言, 他的名字就可以看出是一个可重入锁,其名字是Re entrant Lock重新进入锁。对于Synchronized而言,也是一个可重入锁。可重入锁的一个好处是可一定程度避免死锁。

    1. synchronized void setA() throws Exception{
    2. Thread.sleep(1000);
    3. setB();
    4. }
    5. synchronized void setB() throws Exception{
    6. Thread.sleep(1000);
    7. }

    上面的代码就是一个可重入锁的一个特点,如果不是可重入锁的话,setB可能不会被当前线程执行,可能造成死锁。

    独享锁/共享锁

    独享锁是指该锁一次只能被一个线程所持有。共享锁是指该锁可被多个线程所持有。
    对于Java ReentrantLock而言,其是独享锁。但是对于Lock的另一个实现类ReadWriteLock,其读锁是共享锁,其写锁是独享锁。读锁的共享锁可保证并发读是非常高效的,读写,写读 ,写写的过程是互斥的。独享锁与共享锁也是通过AQS来实现的,通过实现不同的方法,来实现独享或者共享。对于Synchronized而言,当然是独享锁。

    互斥锁/读写锁

    上面讲的独享锁/共享锁就是一种广义的说法,互斥锁/读写锁就是具体的实现。互斥锁在Java中的具体实现就是ReentrantLock读写锁在Java中的具体实现就是ReadWriteLock

    乐观锁/悲观锁

    乐观锁与悲观锁不是指具体的什么类型的锁,而是指看待并发同步的角度。悲观锁认为对于同一个数据的并发操作,一定是会发生修改的,哪怕没有修改,也会认为修改。因此对于同一个数据的并发操作,悲观锁采取加锁的形式。悲观的认为,不加锁的并发操作一定会出问题。乐观锁则认为对于同一个数据的并发操作,是不会发生修改的。在更新数据的时候,会采用尝试更新,不断重新的方式更新数据。乐观的认为,不加锁的并发操作是没有事情的。
    从上面的描述我们可以看出,悲观锁适合写操作非常多的场景,乐观锁适合读操作非常多的场景,不加锁会带来大量的性能提升。悲观锁在Java中的使用,就是利用各种锁。乐观锁在Java中的使用,是无锁编程,常常采用的是CAS算法,典型的例子就是原子类,通过CAS自旋实现原子操作的更新。

    分段锁
    分段锁其实是一种锁的设计,并不是具体的一种锁,对于ConcurrentHashMap而言,其并发的实现就是通过分段锁的形式来实现高效的并发操作。我们以ConcurrentHashMap来说一下分段锁的含义以及设计思想,ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap(JDK7与JDK8中HashMap的实现)的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表;同时又是一个ReentrantLock(Segment继承了ReentrantLock)。当需要put元素的时候,并不是对整个hashmap进行加锁,而是先通过hashcode来知道他要放在那一个分段中,然后对这个分段进行加锁,所以当多线程put的时候,只要不是放在一个分段中,就实现了真正的并行的插入。但是,在统计size的时候,可就是获取hashmap全局信息的时候,就需要获取所有的分段锁才能统计。分段锁的设计目的是细化锁的粒度,当操作不需要更新整个数组的时候,就仅仅针对数组中的一项进行加锁操作。

    偏向锁/轻量级锁/重量级锁

    这三种锁是指锁的状态,并且是针对Synchronized。在Java 5通过引入锁升级的机制来实现高效Synchronized。这三种锁的状态是通过对象监视器在对象头中的字段来表明的。偏向锁是指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁。降低获取锁的代价。轻量级锁是指当锁是偏向锁的时候,被另一个线程所访问,偏向锁就会升级为轻量级锁,其他线程会通过自旋的形式尝试获取锁,不会阻塞,提高性能。重量级锁是指当锁为轻量级锁的时候,另一个线程虽然是自旋,但自旋不会一直持续下去,当自旋一定次数的时候,还没有获取到锁,就会进入阻塞,该锁膨胀为重量级锁。重量级锁会让其他申请的线程进入阻塞,性能降低。

    自旋锁
    在Java中,自旋锁是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式去尝试获取锁,这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

    Lock接口

    在Lock接口出现之前,Java程序是靠synchronized关键字实现锁功能的。JDK1.5之后并发包中新增了Lock接口以及相关实现类来实现锁功能。
    虽然synchronized方法和语句的范围机制使得使用监视器锁更容易编程,并且有助于避免涉及锁的许多常见编程错误,但是有时您需要以更灵活的方式处理锁。例如,用于遍历并发访问的数据结构的一些算法需要使用“手动”或“链锁定”:您获取节点A的锁定,然后获取节点B,然后释放A并获取C,然后释放B并获得D等。在这种场景中synchronized关键字就不那么容易实现了,使用Lock接口容易很多。
    Lock接口的实现类:ReentrantLock , ReentrantReadWriteLock.ReadLock , ReentrantReadWriteLock.WriteLock

    AbstractQueuedSynchronizer

    当你查看源码时你会惊讶的发现ReentrantLock并没有多少代码,另外有一个很明显的特点是:基本上所有的方法的实现实际上都是调用了其静态内存类Sync中的方法,而Sync类继承了AbstractQueuedSynchronizer(AQS)。可以看出要想理解ReentrantLock关键核心在于对队列同步器AbstractQueuedSynchronizer(简称同步器)的理解。

    在同步组件的实现中,AQS是核心部分,同步组件的实现者通过使用AQS提供的模板方法实现同步组件语义,AQS则实现了对同步状态的管理,以及对阻塞线程进行排队,等待通知等等一些底层的实现处理。AQS的核心也包括了这些方面:同步队列,独占式锁的获取和释放,共享锁的获取和释放以及可中断锁,超时等待锁获取这些特性的实现,而这些实际上则是AQS提供出来的模板方法,归纳整理如下:

    独占式锁:
    void acquire(int arg):独占式获取同步状态,如果获取失败则插入同步队列进行等待;void acquireInterruptibly(int arg):与acquire方法相同,但在同步队列中进行等待的时候可以检测中断;boolean tryAcquireNanos(int arg, long nanosTimeout):在acquireInterruptibly基础上增加了超时等待功能,在超时时间内没有获得同步状态返回false;boolean release(int arg):释放同步状态,该方法会唤醒在同步队列中的下一个节点

    共享式锁:
    void acquireShared(int arg):共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态void acquireSharedInterruptibly(int arg):在acquireShared方法基础上增加了能响应中断的功能boolean tryAcquireSharedNanos(int arg, long nanosTimeout):在acquireSharedInterruptibly基础上增加了超时等待的功能boolean releaseShared(int arg):共享式释放同步状态ReentrantLockReentrantLock重入锁,是实现Lock接口的一个类,也是在实际编程中使用频率很高的一个锁,支持重入性,表示能够对共享资源能够重复加锁,即当前线程获取该锁再次获取不会被阻塞。在java关键字synchronized隐式支持重入性,synchronized通过获取自增,释放自减的方式实现重入。与此同时,ReentrantLock还支持公平锁和非公平锁两种方式。那么,要想完完全全的弄懂ReentrantLock的话,主要也就是ReentrantLock同步语义的学习:
    1. 重入性的实现原理;
    2. 公平锁和非公平锁。
    重入性的实现原理要想支持重入性,就要解决两个问题:1. 在线程获取锁的时候,如果已经获取锁的线程是当前线程的话则直接再次获取成功;2. 由于锁会被获取n次,那么只有锁在被释放同样的n次之后,该锁才算是完全释放成功。通过这篇文章,我们知道,同步组件主要是通过重写AQS的几个protected方法来表达自己的同步语义。针对第一个问题,我们来看看ReentrantLock是怎样实现的,以非公平锁为例,判断当前线程能否获得锁为例,核心方法为nonfairTryAcquire:

    1. final boolean nonfairTryAcquire(int acquires) {
    2. final Thread current = Thread.currentThread();
    3. int c = getState();
    4. //1. 如果该锁未被任何线程占有,该锁能被当前线程获取
    5. if (c == 0) {
    6. if (compareAndSetState(0, acquires)) {
    7. setExclusiveOwnerThread(current);
    8. return true;
    9. }
    10. }
    11. //2.若被占有,检查占有线程是否是当前线程
    12. else if (current == getExclusiveOwnerThread()) {
    13. // 3. 再次获取,计数加一
    14. int nextc = c + acquires;
    15. if (nextc < 0) // overflow
    16. throw new Error("Maximum lock count exceeded");
    17. setState(nextc);
    18. return true;
    19. }
    20. return false;
    21. }

    这段代码的逻辑也很简单,具体请看注释。为了支持重入性,在第二步增加了处理逻辑,如果该锁已经被线程所占有了,会继续检查占有线程是否为当前线程,如果是的话,同步状态加1返回true,表示可以再次获取成功。每次重新获取都会对同步状态进行加一的操作,那么释放的时候处理思路是怎样的了?(依然还是以非公平锁为例)核心方法为tryRelease:

    1. protected final boolean tryRelease(int releases) {
    2. //1. 同步状态减1
    3. int c = getState() - releases;
    4. if (Thread.currentThread() != getExclusiveOwnerThread())
    5. throw new IllegalMonitorStateException();
    6. boolean free = false;
    7. if (c == 0) {
    8. //2. 只有当同步状态为0时,锁成功被释放,返回true
    9. free = true;
    10. setExclusiveOwnerThread(null);
    11. }
    12. // 3. 锁未被完全释放,返回false
    13. setState(c);
    14. return free;
    15. }

    代码的逻辑请看注释,需要注意的是,重入锁的释放必须得等到同步状态为0时锁才算成功释放,否则锁仍未释放。如果锁被获取n次,释放了n-1次,该锁未完全释放返回false,只有被释放n次才算成功释放,返回true。到现在我们可以理清ReentrantLock重入性的实现了,也就是理解了同步语义的第一条.

    公平锁与非公平锁
    ReentrantLock支持两种锁:公平锁和非公平锁。何谓公平性,是针对获取锁而言的,如果一个锁是公平的,那么锁的获取顺序就应该符合请求上的绝对时间顺序,满足FIFO。ReentrantLock的构造方法无参时是构造非公平锁,源码为:

    1. public ReentrantLock() {
    2. sync = new NonfairSync();
    3. }

    另外还提供了另外一种方式,可传入一个boolean值,true时为公平锁,false时为非公平锁,源码为:

    1. public ReentrantLock(boolean fair) {
    2. sync = fair ? new FairSync() : new NonfairSync();
    3. }

    在上面非公平锁获取时(nonfairTryAcquire方法)只是简单的获取了一下当前状态做了一些逻辑处理,并没有考虑到当前同步队列中线程等待的情况。我们来看看公平锁的处理逻辑是怎样的,核心方法为:

    1. protected final boolean tryAcquire(int acquires) {
    2. final Thread current = Thread.currentThread();
    3. int c = getState();
    4. if (c == 0) {
    5. if (!hasQueuedPredecessors() &&
    6. compareAndSetState(0, acquires)) {
    7. setExclusiveOwnerThread(current);
    8. return true;
    9. }
    10. }
    11. else if (current == getExclusiveOwnerThread()) {
    12. int nextc = c + acquires;
    13. if (nextc < 0)
    14. throw new Error("Maximum lock count exceeded");
    15. setState(nextc);
    16. return true;
    17. }
    18. return false;
    19. }
    20. }

    这段代码的逻辑与nonfairTryAcquire基本上一直,唯一的不同在于增加了hasQueuedPredecessors的逻辑判断,方法名就可知道该方法用来判断当前节点在同步队列中是否有前驱节点的判断,如果有前驱节点说明有线程比当前线程更早的请求资源,根据公平性,当前线程请求资源失败。如果当前节点没有前驱节点的话,再才有做后面的逻辑判断的必要性。公平锁每次都是从同步队列中的第一个节点获取到锁,而非公平性锁则不一定,有可能刚释放锁的线程能再次获取到锁。

    公平锁 VS 非公平锁 公平锁每次获取到锁为同步队列中的第一个节点,保证请求资源时间上的绝对顺序,而非公平锁有可能刚释放锁的线程下次继续获取该锁,则有可能导致其他线程永远无法获取到锁,造成“饥饿”现象。公平锁为了保证时间上的绝对顺序,需要频繁的上下文切换,而非公平锁会降低一定的上下文切换,降低性能开销。因此,ReentrantLock默认选择的是非公平锁,则是为了减少一部分上下文切换,保证了系统更大的吞吐量。 ReentrantReadWriteLock

    在并发场景中用于解决线程安全的问题,我们几乎会高频率的使用到独占式锁,通常使用java提供的关键字synchronized或者concurrents包中实现了Lock接口的ReentrantLock。它们都是独占式获取锁,也就是在同一时刻只有一个线程能够获取锁。而在一些业务场景中,大部分只是读数据,写数据很少,如果仅仅是读数据的话并不会影响数据正确性(出现脏读),而如果在这种业务场景下,依然使用独占锁的话,很显然这将是出现性能瓶颈的地方。针对这种读多写少的情况,java还提供了另外一个实现Lock接口的ReentrantReadWriteLock(读写锁)。读写所允许同一时刻被多个读线程访问,但是在写线程访问时,所有的读线程和其他的写线程都会被阻塞。在分析WirteLock和ReadLock的互斥性时可以按照WriteLock与WriteLock之间,WriteLock与ReadLock之间以及ReadLock与ReadLock之间进行分析。这里做一个归纳总结:
    公平性选择:支持非公平性(默认)和公平的锁获取方式,吞吐量还是非公平优于公平;重入性:支持重入,读锁获取后能再次获取,写锁获取之后能够再次获取写锁,同时也能够获取读锁;锁降级:遵循获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁

    要想能够彻底的理解读写锁必须能够理解这样几个问题:
    1. 读写锁是怎样实现分别记录读写状态的?
    2. 写锁是怎样获取和释放的?
    3.读锁是怎样获取和释放的?我们带着这样的三个问题,再去了解下读写锁。

    写锁详解写锁的获取
    同步组件的实现聚合了同步器(AQS),并通过重写重写同步器(AQS)中的方法实现同步组件的同步语义。因此,写锁的实现依然也是采用这种方式。在同一时刻写锁是不能被多个线程所获取,很显然写锁是独占式锁,而实现写锁的同步语义是通过重写AQS中的tryAcquire方法实现的。源码为:

    1. protected final boolean tryAcquire(int acquires) {
    2. /*
    3. * Walkthrough:
    4. * 1. If read count nonzero or write count nonzero
    5. * and owner is a different thread, fail.
    6. * 2. If count would saturate, fail. (This can only
    7. * happen if count is already nonzero.)
    8. * 3. Otherwise, this thread is eligible for lock if
    9. * it is either a reentrant acquire or
    10. * queue policy allows it. If so, update state
    11. * and set owner.
    12. */
    13. Thread current = Thread.currentThread();
    14. // 1. 获取写锁当前的同步状态
    15. int c = getState();
    16. // 2. 获取写锁获取的次数
    17. int w = exclusiveCount(c);
    18. if (c != 0) {
    19. // (Note: if c != 0 and w == 0 then shared count != 0)
    20. // 3.1 当读锁已被读线程获取或者当前线程不是已经获取写锁的线程的话
    21. // 当前线程获取写锁失败
    22. if (w == 0 || current != getExclusiveOwnerThread())
    23. return false;
    24. if (w + exclusiveCount(acquires) > MAX_COUNT)
    25. throw new Error("Maximum lock count exceeded");
    26. // Reentrant acquire
    27. // 3.2 当前线程获取写锁,支持可重复加锁
    28. setState(c + acquires);
    29. return true;
    30. }
    31. // 3.3 写锁未被任何线程获取,当前线程可获取写锁
    32. if (writerShouldBlock() ||
    33. !compareAndSetState(c, c + acquires))
    34. return false;
    35. setExclusiveOwnerThread(current);
    36. return true;
    37. }

    这段代码的逻辑请看注释,这里有一个地方需要重点关注,exclusiveCount(c)方法,该方法源码为:

    1. static int exclusiveCount(int c) {
    2. return c & EXCLUSIVE_MASK;
    3. }

    其中EXCLUSIVE_MASK为: static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
    EXCLUSIVE_MASK为1左移16位然后减1,即为0x0000FFFF。而exclusiveCount方法是将同步状态(state为int类型)与0x0000FFFF相与,即取同步状态的低16位。那么低16位代表什么呢?根据exclusiveCount方法的注释为独占式获取的次数即写锁被获取的次数,现在就可以得出来一个结论同步状态的低16位用来表示写锁的获取次数。同时还有一个方法值得我们注意:

    1. static int sharedCount(int c) { return c >>> SHARED_SHIFT; }

    该方法是获取读锁被获取的次数,是将同步状态(int c)右移16次,即取同步状态的高16位,现在我们可以得出另外一个结论同步状态的高16位用来表示读锁被获取的次数。现在还记得我们开篇说的需要弄懂的第一个问题吗?读写锁是怎样实现分别记录读锁和写锁的状态的,现在这个问题的答案就已经被我们弄清楚了,其示意图如下图所示:
    image.png
    现在我们回过头来看写锁获取方法tryAcquire,其主要逻辑为:当读锁已经被读线程获取或者写锁已经被其他写线程获取,则写锁获取失败;否则,获取成功并支持重入,增加写状态。

    写锁的释放
    写锁释放通过重写AQS的tryRelease方法,源码为:

    1. protected final boolean tryRelease(int releases) {
    2. if (!isHeldExclusively())
    3. throw new IllegalMonitorStateException();
    4. //1. 同步状态减去写状态
    5. int nextc = getState() - releases;
    6. //2. 当前写状态是否为0,为0则释放写锁
    7. boolean free = exclusiveCount(nextc) == 0;
    8. if (free)
    9. setExclusiveOwnerThread(null);
    10. //3. 不为0则更新同步状态
    11. setState(nextc);
    12. return free;
    13. }

    源码的实现逻辑请看注释,不难理解与ReentrantLock基本一致,这里需要注意的是,减少写状态int nextc = getState() - releases;只需要用当前同步状态直接减去写状态的原因正是我们刚才所说的写状态是由同步状态的低16位表示的。

    读锁详解读锁的获取
    看完了写锁,现在来看看读锁,读锁不是独占式锁,即同一时刻该锁可以被多个读线程获取也就是一种共享式锁。按照之前对AQS介绍,实现共享式同步组件的同步语义需要通过重写AQS的tryAcquireShared方法和tryReleaseShared方法。读锁的获取实现方法为:

    1. protected final int tryAcquireShared(int unused) {
    2. /*
    3. * Walkthrough:
    4. * 1. If write lock held by another thread, fail.
    5. * 2. Otherwise, this thread is eligible for
    6. * lock wrt state, so ask if it should block
    7. * because of queue policy. If not, try
    8. * to grant by CASing state and updating count.
    9. * Note that step does not check for reentrant
    10. * acquires, which is postponed to full version
    11. * to avoid having to check hold count in
    12. * the more typical non-reentrant case.
    13. * 3. If step 2 fails either because thread
    14. * apparently not eligible or CAS fails or count
    15. * saturated, chain to version with full retry loop.
    16. */
    17. Thread current = Thread.currentThread();
    18. int c = getState();
    19. //1. 如果写锁已经被获取并且获取写锁的线程不是当前线程的话,当前
    20. // 线程获取读锁失败返回-1
    21. if (exclusiveCount(c) != 0 &&
    22. getExclusiveOwnerThread() != current)
    23. return -1;
    24. int r = sharedCount(c);
    25. if (!readerShouldBlock() &&
    26. r < MAX_COUNT &&
    27. //2. 当前线程获取读锁
    28. compareAndSetState(c, c + SHARED_UNIT)) {
    29. //3. 下面的代码主要是新增的一些功能,比如getReadHoldCount()方法
    30. //返回当前获取读锁的次数
    31. if (r == 0) {
    32. firstReader = current;
    33. firstReaderHoldCount = 1;
    34. } else if (firstReader == current) {
    35. firstReaderHoldCount++;
    36. } else {
    37. HoldCounter rh = cachedHoldCounter;
    38. if (rh == null || rh.tid != getThreadId(current))
    39. cachedHoldCounter = rh = readHolds.get();
    40. else if (rh.count == 0)
    41. readHolds.set(rh);
    42. rh.count++;
    43. }
    44. return 1;
    45. }
    46. //4. 处理在第二步中CAS操作失败的自旋已经实现重入性
    47. return fullTryAcquireShared(current);
    48. }

    代码的逻辑请看注释,需要注意的是 当写锁被其他线程获取后,读锁获取失败,否则获取成功利用CAS更新同步状态。另外,当前同步状态需要加上SHARED_UNIT((1 << SHARED_SHIFT)即0x00010000)的原因这是我们在上面所说的同步状态的高16位用来表示读锁被获取的次数。如果CAS失败或者已经获取读锁的线程再次获取读锁时,是靠fullTryAcquireShared方法实现的,有兴趣可以看看。

    读锁的释放
    读锁释放的实现主要通过方法tryReleaseShared,源码如下,主要逻辑请看注释:

    1. protected final boolean tryReleaseShared(int unused) {
    2. Thread current = Thread.currentThread();
    3. // 前面还是为了实现getReadHoldCount等新功能
    4. if (firstReader == current) {
    5. // assert firstReaderHoldCount > 0;
    6. if (firstReaderHoldCount == 1)
    7. firstReader = null;
    8. else
    9. firstReaderHoldCount--;
    10. } else {
    11. HoldCounter rh = cachedHoldCounter;
    12. if (rh == null || rh.tid != getThreadId(current))
    13. rh = readHolds.get();
    14. int count = rh.count;
    15. if (count <= 1) {
    16. readHolds.remove();
    17. if (count <= 0)
    18. throw unmatchedUnlockException();
    19. }
    20. --rh.count;
    21. }
    22. for (;;) {
    23. int c = getState();
    24. // 读锁释放 将同步状态减去读状态即可
    25. int nextc = c - SHARED_UNIT;
    26. if (compareAndSetState(c, nextc))
    27. // Releasing the read lock has no effect on readers,
    28. // but it may allow waiting writers to proceed if
    29. // both read and write locks are now free.
    30. return nextc == 0;
    31. }
    32. }

    锁降级读写锁支持锁降级,遵循按照获取写锁,获取读锁再释放写锁的次序,写锁能够降级成为读锁,不支持锁升级,关于锁降级下面的示例代码摘自ReentrantWriteReadLock源码中:

    1. void processCachedData() {
    2. rwl.readLock().lock();
    3. if (!cacheValid) {
    4. // Must release read lock before acquiring write lock
    5. rwl.readLock().unlock();
    6. rwl.writeLock().lock();
    7. try {
    8. // Recheck state because another thread might have
    9. // acquired write lock and changed state before we did.
    10. if (!cacheValid) {
    11. data = ...
    12. cacheValid = true;
    13. }
    14. // Downgrade by acquiring read lock before releasing write lock
    15. rwl.readLock().lock();
    16. } finally {
    17. rwl.writeLock().unlock(); // Unlock write, still hold read
    18. }
    19. }
    20. try {
    21. use(data);
    22. } finally {
    23. rwl.readLock().unlock();
    24. }
    25. }
    26. }