前面我们看了可重入锁ReentrantLock,其实这个锁只适用于写多读少的情况,就是多个线程去修改一个数据的时候,适合用这个锁,但是如果多个线程都去读一个数据,还用这个锁的话会降低效率,因为同一时刻只能是一个线程去读取!

  本次我们看看读写锁ReentantReadWriteLock,这个锁采用了读写分离的策略,分成了读锁和写锁,多个线程可以同时获取读锁;

一.简单使用读写锁

  啥也别问,问就是先会用了再说,还记得前面用ReentrantLock实现了一个线程安全的List吗?我们可以使用读写锁稍微改造一下就好了;

package com.example.demo.study;

import java.util.ArrayList;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantReadWriteLock; public class Study0204 {
// 线程不安全的List
private ArrayList<String> list = new ArrayList<String>();
// 读写锁
private final ReentrantReadWriteLock lock = new ReentrantReadWriteLock();
// 获取读锁
private final Lock readLock = lock.readLock();
// 获取写锁
private final Lock writeLock = lock.writeLock(); // 往集合中添加元素,写锁
public void add(String str) {
writeLock.lock();
try {
list.add(str);
} finally {
writeLock.unlock();
}
} // 删除集合中的元素,写锁
public void remove(String str) {
writeLock.lock();
try {
list.remove(str);
} finally {
writeLock.unlock();
}
} // 根据索引获取集合中某个元素,读锁
public String get(int index) {
readLock.lock();
try {
return list.get(index);
} finally {
readLock.unlock();
}
} }

二.读写锁的结构

  这里最核心的还是用了AQS,可以看到里面还是有一个Sync这个内部工具类,然后还有两个内部工具类,一个是读锁ReadLock,一个是写锁WriteLock

  我们还能看到Sync这个类就是继承AQS,然后有NonfairSync和FairSync这两个类去继承Sync,到这里结构还是和ReentrantLock是一样的;

  我们再看看读锁和写锁,可以看出来就是实现了Lock这个接口,然后通过传进去的sync对象去实现Lock中的所有方法

  大概的结构就是这样的,我们可以使用下面这个图显示出来,其实ReentrantReadWriteLock最重要的就是三个类:

    一个是Sync工具类用于操作AQS阻塞队列和state的值,而且有基于Sync实现的公平策略和非公平策略;

    一个是写锁,实现了Lock接口,内部有个Sync字段,在Lock的实现方法中就是调用Sync对象的方法去实现的

    另外一个是读锁,和写锁一样,实现了Lock接口,内部有个Sync字段,在Lock的实现方法也都是调用Sync对象的方法实现

 二.分析Sync

  上篇博客中我们知道了在ReentrantLock中的state表示的是锁的可重入次数,而且state是AQS中定义的int类型,那么在读写锁这里有两个状态是怎么表示呢?

  总有一些人会想到一些花里胡哨的东西,还别说,挺管用的,由于state是int类型,共有32位,我们可以一分为二,前面的16位叫做高16位,表示获取读锁的次数,后面的叫做的低16位,表示写锁的可重入次数,具体的,我们可以看看Sync类的属性,主要是涉及到基本的二进制运算,有兴趣的可以研究一下;

abstract static class Sync extends AbstractQueuedSynchronizer {

    //这个可以说是读锁(共享锁)移动的位数
static final int SHARED_SHIFT = 16;
//读锁状态单位值,这里就是将1有符号左移16位,1用二进制表示为:00000000 00000000 00000000 00000001
//左移16位之后:00000000 00000001 00000000 00000000,也就是2的16次方,就是65536
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
//读锁的线程最大数65535个
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1; //写锁(排它锁)掩码,这里用二进制表示 00000000 00000000 11111111 11111111
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1; //返回读锁的个数,这里也就是将state无符号右移16位,那么有效数字肯定就是高16位,转成十进制后就是获取读锁的次数
static int sharedCount(int c) {
return c >>> SHARED_SHIFT;
} //返回写锁的个数,这里就是将state和上面的写锁掩码做按位与运算,高16位被置为0,有效数字位第16位,转成十进制就是写锁的可重入次数
static int exclusiveCount(int c) {
return c & EXCLUSIVE_MASK;
} //省略很多代码
}

  其中,Sync中还有几个比较重要的属性如下,不懂不要紧,后面用到了再回头看看就好;

 //记录第一个获取读锁的线程
private transient Thread firstReader = null; //记录第一个获取读锁的线程继续获取读锁的可重入次数
private transient int firstReaderHoldCount; //记录最后一个获取读锁的线程获取读锁的可重入次数,HoldCounter类如下
private transient HoldCounter cachedHoldCounter; static final class HoldCounter {
int count = 0;
final long tid = getThreadId(Thread.currentThread());
} //记录除去第一个获取读锁的线程外,获取的读锁的可重入次数,ThreadLocalHoldCounter类如下
private transient ThreadLocalHoldCounter readHolds; static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}

三.写锁的获取和释放

  写锁在获取的时候有一个前提:没有其他线程持有写锁或者读锁,当前线程才能获取写锁,否则就把当前线程丢到阻塞队列里去了,记住,不能一边读还一边写

  1.lock方法:

  写锁主要是ReentantReadWriteLock中的内部类WriteLock类实现的,这是一个独占锁,同一时刻只能有一个线程可以获取该锁,时刻重入的;

public void lock() {
sync.acquire(1);
} public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
} protected final boolean tryAcquire(int acquires) { Thread current = Thread.currentThread();
int c = getState();
//这个方法的是实现static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; },就是将state的低十六位转为十进制,
//也就是写锁的可重入次数
int w = exclusiveCount(c);
//state不为0,说明读锁或者写锁被占用了
if (c != 0) {
//如果w==0,而c!=0,说明c的高16为不为0,即有线程获取了读锁,此时写锁是不能获取的,注意,别人在读的时候是不能写入的呀!返回false
//如果w!=0表示有线程获取了写锁,但是占用锁的线程不是当前线程,那么线程获取写锁失败,返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
//到这里说明写锁可以获取成功,那么就要判断写锁的可重入次数是否大于65535
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//将state加一
setState(c + acquires);
return true;
}
//能到这里来,说明c==0,也就是说读锁和写锁都在空闲着,下面我们要看看公平策略下和非公平下的writerShouldBlock实现
if (writerShouldBlock() || !compareAndSetState(c, c + acquires))
return false;
setExclusiveOwnerThread(current);
return true;
}

  我们要看看最后的那里的if语句,其中writerShouldBlock的实现,能到这里说明读锁和写锁都在空闲这,可以随时去获取;

  非公平策略下始终返回的是false,于是会走到compareAndSetState(c, c + acquires),这里是用CAS尝试获取写锁,获取失败的话就返回发了;获取成功的话就走到setExclusiveOwnerThread(current);设置占用读锁的线程是当前线程;

  公平策略下的话,这个方法前面好像说过,就是判断当前线程节点前面有没有前驱节点,如果有的话那就肯定获取失败啊,要让前驱节点先获取,于是在上面最后的if那里直接返回false;如果这里判断没有前驱节点,这里返回true,那么上面就会走到最后setExclusiveOwnerThread(current)设置当前线程占有写锁

  2.tryLock方法

  这个方法和上面的lock方法一样,注意,这里最后那里默认使用的是非公平模式;

public boolean tryLock( ) {
return sync.tryWriteLock();
} final boolean tryWriteLock() {
Thread current = Thread.currentThread();
int c = getState();
if (c != 0) {
int w = exclusiveCount(c);
if (w == 0 || current != getExclusiveOwnerThread())
return false;
if (w == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
}
//这里默认使用的是非公平模式
if (!compareAndSetState(c, c + 1))
return false;
setExclusiveOwnerThread(current);
return true;
}

  3.unlock方法

public void unlock() {
sync.release(1);
}
//这个是AQS中的方法,说过tryRelease留给具体子类去实现的,重点看看怎么实现的
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
} protected final boolean tryRelease(int releases) {
//isHeldExclusively方法在下面,因为是当前线程调用的release方法,要判断当前线程是不是持有写锁的线程,不是的话就抛错了
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
//state减一
int nextc = getState() - releases;
//获取低十六位,看是不是等于0,如果等于0,说明此时没有线程占用写锁,于是就调用setExclusiveOwnerThread(null)
//将占用写锁的线程设置为null,最后更新state就行了
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
} protected final boolean isHeldExclusively() {
return getExclusiveOwnerThread() == Thread.currentThread();
}

