基于前面讲解的CAS内容,本章主要内容是Lock以及Lock的常见实现加锁原理和方式。
一、Lock接口
Java1.5以后官方在concurrent包下引入了Lock接口和其对应实现。Lock接口是显示的锁,加锁和解锁都需要手动实现,接口内容有:
public interface Lock {
void lock();
//可中断获取锁,在获取锁的过程中可中断,synchronized在获取锁时是不可中断的
void lockInterruptibly() throws InterruptedException;
//尝试非阻塞获取锁,调用该方法后立即返回结果,如果能够获取则返回true,否则返回false
boolean tryLock();
//根据时间段获取锁,在指定时间内没有获取锁则返回false,如果在指定时间内当前线程未被中并断
获取到锁则返回true
boolean tryLock(long time, TimeUnit unit) throws InterruptedException;
// 与当前锁绑定,当前线程只有获得了锁才能调用该组件的wait()方法,而调用后,当前线程将释放锁。
Condition newCondition();
void unLock();
}
二、可重入锁ReetrantLock
ReetrantLock本身也是一种支持重进入的锁,即该锁可以支持一个线程对资源重复加锁,同时也支持公平锁与非公平锁。ReeTrantLock底层是通过AbstractQueuedSynchronize队列同步器实现的,提供了如下一些方法:
//查询此锁是否由任意线程保持。
boolean isLocked()
//如果此锁的公平设置为 true,则返回 true。
boolean isFair()
//查询当前线程是否保持此锁。
boolean isHeldByCurrentThread()
//查询当前线程保持此锁的次数。
int getHoldCount()
//返回目前拥有此锁的线程,如果此锁不被任何线程拥有,则返回 null。
protected Thread getOwner();
//返回一个 collection,它包含可能正等待获取此锁的线程,其内部维持一个队列,这点稍后会分析。
protected Collection<Thread> getQueuedThreads();
//返回正等待获取此锁的线程估计数。
int getQueueLength();
// 返回一个 collection,它包含可能正在等待与此锁相关给定条件的那些线程。
protected Collection<Thread> getWaitingThreads(Condition condition);
//返回等待与此锁相关的给定条件的线程估计数。
int getWaitQueueLength(Condition condition);
// 查询给定线程是否正在等待获取此锁。
boolean hasQueuedThread(Thread thread);
//查询是否有些线程正在等待获取此锁。
boolean hasQueuedThreads();
//查询是否有些线程正在等待与此锁有关的给定条件。
boolean hasWaiters(Condition condition);
三、AQS
3.1 AQS的模型
AQS是AbstractQueuedSynchronize简写,核心思想是通过volatile int state这样的volatile变量,配合Unsafe类的原子操作来实现对当前锁状态的修改。当state=0时,说明没有线程占用共享资源;state=1时,说明有线程正在使用共享资源,其他的线程需要进入同步队列等待。AQS内部通过内部类Node构成FIFO的双向同步队列来完成线程获取锁的排队工作,利用ConditionObject构建等待队列,当Condition调用wait()方法时,线程会加入等待队列;当Condition调用signal()方法后,线程会从等待队列加入同步队列竞争锁。
public abstract class AbstractQueuedSynchronizer extends AbstractOwnableSynchronizer
implements java.io.Serializable {
private transient volatile Node head;
private transient volatile Node tail;
private volatile int state;
...
}
AQS的模型如图:
其中head是头结点,是一个空节点,不存储数据;tail是尾节点。state是同步状态,state的值为0,说明当前线程可以获取到锁,同时将state设置为1,表示获取成功。如果state已为1,也就是当前锁已被其他线程持有,那么当前执行线程将被封装为Node结点加入同步队列等待。
3.2 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;
// node本身的等待状态, 1表示
volatile int waitStatus;
// 前一个节点
volatile Node prev;
// 后一个节点
volatile Node next;
// 对应的等待线程
volatile Thread thread;
// 下一个等待着者
Node nextWaiter;
}
共享模式和独享模式:
共享模式是一个锁允许多条线程同时操作,如信号量Semaphore采用的就是基于AQS的共享模式实现的,而独占模式则是同一个时间段只能有一个线程对共享资源进行操作,多余的请求线程需要排队等待,如ReentranLock。
Node中waitStatus有四种状态:
- CANCELLED:1,在同步队列中等待的线程等待超时或被中断,需要从同步队列中取消该Node的结点。
- SIGNAL:-1,处于唤醒状态,只要前继结点释放锁,就会通知标识为SIGNAL状态的后继结点的线程执行。
- CONDITION:-2,与Condition相关,该标识的结点处于等待队列中,结点的线程等待在Condition上,当其他线程调用了Condition的signal()方法后,CONDITION状态的结点将从等待队列转移到同步队列中,等待获取同步锁。
- PROPAGATE:值为-3,与共享模式相关,在共享模式中,该状态标识结点的线程处于可运行状态。
四、ReeTrantLock的底层实现
4.1 ReeTrantLock和AQS的关系图:
ReentrantLock内部存在3个实现类,分别是Sync、NonfairSync、FairSync,其中Sync继承自AQS实现了解锁tryRelease()方法,而NonfairSync(非公平锁)、 FairSync(公平锁)则继承自Sync,实现了获取锁的tryAcquire()方法,ReentrantLock的所有方法调用都通过间接调用AQS和Sync类及其子类来完成的。
4.2 ReentrantLock的非公平锁实现
//默认构造,创建非公平锁NonfairSync
public ReentrantLock() {
sync = new NonfairSync();
}
//根据传入参数创建锁类型
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//加锁操作
public void lock() {
sync.lock();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
if (compareAndSetState(0, 1)) // 通过CAS方式,获取锁,设置状态为1
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 再次请求同步锁
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
acquire(int arg)是AQS的方法,用于再次请求同步锁。它对中断不敏感,即使线程获取同步状态失败,进入同步队列,后续对该线程执行中断操作也不会从同步队列中移出,方法如下
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
static void selfInterrupt() {
Thread.currentThread().interrupt();
}
//Sync类
abstract static class Sync extends AbstractQueuedSynchronizer {
//nonfairTryAcquire方法
final boolean nonfairTryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
//判断同步状态是否为0,并尝试再次获取同步状态
if (c == 0) {
//执行CAS操作
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
//如果当前线程已获取锁,属于重入锁,再次获取锁后将status值加1
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
//设置当前同步状态,当前只有一个线程持有锁,因为不会发生线程安全问题,可以直接执行 setState(nextc);
setState(nextc);
return true;
}
return false;
}
//省略其他代码
当tryAcquire(arg)返回false,则会执行addWaiter(Node.EXCLUSIVE)
进行入队操作。
private Node addWaiter(Node mode) {
//将请求同步状态失败的线程封装成结点
Node node = new Node(Thread.currentThread(), mode);
Node pred = tail;
//如果是第一个结点加入肯定为空,跳过。
//如果非第一个结点则直接执行CAS入队操作,尝试在尾部快速添加
if (pred != null) {
node.prev = pred;
//使用CAS执行尾部结点替换,尝试在尾部快速添加
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//如果第一次加入或者CAS操作没有成功执行enq入队操作
enq(node);
return node;
}
如果第一次CAS没有成功或者头结点为空,会用enq(node)方式,不停的用CAS加入尾节点。
private Node enq(final Node node) {
//死循环
for (;;) {
Node t = tail;
//如果队列为null,即没有头结点
if (t == null) { // Must initialize
//创建并使用CAS设置头结点
if (compareAndSetHead(new Node()))
tail = head;
} else {//队尾添加新结点
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
enq方法非常重要的一点,使用CAS原子操作进行头结点设置和尾结点tail替换可以保证线程安全,可以多线程同时竞争执行。如果有一个线程修改head和tail成功,其它线程将继续循环,直到修改成功。head结点本身不存在任何数据,它只是作为一个牵头结点,而tail永远指向尾部结点(前提是队列不为null)。
添加到同步队列后,节点会进入自旋,是在acquireQueued方法中实现。当且仅当前驱结点为头结点才尝试获取同步状态,这符合FIFO的规则,即先进先出,其次head是当前获取同步状态的线程结点,具体有:
final boolean acquireQueued(final Node node, int arg){
boolean failed = true;
try{
boolean interrupted = false;
// 不停的循环自旋,等待前驱节点是head
for(;;){
final Node p = node.predecessor();
// 当前节点的前驱节点时head的时候,才开始尝试获取同步状态
if(p == head && tryAcquire(arg)){
// 获取到同步状态后,设置为head
setHead(node);
p.next = null;
failed = false;
return interrupted;
}
// 前驱节点不是head,判断是否需要挂起
if(shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()){
interrupted = true;
}
}
}finally{
if(failed){
//最终都没能获取同步状态,结束该线程的请求
cancelAcquire(node);
}
}
}
如果前驱节点不是head,线程是否挂起的方法有:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node){
//获取当前结点的等待状态
int ws = pred.waitStatus;
if(ws == Node.SIGNAL){
// signal 表示前置节点处于等待被唤醒的状态,所以返回true
return true;
}
if(ws >0){
// 如果waitStatus > 0 表示前置节点时结束,则遍历之前未结束的节点
do{
pred = pred.prev;
node.prev = pred;
}while(pred.waitStatus > 0);
pred.next = node;
}else{
// 如果不是等待被唤醒的状态,则将其设置为SIGNAL状态,代表该结点的线程正在等待唤醒
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
private final boolean parkAndCheckInterrupt() {
//将当前线程挂起
LockSupport.park(this);
//获取线程中断状态,interrupted()是判断当前中断状态,
//并非中断线程,因此可能true也可能false,并返回
return Thread.interrupted();
}
若shouldParkAfterFailedAcquire()方法返回true,即前驱结点为SIGNAL状态同时又不是head结点,那么使用parkAndCheckInterrupt()方法挂起当前线程,称为WAITING状态,需要等待一个unpark()操作来唤醒它,到此ReetrantLock内部间接通过AQS的FIFO的同步队列就完成了lock()操作。
对于lockInterruptibly()
或者tryLock()
方法,最终它们都间接调用到doAcquireInterruptibly(),其实现和acquireQueued唯一的区别是:判断线程需要被挂起时,直接抛出中断异常。
if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt())
//直接抛异常,中断线程的同步状态请求
throw new InterruptedException();
unLock操作有:
public void unlock(){
sync.release(1);
}
//AQS类的release()方法
public final boolean release(int arg){
if(tryRelease(arg)){
Node h = head;
if(h != null && h.waitStatus != 0){
unparkSuccessor(h);
}
return true;
}
return flase;
}
//ReentrantLock类中的内部类Sync实现的tryRelease(int releases)
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
//判断状态是否为0,如果是则说明已释放同步状态
if (c == 0) {
free = true;
//设置Owner为null
setExclusiveOwnerThread(null);
}
//设置更新同步状态
setState(c);
return free;
}
private void unparkSuccessor(Node node){
int ws = node.waitStatus;
if (ws < 0){
//置零当前线程所在的结点状态,允许失败。
compareAndSetWaitStatus(node, ws, 0);
}
// 找到下一个节点
Node s = node.next;
if(s == null || s.waitStatus > 0){
s = null;
// 从尾结点开始倒叙查找最前的一个有效节点
for (Node t = tail; t != null && t != node; t = t.prev)
if (t.waitStatus <= 0)//从这里可以看出,<=0的结点,都是还有效的结点。
s = t;
}
if (s != null)
LockSupport.unpark(s.thread);//唤醒
}
加锁的过程表述为:如果前置节点不是head,并且前置节点时等待被唤醒的SIGNAL状态,当前节点的线程就进入park。解锁的过程则是:首先释放当前节点的锁,并对head的后一个有效节点做线程唤醒park操作。
4.3 ReetrantLock的公平锁实现:
非公平锁获取的方法是nonfairTryAcquire(int acquires),公平锁实现的方法是tryAcquire(int acquires).
protected final boolean tryAcquire(int acquires){
final Thread current = Thread.currentThread();
int c = getState();
if(c == 0){
// 这里和非公平锁不同,非公平锁不会做是否有前置的节点存在
if(!hasQueuedPredcessor() && compareAndSetState(0, acquires)){
setExclusiveOwnerThread(current);
retur true;
}
}
else if(current == getExclusiveOwnerThread()){
int nextc = acquires+c;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
公平锁借助hasQueuedPredecessors()判断同步队列是否存在结点,如果存在必须先执行完同步队列中结点的线程,当前线程进入等待状态。公平锁在线程请求到来时先会判断同步队列是否存在结点,如果存在先执行同步队列中的结点线程,当前线程将封装成node加入同步队列等待。非公平是当线程请求到来时,不管同步队列是否存在线程结点,直接尝试获取同步状态,获取成功直接访问共享资源。
参考来自:https://blog.youkuaiyun.com/javazejian/article/details/75043422