读写锁,对于读操作来说是共享锁,对于写操作来说是排他锁,两种操作都可重入的一种锁。底层也是用AQS来实现的,我们来看一下它的结构跟代码:
-----------------------------------------------------------------------------------------------
读写锁,当然要区分读跟写两种操作,因此其内部有ReadLock跟WriteLock两种具体实现。但两者也有交互的地方,比如获取写锁要判断当前是否有线程在读,有的话就需要等待,因此内部是使用的同一个队列同步器。因为获取锁的时候,支持公平与非公平两种方式,故而同步器的包装类Sync也有两个:FairSync跟NonfairSync。
从写锁的获取开始:
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();monitor
int c = getState(); // 重入锁的计数
int w = exclusiveCount(c); // 计数高低位拆开为读计数跟写计数,计算写计数
if (c != 0) { // 有人在占有锁
if (w == 0 || current != getExclusiveOwnerThread()) // 写计数为0,只有读锁直接返回(避免了读锁升级为写锁) 或者 当前线程不是执行线程(执行线程可能读也可能写)也返回
return false;
if (w + exclusiveCount(acquires) > MAX_COUNT) //写锁重入次数 > 65525,抛出异常
throw new Error("Maximum lock count exceeded");
setState(c + acquires); //重入的写线程,直接设置状态(第6行代码没有return,说明当前线程是重入的写线程(写计数不是0,且current就是获取锁的线程))
return true;
}
if (writerShouldBlock() || !compareAndSetState(c, c + acquires)) //c!=0没有return,说明当前锁是空着的,所以cas抢占
return false;
setExclusiveOwnerThread(current); // 当前线程参数设置
return true;
}
  这里有个writerShouldBlock(),看一下这个是干啥的:
  追踪源码可以发现,在FairSync跟NonfairSync分别都有这个方法,nonfair中直接return false,fair中是调用的hasQueuedPredecessors(),该方法是用来判断是否有线程排队的,这个writerShouldBlock()就是字面上意思(读锁是否该阻塞(排队)),如果是非公平锁,直接compareAndSetState进行抢占,抢占不到则进入排队挂起,公平锁,则判断是否有排队的,有则自己进入排队,而不是先进行抢占。公平锁就像一个老实人,先排队,没人排队(自己是第一个)则自己抢座位(为啥要抢,因为可能这时候也来了一个认为自己是第一个的老实人),非公平锁就像土匪,管你排不排队,老子先抢一把座位试试,抢不到(被别的线程抢走了),被别人打脸了再去排队。综上,第13行的逻辑就是:对于写锁,当前锁没有被占用,如果是非公平方式获取,直接抢占,失败则直接返回,公平方式则查看是否有排队,有则获取失败直接返回,无则进行抢占。方法整体上,没获取到锁返回false,获取到了返回true,然后结合AQS的代码:

public final void acquire(int arg) {
if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) //没有获取到,就往等待队列添加节点,然后挂起线程
selfInterrupt();
}
  so,整个这个获取写锁的过程就是:查看有没有人占用锁,如果有读锁,则进入队列等待;有写锁,则看是否是自己的线程,是则重设state然后继续执行,不是则进入队列等待。tryAcquire一定要跟acquire联合起来看,否则难以理清整个流程。
  对了,还有个exclusiveCount(int c)用来拆分高低位的方法要说一下,因为读写锁要分别记录读跟写被重入的次数,按照一般设计,这分两个变量来计数就行了,但jdk就是jdk,它把这两个用一个int变量来记录了。方法么,就是高低位分开计算的,高16位表示读状态,低16位表示写状态。假设同步状态为s,则写状态为s & 0x0000FFFF,相当于高16位全部清0,同理,读状态则为s >>> 16,也就是右移,相当于把低16位都移除了。当然,复杂的就是读状态变化的时候,不是简单的s +1,这样的话就加到了写操作上,而是s + 0x00010000,把低位补上0就行了。类似的与操作、位移等等,jdk中有大量的应用,比如hashmap中确定元素所在链表等操作都有应用。
写锁释放代码:

protected final boolean tryRelease(int releases) {
if (!isHeldExclusively()) //没有写锁,抛异常
throw new IllegalMonitorStateException();
int nextc = getState() - releases;
boolean free = exclusiveCount(nextc) == 0; //次数是否清0了
if (free) //清0 了,说明完全释放了
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
  释放锁比较简单,不做赘述。
读锁获取:

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)) { // 若无需排队 && 读计数<65535(16位最大值) && 状态设置成功(读锁的整体计数就是在这里改的,注意加了一个默认值的操作)
if (r == 0) { //读锁为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++; //当前线程的读计数+1
}
return 1;
}
return fullTryAcquireShared(current); //第7行条件不满足,则for循环获取读锁(实际不会死循环的)
}
  读锁的获取比较复杂,这里主要有一个多线程各自计数的问题。对于读锁,除了要对全局的state中的读锁的计数进行修改,还要每个线程各自维护一份自己重入的次数计数,这个计数存在一个ThreadLocal(readHolds)中的一个对象(cachedHoldCounter)里边。读锁获取的逻辑是:没有写锁占用,则直接获取读锁,这就是第7行的逻辑(当然,可能跟其它读线程冲突导致获取失败,则进入fullTryAcquireShared(current));如果有写锁占用了呢,就调用fullTryAcquireShared(current)获取锁,看一下源码:

final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
for (;;) {
int c = getState();
if (exclusiveCount(c) != 0) { // 有写锁
if (getExclusiveOwnerThread() != current) // 写锁持有者不是当前线程,获取失败,通过aqs的doAcquireShared()进入排队
return -1; //这里只做了不是当前线程的判断,如果是当前线程,这个地方不能进行排队,因为若已有写线程在排队的话,就会造成死锁,源码中else一句的英文备注就是说这个
} else if (readerShouldBlock()) { //没写锁,但可能有写锁在等待读锁释放!!需要排队
// 写锁空闲 且 公平策略决定 线程应当被阻塞
// 下面的处理是说,如果是已获取读锁的线程重入读锁时, 即使公平策略指示应当阻塞也不会阻塞。
// 否则,这也会导致死锁的。
if (firstReader == current) { //
// assert firstReaderHoldCount > 0;
} else {
// threadlocal 相关处理
if (rh.count == 0) // 需要阻塞且是非重入(还未获取读锁的),获取失败
return -1;
}
}
if (sharedCount(c) == MAX_COUNT)
throw new Error("Maximum lock count exceeded");
if (compareAndSetState(c, c + SHARED_UNIT)) { // cas 抢锁成功
//threadlocal相关处理
return 1;
}
}
}
  读锁的释放:
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {// 清理firstReader缓存 或 readHolds里的重入计数
// assert firstReaderHoldCount > 0;
if (firstReaderHoldCount == 1)
firstReader = null;
else
firstReaderHoldCount--;
} 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; // 主要用于重入退出
}
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// 释放读锁对其他读线程没有任何影响,
// 但可以允许等待的写线程继续,如果读锁、写锁都空闲。
return nextc == 0;
}
}
-------------------------------------------------------------------------
  读写锁的源码,读起来比想象中的难度大得多,原因是读锁的部分设计比较复杂,主要涉及锁降级,以及几个变量跟threadlocal优化性能的处理。照着《java并发编程的艺术》跟一些博客看了2天,还是没把这部分完全理清楚,这个艰苦的工作以后继续搞吧。
  我们看一下关于锁的降级跟升级的问题,看是如何实现的:
  锁降级的定义:锁降级指的是写锁降级为读锁。如果当前线程持有写锁,然后将其释放,最后再获取读锁,这种分段完成的过程不能称之为锁降级。锁降级指的是把持住(当前拥有的)写锁,再获取到读锁,随后释放(先前拥有的)写锁的过程。----《java并发编程的艺术》
  这段话的理解是不难的,但要在读写锁的源码中去找到与之对应的逻辑是不太好找的。实际上:

if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
  这个就是与之相关的逻辑代码,在tryAcquireShared跟fullTryAcquireShared(Thread current) 中都有体现。
  上面的代码的意思是:当写锁被持有时,如果持有该锁的线程不是当前线程,就返回 “获取锁失败”,反之就会继续获取读锁。称之为锁降级。
  关于锁降级,书中还给出了一个例子:

public void processCachedData() {
readLock.lock();
if(!update){
//必须先释放读锁
readLock.unlock();
//锁降级从写锁获取到开始
writeLock.lock();
try{
if(!update){
//准备数据的流程(略)
update = true;
}
readLock.lock();
}finally {
writeLock.unlock();
}
//锁降级完成,写锁降级为读锁
}
try{
//使用数据的流程
}finally {
readLock.unlock();
}
}
  有一段文字说明:锁降级中的读锁获取是否必要呢?答案是必要的。主要是为了保证数据的可见性,如果当前线程不获取读锁而是直接释放写锁,假设另一个线程获取了写锁并修改了数据,那么当前线程无法感知该线程的数据更新。
  可是,,,这里有个疑问,另一个线程获取了写锁,你当前线程还能获取读锁吗?既然不能获取,何来无法感受数据更新一说?这个地方感觉有点问题。网上博文基本千篇一律也是说可见性如何的,跟书中观点一样,我觉得不对,比较赞同https://www.jianshu.com/p/cd485e16456e这个所说的。
  至于ThreadLocal相关代码,稍后再去理这里边的逻辑。