四.读锁的获取和释放

  结合前面的写锁一起说一下:

    (1).没有其他线程持有写锁或者读锁,当前线程才能获取写锁,否则就把当前线程丢到阻塞队列里去了;当前线程获取了写锁之后,其他线程不能获取写锁和读锁;

    (2)没有其他线程获取写锁时,当前线程才能获取读锁,否则就丢到阻塞队列里去了,不能 一边读一边写;当前线程获取了读锁之后,其他线程只能获取读锁,不能获取写锁;

    (3)当前线程获取的写锁之后,还能继续获取写锁,这叫做可重入;也可以继续获取读锁,这叫做锁降级;

    (4)当前线程获取的读锁之后,还能继续获取读锁;

  1.lock方法

public void lock() {
//acquireShared方法在AQS中
sync.acquireShared(1);
} public final void acquireShared(int arg) {
//tryAcquireShared实现在ReentrantReadWriteLock中的Sync中
if (tryAcquireShared(arg) < 0)
//这个方法在AQS中,主要是将当前线程放到阻塞队列中
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) { Thread current = Thread.currentThread();
int c = getState();
//这里就是判断:如果其他线程获取了写锁,那么就返回-1
//先判断写锁的可重入次数不为0,表示有线程占用写锁,而且还不是当前线程,那么直接返回-1
//这里注意一下:一个线程在获取写锁之后,还可以再获取读锁,释放的时候两个所都要释放啊!!!
if (exclusiveCount(c) != 0 && getExclusiveOwnerThread() != current)
return -1;
//这个方法获取读锁的次数,读锁的次数最多只能是65535个
int r = sharedCount(c);
//readerShouldBlock方法分为公平策略和非公平策略,这个方法的意思就是:当前线程获取已经获取读锁,再读锁被阻塞了,那么说明还有其他线程正在获取写锁
//如果返回false,说明此时没有线程获取写锁,而且这个方法分为公平策略和非公平策略
//公平策略的话,如果当前线程节点有前驱节点就返回true,没有前驱节点返回false;
//非公平策略的话,判断阻塞队列中哨兵节点后面的那个节点是不是正在获取写锁,是的话返回true,不是的话返回false
//compareAndSetState(c, c + SHARED_UNIT)方法中,SHARED_UNIT表示65536,这个CAS表示对高16为增加1,对于整个32位来说,就是加2的16次方
if (!readerShouldBlock() && r < MAX_COUNT && compareAndSetState(c, c + SHARED_UNIT)) {
//r==0表示读锁空闲,于是记录第一个读锁,和第一个获取读锁的线程获取读锁的可重入次数
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//如果当前线程就是第一个获取读锁的线程,再获取读锁,这里就将可重入次数加一即可
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//能到这里就说明读锁已经被其他线程占用,当前线程是最后一个是最后获取读锁的线程,我们更新一下cacheHoldCounter和readHolds就行了
//cacheHoldCounter表示最后一个获取读锁的线程获取读锁的可重入次数
//readHolds记录了除了第一个获取读锁的线程外,其他线程获取读锁的可重入次数
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;
}
//能到这里说明readerShouldBlock方法返回的是true,而且当前线程在之前已经获取了写锁,再获取读锁,就是锁降级!!!
return fullTryAcquireShared(current);
} //锁降级操作
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
//写锁被其他线程占用,就返回-1
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
//获取读锁被阻塞,此时还有其他线程在获取写锁,
} else if (readerShouldBlock()) {
if (firstReader == current) {
}
else {
//有其他线程在尝试获取写锁,结束当前线程获取读锁,就更新一下readHolds就行了
//就从readHolds中移除当前线程的持有数,然后返回-1,结束尝试获取锁步骤
if (rh == null) {
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current)) {
rh = readHolds.get();
if (rh.count == 0)
readHolds.remove();
}
}
if (rh.count == 0)
return -1;
}
}
//读锁数量达到了最大数量就抛错
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//CAS更新读锁的数量,然后更新一些变量
if (compareAndSetState(c, c + SHARED_UNIT)) {
//读锁数量为0,就让当前线程作为第一个获取读锁的线程
if (sharedCount(c) == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//当前线程已经获取过读锁了,就把第一个获取读锁的可重入次数加一
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//这里前面说过了
if (rh == null)
rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
else if (rh.count == 0)
readHolds.set(rh);
rh.count++;
cachedHoldCounter = rh; // cache for release
}
return 1;
}
}
}

  2.tryLock方法

