locks包的描述
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/package-summary.html
Lock接口
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/Lock.html
- 简介:
通过加锁/释放锁可以实现类似于synchronized关键词实现的互斥和同步访问资源的效果。相较于synchronized更加的灵活,同时支持条件变量(Condition)
锁(Lock):锁机制提供了一套多线程情况下线程访问共享资源的解决方案,线程要想访问资源,必须先获得锁,大部分情况下,如果某个线程获得了访问某个资源所需的锁,其他的线程不允许获得该锁,更不允许访问共享资源,但是也有例外,例如读写锁(ReadWriteLock),允许多个线程获得读锁,同时访问共享资源
使用synchronized关键词修饰的方法和语句实际上也使用了锁机制,只不过使用的隐含锁(implicit monitor lock),并且锁的释放是在使用完资源后自动进行的;而Lock的实现类需要程序员显示的,手动的释放锁
使用synchronized关键词可以帮助我们处理大部分的共享资源访问问题,但是在一些特殊的场景使用Lock不失为一种更好的选择:
- 申请锁需要多次,需要反复的申请锁,释放锁,需要控制锁的申请顺序和释放顺序;
在使用时推荐使用下面的语法模板:
Lock l = ...;
l.lock();
try {
// access the resource protected by this lock
} finally {
l.unlock();
}
注意:使用Lock时申请某个锁,在访问完共享资源以后,一定要及时的释放
Lock接口中提供了三个和锁申请有关的方法:
- tryLock():锁空闲时,返回True,申请成功;申请失败时,返回false。锁的申请阶段是不能被阻塞的
- lockInterruptily():在锁空闲的时候获得锁,除非该线程被打断;如果锁不空闲,被其他线程占用,此时该线程将处于休眠状态,线程得不到访问资源的机会,除非如下情况发生:
- 该线程获得锁
- 其他的线程打断了该线程,并且该实现方法支持在锁申请阶段能够被打断,此时会抛出异常
在实现该接口的时候需要注意如下事项:
- 在锁申请阶段是否允许被打断:允许,不允许,一段时间未申请成功便放弃申请
- 对于上面三种选择,不要求都实现,也不可能都实现,关键是要在实现类里面进行说明
Condition接口
如果说Lock接口类似于synchronized关键词,实现对共享资源的锁机制,那么Condition接口里面提供了一套类似于Object类里面的wait和notify方法,并且在此基础进一步丰富了功能:针对同一个共享资源可以派生出不同的条件变量。
条件变量:描述线程被阻塞的原因
对给定的Object(某个对象,某个成员变量)进行多种的PV操作
其中await操作:在线程执行条件不满足时将该线程阻塞,调用的形式为condition.await当前线程因为condition条件不满足而被阻塞
signal操作:condition.signal操作,唤醒一个因为condition条件不满足而被阻塞的线程
通常Condition和Lock配合使用,在对condition进行await和signal操作之前,应当通过lock.lock()方法加锁;在使用完毕以后,使用lock.unlock()方法释放锁
Lock对象自带condition,通过newCondition()方法创建条件变量
为了避免出现虚假的唤醒(spurious wakeup),通常将await操作放进loop循环当中
所谓虚假的唤醒,指的是:
为什么将condition.await操作放在循环里面?
- 防止所谓得虚假唤醒
- 和具体得硬件机制有关:
在Linux当中,条件变量得等待是通过阻塞(futex)系统调用实现的,阻塞的系统调用可能会被其他的信号中断,会中止阻塞返回EINTR错误。
- 举一个例子:
线程A在执行时,如果不将condition.await放进循环里面,在执行该语句的时候,会调用阻塞系统调用,假设此时被其他的信号中断,导致阻塞操作不能完成,返回EINTR错误,await方法拿到EINTR错误后,不能立马重新执行阻塞系统调用,需要等待一段时间,在此期间可能有其他的线程尝试唤醒线程A,线程A将会被错误的执行,进一步影响并发操作
放进循环里面以后
while(线程访问资源条件不成立?) condition.await;
即使被虚假的唤醒,线程会重新执行while循环判断,如果线程访问资源条件不成立,线程还是会继续执行await操作,避免被唤醒。
ReadWriteLock
相较于普通的锁,实际上包含两个锁,其中readLock方法返回一个处理读的锁,允许多个读线程同时访问共享变量,而writeLock方法返回一个写锁,在同一个时刻只允许一个线程对共享变量进行写操作
读写锁的性能分析:使用读写锁虽然能进一步提高线程的并发度,但主要几种在读线程上面,如果读操作频繁且时间比较长,此时使用读写锁对性能有一定的提升,但不是百分之百,具体还是要看程序实际运行的结果
在实现该接口时,需要注意如下问题:
- 是否支持锁重入:
- 读锁和写锁的优先级确定:
LockSupport类
工具类:
简介:
提供基本的线程park和线程unpark操作,相较于Thread.suspend and Thread.resume方法,能够更好的处理同时出现的线程唤醒和线程阻塞信号导致的死锁问题;某个线程在调用park方法的时候,即使该线程被阻塞,park方法还是可以正常返回;通常在使用park方法的时候需要将其放进while循环中防止出现错误的唤醒。
blocker object【应该是线程里面提供的机制】
用来描述线程被阻塞的原因。通过调用getBolocker(Thread t)方法:
getBlock方法返回对t线程进行park操作的线程,此时t线程仍处于阻塞状态;否则返回空,返回的值是一个snapshot,t线程可能被唤醒同时t线程可能又被其他的线程阻塞
使用范例
class FIFOMutex {
private final AtomicBoolean locked = new AtomicBoolean(false);//原子布尔变量
private final Queue<Thread> waiters
= new ConcurrentLinkedQueue<Thread>();//先进先出队列
public void lock() {
boolean wasInterrupted = false;
Thread current = Thread.currentThread();
waiters.add(current);
// Block while not first in queue or cannot acquire lock
while (waiters.peek() != current ||
!locked.compareAndSet(false, true)) {
LockSupport.park(this);
if (Thread.interrupted()) // ignore interrupts while waiting
wasInterrupted = true;
}
waiters.remove();
if (wasInterrupted) // reassert interrupt status on exit
current.interrupt();
}
public void unlock() {
locked.set(false);
LockSupport.unpark(waiters.peek());
}
}
AbstractQueuedSynchronizer
StampLock的实现用到了该抽象类
提供了一套基于先进先出队列的同步机制,通常用来处理和int类型的原子变量有关的同步
其核心是synchronizer(同步器),需要实现确定原子变量取值的含义,通常使用1表示当前资源已被线程访问,其他线程无法访问;使用0表示当前资源没有线程访问
对于该原子变量的值的获取以及跟新,只能使用下面的几个原子操作:
- getState()
- setState(int)
- compareAndSetState(int expect, int target):如果所修改的值等于expect,则将其修改为target,返回true;否则返回false
没有使用其他的同步接口,(相较于Lock里面的原生方法)
支持互斥访问独占资源和同步访问共享资源
支持条件变量
动态获取排队队列的状态属性
阅读源码
CLH阻塞队列
CLH locks 通常用在spinlocks 自旋锁,在这里使用类似的逻辑实现线程同步,其基本的原理为将线程的控制信息保存在当前结点的前驱当中,说到这里,不得提一下结点Node的定义。阻塞队列的每一个结点的定义如下:
static final class Node {
static final Node SHARED = new Node();
static final Node EXCLUSIVE = null;
static final int CANCELLED = 1;
static final int SIGNAL = -1;
static final int CONDITION = -2;
static final int PROPAGATE = -3;
volatile int waitStatus;
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
现在先介绍里面两个变量:
thread:线程尝试申请访问资源时,首先会新建一个结点将其插入到队列的维部,结点中thread变量保存的就是该线程,而nextWaiter表示的是线程访问共享资源的两种模式:
- 独占访问 EXCLUSIVE
- 共享访问 SHARED
prev指示当前结点的前驱,在CLH队列当中,当前结点的前驱结点保存着当前线程所需的
控制信息,只有在前驱结点中的线程放弃对共享资源的访问以后,当前结点中的线程才会被唤醒。一个结点只有在队列首部的时候才有机会去尝试申请访问资源(tryAcquire),但是不一定保证成功。
入队操作:修改队列的尾指针(tail)
出队操作:通过设置头指针head完成出队操作【有点费解】
首先队列的定义包含一个头指针(head)和一个尾指针(tail)。而队列的首部和头指针不是同一个概念,根据CLH对列的特点,一个结点只有在队列的首部时才有机会tryAcquire,一旦tryAcquire成功,此时表明线程可以访问资源,于是将该结点设置为头结点,源码acquireQueued方法用来申请独占访问资源,申请成功返回true;申请失败,根据结点前驱状态等影响因素决定是否阻塞(park)当前线程【关注阴影部分】
final boolean acquireQueued(final Node node, int arg) {
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
failed = false;
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
接下来需要重点理解Nod类里面两个成员变量:prev和next
prev变量指示当前结点的前驱,用来处理结点取消(cancellation)的情况,所谓“取消”指的是结点中的线程由于各种各样的原因,始终不能访问资源不得已终止该线程,但是此时该节点还是在队列当中,因此需要使用prev指针跳过被取消的结点,使其重新指向一个没有被取消的结点
此机制最开始在自旋锁(spinlocks)中得到运用http://www.cs.rochester.edu/u/scott/synchronization/
next变量的作用为:前面已经提到head结点中保存的线程是正在访问共享资源的线程,在其访问完毕以后,其需要唤醒后面的结点,通过next域可以直接找到当前结点的后继结点。
Determination of
successor must avoid races with newly queued nodes to set
the "next" fields of their predecessors. This is solved
when necessary by checking backwards from the atomically
updated "tail" when a node's successor appears to be null.
(Or, said differently, the next-links are an optimization
so that we don't usually need a backward scan.)
需要注意的是,由于多线程操作的并发性,在寻找某个结点的后继结点的时候必须避免新插入的结点修改这些结点的前驱【不知道该怎么翻译但是后面举了一个例子】,当某个结点的next为空时,此时并不能说明后面没有线程需要进行诸如唤醒等操作,因此必须从尾指针tail开始向前遍历,找到最后一个没有被取消的结点将其作为结点的next
Cancellation introduces some conservatism to the basic
* algorithms. Since we must poll for cancellation of other
* nodes, we can miss noticing whether a cancelled node is
* ahead or behind us. This is dealt with by always unparking
* successors upon cancellation, allowing them to stabilize on
* a new predecessor, unless we can identify an uncancelled
* predecessor who will carry this responsibility.
还是在强掉队列当中可能会无规律的分布一些被取消的结点,对于这些结点是不能被唤醒的,需要被跳过
原来的CLH队列需要一个虚拟头结点,在初始化队列的时候就需要创建,但是这里并不是在初始化队列的时候创建,而是在插入第一个结点的时候创建一个虚拟头结点,参见源码addWaiter
private Node enq(final Node node) {
for (;;) {
Node t = tail;
if (t == null) { // Must initialize
if (compareAndSetHead(new Node()))
tail = head;
} else {
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
最后简单介绍如何在CLH队列使用条件变量(Condition),使用相同的结点,不过需要使用一个额外的指针:
Threads waiting on Conditions use the same nodes, but
use an additional link. Conditions only need to link nodes
in simple (non-concurrent) linked queues because they are
only accessed when exclusively held. Upon await, a node is
inserted into a condition queue. Upon signal, the node is
transferred to the main queue. A special value of status
field is used to mark which queue a node is on.
通过上面的藐视简单介绍了CLH队列的特点
接下来是Node类的设计,前面简单提到了Node的源码,接下来需要说明每个变量的含义和取值范围
变量waitStatus:相当重要,用来指明结点的状态,其取值范围为:
/** waitStatus value to indicate thread has cancelled */
static final int CANCELLED = 1;
/** waitStatus value to indicate successor's thread needs unparking */
static final int SIGNAL = -1;
/** waitStatus value to indicate thread is waiting on condition */
static final int CONDITION = -2;
//上面都比较好理解,关键是下面的PROPAGATE,后面需要结合具体的源码才能理解
static final int PROPAGATE = -3;
变量prev:前面多次介绍
变量next:前面多次介绍
acquireShared方法逻辑:以共享访问的方式访问资源
有多个线程需要访问共享资源,访问资源的模式有两种:
- 独占访问:在同一个时刻仅有一个线程可以访问,修改资源,且该操作除非线程本身自己放弃,或者其他原因阻塞,其他线程无法介入该线程对共享资源的访问,其他的线程只能等待
- 共享访问:在同一个时刻允许多个线程访问共享资源
假设某场景中,有若干个线程,这些线程既有独占访问资源的需求,也有共享访问资源的需求,安排如下:
线程名称 | 访问模式 |
Thread-1 | 独占访问 |
Thread-2 | 共享访问 |
Thread-3 | 独占访问 |
处理机调度时,假设三个线程均已经启动(start),Thread-1请求独占访问资源,调用tryAcquire方法,线程2调用tryAcquiredShared方法,线程3调用tryAcquire方法,由于线程推进的顺序无法确定,假设Thread-1 tryAcquire成功,此时Thread-1得到执行。Thread-2和Thread-3的状态如下:
Thread-2调用tryAcquireShared方法失败,转而执行doAcquireShared方法
Thread-3调用tryAcquire方法失败,转而执行acquireQueued方法
注意上面两个方法的执行处于并发的模式下,可能先执行doAcquireShared方法里面的某条语句,转而执行acquireQueued方法里面的某条语句
以执行tryAcquireShared方法为例:
调用addWaiter方法:该方法在CLH队列后面插入一个新的结点node,结点的模式为SHARED,表示Thread-2线程将以共享模式访问共享资源。
改方法忽视interrupt操作,即使被打断,正常返回不抛出异常,使用interrupted变量表示线程在申请访问资源期间是否被打断
for死循环,退出循环的唯一语句为
只有在申请访问资源成功的时候才正常返回
循环内部逻辑为:
找到node的前驱,如果node的前驱为head,注意head初始的时候仅为一个结点,里面不包含任何的线程信息,node此时为队列的头结点,此时node机会才有机会申请访问共享资源,否则连资格都没有!
申请成功时:表示node里面的线程可以正常执行
同时调用setHeadAndParopagate(node,r)方法,该方法一方面将node设置为头结点,实际上执行出队操作,另外一方面进行propagate操作:
将node设置为头结点,并保留原有的头结点h
什么时候唤醒下一个结点,关键是看两个结点
- 旧的头结点h
- 新的头结点head或node
/*
* Try to signal next queued node if:
* Propagation was indicated by caller,
* or was recorded (as h.waitStatus either before
* or after setHead) by a previous operation【旧的头结点】
* (note: this uses sign-check of waitStatus because
* PROPAGATE status may transition to SIGNAL.)
* and【针对新的头结点】
* The next node is waiting in shared mode,
* or we don't know, because it appears null
*
* The conservatism in both of these checks may cause
* unnecessary wake-ups, but only when there are multiple
* racing acquires/releases, so most need signals now or soon
* anyway.
*/
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
doReleaseShared();
}
上述条件都满足时,进行doReleaseShared()操作,唤醒下一个等待的结点;如果唤醒失败需要将头结点的状态设置为PROPAGATE
/*
* Ensure that a release propagates, even if there are other
* in-progress acquires/releases. This proceeds in the usual
* way of trying to unparkSuccessor of head if it needs
* signal. But if it does not, status is set to PROPAGATE to
* ensure that upon release, propagation continues.
* Additionally, we must loop in case a new node is added
* while we are doing this. Also, unlike other uses of
* unparkSuccessor, we need to know if CAS to reset status
* fails, if so rechecking.
*/
死循环,为什么使用死循环,在唤醒操作过程中有新的结点插入到CLH队列的时候,同样将其中符合要求的结点唤醒,即对里面的线程执行unpark操作
for (;;) {
Node h = head;
if (h != null && h != tail) {//头结点非空,且CLH队列里面不止一个结点的时候
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {//头结点后面有需要唤醒的结点
//需要注意的是,调用改方法时,头结点的状态为SHARED,但是可能会有其他操作将其状态修改为SIGNAL,【因为此时访问模式为SHARED】
Shared mode acquires by multiple threads may (but need not) succeed. This class does not "understand" these differences except in the mechanical sense that when a shared mode acquire succeeds, the next waiting thread (if one exists) must also determine whether it can acquire as well.
此时需要使用原子操作将其状态修改为默认状态0,然后再唤醒后面线程,why?
为什么修改为0?===》看后面!
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);//唤醒h后面的某个结点
}
//head结点的状态原来为SIGNAL,经过上面的if处理其状态变成0
//此时进一步将其状态修改为PROPAGATE
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
//头结点没有改变的时候,表明没有在前面的处理过程中,没有新的结点尝试申请访问资源,或者有节点尝试申请资源但失败,此时可以直接返回,继续执行线程的后续操作
if (h == head) // loop if head changed
break;
}
如果线程在doAcquireShared方法申请访问资源时失败,此时调用shouldParkAfterFailedAcquire(p, node)方法,p为node结点的前驱,这个方法很重要,一方面按照方法字面意思,另外一方面还承担线程的唤醒工作,换句话说,执行完改方法,node的前驱的状态应该修改为SIGNAL,否则线程将一直尝试申请访问资源
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
return true;//大概率从这里返回
if (ws > 0) {
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
只有在上面方法和parkAndCheckInterrupt()方法均返回True时,当前线程才被打断,但是不抛出异常,也就是方法文档里面的“ignoring interrupt”,此时,该线程申请资源失败,需要将对应结点的状态修改为CANCELD,调用下面的方法
如果线程在doAcquireShared方法无论怎么申请访问资源都以失败告终,此时只能强制放弃争用资源,将结点的状态改为CANCELLED
private void cancelAcquire(Node node) {
if (node == null)
return;
node.thread = null;
// Skip cancelled predecessors
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;//经过该循环,pred的状态小于等于0,接下来需要确定pred.next,但是看看作者的处理方式
// predNext is the apparent node to unsplice. CASes below will
// fail if not, in which case, we lost race vs another cancel
// or signal, so no further action is necessary.
Node predNext = pred.next;
// Can use unconditional write instead of CAS here.
// After this atomic step, other Nodes can skip past us.
// Before, we are free of interference from other threads.
node.waitStatus = Node.CANCELLED;
// If we are the tail, remove ourselves.
if (node == tail && compareAndSetTail(node, pred)) {
compareAndSetNext(pred, predNext, null);//修改成功与否不影响最后的结果,因为其状态已经修改为CANCELD,后续唤醒操作会直接跳过它
} else {
// If successor needs signal, try to set pred's next-link
// so it will get one. Otherwise wake it up to propagate.
int ws;
if (pred != head &&
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
//node结点需要取消,删除node结点,将node.next和前面链接起来
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
acquire方法逻辑:
ReentrantLock:可重入锁
可重入锁,实现了Lock接口,源码内部是基于AbstractQueuedSynchronizer(AQS)实现的,提供了两种模式:一种是非安全模式,各个线程获得锁的几率相等;另一种是安全模式——长时间等待的线程获得锁的几率更大,但不一定保证真正得到处理机执行的概率更大【结合源码理解】
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {//锁空闲
if (!hasQueuedPredecessors() &&//在队列中查询是否有线程的等待时间比当前尝试申请锁的线程长,如果有返回True,此时当前线程申请锁失败
compareAndSetState(0, acquires)) {//第二个判断尝试修改状态值,只有修改成功才能真正得到锁
setExclusiveOwnerThread(current);
return true;
}
}
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0)//语法糖,nextc最大值为2^32-1,即7FFFFFFFH,如果超过该值,nextc就会小于0
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
}
在官方文档中,推荐使用不公平模式,同时也是默认模式,因为使用安全模式,会导致线程的并发程度降低,降低吞吐率;同时还可能出现饥饿现象。注意tryLock方法会无视安全模式的设置,在申请锁时按照非安全策略进行。
支持可重入:某个线程在获得锁的情况下,可以重新获得该锁,最多可从入2^31-1次,int型变量的最大值,超过该值会报错【上面源码也说明了】
关于类里面的方法,除了实现Lock接口的基本方法以外,还提供了诸如锁状态检测,线程检测,线程排队情况检测等方法用来监控线程的并发执行和代码调试。
源码阅读:
package java.util.concurrent.locks;
import java.util.concurrent.TimeUnit;
import java.util.Collection;
public class ReentrantLock implements Lock, java.io.Serializable {
private static final long serialVersionUID = 7373984872572414699L;
/** Synchronizer providing all implementation mechanics */
private final Sync sync;
//使用AQS队列辅助实现
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
//非安全策略下tryLock
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
if (compareAndSetState(0, acquires)) {//使用原子操作修改state成功,state变量为AQS内置成员变量,用来实现进程之间的同步和互斥
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;
}
//释放锁
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();//错误调用方法,只有在获得锁的前提下才能调用该方法
boolean free = false;
if (c == 0) {//由于支持锁重入,可能线程多次申请锁,只有在state值为0的前提下,才表示锁释放成功
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
-------------------------------------------------------------------------------
//上面的部分都是AQS内部类,下面具体实现ReenTrantLock类的成员方法
public ReentrantLock() {
sync = new NonfairSync();
}
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
public void lock() {
sync.lock();
}
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
//需要注意的是,tryLock方法调用的是非安全模式的nonfairTryAcquire方法
public boolean tryLock() {
return sync.nonfairTryAcquire(1);
}
//如果必须要调用安全模式下的teyAcquire方法,使用如下方法,但性能会受到影响
public boolean tryLock(long timeout, TimeUnit unit)
throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}
public void unlock() {
sync.release(1);
}
public Condition newCondition() {
return sync.newCondition();
}
ReentrantReadWriteLock
实现了ReadWriteLock接口,在一定条件支持可重入,具体表现为:
- 提供类似于ReentrantLock的锁重入机制
- 直到写线程释放所有的写锁才允许该线程可重入读锁
- 写线程可以获得读锁,从而完成读的操作,反过来不行
除此之外该锁还有如下的特点:
非安全模式:读线程和写线程在进行锁申请时顺序是没有指定的
安全模式:线程进行锁申请的顺序近似为线程的到达顺序
可重入性:
锁降级:
锁申请阶段被打断:
条件变量支持:
显示线程并发状态:
源码阅读:
内部类的语义及实现:
最重要的是在内部定义了抽象类Sync继承了抽象类AQS(AbstractQueuedSynchronizer)进一步提供两个抽象方法readerShoulderBlock和writerShoulderBlock。
在此基础上分别定义了类NoFairSync和FairSync,两者都继承了Sync抽象类
接下来提供读锁ReadLock和写锁WriteLock类,两者都实现了Lock接口,通过相关方法供给Java程序员调用。
详细说明
首先是Sync类,包含的成员变量较多,还包含内部类说明如下:
内部类:HoldCounter
由于ReentrantReadWriteLock无论是读锁还是写锁都支持可重入,需要对线程重复获取锁的次数进行统计,方便释放锁的正常执行【申请所和释放锁需要匹配】,该类记录了重复获得某个锁的次数
static final class HoldCounter {
int count = 0;
// Use id, not reference, to avoid garbage retention【提高垃圾回收效率】
final long tid = getThreadId(Thread.currentThread());
}
ThreadLocalHoldCountern内部类继承了ThreadLocal类,保存的参数是上面提到的HoldCounter类,主要用于读锁相关逻辑。由于读锁支持可重入,在一定条件下可能有多个线程同时持有读锁,为了能记录每个线程的的HoldCounter同时在线程需要查看自己对读锁的重复获得次数时立马返回对应的值,使用ThreadLocal类,通过get方法即可返回
static final class ThreadLocalHoldCounter
extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
和父类(AQS)state变量有关的内部变量:
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; | 用来记录当前线程重复获得读锁的次数,调用get方法返回HoldCounter对象,进一步可以获得count值,当count值为0时,删除该HoldCounter对象 |
private transient HoldCounter cachedHoldCounter; | 用来记录最后一个获得读锁的线程重复获得读锁的次数,可以更快的获取对应的hold count |
private transient Thread firstReader = null; private transient int firstReaderHoldCount; | 用来记录第一个获得读锁的线程重复获得读锁的次数,可以更快的获取对应的hold count |
总结:上面的变量都和读锁的可重入性有关 |
围绕上面的成员变量,相关方法设计如下:
构造方法:初始化readHolds同时确保其通过setState(getState())方法对其他线程可见
提供给子类的抽象方法,用于子类实现具体的读写策略,读操作和写操作的优先级
接下来需要重写父类(AQS)的一些抽象方法
tryRelease方法:释放写锁
首先判断当前线程是否占有写锁【处理异常】
修改state的值,此时有两种可能:一修改state值以后,发现是最后一次释放写锁,此时应当真正释放(setExclusiveOwnerThread(null)),返回True;否则返回False
tryAcquire方法:独占访问共享资源
获取当前线程
state的值不为0时:
读线程占据锁时,返回False
写线程占据锁时,如果占据的线程不是当前线程返回False
此时只有一种可能:当前线程占据写锁,修改state的值,返回True
state值为0时:
根据读写策略判断是否应该阻塞当前当前线程,在否的情况下,设置当前线程独占锁
tryreleaseShared方法,释放对共享资源的独占访问
释放读锁的同时还应当修改和记录线程重复获得读锁的相关的变量:firstReader,firstReaderHoldCount和cacheHoldCounter和readHolds
紧接着修改state变量,释放读锁。对state变量的修改需要使用CAS操作,可能修改失败因此需要放进死循环里面直到修改成功:
for (;;) {
int c = getState();
int nextc = c - SHARED_UNIT;
if (compareAndSetState(c, nextc))
return nextc == 0;//读锁是否释放成功对线程没有影响关键是影响写线程
}
tryAcquireShared方法:尝试以共享的方式访问共享资源,访问成功返回True;否则返回False
代码编写技巧:
总体目标意识:代码需要完成的根本任务是什么,如何完成
基本操作意识:找准问题的关键操作以及关键操作发生的条件围绕关键操作,完善相关逻辑
获取当前线程
共享资源被写线程占据,放弃访问返回False【这是唯一返回False的情况】
此时当前线程有资格访问共享资源,当条件允许,访问成功的时候,修改相关和读锁可重入性的相关变量集中在HoldCounter类和LocalThread类
条件不允许的时候,进行额外操作(fullTryAcquireShared),进一步尝试申请访问
fullTryAcquireShared方法:在第一次执行tryAccquireShared方法失败时调用改方法,将重复申请读锁的逻辑放在改方法里面
死循环保证当前线程申请共享访问资源的机会
其他的逻辑和tryAccquireShared方法相似
tryWrite方法:由用于尝试申请访问写锁的tryLock方法调用
tryRead方法:用于尝试申请访问读锁的tryLock方法调用
其他的方法为相关的get方法,不作详细介绍
要注重自己的表达能力
StampedLock:版本锁
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/locks/StampedLock.html
简介:stamp
三种模式:Writing,Reading,OptimisticRead【使用场景】
方法介绍:try***,read***,write***,unlock,unReadLock,unWriteLock
三种模式之间的转化:
检测锁的状态: