Java并发之ReentrantReadWriteLock
上篇文章简单的介绍了ReentrantLock可重入锁。事实上我们可以理解可重入锁是一种排他锁,排他锁在同一个时刻只能够由一个线程进行访问。这就与我们实际使用过程中有点不想符合了,比如说当我们进行读写文件操作的时候,我们可能允许多个线程进行读文件操作,而对写文件只需要控制一个线程既可以。在这种业务情况下如果使用排他锁,可能不太符合而且效率也可能有些低下。
一、ReentrantReadWriteLock介绍
读写锁维护了一对锁,一个读锁和一个写锁。通过分离读锁和写锁使得并发性相比一般的排他锁在性能上有更好的一些优势。
二、ReentrantReadWriteLock的特性
公平性选择:支持公平锁和非公平锁。
可重入性:支持可重入性。例读线程获取了读锁以后可以继续获取读锁。写线程获取写锁以后可以继续获取写锁。
锁降级:遵循获取读锁,获取写锁在释放写锁的次序。支持写锁降级为读锁。
三、接口和API
读写锁ReentrantReadWriteLock实现接口ReadWriteLock。接口ReadWriteLock实现了两个方法。
即readLock()方法和writeLock()方法。
1 public interface ReadWriteLock {
2 Lock readLock();
3 Lock writeLock();
4 }
ReentrantReadWriteLock定义如下:
1 /** 内部类 读锁 */
2 private final ReentrantReadWriteLock.ReadLock readerLock;
3 /** 内部类 写锁 */
4 private final ReentrantReadWriteLock.WriteLock writerLock;
5 final Sync sync;
6 /** 使用默认(非公平)的排序属性创建一个新的 ReentrantReadWriteLock */
7 public ReentrantReadWriteLock() {
8 this(false);
9 }
10 /** 使用给定的公平策略创建一个新的 ReentrantReadWriteLock */
11 public ReentrantReadWriteLock(boolean fair) {
12 sync = fair ? new FairSync() : new NonfairSync();
13 readerLock = new ReadLock(this);
14 writerLock = new WriteLock(this);
15 }
16 /** 返回用于写入操作的锁 */
17 public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
18 /** 返回用于读取操作的锁 */
19 public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
20 abstract static class Sync extends AbstractQueuedSynchronizer {
21
22 }
23 public static class WriteLock implements Lock, java.io.Serializable{
24
25 }
26 public static class ReadLock implements Lock, java.io.Serializable {
27
28 }
举例读写锁使用。使用读写锁将线程不安全的HashMap集合缓存变为线程安全的。
1 public class Cache {
2 static Map<String, Object> map = new HashMap<String, Object>(); static ReentrantReadWriteLock rwl = new ReentrantReadWriteLock(); static Lock r = rwl.readLock();
3 static Lock w = rwl.writeLock();
4 // 获取一个key对应的value
5 public static final Object get(String key) {
6 r.lock(); try {
7 return map.get(key);
8 } finally {
9 r.unlock();
10 }
11 }
12 // 设置key对应的value,并返回旧的value
13 public static final Object put(String key, Object value) {
14 w.lock(); try {
15 return map.put(key, value);
16 } finally {
17 w.unlock();
18 }
19 }
20
21 // 清空所有的内容
22 public static final void clear() {
23 w.lock(); try {
24 map.clear();
25 } finally {
26 w.unlock();
27 }
28 }
29 }
四、读写锁的实现简单分析
在ReentrantLock中使用一个int类型的state来表示同步状态,该值表示锁被一个线程重复获取的次数。但是读写锁ReentrantReadWriteLock内部维护着一对锁,需要用一个变量维护多种状态。所以读写锁采用“按位切割使用”的方式来维护这个变量,将其切分为两部分,高16为表示读,低16为表示写。
当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。那么读写锁是如何迅速确定读锁和写锁的状态呢?通过为运算。假如当前同步状态为S,那么写状态等于 S & 0x0000FFFF(将高16位全部抹去),读状态等于S >>> 16(无符号补0右移16位)。
4.1、写锁的获取。写锁是一个可支持重入的排他锁。写锁的获取最终会调用tryAcquire(int arg)。注意:如果存在读锁,则写锁不能被获取,原因在于:读写锁要确保写锁的操作对读锁可见,如果允许读锁在已被获取的情况下对写锁的获取,那么正在运行的其他读线程就无法感知到当 前写线程的操作。
源码中该方法为以下代码:
1 protected final boolean tryAcquire(int acquires) {
2 Thread current = Thread.currentThread();
3 //当前锁个数
4 int c = getState();
5 //写锁
6 int w = exclusiveCount(c);
7 if (c != 0) {
8 //c != 0 && w == 0 表示存在读锁
9 //当前线程不是已经获取写锁的线程
10 if (w == 0 || current != getExclusiveOwnerThread())
11 return false;
12 //超出最大范围
13 if (w + exclusiveCount(acquires) > MAX_COUNT)
14 throw new Error("Maximum lock count exceeded");
15 setState(c + acquires);
16 return true;
17 }
18 //是否需要阻塞
19 if (writerShouldBlock() ||
20 !compareAndSetState(c, c + acquires))
21 return false;
22 //设置获取锁的线程为当前线程
23 setExclusiveOwnerThread(current);
24 return true;
25 }
4.2、写锁的释放:当写锁中写锁的状态值为0的时候。从而等待的线程这个时候可以获取读写锁了。
4.3、读锁的获取:读锁是一个支持重进入的共享锁,它能够被多个线程同时获取,在没有其他写线程访问(或者写状态为0)时,读锁总会被成功地获取,而所做的也只是(线程安全的)增加读状态。如果当前线程已经获取了读锁,则增加读状态(依靠CAS保证线程安全)。如果当前线程在获取读锁时,写锁已被其他线程 获取,则进入等待状态。
4.4、读锁的释放:读锁的每次释放(线程安全的,可能有多个读线程同时释放读锁)均减少读状态,减少的值是(1<<16)。
五、锁降级
锁降级指的是写锁降级为读锁的情况。详细具体指的是当前拥有写锁的线程,在获取到读锁,然后在释放写锁的过程。注意:如果当前线程拥有写锁,然后将其释放,最后再获取读 锁,这种分段完成的过程不能称之为锁降级。
1 public void processData() {
2 readLock.lock();
3 if (!update) {
4 // 必须先释放读锁
5 readLock.unlock();
6 // 锁降级从写锁获取到开始
7 writeLock.lock();
8 try {
9 if (!update) {
10 // 准备数据的流程(略)
11 update = true;
12 }
13 readLock.lock();
14 } finally {
15 writeLock.unlock();
17 }
18 try {
19
20 }
21 // 锁降级完成,写锁降级为读锁
22 // 使用数据的流程(略)
23 } finally {
24 readLock.unlock();
25 }
26 }
锁降级中读锁的获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前A线程不获取读锁而是直接释放写锁,假设此刻另一个线程B获取了写锁并修改了数据,那么当前A线程无法感知线程B的数据更新。如果当前线程A获取读锁,即遵循锁降级的步骤,则线程T将会被阻塞,直到当前线程使用数据并释放读锁之后,线程T才能获取写锁进 行数据更新。
RentrantReadWriteLock不支持锁升级(把持读锁、获取写锁,最后释放读锁的过程)。目的也是保证数据可见性,如果读锁已被多个线程获取,其中任意线程成功获取了写锁并更新了 数据,则其更新对其他获取到读锁的线程是不可见的。
参考:
1、Java并发编程的艺术
2、Java并发编程网
Java并发之ReentrantReadWriteLock的更多相关文章
- Java并发之ReentrantReadWriteLock源码解析(一)
ReentrantReadWriteLock 前情提要:在学习本章前,需要先了解笔者先前讲解过的ReentrantLock源码解析和Semaphore源码解析,这两章介绍了很多方法都是本章的铺垫.下面 ...
- Java并发之ReentrantReadWriteLock源码解析(二)
先前,笔者和大家一起了解了ReentrantReadWriteLock的写锁实现,其实写锁本身实现的逻辑很少,基本上还是复用AQS内部的等待队列思想.下面,我们来看看ReentrantReadWrit ...
- java并发之固定对象与实例
java并发之固定对象与实例 Immutable Objects An object is considered immutable if its state cannot change after ...
- Java并发之BlockingQueue的使用
Java并发之BlockingQueue的使用 一.简介 前段时间看到有些朋友在网上发了一道面试题,题目的大意就是:有两个线程A,B, A线程每200ms就生成一个[0,100]之间的随机数, B线 ...
- 深入理解Java并发之synchronized实现原理
深入理解Java类型信息(Class对象)与反射机制 深入理解Java枚举类型(enum) 深入理解Java注解类型(@Annotation) 深入理解Java类加载器(ClassLoader) 深入 ...
- Java并发之Semaphore的使用
Java并发之Semaphore的使用 一.简介 今天突然发现,看着自己喜欢的球队发挥如此的棒,然后写着博客,这种感觉很爽.现在是半场时间,就趁着这个时间的空隙,说说Java并发包中另外一个重量级的类 ...
- Java并发之CyclicBarria的使用(二)
Java并发之CyclicBarria的使用(二) 一.简介 之前借助于其他大神写过一篇关于CyclicBarria用法的博文,但是内心总是感觉丝丝的愧疚,因为笔者喜欢原创,而不喜欢去转载一些其他的文 ...
- Java并发之CyclicBarria的使用
Java并发之CyclicBarria的使用 一.简介 笔者在写CountDownLatch这个类的时候,看到了博客园上的<浅析Java中CountDownLatch用法>这篇博文,为博主 ...
- Java并发之CountDownLatch的使用
Java并发之CountDownLatch的使用 一. 简介 Java的并发包早在JDK5这个版本中就已经推出,而且Java的并发编程是几乎每个Java程序员都无法绕开的屏障.笔者今晚在家闲来无事,翻 ...
随机推荐
- Mahout LDA 聚类
Mahout LDA 聚类 一.LDA简介 (一)主题模型 在主题模型中,主题表示一个概念.一个方面,表现为一系列相关的单词,是这些单词的条件概率.形象来说,主题就是一个桶,里面装了出现概率较高的 ...
- spring揭秘 读书笔记 二 BeanFactory的对象注册与依赖绑定
本文是王福强所著<<spring揭秘>>一书的读书笔记 我们前面就说过,Spring的IoC容器时一个IoC Service Provider,而且IoC Service Pr ...
- STL算法设计理念 - 函数对象和函数对象当参数和返回值
函数对象: 重载函数调用操作符的类,其对象常称为函数对象(function object),即它们是行为类似函数的对象.一个类对象,表现出一个函数的特征,就是通过"对象名+(参数列表)&qu ...
- Java进阶(十三)servlet监听器
servlet监听器 Listener是Servlet的监听器,它可以监听客户端的请求.服务端的操作等.通过监听器,可以自动激发一些操作,比如监听在线的用户的数量.当 增加一个HttpSession时 ...
- Android UI技巧(一)——Android中伸缩自如的9patch图片切法,没有美工自给自足
Android UI技巧(一)--Android中伸缩自如的点9图片切法,没有美工自给自足 相信大家对.9 图片应该都很熟悉吧,有些人可能自己都会了,此篇献给那些不会的同学,咱们一起来聊聊.9图片的切 ...
- 一键安装Android开发环境
一键安装Android开发环境 1 下载tadp-3.0r4-linux-x64.run 进入下面的地址下载: https://developer.nvidia.com/gameworksdownlo ...
- ubuntu12.04:Mysql数据库:自动安装
打开终端,输入下面命令: 1 sudo apt-get install mysql-server 2 sudo apt-get install mysql-client 一旦安装完成,MySQL 服务 ...
- erb自动生成html页面一例
原来的html蛮长的,源代码如下: <html> <head> <style type="text/css"> ul.none {list-st ...
- Java不走弯路教程(5.Client-Server模式(2)-Client)
5.Client-Server模式(2)-Client 在上一章,我们完成一个简单的数据库服务器,并在客户端用telnet方式成功进行通信. 本章将用Java实现客户端程序,来代替telnet. 先看 ...
- Nginx实现文件的上传和下载
文件的上传只要保证特殊的地址先到达Nginx,然后通过Nginx指定至指定的服务器即可,目前配置是本机.文件的下载的做法就是把本机的当前目录下面的文件给返回回去. server { ; server_ ...