public boolean tryLock() {
return sync.tryReadLock();
} final boolean tryReadLock() {
Thread current = Thread.currentThread();
for (;;) {
int c = getState();
//如果当前写锁已经被占用,获取读锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return false;
int r = sharedCount(c);
//读锁不能超过最大数量
if (r == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
//更新state,将高16位加一,更新成功,如果读锁没有线程占有,就把当前线程更新为第一个获取读锁的线程和更新第一个获取读锁的可重入次数
if (compareAndSetState(c, c + SHARED_UNIT)) {
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
//当前线程就是第一个获取读锁的线程,就将可重入次数加一
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
//到这里说明当前线程获取读锁成功,虽然不是第一个获取读锁的线程,于是更新一下cachedHoldCounter和readHolds
//cachedHoldCounter:最后一个线程获取读锁的可重入次数
//readHolds:除去第一个线程,其他线程获取读锁的可重入次数
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 true;
}
}
}

  3.unlock方法

public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
//实现在下面,就是尝试释放读锁,并判断还有没有线程占用读锁,没有线程占用读锁,就会进入到if里面doReleaseShared方法
if (tryReleaseShared(arg)) {
//前面可能有一些线程在获取写锁的时候,由于当前线程读锁没有释放,所以那些线程就被阻塞了
//当前方法就是把那些线程释放一个
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
//如果当前线程是第一个获取读锁的线程
if (firstReader == current) {
//第一个线程获取读锁的可重入次数为1,就释放,否则,可重入次数减一
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
//当前线程不是第一个获取读锁的线程就更新cachedHoldCounter和readHolds
} else {
HoldCounter rh = cachedHoldCounter;
if (rh == null || rh.tid != getThreadId(current))
rh = readHolds.get();
int count = rh.count;
if (count <= 1) {
readHolds.remove();
if (count <= 0)
throw unmatchedUnlockException();
}
--rh.count;
}
//这里一个无限循环,获取state,将高十六位减一,用CAS更新,更新成功的话,就判断读锁有没有被占用
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}

  

五.总结

  我们用下面这个图来总结一下ReentrantReadWriteLock,这个锁就是利用state是32位的,高16位用于表示读锁的个数,低16位表示写锁的可重入次数,通过CAS对其进行了读写分离,适用于读多写少的场景;

简单看看读写锁ReentantReadWriteLock的更多相关文章

  1. 聊聊高并发(二十八)解析java.util.concurrent各个组件(十) 理解ReentrantReadWriteLock可重入读-写锁

    这篇讲讲ReentrantReadWriteLock可重入读写锁,它不仅是读写锁的实现,而且支持可重入性. 聊聊高并发(十五)实现一个简单的读-写锁(共享-排他锁) 这篇讲了怎样模拟一个读写锁. 可重 ...

  2. C#使用读写锁三行代码简单解决多线程并发写入文件时线程同步的问题

    (补充:初始化FileStream时使用包含文件共享属性(System.IO.FileShare)的构造函数比使用自定义线程锁更为安全和高效,更多内容可点击参阅) 在开发程序的过程中,难免少不了写入错 ...

  3. C# 防止同时调用=========使用读写锁三行代码简单解决多线程并发的问题

    http://www.jb51.net/article/99718.htm     本文主要介绍了C#使用读写锁三行代码简单解决多线程并发写入文件时提示"文件正在由另一进程使用,因此该进程无 ...

  4. ReentrantReadWriteLock读写锁简单原理案例证明

    ReentrantReadWriteLock存在原因? 我们知道List的实现类ArrayList,LinkedList都是非线程安全的,Vector类通过用synchronized修饰方法保证了Li ...

  5. 技术笔记:Delphi多线程应用读写锁

    在多线程应用中锁是一个很简单又很复杂的技术,之所以要用到锁是因为在多进程/线程环境下,一段代码可能会被同时访问到,如果这段代码涉及到了共享资源(数据)就需要保证数据的正确性.也就是所谓的线程安全.之前 ...

  6. java多线程-读写锁

    Java5 在 java.util.concurrent 包中已经包含了读写锁.尽管如此,我们还是应该了解其实现背后的原理. 读/写锁的 Java 实现(Read / Write Lock Java ...

  7. 让C#轻松实现读写锁分离

    ReaderWriterLockSlim 类 表示用于管理资源访问的锁定状态,可实现多线程读取或进行独占式写入访问. 使用 ReaderWriterLockSlim 来保护由多个线程读取但每次只采用一 ...

  8. C#读写锁ReaderWriterLockSlim的使用

    读写锁的概念很简单,允许多个线程同时获取读锁,但同一时间只允许一个线程获得写锁,因此也称作共享-独占锁.在C#中,推荐使用ReaderWriterLockSlim类来完成读写锁的功能. 某些场合下,对 ...

  9. 可重入锁 公平锁 读写锁、CLH队列、CLH队列锁、自旋锁、排队自旋锁、MCS锁、CLH锁

    1.可重入锁 如果锁具备可重入性,则称作为可重入锁. ========================================== (转)可重入和不可重入 2011-10-04 21:38 这 ...

随机推荐

  1. Spring注解之@Autowired、@Qualifier、@Resource、@Value

    前言 @Autowired.@Qualifier.@Resource.@Value四个注解都是用于注入数据的,他们的作用就和在xml配置文件中的bean标签中写一个标签的作用是一样的!本篇中特别要讲解 ...

  2. javaweb项目启动时自动启动rmi服务器实例

    1.我们先写一个web项目启动时需要运行的类RmiRegisterServlet.java package com.mx.Servlet; import javax.servlet.ServletCo ...

  3. mysql主从之配置验证

    实验环境: master  192.168.132.121 主库 slave     192.168.132.122 从库 一 mysql主从复制的配置 1.1 mysql主库给从库复制的权限 mys ...

  4. Analysis of Two-Channel Generalized Sidelobe Canceller (GSC) With Post-Filtering

    作者:凌逆战 地址:https://www.cnblogs.com/LXP-Never/p/12071748.html 题目:带后置滤波的双通道广义旁瓣相消器(GSC)的分析 作者:Israel Co ...

  5. 【题解】Comet OJ 国庆欢乐赛 简要题解

    [题解]Comet OJ 国庆欢乐赛 简要题解 A 直接做 B 直接做,结论: \[ ans=\max([Max\ge \mathrm{sum}] Max,s[n]/2) \] C 考虑这样一个做法: ...

  6. 【题解】CTS2019珍珠(二项式反演+卷积)

    [题解]CTS2019珍珠 题目就是要满足这样一个条件\(c_i\)代表出现次数 \[ \sum {[\dfrac {c_i } 2]} \ge 2m \] 显然\(\sum c_i=n\)所以,而且 ...

  7. $Poj1952\ $洛谷$1687\ Buy\ Low,Buy\ Lower$ 线性$DP+$方案计数

    Luogu Description 求一个长度为n的序列a的最长下降子序列的长度,以及这个长度的子序列种数,注意相同的几个子序列只能算作一个子序列. n<=5000,a[i]不超过long范围 ...

  8. 小小知识点(二十一)如何修改PPT母版上无法直接点击修改的文字

    1. 进入PPT后,选择下图右上角红色圈出的“视图”,接着选择下方红色圈出的“幻灯片母版”: 2.点击进入母版,如下图所示,最上面一栏第一个选项变成了“幻灯片母版”,在下面一栏最右边变成了“关闭母版视 ...

  9. Theia APIs——Preferences

    上一篇:Theia APIs——命令和快捷键 Preferences Theia有一个preference service,模块可以通过它来获取preference的值,提供默认的preference ...

  10. Netty 的基本简单实例【服务端-客户端通信】

    Netty是建立在NIO基础之上,Netty在NIO之上又提供了更高层次的抽象. 在Netty里面,Accept连接可以使用单独的线程池去处理,读写操作又是另外的线程池来处理. Accept连接和读写 ...