一、读写锁简介
事实中有这样一种场景:对共享资源有读和写的操作,且写操作没有读操作那么频繁。在没有写操作的时候,多个线程同时读一个资源没有任何问题,所以应该容许多个线程同时读取共享资源;然而如果一个线程想去写这些共享资源,就不应该容许其余线程对该资源进行读和写的操作了。
针对这种场景,JAVA的并发包提供了读写锁ReentrantReadWriteLock,它示意两个锁,一个是读操作相干的锁,称为共享锁;一个是写相干的锁,称为排他锁,形容如下:
线程进入读锁的前提条件:
没有其余线程的写锁,
没有写申请或者有写申请,但调用线程和持有锁的线程是同一个。
线程进入写锁的前提条件:
没有其余线程的读锁
没有其余线程的写锁
而读写锁有以下三个重要的个性:
(1)偏心选择性:反对非偏心(默认)和偏心的锁获取形式,吞吐量还是非偏心优于偏心。
(2)重进入:读锁和写锁都反对线程重进入。
(3)锁降级:遵循获取写锁、获取读锁再开释写锁的秩序,写锁可能降级成为读锁。
二、源码解读
咱们先来看下 ReentrantReadWriteLock 类的整体构造:
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable { /** 读锁 */ private final ReentrantReadWriteLock.ReadLock readerLock; /** 写锁 */ private final ReentrantReadWriteLock.WriteLock writerLock; final Sync sync; /** 应用默认(非偏心)的排序属性创立一个新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock() { this(false); } /** 应用给定的偏心策略创立一个新的 ReentrantReadWriteLock */ public ReentrantReadWriteLock(boolean fair) { sync = fair ? new FairSync() : new NonfairSync(); readerLock = new ReadLock(this); writerLock = new WriteLock(this); } /** 返回用于写入操作的锁 */ public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; } /** 返回用于读取操作的锁 */ public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; } abstract static class Sync extends AbstractQueuedSynchronizer {} static final class NonfairSync extends Sync {} static final class FairSync extends Sync {} public static class ReadLock implements Lock, java.io.Serializable {} public static class WriteLock implements Lock, java.io.Serializable {}}
1、类的继承关系
public class ReentrantReadWriteLock implements ReadWriteLock, java.io.Serializable {}
阐明:能够看到,ReentrantReadWriteLock实现了ReadWriteLock接口,ReadWriteLock接口定义了获取读锁和写锁的标准,具体须要实现类去实现;同时其还实现了Serializable接口,示意能够进行序列化,在源代码中能够看到ReentrantReadWriteLock实现了本人的序列化逻辑。
2、类的外部类
ReentrantReadWriteLock有五个外部类,五个外部类之间也是互相关联的。外部类的关系如下图所示。
阐明:如上图所示,Sync继承自AQS、NonfairSync继承自Sync类、FairSync继承自Sync类(通过构造函数传入的布尔值决定要结构哪一种Sync实例);ReadLock实现了Lock接口、WriteLock也实现了Lock接口。
Sync类:
(1)类的继承关系
abstract static class Sync extends AbstractQueuedSynchronizer {}
阐明:Sync抽象类继承自AQS抽象类,Sync类提供了对ReentrantReadWriteLock的反对。
(2)类的外部类
Sync类外部存在两个外部类,别离为HoldCounter和ThreadLocalHoldCounter,其中HoldCounter次要与读锁配套应用,其中,HoldCounter源码如下。
// 计数器static final class HoldCounter { // 计数 int count = 0; // Use id, not reference, to avoid garbage retention // 获取以后线程的TID属性的值 final long tid = getThreadId(Thread.currentThread());}
阐明:HoldCounter次要有两个属性,count和tid,其中count示意某个读线程重入的次数,tid示意该线程的tid字段的值,该字段能够用来惟一标识一个线程。ThreadLocalHoldCounter的源码如下
// 本地线程计数器static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { // 重写初始化办法,在没有进行set的状况下,获取的都是该HoldCounter值 public HoldCounter initialValue() { return new HoldCounter(); }}
阐明:ThreadLocalHoldCounter重写了ThreadLocal的initialValue办法,ThreadLocal类能够将线程与对象相关联。在没有进行set的状况下,get到的均是initialValue办法外面生成的那个HolderCounter对象。
(3)类的属性
abstract static class Sync extends AbstractQueuedSynchronizer { // 版本序列号 private static final long serialVersionUID = 6317671515068378041L; // 高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; // 本地线程计数器 private transient ThreadLocalHoldCounter readHolds; // 缓存的计数器 private transient HoldCounter cachedHoldCounter; // 第一个读线程 private transient Thread firstReader = null; // 第一个读线程的计数 private transient int firstReaderHoldCount;}
阐明:该属性中包含了读锁、写锁线程的最大量。本地线程计数器等。
(4)类的构造函数
// 构造函数Sync() { // 本地线程计数器 readHolds = new ThreadLocalHoldCounter(); // 设置AQS的状态 setState(getState()); // ensures visibility of readHolds}
阐明:在Sync的构造函数中设置了本地线程计数器和AQS的状态state。
3、读写状态的设计
同步状态在重入锁的实现中是示意被同一个线程反复获取的次数,即一个整形变量来保护,然而之前的那个示意仅仅示意是否锁定,而不必辨别是读锁还是写锁。而读写锁须要在同步状态(一个整形变量)上保护多个读线程和一个写线程的状态。
读写锁对于同步状态的实现是在一个整形变量上通过“按位切割应用”:将变量切割成两局部,高16位示意读,低16位示意写。
假如以后同步状态值为S,get和set的操作如下:
(1)获取写状态:
S&0x0000FFFF:将高16位全副抹去
(2)获取读状态:
S>>>16:无符号补0,右移16位
(3)写状态加1:
S+1
(4)读状态加1:
S+(1<<16)即S + 0x00010000
在代码层的判断中,如果S不等于0,当写状态(S&0x0000FFFF),而读状态(S>>>16)大于0,则示意该读写锁的读锁已被获取。
4、写锁的获取与开释
看下WriteLock类中的lock和unlock办法:
public void lock() { sync.acquire(1);}public void unlock() { sync.release(1);}
能够看到就是调用的独占式同步状态的获取与开释,因而实在的实现就是Sync的 tryAcquire和 tryRelease。
写锁的获取,看下tryAcquire:
1 protected final boolean tryAcquire(int acquires) { 2 //以后线程 3 Thread current = Thread.currentThread(); 4 //获取状态 5 int c = getState(); 6 //写线程数量(即获取独占锁的重入数) 7 int w = exclusiveCount(c); 8 9 //以后同步状态state != 0,阐明曾经有其余线程获取了读锁或写锁10 if (c != 0) {11 // 以后state不为0,此时:如果写锁状态为0阐明读锁此时被占用返回false;12 // 如果写锁状态不为0且写锁没有被以后线程持有返回false13 if (w == 0 || current != getExclusiveOwnerThread())14 return false;15 16 //判断同一线程获取写锁是否超过最大次数(65535),反对可重入17 if (w + exclusiveCount(acquires) > MAX_COUNT)18 throw new Error("Maximum lock count exceeded");19 //更新状态20 //此时以后线程已持有写锁,当初是重入,所以只须要批改锁的数量即可。21 setState(c + acquires);22 return true;23 }24 25 //到这里阐明此时c=0,读锁和写锁都没有被获取26 //writerShouldBlock示意是否阻塞27 if (writerShouldBlock() ||28 !compareAndSetState(c, c + acquires))29 return false;30 31 //设置锁为以后线程所有32 setExclusiveOwnerThread(current);33 return true;34 }
其中exclusiveCount办法示意占有写锁的线程数量,源码如下:
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
阐明:间接将状态state和(2^16 - 1)做与运算,其等效于将state模上2^16。写锁数量由state的低十六位示意。
从源代码能够看出,获取写锁的步骤如下:
(1)首先获取c、w。c示意以后锁状态;w示意写线程数量。而后判断同步状态state是否为0。如果state!=0,阐明曾经有其余线程获取了读锁或写锁,执行(2);否则执行(5)。
(2)如果锁状态不为零(c != 0),而写锁的状态为0(w = 0),阐明读锁此时被其余线程占用,所以以后线程不能获取写锁,天然返回false。或者锁状态不为零,而写锁的状态也不为0,然而获取写锁的线程不是以后线程,则以后线程也不能获取写锁。
(3)判断以后线程获取写锁是否超过最大次数,若超过,抛异样,反之更新同步状态(此时以后线程已获取写锁,更新是线程平安的),返回true。
(4)如果state为0,此时读锁或写锁都没有被获取,判断是否须要阻塞(偏心和非偏心形式实现不同),在非偏心策略下总是不会被阻塞,在偏心策略下会进行判断(判断同步队列中是否有等待时间更长的线程,若存在,则须要被阻塞,否则,无需阻塞),如果不须要阻塞,则CAS更新同步状态,若CAS胜利则返回true,失败则阐明锁被别的线程抢去了,返回false。如果须要阻塞则也返回false。
(5)胜利获取写锁后,将以后线程设置为占有写锁的线程,返回true。
办法流程图如下:
写锁的开释,tryRelease办法:
1 protected final boolean tryRelease(int releases) { 2 //若锁的持有者不是以后线程,抛出异样 3 if (!isHeldExclusively()) 4 throw new IllegalMonitorStateException(); 5 //写锁的新线程数 6 int nextc = getState() - releases; 7 //如果独占模式重入数为0了,阐明独占模式被开释 8 boolean free = exclusiveCount(nextc) == 0; 9 if (free)10 //若写锁的新线程数为0,则将锁的持有者设置为null11 setExclusiveOwnerThread(null);12 //设置写锁的新线程数13 //不论独占模式是否被开释,更新独占重入数14 setState(nextc);15 return free;16 }
写锁的开释过程还是相对而言比较简单的:首先查看以后线程是否为写锁的持有者,如果不是抛出异样。而后查看开释后写锁的线程数是否为0,如果为0则示意写锁闲暇了,开释锁资源将锁的持有线程设置为null,否则开释仅仅只是一次重入锁而已,并不能将写锁的线程清空。
阐明:此办法用于开释写锁资源,首先会判断该线程是否为独占线程,若不为独占线程,则抛出异样,否则,计算开释资源后的写锁的数量,若为0,示意胜利开释,资源不将被占用,否则,示意资源还被占用。其办法流程图如下。
5、读锁的获取与开释
相似于写锁,读锁的lock和unlock的理论实现对应Sync的 tryAcquireShared 和 tryReleaseShared办法。
读锁的获取,看下tryAcquireShared办法
1 protected final int tryAcquireShared(int unused) { 2 // 获取以后线程 3 Thread current = Thread.currentThread(); 4 // 获取状态 5 int c = getState(); 6 7 //如果写锁线程数 != 0 ,且独占锁不是以后线程则返回失败,因为存在锁降级 8 if (exclusiveCount(c) != 0 && 9 getExclusiveOwnerThread() != current)10 return -1;11 // 读锁数量12 int r = sharedCount(c);13 /*14 * readerShouldBlock():读锁是否须要期待(偏心锁准则)15 * r < MAX_COUNT:持有线程小于最大数(65535)16 * compareAndSetState(c, c + SHARED_UNIT):设置读取锁状态17 */18 // 读线程是否应该被阻塞、并且小于最大值、并且比拟设置胜利19 if (!readerShouldBlock() &&20 r < MAX_COUNT &&21 compareAndSetState(c, c + SHARED_UNIT)) {22 //r == 0,示意第一个读锁线程,第一个读锁firstRead是不会退出到readHolds中23 if (r == 0) { // 读锁数量为024 // 设置第一个读线程25 firstReader = current;26 // 读线程占用的资源数为127 firstReaderHoldCount = 1;28 } else if (firstReader == current) { // 以后线程为第一个读线程,示意第一个读锁线程重入29 // 占用资源数加130 firstReaderHoldCount++;31 } else { // 读锁数量不为0并且不为以后线程32 // 获取计数器33 HoldCounter rh = cachedHoldCounter;34 // 计数器为空或者计数器的tid不为以后正在运行的线程的tid35 if (rh == null || rh.tid != getThreadId(current)) 36 // 获取以后线程对应的计数器37 cachedHoldCounter = rh = readHolds.get();38 else if (rh.count == 0) // 计数为039 //退出到readHolds中40 readHolds.set(rh);41 //计数+142 rh.count++;43 }44 return 1;45 }46 return fullTryAcquireShared(current);47 }
其中sharedCount办法示意占有读锁的线程数量,源码如下:
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
阐明:间接将state右移16位,就能够失去读锁的线程数量,因为state的高16位示意读锁,对应的第十六位示意写锁数量。
读锁获取锁的过程比写锁略微简单些,首先判断写锁是否为0并且以后线程不占有独占锁,间接返回;否则,判断读线程是否须要被阻塞并且读锁数量是否小于最大值并且比拟设置状态胜利,若以后没有读锁,则设置第一个读线程firstReader和firstReaderHoldCount;若以后线程线程为第一个读线程,则减少firstReaderHoldCount;否则,将设置以后线程对应的HoldCounter对象的值。流程图如下。
留神:更新胜利后会在firstReaderHoldCount中或readHolds(ThreadLocal类型的)的本线程正本中记录以后线程重入数(23行至43行代码),这是为了实现jdk1.6中退出的getReadHoldCount()办法的,这个办法能获取以后线程重入共享锁的次数(state中记录的是多个线程的总重入次数),退出了这个办法让代码简单了不少,然而其原理还是很简略的:如果以后只有一个线程的话,还不须要动用ThreadLocal,间接往firstReaderHoldCount这个成员变量里存重入数,当有第二个线程来的时候,就要动用ThreadLocal变量readHolds了,每个线程领有本人的正本,用来保留本人的重入数。
fullTryAcquireShared办法:
final int fullTryAcquireShared(Thread current) { HoldCounter rh = null; for (;;) { // 有限循环 // 获取状态 int c = getState(); if (exclusiveCount(c) != 0) { // 写线程数量不为0 if (getExclusiveOwnerThread() != current) // 不为以后线程 return -1; } else if (readerShouldBlock()) { // 写线程数量为0并且读线程被阻塞 // Make sure we're not acquiring read lock reentrantly if (firstReader == current) { // 以后线程为第一个读线程 // assert firstReaderHoldCount > 0; } else { // 以后线程不为第一个读线程 if (rh == null) { // 计数器不为空 // rh = cachedHoldCounter; if (rh == null || rh.tid != getThreadId(current)) { // 计数器为空或者计数器的tid不为以后正在运行的线程的tid 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"); if (compareAndSetState(c, c + SHARED_UNIT)) { // 比拟并且设置胜利 if (sharedCount(c) == 0) { // 读线程数量为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; } }}
阐明:在tryAcquireShared函数中,如果下列三个条件不满足(读线程是否应该被阻塞、小于最大值、比拟设置胜利)则会进行fullTryAcquireShared函数中,它用来保障相干操作能够胜利。其逻辑与tryAcquireShared逻辑相似,不再累赘。
读锁的开释,tryReleaseShared办法
1 protected final boolean tryReleaseShared(int unused) { 2 // 获取以后线程 3 Thread current = Thread.currentThread(); 4 if (firstReader == current) { // 以后线程为第一个读线程 5 // assert firstReaderHoldCount > 0; 6 if (firstReaderHoldCount == 1) // 读线程占用的资源数为1 7 firstReader = null; 8 else // 缩小占用的资源 9 firstReaderHoldCount--;10 } else { // 以后线程不为第一个读线程11 // 获取缓存的计数器12 HoldCounter rh = cachedHoldCounter;13 if (rh == null || rh.tid != getThreadId(current)) // 计数器为空或者计数器的tid不为以后正在运行的线程的tid14 // 获取以后线程对应的计数器15 rh = readHolds.get();16 // 获取计数17 int count = rh.count;18 if (count <= 1) { // 计数小于等于119 // 移除20 readHolds.remove();21 if (count <= 0) // 计数小于等于0,抛出异样22 throw unmatchedUnlockException();23 }24 // 缩小计数25 --rh.count;26 }27 for (;;) { // 有限循环28 // 获取状态29 int c = getState();30 // 获取状态31 int nextc = c - SHARED_UNIT;32 if (compareAndSetState(c, nextc)) // 比拟并进行设置33 // Releasing the read lock has no effect on readers,34 // but it may allow waiting writers to proceed if35 // both read and write locks are now free.36 return nextc == 0;37 }38 }
阐明:此办法示意读锁线程开释锁。首先判断以后线程是否为第一个读线程firstReader,若是,则判断第一个读线程占有的资源数firstReaderHoldCount是否为1,若是,则设置第一个读线程firstReader为空,否则,将第一个读线程占有的资源数firstReaderHoldCount减1;若以后线程不是第一个读线程,那么首先会获取缓存计数器(上一个读锁线程对应的计数器 ),若计数器为空或者tid不等于以后线程的tid值,则获取以后线程的计数器,如果计数器的计数count小于等于1,则移除以后线程对应的计数器,如果计数器的计数count小于等于0,则抛出异样,之后再缩小计数即可。无论何种状况,都会进入有限循环,该循环能够确保胜利设置状态state。其流程图如下。
在读锁的获取、开释过程中,总是会有一个对象存在着,同时该对象在获取线程获取读锁是+1,开释读锁时-1,该对象就是HoldCounter。
要明确HoldCounter就要先明确读锁。后面提过读锁的外在实现机制就是共享锁,对于共享锁其实咱们能够略微的认为它不是一个锁的概念,它更加像一个计数器的概念。一次共享锁操作就相当于一次计数器的操作,获取共享锁计数器+1,开释共享锁计数器-1。只有当线程获取共享锁后能力对共享锁进行开释、重入操作。所以HoldCounter的作用就是以后线程持有共享锁的数量,这个数量必须要与线程绑定在一起,否则操作其余线程锁就会抛出异样。
先看读锁获取锁的局部:
if (r == 0) {//r == 0,示意第一个读锁线程,第一个读锁firstRead是不会退出到readHolds中 firstReader = current; firstReaderHoldCount = 1;} else if (firstReader == current) {//第一个读锁线程重入 firstReaderHoldCount++; } else { //非firstReader计数 HoldCounter rh = cachedHoldCounter;//readHoldCounter缓存 //rh == null 或者 rh.tid != current.getId(),须要获取rh if (rh == null || rh.tid != current.getId()) cachedHoldCounter = rh = readHolds.get(); else if (rh.count == 0) readHolds.set(rh); //退出到readHolds中 rh.count++; //计数+1}
这里为什么要搞一个firstRead、firstReaderHoldCount呢?而不是间接应用else那段代码?这是为了一个效率问题,firstReader是不会放入到readHolds中的,如果读锁仅有一个的状况下就会防止查找readHolds。可能就看这个代码还不是很了解HoldCounter。咱们先看firstReader、firstReaderHoldCount的定义:
private transient Thread firstReader = null;private transient int firstReaderHoldCount;
这两个变量比较简单,一个示意线程,当然该线程是一个非凡的线程,一个是firstReader的重入计数。
HoldCounter的定义:
static final class HoldCounter { int count = 0; final long tid = Thread.currentThread().getId();}
在HoldCounter中仅有count和tid两个变量,其中count代表着计数器,tid是线程的id。然而如果要将一个对象和线程绑定起来仅记录tid必定不够的,而且HoldCounter基本不能起到绑定对象的作用,只是记录线程tid而已。
诚然,在java中,咱们晓得如果要将一个线程和对象绑定在一起只有ThreadLocal能力实现。所以如下:
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> { public HoldCounter initialValue() { return new HoldCounter(); }} ThreadLocalHoldCounter继承ThreadLocal,并且重写了initialValue办法。
故而,HoldCounter应该就是绑定线程上的一个计数器,而ThradLocalHoldCounter则是线程绑定的ThreadLocal。从下面咱们能够看到ThreadLocal将HoldCounter绑定到以后线程上,同时HoldCounter也持有线程Id,这样在开释锁的时候能力晓得ReadWriteLock外面缓存的上一个读取线程(cachedHoldCounter)是否是以后线程。这样做的益处是能够缩小ThreadLocal.get()的次数,因为这也是一个耗时操作。须要阐明的是这样HoldCounter绑定线程id而不绑定线程对象的起因是防止HoldCounter和ThreadLocal相互绑定而GC难以开释它们(只管GC可能智能的发现这种援用而回收它们,然而这须要肯定的代价),所以其实这样做只是为了帮忙GC疾速回收对象而已。
三、总结
通过下面的源码剖析,咱们能够发现一个景象:
在线程持有读锁的状况下,该线程不能获得写锁(因为获取写锁的时候,如果发现以后的读锁被占用,就马上获取失败,不论读锁是不是被以后线程持有)。
在线程持有写锁的状况下,该线程能够持续获取读锁(获取读锁时如果发现写锁被占用,只有写锁没有被以后线程占用的状况才会获取失败)。
认真想想,这个设计是正当的:因为当线程获取读锁的时候,可能有其余线程同时也在持有读锁,因而不能把获取读锁的线程“降级”为写锁;而对于取得写锁的线程,它肯定独占了读写锁,因而能够持续让它获取读锁,当它同时获取了写锁和读锁后,还能够先开释写锁持续持有读锁,这样一个写锁就“降级”为了读锁。
综上:
一个线程要想同时持有写锁和读锁,必须先获取写锁再获取读锁;写锁能够“降级”为读锁;读锁不能“降级”为写锁。
欢送关注公众号 【码农开花】一起学习成长
我会始终分享Java干货,也会分享收费的学习材料课程和面试宝典
回复:【计算机】【设计模式】【面试】有惊喜哦