读写锁--ReentrantReadWriteLock的更多相关文章

  1. java 可重入读写锁 ReentrantReadWriteLock 详解

    详见:http://blog.yemou.net/article/query/info/tytfjhfascvhzxcyt206 读写锁 ReadWriteLock读写锁维护了一对相关的锁,一个用于只 ...

  2. [图解Java]读写锁ReentrantReadWriteLock

    图解ReentrantReadWriteLock 如果之前使用过读写锁, 那么可以直接看本篇文章. 如果之前未使用过, 那么请配合我的另一篇文章一起看:[源码分析]读写锁ReentrantReadWr ...

  3. 读写锁ReentrantReadWriteLock:读读共享,读写互斥,写写互斥

    介绍 DK1.5之后,提供了读写锁ReentrantReadWriteLock,读写锁维护了一对锁:一个读锁,一个写锁.通过分离读锁和写锁,使得并发性相比一般的排他锁有了很大提升.在读多写少的情况下, ...

  4. Java并发(十):读写锁ReentrantReadWriteLock

    先做总结: 1.为什么用读写锁 ReentrantReadWriteLock? 重入锁ReentrantLock是排他锁,在同一时刻仅有一个线程可以进行访问,但是在大多数场景下,大部分时间都是提供读服 ...

  5. 轻松掌握java读写锁(ReentrantReadWriteLock)的实现原理

    转载:https://blog.csdn.net/yanyan19880509/article/details/52435135 前言 前面介绍了java中排它锁,共享锁的底层实现机制,本篇再进一步, ...

  6. Java并发指南10:Java 读写锁 ReentrantReadWriteLock 源码分析

    Java 读写锁 ReentrantReadWriteLock 源码分析 转自:https://www.javadoop.com/post/reentrant-read-write-lock#toc5 ...

  7. 线程高级篇-读写锁ReentrantReadWriteLock

    转载原文:http://blog.csdn.net/john8169/article/details/53228016 读写锁: 分为读锁和写锁,多个读锁不互斥,读锁和写锁互斥,这是有JVM自己控制的 ...

  8. java并发之读写锁ReentrantReadWriteLock的使用

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

  9. [源码分析]读写锁ReentrantReadWriteLock

    一.简介 读写锁. 读锁之间是共享的. 写锁是独占的. 首先声明一点: 我在分析源码的时候, 把jdk源码复制出来进行中文的注释, 有时还进行编译调试什么的, 为了避免和jdk原生的类混淆, 我在类前 ...

  10. 读写锁ReentrantReadWriteLock的使用

    package com.thread.test.Lock; import java.util.Random; import java.util.concurrent.locks.Lock; impor ...

随机推荐

  1. Replication--镜像+复制

    场景:主服务器:Server1从服务器:Server2订阅服务器: Server3镜像DB: RepDB配置:1>配置SERVER3为分发服务器,在Server3上指定发布服务器SERVER1和 ...

  2. webUploader 的使用

    地址:http://fex.baidu.com/webuploader/demo.html 这个主要是扒的demo 引用<link href="~/Scripts/webuploade ...

  3. sh脚本

    通过. xxx.sh执行 经常在第一行加上#!/bin/bash,表示需要使用的bash shell环境 关于#!/bin/bash和#!/bin/sh 可以直接放linux命令 使用for循环 使用 ...

  4. Javascript:splice() 方法浅析

    定义和用法: splice()方法用于插入.删除或替换数组的元素. 注:该方法会改变原始数组,splice() 方法与 slice() 方法的作用是不同的,splice() 方法会直接对数组进行修改 ...

  5. 「HNOI 2015」落忆枫音

    题目链接 戳我 \(Description\) 给一张\(n\)割点\(m\)条边的\(DAG\),保证点\(1\)不存在入边,现在需要在\(DAG\)中加入一条不在原图中的边\((x,y)\),求这 ...

  6. day05.1-文件处理

    1. 文件处理流程 打开文件,得到文件句柄并赋值给一个变量: 通过句柄对文件进行操作: 关闭文件 with open("filename","r",encodi ...

  7. jvisualvm_使用jmx连接远程linux应用

    [前提] JVisualVM是由Sun提供的性能分析工具,在Jdk6.0以后的版本中是自带的,如果是用Jdk1.5或以前版本的就得要单独安装了. [1]远程机器需要开启jmx 在使用jvisualvm ...

  8. 【lojg152】 乘法逆元 2(数学)

    题面 传送门 题解 orz Wa自动机 这是一个可以\(O(n)\)求出\(n\)个数逆元的方案 先把所有的数做一个前缀积,记为\(s_i\) 然后我们用快速幂求出\(s_n\)的逆元,记为\(sv_ ...

  9. [转] Linux 中提高 VsFTP 服务器的安全性

    FTP是互联网应用中的一个元老级人物了,其方便企业用户文件的共享.但是,安全问题也一直伴随在FTP左右.如何防止攻击者通过非法手段窃取FTP服务器中的重要信息;如何防止攻击者利用FTP服务器来传播木马 ...

  10. Angular2 内置指令 NgFor 和 NgIf 详解

    http://www.jb51.net/article/89781.htm 在这一章节中,我们来学习如何使用Angular2来展示数据,以及如何使用它的内置指令NgFor和NgIf 首先要确保你有一个 ...