真实的多线程业务开发中,最常用到的逻辑就是数据的读写,ReentrantLock虽然具有完全互斥排他的效果(即同一时间只有一个线程正在执行lock后面的任务),

这样做虽然保证了实例变量的线程安全性,但效率却是非常低下的。所以在JDK中提供了一种读写锁ReentrantReadWriteLock类,使用它可以加快运行效率。

读写锁表示两个锁,一个是读操作相关的锁,称为共享锁;另一个是写操作相关的锁,称为排他锁

下面我们通过代码去验证下读写锁之间的互斥性

ReentrantReadWriteLock

读读共享

首先创建一个对象,分别定义一个加读锁方法和一个加写锁的方法,

public class MyDomain3 {

    private ReentrantReadWriteLock lock = new ReentrantReadWriteLock();

    public void testReadLock() {
try {
lock.readLock().lock();
System.out.println(System.currentTimeMillis() + " 获取读锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.readLock().unlock();
}
} public void testWriteLock() {
try {
lock.writeLock().lock();
System.out.println(System.currentTimeMillis() + " 获取写锁");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.writeLock().unlock();
}
} }

  创建线程类1 调用加读锁方法

public class Mythread3_1 extends Thread {

    private MyDomain3 myDomain3;

    public Mythread3_1(MyDomain3 myDomain3) {
this.myDomain3 = myDomain3;
} @Override
public void run() {
myDomain3.testReadLock();
}
}

  

@Test
public void test3() throws InterruptedException {
MyDomain3 myDomain3 = new MyDomain3();
Mythread3_1 readLock = new Mythread3_1(myDomain3);
Mythread3_1 readLock2 = new Mythread3_1(myDomain3);
readLock.start();
readLock2.start(); Thread.sleep(3000);
}

  执行结果:

1639621812838 获取读锁
1639621812839 获取读锁

  可以看出两个读锁几乎同时执行,说明读和读之间是共享的,因为读操作不会有线程安全问题。

写写互斥

创建线程类2,调用加写锁方法

public class Mythread3_2 extends Thread {

    private MyDomain3 myDomain3;

    public Mythread3_2(MyDomain3 myDomain3) {
this.myDomain3 = myDomain3;
} @Override
public void run() {
myDomain3.testWriteLock();
}
}
@Test
public void test3() throws InterruptedException {
MyDomain3 myDomain3 = new MyDomain3();
Mythread3_2 writeLock = new Mythread3_2(myDomain3);
Mythread3_2 writeLock2 = new Mythread3_2(myDomain3); writeLock.start();
writeLock2.start(); Thread.sleep(3000);
}

  执行结果:

1639622063226 获取写锁
1639622064226 获取写锁

  从时间上看,间隔是1000ms即1s,说明写锁和写锁之间互斥。

读写互斥

再用线程1和线程2分别调用读锁与写锁

@Test
public void test3() throws InterruptedException {
MyDomain3 myDomain3 = new MyDomain3();
Mythread3_1 readLock = new Mythread3_1(myDomain3);
Mythread3_2 writeLock = new Mythread3_2(myDomain3); readLock.start();
writeLock.start(); Thread.sleep(3000);
}

  执行结果:

1639622338402 获取读锁
1639622339402 获取写锁

  从时间上看,间隔是1000ms即1s,和代码里面是一致的,证明了读和写之间是互斥的。

注意一下,"读和写互斥"和"写和读互斥"是两种不同的场景,但是证明方式和结论是一致的,所以就不证明了。

最终测试结果下:

1、读和读之间不互斥,因为读操作不会有线程安全问题

2、写和写之间互斥,避免一个写操作影响另外一个写操作,引发线程安全问题

3、读和写之间互斥,避免读操作的时候写操作修改了内容,引发线程安全问题

总结起来就是,多个Thread可以同时进行读取操作,但是同一时刻只允许一个Thread进行写入操作

源码分析

读写锁中的Sync也是同样实现了AQS,回想ReentrantLock中自定义同步器的实现,同步状态表示锁被一个线程重复获取的次数,

而读写锁的自定义同步器需要在同步状态(一个整型变量)上维护多个读线程和一个写线程的状态,使得该状态的设计成为读写锁实现的关键。

读写锁将变量切分成了两个部分,高16位表示读,低16位表示写

当前同步状态表示一个线程已经获取了写锁,且重进入了两次,同时也连续获取了两次读锁。读写锁是如何迅速确定读和写各自的状态呢?

static final int SHARED_SHIFT   = 16;
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; /** Returns the number of shared holds represented in count */
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
/** Returns the number of exclusive holds represented in count */
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }

  其实是通过位运算。假设当前同步状态值为c,写状态等于c & EXCLUSIVE_MASK (c&0x0000FFFF(将高16位全部抹去)),

