1 Lock接口
1.1 Lock与synchronized
在Lock接口呈现之前,Java程序是靠synchronized关键字用来实现锁性能,应用时隐式地获取和开释锁,然而它将锁的获取和开释固化了。
所以,如果占有锁的线程因为要期待I/O或者其余起因(比方调用sleep办法)被阻塞了,其余线程就会只能始终期待,直到占有锁的线程开释掉锁,开释锁有以下几种状况:
(1)获取锁的线程执行完了该代码块,而后会主动开释锁。
(2)执行线程产生了异样,JVM会主动开释掉线程的锁。
(3)占有锁的线程进入 WAITING 状态从而开释锁,例如调用了wait()办法等。
这会极大影响程序执行效率。因而,须要有一种机制保障期待的线程不是始终处于无期限地期待的状态(解决方案:tryLock(long time, TimeUnit unit))/lockInterruptibly())。
应用synchronized的局限性还有:
- 当多个线程读写文件时,读操作与读操作之间不会发生冲突。但采纳synchronized关键字实现同步时,还是只能一个线程进行读操作,其余读线程只能期待锁的开释而无奈进行读操作。因而,须要一种机制来保障多线程都只是进行读操作时,线程之间不会发生冲突(解决方案:ReentrantReadWriteLock) 。
- synchronized无奈得悉线程是否胜利获取到锁 (解决方案:ReentrantLock)。
- … …
为补救synchronized应用的局限性,Java SE 5之后,并发包中新增了Lock、ReadWriteLock等接口(以及相干实现)。尽管它们短少了隐式获取开释锁的便捷性,然而却领有了锁获取与开释的可操作性、可中断的获取锁以及超时获取锁等多种synchronized关键字所不具备的同步个性。
1.2 Lock接口办法
Lock接口有6个办法:
// 获取锁
void lock()
// 如果以后线程未被中断,则获取锁,能够响应中断
void lockInterruptibly()
// 返回绑定到此 Lock 实例的新 Condition 实例
Condition newCondition()
// 仅在调用时锁为闲暇状态才获取该锁,能够响应中断
boolean tryLock()
// 如果锁在给定的等待时间内闲暇,并且以后线程未被中断,则获取锁
boolean tryLock(long time, TimeUnit unit)
// 开释锁
void unlock()
lock()、tryLock()、tryLock(long time, TimeUnit unit)和lockInterruptibly()
办法是用来获取锁的。unLock()
办法是用来开释锁的。
(1)lock() & unlock()
lock()
用来获取锁。如果锁已被其余线程获取,则进行期待。应用Lock,必须被动去开释锁,并且在产生异样时,不会主动开释锁。因而,一般来说,应用Lock必须在try()/catch()块中进行,并且将开释锁的操作放在finally块中进行,以保障锁肯定被被开释,避免死锁的产生。通常应用Lock来进行同步的话,是以上面这种模式去应用的:
Lock lock = ...;
lock.lock();// 获取锁
try{
//解决工作
}catch(Exception ex){
}finally{
lock.unlock(); //开释锁
}
(2)tryLock() & tryLock(long time, TimeUnit unit)
tryLock()
办法有返回值。它示意尝试获取锁,如果获取胜利,则返回true;如果获取失败(即锁已被其余线程获取),则返回false。值得注意的是,这个办法无论如何都会立刻返回(在拿不到锁时不会始终在那期待)。
tryLock(long time, TimeUnit unit)
办法和tryLock()
办法相似,区别在于tryLock(long time, TimeUnit unit)
在拿不到锁时会期待肯定的工夫,在工夫期限之内如果还拿不到锁,就返回false,同时能够响应中断。如果一开始拿到锁或者在期待期间内拿到了锁,则返回true。
个别状况下,通过tryLock()是这样应用的:
Lock lock = ...;
if(lock.tryLock()) {
try{
//解决工作
}catch(Exception ex){
}finally{
lock.unlock(); //开释锁
}
}else {
//如果不能获取锁,则间接做其余事件
}
(3)lockInterruptibly()
应用lockInterruptibly()
办法可能响应中断,即中断线程的期待状态。例如,当两个线程A、B同时通过lock.lockInterruptibly()
获取锁时,假若此时线程A获取到了锁,而线程B进入期待状态,那么线程B就可调用interrupt()
办法中断线程B的期待过程。(interrupt()
办法只能中断阻塞过程中的线程)
lockInterruptibly()
个别的应用模式如下:
public void method() throws InterruptedException {
lock.lockInterruptibly();
try {
//.....
}
finally {
lock.unlock();
}
}
Lock接口的实现类有:
2 ReentrantLock
ReentrantLock是Lock接口的次要实现类,ReentrantLock是可重入锁,顾名思义,就是反对重进入的锁,它示意该锁可能反对一个线程对资源的反复加锁。
它实现了 Lock 接口,外部类 Sync 是 AQS 的子类;Sync的两个子类 NonfairSync 和 FairSync 别离对应偏心锁和非偏心锁两种策略。(如果在相对工夫上,先对锁进行获取的申请肯定先被满足,那么这个锁是偏心的;反之,是不偏心的。)偏心的获取锁,也就是等待时间最长的线程最优先获取锁,也能够说锁获取是程序的。ReentrantLock提供了一个构造函数,可能管制锁是否是偏心的。
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
也就是说,调用ReentrantLock时,不传参数或者传入参数true
,即是偏心锁;传入参数false
,就是非偏心锁。(ReentrantLock默认采纳非偏心的策略)
2.1 可重入
可重入锁又名递归锁。可重入指的是任意线程在获取到锁之后可能再次获取该锁而不会被锁所阻塞(前提锁对象得是同一个对象或者class)。Java中ReentrantLock和synchronized都是可重入锁,可重入锁的一个长处是可肯定水平防止死锁。
例如:
public synchronized void fun1() {
System.out.println("办法1执行...");
fun2();
}
public synchronized void fun2() {
System.out.println("办法2执行...");
}
类中的两个办法都是被内置锁synchronized润饰的,fun1()办法中调用fun2()办法。因为内置锁是可重入的,所以同一个线程在调用fun2()时能够间接取得以后对象的锁,进入fun2()进行操作。
如果是一个不可重入锁,那么以后线程在调用fun2()之前须要将执行fun1()时获取以后对象的锁开释掉,实际上该对象锁已被以后线程所持有,且无奈开释,这种状况下会呈现死锁。
那么ReentrantLock是如何实现可重入的呢?上面以非偏心锁为例,剖析可重入实现原理。
首先查看NonfairSync办法:
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// 利用CAS尝试设置AQS的state为1。设置胜利,示意获取锁胜利;如果设置失败,示意state曾经>=1。
if (compareAndSetState(0, 1))
// 线程获取AQS锁胜利,须要设置AQS中的变量exclusiveOwnerThread为以后持有锁的线程,做保留记录
setExclusiveOwnerThread(Thread.currentThread());
else
// 调用acquire(),再次尝试或者线程进入期待队列。
acquire(1);
}
// 子类重写的tryAcquire办法
protected final boolean tryAcquire(int acquires) {
// 调用nonfairTryAcquire办法
return nonfairTryAcquire(acquires);
}
}
查看acquire办法:
public final void acquire(int arg) {
// 调用子类重写的tryAcquire办法,如果tryAcquire办法返回false,那么线程就会进入同步队列。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
查看nonfairTryAcquire办法:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();// 获取锁状态(0未加锁;1已加锁)
if (c == 0) {
if (compareAndSetState(0, acquires)) {// 间接CAS尝试获取锁,间接返回true,以后线程不会进入同步队列。
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) { // 如果以后线程已占用锁,再次获取锁
int nextc = c + acquires;// status+1(可重入性)
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);// 从新设置锁的状态
return true;
}
return false;
}
ReentrantLock继承父类AQS,重写了父类tryAcquire办法。其父类AQS中保护了一个同步状态status来计数重入次数,status初始值为0。
当线程尝试获取锁时,可重入锁先尝试获取并更新status值,如果status == 0示意没有其余线程在执行同步代码,则把status置为1,以后线程开始执行。如果status != 0,则判断以后线程是否是获取到这个锁的线程,如果是的话执行status+1,且以后线程能够再次获取锁。
开释锁时,调用tryRelease()办法:
protected final boolean tryRelease(int releases) {
int c = getState() - releases;// 开释锁时status-1
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();// 先判断以后线程是否已是占用锁的线程
boolean free = false;
if (c == 0) {// 只有status=0时才开释锁。
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
开释锁时,可重入锁同样先获取以后status的值,在以后线程是持有锁的线程的前提下。如果status-1 == 0,则示意以后线程所有反复获取锁的操作都曾经执行结束,而后该线程才会真正开释锁。如果该锁被获取了n次,那么前(n-1)次tryRelease(int releases)办法必然返回false,而只有同步状态齐全开释了,能力返回true。
2.2 偏心/非偏心
偏心锁是指多个线程依照申请锁的程序来获取锁,线程间接进入队列中排队,队列中的第一个线程能力取得锁(不可插队,等待时间越长,申请锁时会被优先满足)。偏心锁的长处是期待锁的线程不会饥饿。毛病是整体吞吐效率绝对非偏心锁要低,期待队列中除第一个线程以外的所有线程都会阻塞,CPU唤醒阻塞线程的开销比非偏心锁大。
非偏心锁是多个线程加锁时间接尝试获取锁,获取不到才会到期待队列的队尾期待(可插队的)。但如果此时锁刚好可用,那么这个线程能够无需阻塞间接获取到锁,所以非偏心锁有可能呈现后申请锁的线程先获取锁的场景。非偏心锁的长处是能够缩小唤起线程的开销,整体的吞吐效率高,因为线程有几率不阻塞间接取得锁,CPU不用唤醒所有线程。毛病是处于期待队列中的线程可能会饥饿,或者等很久才会取得锁。
Tips:如果一个过程被屡次回滚,迟迟不能占用必须的系统资源,可能会导致过程饥饿
导致线程饥饿常见起因:
- 高优先级线程吞噬所有的低优先级线程的CPU工夫。
- 线程被永恒梗塞在一个期待进入同步快的状态。
- 期待的线程永远不被唤醒。
查看偏心加锁办法的源码:
protected final boolean tryAcquire(int acquires) {
// 获取以后的线程
final Thread current = Thread.currentThread();
// 获取锁的状态
int c = getState();
if (c == 0) {
// hasQueuedPredecessors 判断队列还有没有其它node
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
// 设置获取锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 设置获取锁的线程
else if (current == getExclusiveOwnerThread()) {
// 获取过了就累加,因为能够重入
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 从新设置锁的状态
setState(nextc);
return true;
}
return false;
}
非偏心加锁办法的源码:
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
unlock()
开释锁,其实就是把state从n(可能产生了锁的重入,须要屡次开释)变成0,此办法不辨别偏心与否。
偏心锁与非偏心锁解锁办法的源码:
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//子类重写的tryRelease办法,须要等锁的state=0,即tryRelease返回true的时候,才会去唤醒其它线程进行尝试获取锁。
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 获取锁的状态
int c = getState() - releases;
// 判断锁的所有者是不是该线程
if (Thread.currentThread() != getExclusiveOwnerThread())
// 如果所的所有者不是该线程 则抛出异样 也就是锁开释的前提是线程领有这个锁
throw new IllegalMonitorStateException();
boolean free = false;
// 直到锁的状态是0,阐明锁开释胜利。即锁没有重入,那么间接将将锁的所有者设置成null
// 咱们在一个线程外面调用几次lock,就要调用几次unlock,能力最终开释锁
if (c == 0) {
free = true;
// 开释线程的拥有者
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
可见,平锁的开释和非偏心锁的开释一样的。偏心锁与非偏心锁的lock()办法惟一的区别就在于偏心锁在获取同步状态时多了一个限度条件:hasQueuedPredecessors()。
public final boolean hasQueuedPredecessors() {
// 判断以后线程是否位于同步队列中的第一个。如果是则返回true,否则返回false。
Node t = tail;
Node h = head;
Node s;
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
由此可见,偏心锁就是通过同步队列来实现多个线程依照申请锁的程序来获取锁,从而实现偏心的个性。非偏心锁加锁时不思考排队期待问题,间接尝试获取锁,所以才会存在线程后申请却先取得锁的状况。
2.3 小结
ReentrantLock重入锁执行流程:
2.4 中断与超时期待
(1)lockInterruptibly可中断形式获取锁
lockInterruptibly()
反对中断的获取锁,其实是调用了AQS的lockInterruptibly办法。
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
最终调用AQS的doAcquireInterruptibly(int arg)办法:
public final void acquireInterruptibly(int arg)
// 以后线程曾经中断了,抛出异样。
if (Thread.interrupted())
throw new InterruptedException();
// 以后线程依然未胜利获取锁,则调用doAcquireInterruptibly办法,这个办法和
// acquireQueued办法没什么区别,就是线程在期待状态的过程中,如果线程被中断,线程会抛出异样。
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
(2)tryLock超时期待形式获取锁
tryLock(long timeout, TimeUnit unit)
也反对中断,并且在这个根底上减少了超时设置,其实也是调用了AQS的tryAcquireNanos办法。
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
最终调用AQS的doAcquireNanos(int arg, long nanosTimeout)办法:
public final boolean tryAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
// 如果以后线程曾经中断,则抛出异样
if (Thread.interrupted())
throw new InterruptedException();
//再尝试获取一次,如果不胜利则调用doAcquireNanos办法进行超时期待获取锁。
return tryAcquire(arg) ||
doAcquireNanos(arg, nanosTimeout);
}
查看tryAcquireNanos办法:
private boolean doAcquireNanos(int arg, long nanosTimeout)
throws InterruptedException {
if (nanosTimeout <= 0L)
return false;
// 计算超时的工夫=以后虚拟机的工夫+设置的超时工夫
final long deadline = System.nanoTime() + nanosTimeout;
// 调用addWaiter将以后线程封装成独占模式的节点,并且退出到同步队列尾部。
final Node node = addWaiter(Node.EXCLUSIVE);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
// 如果以后节点的前驱节点为头结点,则让以后节点去尝试获取锁。
setHead(node);
p.next = null; // help GC
failed = false;
return true;
}
// 如果以后节点的前驱节点不是头结点,或以后节点获取锁失败,
// 则再次判断以后线程是否曾经超时。
nanosTimeout = deadline - System.nanoTime();
if (nanosTimeout <= 0L)
return false;
// 调用shouldParkAfterFailedAcquire办法,通知以后节点的前驱节点,马上进入
// 期待状态了,即做好进入期待状态前的筹备。
if (shouldParkAfterFailedAcquire(p, node) &&
nanosTimeout > spinForTimeoutThreshold)
// 调用LockSupport.parkNanos办法,将以后线程设置成超时期待的状态。
LockSupport.parkNanos(this, nanosTimeout);
if (Thread.interrupted())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
3 ReadWriteLock
之前提到锁(如Mutex和ReentrantLock)根本都是排他锁,这些锁在同一时刻只容许一个线程进行拜访。而读写锁(ReadWriteLock)在同一时刻能够容许多个读线程拜访,然而在写线程拜访时,所有的读线程和其余写线程均被阻塞。
ReadWriteLock保护了一组锁,一个是只读的锁,一个是写锁。读锁能够在没有写锁的时候被多个线程同时持有,写锁是独占的。
如何用一个共享变量来辨别锁是写锁还是读锁呢?答案就是按位拆分
。
因为state是int类型的变量,在内存中占用4个字节,也就是32位
。将其拆分为两局部:高16位和低16位,其中高16位用来示意读锁状态,低16位用来示意写锁状态
。当设置读锁胜利时,就将高16位加1,开释读锁时,将高16位减1;当设置写锁胜利时,就将低16位加1,开释写锁时,将第16位减1。
ReadWriteLock 接口只有两个办法:
//返回读锁
Lock readLock()
//返回写锁
Lock writeLock()
Java并发库中ReetrantReadWriteLock实现了ReadWriteLock接口并增加了可重入的个性。
ReentrantReadWriteLock有两个构造方法:
public ReentrantReadWriteLock() {
this(false);// 默认为false,采纳非偏心模式
}
public ReentrantReadWriteLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
能够看到,默认的构造方法应用的是非偏心模式,创立的Sync是NonfairSync对象,而后初始化读锁和写锁。一旦初始化后,ReadWriteLock接口中的两个办法就有返回值了,如下:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
从下面能够看到,构造方法决定了Sync是FairSync还是NonfairSync。Sync继承了AQS,而Sync是一个抽象类,NonfairSync和FairSync继承了Sync,并重写了其中的形象办法。
Sync中提供了很多办法,然而有两个办法是形象的,子类必须实现。
abstract boolean readerShouldBlock();
abstract boolean writerShouldBlock();
FairSync/NonfairSync实现办法如下:
/**
* 非偏心
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -8159625535654395037L;
final boolean writerShouldBlock() {
return false; // 间接返回false,阐明不须要排队
}
final boolean readerShouldBlock() {
/* 以后线程是写锁占用的线程时,返回true;否则返回false。
* 如果以后有一个写线程正在写,那么该读线程应该阻塞。
*/
return apparentlyFirstQueuedIsExclusive();
}
}
/**
* 偏心
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -2274990926593161451L;
final boolean writerShouldBlock() {
return hasQueuedPredecessors();// 判断同步队列中是否有人在排队
}
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
}
writerShouldBlock()办法的作用是判断以后线程是否应该阻塞,对于偏心的写锁和非偏心写锁的具体实现不一样。
- 对于非偏心写锁而言,间接返回false,因为非偏心锁获取锁之前不须要去判断是否排队。
- 对于偏心锁写锁而言,它会判断同步队列中是否有人在排队,有人排队,就返回true,示意以后线程须要阻塞。无人排队就返回false。
3.1 读锁
3.1.1 读锁加锁
获取读锁时,首先调用ReadLock类中的lock办法:
public void lock() {
sync.acquireShared(1);
}
读锁应用的也是AQS的共享模式,AQS的acquireShared办法如下:
public final void acquireShared(int arg) {
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
当tryAcquireShared()办法小于0时,那么会执行doAcquireShared办法将该线程退出到期待队列中。
Sync实现了tryAcquireShared办法,如下:
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount(c)返回的是写锁的数量,如果它不为0,阐明写锁被占用
// 如果此时占用写锁的线程不是以后线程,就返回-1,示意获取锁失败
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// 失去读锁的数量
int r = sharedCount(c);
/**
* 在上面的代码中进行了三个判断:
* 1、读锁是否应该排队。没有排队,就进行if前面的判断。排队,就间接调用fullTryAcquireShared()办法。
* 2、读锁数量是否超过最大值。(最大数量为2的16次方-1=65535)
* 3、是否获取到同步变量的最新状态值
*/
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) {
// 如果读锁数量为0时,以后线程设置为firstReader,即第一个读线程就是以后线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
} else if (firstReader == current) {
// 如果以后读线程重入了,firstReaderHoldCount累加
firstReaderHoldCount++;
} else {
// 读锁数量不为0,且第一个获取到读锁的线程不是以后线程
// 上面这一段逻辑就是保留以后线程获取读锁的次数,如何保留的呢?
// 通过ThreadLocal来实现的,readHolds就是一个ThreadLocal的实例
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);
}
查看fullTryAcquiredShared办法:
final int fullTryAcquireShared(Thread current) {
HoldCounter rh = null;
// for死循环,直到满足相应的条件才会return退出,否则始终循环
for (;;) {
int c = getState();
// 锁的状态为写锁时,持有锁的线程不等于以后线程,阐明以后线程获取锁失败,返回-1
if (exclusiveCount(c) != 0) {
if (getExclusiveOwnerThread() != current)
return -1;
}
// 如果读锁须要排队
else if (readerShouldBlock()) {
// 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)) {
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) {
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;
}
}
}
从下面能够看到屡次调用了readerShouldBlock办法,对于偏心锁,只有队列中有线程在期待,那么将会返回true,也就意味着读线程须要阻塞;对于非偏心锁,如果以后有线程获取了写锁,则返回true。一旦不阻塞,那么读线程将会有机会取得读锁。
3.1.2 读锁开释
当调用readLock.unlock()办法时,会先调用到AQS的releaseShared()办法,在releaseShared()办法中会先调用子类的tryReleaseShared()办法。在这里会调用的是ReentrantReadWriteLock的外部类Sync的tryReleaseShared()
办法。该办法的源码如下。
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
if (firstReader == current) {
// 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();
// 将批改同步变量的值(读锁状态减去1<<16)
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
// Releasing the read lock has no effect on readers,
// but it may allow waiting writers to proceed if
// both read and write locks are now free.
return nextc == 0;
}
}
3.2 写锁
3.2.1 写锁加锁
首先调用WriteLock类中的lock办法:
public void lock() {
sync.acquire(1);
}
AQS的acquire办法如下:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 写锁应用的是AQS的独占模式。首先尝试获取锁,如果获取失败,那么将会把该线程退出到期待队列中。
selfInterrupt();
}
Sync实现了tryAcquire办法用于尝试获取一把锁,如下:
protected final boolean tryAcquire(int acquires) {
// 失去调用lock办法的以后线程
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount()办法的作用是将同步变量与0xFFFF做&运算,计算结果就是写锁的数量。
// w即是写锁的数量。
int w = exclusiveCount(c);
// 如果线程占有了写锁或者读锁
if (c != 0) {
// 如果写锁数量为0,线程占有的必是读锁,而且这个线程并不是以后线程(齐全不合乎重入),返回false
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 如果写锁的数量超过了最大值,抛出异样
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 写锁重入,返回true
setState(c + acquires);
return true;
}
/**
* 1. 当writerShouldBlock()返回true时,示意以后线程还不能间接获取锁,因而tryAcquire()办法间接返回false。
* 2. 当writerShouldBlock()返回false时,示意以后线程能够尝试去获取锁,因而会执行if判断中前面的逻辑
* 即通过CAS办法尝试去批改同步变量的值。
* 3. 如果批改同步变量胜利,则示意以后线程获取到了锁,最终tryAcquire()办法会返回true。
* 如果批改失败,那么tryAcquire()会返回false,示意获取锁失败。
*/
// 如果以后没有写锁或者读锁,如果写线程应该阻塞或者CAS失败,返回false
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
return false;
// 否则将以后线程置为取得写锁的线程,返回true
setExclusiveOwnerThread(current);
return true;
}
从下面能够看到调用了writerShouldBlock办法,FairSync的实现是如果期待队列中有期待线程,则返回false,阐明偏心模式下,只有队列中有线程在期待,那么起初的这个线程也是须要记入队列期待的;NonfairSync中的间接返回的间接是false,阐明不须要阻塞。从下面的代码能够得出,当没有锁时,如果应用的非偏心模式下的写锁的话,那么返回false,间接通过CAS就能够取得写锁。
3.2.2 写锁开释
写锁的开释与排他锁的开释逻辑也简直一样。当调用writeLock.unlock()时,先调用到AQS的release()办法,在release()办法中会先调用子类的tryRelease()办法。在这里调用的是ReentrantReadWriteLock的外部类Sync的tryRelease()办法。写锁的开释逻辑比较简单,能够参考上面源码中的正文。办法的源码和正文如下。
protected final boolean tryRelease(int releases) {
// 判断是否是以后线程持有锁
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 将state的值减去releases
int nextc = getState() - releases;
// 调用exclusiveCount()办法,计算写锁的数量。如果写锁的数量为0,示意写锁被齐全开释,此时将AQS的exclusiveOwnerThread属性置为null
// 并返回free标识,示意写锁是否被齐全开释
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
3.3 小结
ReadWriteLock读写锁执行流程:
读写锁不反对锁降级,反对锁降级。锁降级指的是线程获取到了写锁,在没有开释写锁的状况下,又获取读锁。以上面代码为例:
public class WriteReadLockTest {
Object data;
// 是否无效,如果生效,须要从新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCachedData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须开释读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其它线程曾经获取了写锁、更新了缓存, 防止反复更新
if (!cacheValid) {
data = ...
cacheValid = true;
}
// 降级为读锁, 开释写锁, 这样可能让其它线程读取缓存
rwl.readLock().lock();
} finally {
rwl.writeLock().unlock();
}
}
// 本人用完数据, 开释读锁
try {
use(data);
} finally {
rwl.readLock().unlock();
}
}
}
参考
JAVA并发编程的艺术
不可不说的Java“锁”事
深刻了解ReentrantLock的实现原理
读写锁ReadWriteLock的实现原理
深刻了解读写锁—ReadWriteLock源码剖析
发表回复