7.ReadWriteLock接口及其实现ReentrantReadWriteLock
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的更多相关文章
- Java并发ReadWriteLock接口
java.util.concurrent.locks.ReadWriteLock接口允许一次读取多个线程,但一次只能写入一个线程. 读锁 - 如果没有线程锁定ReadWriteLock进行写入,则多线 ...
- ReadWriteLock 接口详解
ReadWriteLock 接口详解 这是本人阅读ReadWriteLock接口源码的注释后,写出的一篇知识分享博客 读写锁的成分是什么? 读锁 Lock readLock(); 只要没有写锁,读锁可 ...
- Lock接口和ReadWriteLock接口
Lock接口 Lock接口在java.util.concurrent.locks包中,在jdk1.5之后才有. Lock接口有6个方法: void lock(); void lockInterrupt ...
- 从源码来看ReentrantLock和ReentrantReadWriteLock
上一篇花了点时间将同步器看了一下,心中对锁的概念更加明确了一点,知道我们所使用到的锁是怎么样获取同步状态的,我们也写了一个自定义同步组件Mutex,讲到了它其实就是一个简版的ReentrantLock ...
- 线程中的读写锁ReadWriteLock
Lock锁还有两个非常强大的类 ReadWriteLock接口实现类ReentrantReadWriteLock(非常重要的锁) 想实现 读取的时候允许多线程并发访问,写入的时候不允许. 这种效果.. ...
- 源码分析:ReentrantReadWriteLock之读写锁
简介 ReentrantReadWriteLock 从字面意思可以看出,是和重入.读写有关系的锁,实际上 ReentrantReadWriteLock 确实也是支持可重入的读写锁,并且支持公平和非公平 ...
- Synchronized和Lock接口
关于synchronized字段,不管该关键字是修饰方法还是修饰同步代码块,synchronzed拿到的都是对象. 当synchronized修饰的是方法时,synchronized所拿到的是调用该方 ...
- 【转】Java并发编程:Lock
阅读目录 一.synchronized的缺陷 二.java.util.concurrent.locks包下常用的类 三.锁的相关概念介绍 来自: http://www.importnew.com/18 ...
- Java并发编程:Lock
原文出处: 海子 在上一篇文章中我们讲到了如何使用关键字synchronized来实现同步访问.本文我们继续来探讨这个问题,从Java 5之后,在java.util.concurrent.locks包 ...
随机推荐
- 读《effective C++》1
条款一:视C++为一个语言联邦 学习C++半个月了,学了他的面向过程编程,面向对象编程(封装性,继承性,多态性),template泛型编程,开始只是觉得C++基础是面向对象,但是学了这么多块开始有点迷 ...
- C++小技巧之CONTAINING_RECORD
CONTAINING_RECORD Containing record是一个在C++编程中用处很大的一种技巧,它的功能为已知结构体或类的某一成员.对象中该成员的地址以及这一结构体名或类名,从而得到该对 ...
- Eclipse 安装反编译插件
前言:在实际的开发中几乎都会使用到一些框架来辅助项目的开发工作,对于一些框架的代码我们总怀有一些好奇之心,想一探究竟,有源码当然更好了,对于有些JAR包中的代码我们就需要利用反编译工具来看一下了,下面 ...
- UPS对电源故障的处理能力
UPS对电源故障的处理能力 双变换在线式UPS由于其逆变器实时在线工作,因而能对所有的电源故障具有隔离和处理功能.由于目前电网情况发生了很大变化,真正的长时间断电只占所有电源故障的30%甚至更低,而非 ...
- AngularJS的过滤器$filter
过滤器(filter)主要用于数据的格式上,通过某个规则,把值处理后返回结果.例如获得数据集,可排序后再返回. ng内置的共有九种过滤器: currency 货币 使用currency可以将数字格式化 ...
- 记一些让footer始终位于网页底部的方法
上次说把网页的头部和尾部分离出来作为一个单独的文件,所有网页共用,这样比较方便修改,然而,,,我发现某些方法里尾部会紧跟在头部后面,把内容挤在下面..而且有的页面内容少的话不能把尾部挤到最下面,所以, ...
- bzoj4810 [Ynoi2017]由乃的玉米田
Description 由乃在自己的农田边散步,她突然发现田里的一排玉米非常的不美.这排玉米一共有N株,它们的高度参差不齐. 由乃认为玉米田不美,所以她决定出个数据结构题 这个题是这样的: 给你一 ...
- ETL开发面试问题加吐槽加职业发展建议
写在前面: 作为甲方,对于乙方派来的开发人员,我是会自己面一下.总体来说遇到的水平不一,于是经过这三年多的面(cui)试(can),总结了一套自己的面试套路,中间也遇到过很多想吐槽的东西,于是大概记录 ...
- Linux下Tomcat进行远程调试
1.更改tomcat远程调试端口(可以使用默认端口不更改) 打开目录下的catalina.sh文件,找到JPDA_ADDRESS=”8000”,8000代表远程调试端口,可以更改成其他没有被占用的端口 ...
- Tomcat 热部署
Tomcat热部署就是实现不停机的发布项目和更新项目 1.修改conf目录下的tomcat-users.xml文件 在<tomcat-user>添加如下配置 [root@localhost ...