读状态等于c>>>16(无符号补0右移16位)。当写状态增加1时,等于c+1,当读状态增加1时,等于c+(1<<16),也就是c+0x00010000。

根据状态的划分能得出一个推论:c不等于0时,当写状态(c & 0x0000FFFF)等于0时,则读状态(c>>>16)大于0,即读锁已被获取。

写锁的获取与释放

  通过上面的测试,我们知道写锁是一个支持重入的排它锁,看下源码是如何实现写锁的获取

protected final boolean tryAcquire(int acquires) {
/*
* Walkthrough:
* 1. If read count nonzero or write count nonzero
* and owner is a different thread, fail.
* 2. If count would saturate, fail. (This can only
* happen if count is already nonzero.)
* 3. Otherwise, this thread is eligible for lock if
* it is either a reentrant acquire or
* queue policy allows it. If so, update state
* and set owner.
*/
Thread current = Thread.currentThread();
int c = getState();
int w = exclusiveCount(c);
if (c != 0) {
// (Note: if c != 0 and w == 0 then shared count != 0)
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// Reentrant acquire
setState(c + acquires);
return true;
}
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

  第3行到第11行,简单说了下整个方法的实现逻辑,这里要夸一下,这段注释就很容易的让人知道代码的功能。下面我们分析一下,

第13到第15行,分别拿到了当前线程对象current,lock的加锁状态值c 以及写锁的值w,c!=0 表明 当前处于有锁状态,

再继续分析第16行到25行,有个关键的Note:(Note: if c != 0 and w == 0 then shared count != 0):简单说就是:如果一个有锁状态但是没有写锁,那么肯定加了读锁。

第18行if条件,就是判断加了读锁,但是当前线程不是锁拥有的线程,那么获取锁失败,证明读写锁互斥。

第20行到第25行,走到这步,说明 w !=0 ,已经获取了写锁,只要不超过写锁最大值,那么增加写状态然后就可以成功获取写锁。

如果代码走到第26行,说明c==0,当前没有加任何锁,先执行 writerShouldBlock()方法,此方法用来判断写锁是否应该阻塞,

这块是对公平与非公平锁会有不同的逻辑,对于非公平锁,直接返回false,不需要阻塞,

下面是公平锁执行的判断

public final boolean hasQueuedPredecessors() {
// The correctness of this depends on head being initialized
// before tail and on head.next being accurate if the current
// thread is first in queue.
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}

  对于公平锁需要判断当前等待队列中是否存在 等于当前线程并且正在排队等待获取锁的线程。

写锁的释放与ReentrantLock的释放过程基本类似,每次释放均减少写状态,当写状态为0时表示写锁已被释放,

从而等待的读写线程能够继续访问读写锁,同时前次写线程的修改对后续读写线程可见。

读锁的获取与释放

读锁是一个支持重进入的共享锁,它能够被多个线程同时获取。JDK源码如下:

protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
int r = sharedCount(c);
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
}
return 1;
}
return fullTryAcquireShared(current);
}

  第4行到第6行,如果写锁被其他线程持有,则直接返回false,获取读锁失败,证明不同线程间写读互斥。

第8行,readerShouldBlock() 获取读锁是否应该阻塞,这儿也同样要区分公平锁和非公平锁,

公平锁模式需要判断当前等待队列中是否存在 等于当前线程并且正在排队等待获取锁的线程,存在则获取读锁需要等待。

非公平锁模式需要判断当前等待队列中第一个是等待写锁的,则方法返回true,获取读锁需要等待。

fullTryAcquireShared() 主要是处理读锁获取的完整版本,它处理tryAcquireShared()中没有处理的CAS错误和可重入读锁的处理逻辑。

参考文献

1:《Java并发编程的艺术》

2:《Java多线程编程核心技术》

java多线程7:ReentrantReadWriteLock的更多相关文章

  1. java多线程:ReentrantReadWriteLock读写锁使用

    Lock比传统的线程模型synchronized更多的面向对象的方式.锁和生活似,应该是一个对象.两个线程运行的代码片段要实现同步相互排斥的效果.它们必须用同一个Lock对象. 读写锁:分为读锁和写锁 ...

  2. 【Java多线程】ReentrantReadWriteLock

    概述 ReentrantReadWriteLock是Lock的另一种实现方式,ReentrantLock是一个排他锁,同一时间只允许一个线程访问,而ReentrantReadWriteLock允许多个 ...

  3. Java多线程系列--“JUC锁”08之 共享锁和ReentrantReadWriteLock

    概要 Java的JUC(java.util.concurrent)包中的锁包括"独占锁"和"共享锁".在“Java多线程系列--“JUC锁”02之 互斥锁Ree ...

  4. Java多线程(五) Lock接口,ReentranctLock,ReentrantReadWriteLock

    在JDK5里面,提供了一个Lock接口.该接口通过底层框架的形式为设计更面向对象.可更加细粒度控制线程代码.更灵活控制线程通信提供了基础.实现Lock接口且使用得比较多的是可重入锁(Reentrant ...

  5. 40个Java多线程问题总结

    前言 Java多线程分类中写了21篇多线程的文章,21篇文章的内容很多,个人认为,学习,内容越多.越杂的知识,越需要进行深刻的总结,这样才能记忆深刻,将知识变成自己的.这篇文章主要是对多线程的问题进行 ...

  6. Java多线程系列--“JUC锁”03之 公平锁(一)

    概要 本章对“公平锁”的获取锁机制进行介绍(本文的公平锁指的是互斥锁的公平锁),内容包括:基本概念ReentrantLock数据结构参考代码获取公平锁(基于JDK1.7.0_40)一. tryAcqu ...

  7. Java多线程系列--“JUC锁”04之 公平锁(二)

    概要 前面一章,我们学习了“公平锁”获取锁的详细流程:这里,我们再来看看“公平锁”释放锁的过程.内容包括:参考代码释放公平锁(基于JDK1.7.0_40) “公平锁”的获取过程请参考“Java多线程系 ...

  8. Java多线程系列--“JUC锁”10之 CyclicBarrier原理和示例

    概要 本章介绍JUC包中的CyclicBarrier锁.内容包括:CyclicBarrier简介CyclicBarrier数据结构CyclicBarrier源码分析(基于JDK1.7.0_40)Cyc ...

  9. Java多线程系列--“JUC锁”01之 框架

    本章,我们介绍锁的架构:后面的章节将会对它们逐个进行分析介绍.目录如下:01. Java多线程系列--“JUC锁”01之 框架02. Java多线程系列--“JUC锁”02之 互斥锁Reentrant ...

随机推荐

  1. 基于ambari搭建hadoop生态圈大数据组件

    Ambari介绍1Apache Ambari是一种基于Web的工具,支持Apache Hadoop集群的供应.管理和监控.Ambari已支持大多数Hadoop组件,包括HDFS.MapReduce.H ...

  2. dart系列之:元世界pubspec.yaml文件详解

    目录 简介 pubspec.yaml支持的字段 一个例子 字段详情 总结 简介 pubspec.yaml是所有dart项目的灵魂,它包含了所有dart项目的依赖信息和其他元信息,所以pubspec.y ...

  3. CTF入门学习3->Web通信基础

    Web安全基础 01 Web通信 这个部分重点介绍浏览器与Web服务器的详细通信过程. 01-00 URL协议 只要上网访问服务器,就离不开URL. URL是什么? URL就是我们在浏览器里输入的站点 ...

  4. Redis的ACID属性

    事务是数据库的一个重要属性,有关事务的4个特性,原子性.一致性.隔离性.持久性,也就是ACID,这些属性既包含了对事务执行结果的要求,也有数据库在事务执行前后的数据状态变化的要求. Redis可以完全 ...

  5. Web优化躬行记(5)——网站优化

    最近阅读了很多优秀的网站性能优化的文章,所以自己也想总结一些最近优化的手段和方法. 个人感觉性能优化的核心是:减少延迟,加速展现. 本文主要从产品设计.前端.后端和网络四个方面来诉说优化过程. 一.产 ...

  6. 关于uni-app导航栏中 中间大图标的设置

    在uni-app的开发过程中,想要将位于中间的图标设置更大,通过一般的更改导航栏图标大小方式实现不了.经过查看官方文档可以发现,我们可以通过midbutton来设置中间tabbar的样式. 但前提是, ...

  7. bilibili动画下载视频批量改名(python)

    bilib应用 在微软商店中下载哔哩哔哩动画,虽然软件UI古老,但是贵在稳定和支持下载 安装以后搜索自己想要的视频,然后缓存下载 下载后进入下载的路径 视频文件重命名 打开自动命令的程序或者py脚本, ...

  8. Linux环境下R和R包安装及其管理

    前言 R对windows使用很友好,对Linux来说充满了敌意.小数据可以在windows下交互操作,效果很好很棒.可是当我们要处理大数据,或者要在集群上搭建pipeline时,不得不面对在Linux ...

  9. 【Python小试】判断一条序列GC含量高低

    题目: 随便给定一条序列,如果GC含量超过65%,则认为高. 编程: from __future__ import division #整数除法 def is_gc_rich(dna): length ...

  10. do{...}while(0)的用法

    零.导引第一次见到 do{...}while(0)是在学习libevent的时候,看到里面有很多类似#define TT_URI(want) do { \ char *ret = evhttp_uri ...