目录
序言:
对于锁的作用,简单保证临界区(多个线程,进程同时访问的区域,最终我们希望只有一个线程进去执行操作的区域)中数据的一致性,不会因为在并发的时候出现脏数据,错乱数据。
对于java中存在的锁有两种,第一个以Synchronized关键字,第二也是今天重要说明的以AQS(AbstractQueuedSynchronizer)为核心的基于此框架衍生的各种锁。
在java中我们可以从不同的纬度来看待锁,如下脑图所示:
我们可以看到除了Automi各实现,以及Synchronized关键字,基本上java中的锁都与AQS有关。
1:什么是AQS
它全名为AbstractQueuedSynchronizer,提供了一种实现阻塞锁和一系列依赖FIFO等待队列的同步器的框架(来源作者注释),上图所示的ReentrantLock、Semaphore、ReentrantReadWriteLock、CountDownLatch等并发类均是基于AQS来实现的,它们通过AQS实现其模板方法(这里典型的设计模式中的-模版方法模式,说明主要的核心还是AQS来进行实现),来实现不同的场景中的应用。
AQS中通过被volatile修饰的state的大小来代表锁是否被占有(一般0代表为被占有,1占有。作为共享锁可以记录共享线程的数量,作为可重入锁也可以被重入的次数),通过FIFO双端队列来存储等待线程(被阻塞的线程在该队列中等待去获取锁,在这其中即使线程被Interrupt中断也会会阻塞知道线程获取锁后才继续中断)
这里我们通过ReentrantLock(独享式), Semaphore(共享式)来介绍这AQS在其中起到的作用
2:ReentrantLock(独享式)
ReentrantLock源码,当我们创建一个ReentrantLock对象时我们有两种选择,第一创建一个公平锁以及创建一个非公平锁(默认是非公平一个锁)
ReentrantLock reentrantLockNoFair = new ReentrantLock();
ReentrantLock reentrantLockFair = new ReentrantLock(true);
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
而FairSync与NonfairSync为ReentrantLock两种实现,都继承了Sync最终继承AbstractQueuedSynchronizer(AQS)
当我们调用lock()上锁时
/**
* 非公平锁
*/
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
//非公平锁 在获取锁时直接插队 尝试将state变更为
//compareAndSetState AQS第一个方法 该方法实际调用
if (compareAndSetState(0, 1))
//将当前线程置为独享锁拥有对象
setExclusiveOwnerThread(Thread.currentThread());
else
//若果抢占失败 其实和公平锁的做法一样了 调用的是AQS中函数
acquire(1);
}
}
/**
* 公平锁
*/
static final class FairSync extends Sync {
private static final long serialVersionUID = -3000897897090466540L;
final void lock() {
//直接调用AQS中函数
acquire(1);
}
}
我们可以看到使用AQS中2个方法:
compareAndSetState():本质是调用unsfae类中的CAS方法,通过CAS原子性操作将数据写入
acquire():以独占模式获取锁,忽略线程中断
查看acquire()源码如下所示:
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected boolean tryAcquire(int arg) {
throw new UnsupportedOperationException();
}
发现acquire为final修饰使得该方法无法被重写,而tryAcquire()函数直接抛出异常,实际上这里使用了模版方法模式,acquire定义为 需要执行的模版,上文中也存在4个AQS中的函数:
tryAcquire():尝试获取独享锁,是需要子类进行重写的,可以根据该函数不同的实现来定义锁的类型,公平锁或非公平锁
addWaiter(): 因为获取锁失败所以将当前线程以node节点的方式,追加到双端列表的尾部
acquireQueued():线程在队列中不间断(死循环)的方式等待获取锁,如果该线程被外部中断也要等待获取锁之后通过selfInterrupt()执行中断操作
在上文的FairSync与NonfairSync中对tryAcquire进行了重写如下所示:
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
//非公平锁获取独享锁
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;
}
//公平锁尝试获取独享锁
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
//获取锁标示符状态
int c = getState();
if (c == 0) {
//未有线程获取到锁
// hasQueuedPredecessors()AQS中函数-判断当前如果当前队列为空
// 如果为空 那么此时可以尝试直接获取锁
if (!hasQueuedPredecessors() &&
compareAndSetState(0, acquires)) {
//获取到锁了 当前线程写入锁中
setExclusiveOwnerThread(current);
return true;
}
}
//已经存在锁了
else if (current == getExclusiveOwnerThread()) {
//判断是否是当前线程所有 可重入锁的实现 没获取该锁一次 state+1 释放-1
int nextc = c + acquires;
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
setState(nextc);
return true;
}
return false;
}
addWaiter()源码分析:
/**
* 给定模式(独享或共享模式)下为当前线程在队列尾部创建新的节点 线程排队
*
*/
private Node addWaiter(Node mode) {
//创建节点
Node node = new Node(Thread.currentThread(), mode);
//尝试的节点快速插入等待队列队尾,若失败则执行常规插入(enq方法)
//尾node
Node pred = tail;
if (pred != null) {
//尝试将当前节点与tail节点相连 若成功直接返回
node.prev = pred;
//尾部替换
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
//出现竞争cas插入失败 使用enq方法
enq(node);
return node;
}
enq()源码分析:
/**
* 将节点插入队列,进行初始化
*/
private Node enq(final Node node) {
//自旋将节点插入到队列中
for (;;) {
//获取tail
Node t = tail;
if (t == null) {
//tail为null 说明该队列不存在 初始化队列 赋值head与tail
if (compareAndSetHead(new Node()))
tail = head;
} else {
//不断尝试将节点插入对尾
node.prev = t;
if (compareAndSetTail(t, node)) {
t.next = node;
return t;
}
}
}
}
上述中不论是常规插入还是快速插入本质都是通过unsafe中的cas操作来实现,该方法为native方法,由计算机CPU的cmpxchg指令来保证其原子性。
acquireQueued()源码分析:
/**
* 线程在队列中不间断自旋(死循环)+阻塞的方式等待获取锁,如果该线程被外部中断只会记录
* 还是要等待获取锁之后通过selfInterrupt()执行中断操作
*
* @param node the node
* @param arg 锁标准位
* @return {@code true} if interrupted while waiting
*/
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)) {
//将自己置为head 严格按照FIFO原则
setHead(node);
//断开与上一节点连接 避免产生内存泄露
p.next = null; // help GC
//获取锁成功
failed = false;
//返回中断标示
return interrupted;
}
//shouldParkAfterFailedAcquire 根据上一个节点判断是否需要阻塞当前节点的线程
if (shouldParkAfterFailedAcquire(p, node) &&
//parkAndCheckInterrupt()阻塞当前线程并检查线程是否被中断过 若被中断过变更中断标示位
parkAndCheckInterrupt())
//变更中断标示状态
interrupted = true;
}
} finally {
if (failed)
//该线程 放弃获取锁
cancelAcquire(node);
}
}
查看shouldParkAfterFailedAcquire()源码:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* This node has already set status asking a release
* to signal it, so it can safely park.
*/
return true;
if (ws > 0) {
/*
* Predecessor was cancelled. Skip over predecessors and
* indicate retry.
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* waitStatus must be 0 or PROPAGATE. Indicate that we
* need a signal, but don't park yet. Caller will need to
* retry to make sure it cannot acquire before parking.
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
我们依赖于上一节点的waitStatus-当前线程等待状态,来判定当前节点是否可以竞争锁还是需要进行阻塞等待,关于此状态在Node类中进行了定义,如下所示:
static final class Node {
/** 表示节点正在共享模式下等待的标记 */
static final Node SHARED = new Node();
/** 表示节点正在独占模式下等待的标记 */
static final Node EXCLUSIVE = null;
/** 当前线程已取消获取state对应的waitStatus值 实际上是线程因timeout和interrupt而放弃竞争state */
static final int CANCELLED = 1;
/** 表示当前节点需要唤醒后续线程 代表当前节点是一个可正常执行的节点流程 当该节点释放state或者取消竞争之后,将通知后续节点可以竞争state*/
static final int SIGNAL = -1;
/** 表征线程正在等待触发条件(condition) 当前节点处于条件队列中,它将不能用作同步队列节点,直到其waitStatus被重置为0 */
static final int CONDITION = -2;
/**
* 下一个acquireShared应无条件传播的
*/
static final int PROPAGATE = -3;
/**
*
*记录当前节点状态 对应上述4种情况对应的值
*若都不为上述情况 则指定为0
*
**/
volatile int waitStatus;
//当前节点上一个节点 前继节点
volatile Node prev;
//当前节点下一个节点 后继节点
volatile Node next;
//存储待竞争的线程
volatile Thread thread;
//链接下一个等待条件触发的节点
Node nextWaiter;
/**
* 返回当前是否为共享类型
*/
final boolean isShared() {
return nextWaiter == SHARED;
}
/**
* 获取前继节点
*/
final Node predecessor() throws NullPointerException {
Node p = prev;
if (p == null)
throw new NullPointerException();
else
return p;
}
Node() { // Used to establish initial head or SHARED marker
}
Node(Thread thread, Node mode) { // Used by addWaiter 函数
this.nextWaiter = mode;
this.thread = thread;
}
Node(Thread thread, int waitStatus) { // Used by Condition 函数中
this.waitStatus = waitStatus;
this.thread = thread;
}
}
此时再来看待shouldParkAfterFailedAcquire函数:
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
//获取前继节点的waitStatus状态
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
/*
* 如果前继节点状态为signal 说明前继节点是一个正常节点
* 那么该节点会通知下一节点你可以获取state了 这样就不需要后续的自旋寻求前继节点状态了
*通过返回true 来触发后续parkAndCheckInterrupt()执行从而达到线程阻塞的效果
*/
return true;
if (ws > 0) {
/*
*上文对waitStatus描述只有当CANCELLED=1(放弃的状态) 才满足ws>1情况
*直接从该节点向前遍历 直到前置节点中的状态不为CANCELLED
*通过该动作使得我们的节点可以向前移动
*/
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
/*
* 当前节点不为上述两个节点 通过CAS修改前继节点状态为 SIGNAL 正常状态 使得后续自旋的时候 可以通过parkAndCheckInterrupt阻塞线程
*/
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
return false;
}
parkAndCheckInterrupt()函数较为简单如下所示:
private final boolean parkAndCheckInterrupt() {
//通过LockSupport阻塞线程 查看该源码本质还是通过Unsafe类中的park来进行阻塞的
LockSupport.park(this);
//被前继节点唤醒时判断是否该线程被中断了
return Thread.interrupted();
}
上述描述完获取锁的动作,那么释放锁是如果做到的呢,它又做了其它什么事情呢,如下所示:
reentrantLock.unlock();
public void unlock() {
sync.release(1);
}
public final boolean release(int arg) {
//tryRelease 释放独占锁由AQS子类实现
if (tryRelease(arg)) {
//获取head节点 只有head才会才拥有锁
Node h = head;
//说明当前FIFO队列中存在等待线程
if (h != null && h.waitStatus != 0)
//AQS中函数 用于唤醒该节点下一个节点中等待的线程
unparkSuccessor(h);
return true;
}
return false;
}
//AQS子类中释放锁实现
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
//获取最终state状态这里是为了兼容可重入锁 释放一次那么对应锁数-1
int c = getState() - releases;
//验证该锁是否由创建锁线程进行释放
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
//锁释放成功 重置锁拥有线程
free = true;
setExclusiveOwnerThread(null);
}
//重写state
setState(c);
return free;
}
}
private void unparkSuccessor(Node node) {
//获取当前节点
int ws = node.waitStatus;
//非0的waitStatus代表该线程仍然会竞争state(除了1的CANCELLED)
if (ws < 0)
//通过CAS将waitStatus 初始0状态
compareAndSetWaitStatus(node, ws, 0);
/*
* 需要被unpark的线程会保存在后续节点中 一般是下一个节点。
* 如果该节点被取消或为空,则直接向后遍历,直到找到未取消(waitStatus=CANCELLED)的节点
*/
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)
s = t;
}
//获取该节点对应的被阻塞的线程 进行unpark唤醒
if (s != null)
LockSupport.unpark(s.thread);
}
看完上述的描述,我们将整体的加锁和解锁的动作一起来分析整体的流程,如下所示:
- 当一个新的线程进行lock加锁,那么此时根据自身的实现,如果为公平锁,那么严格遵守FIFO队列原则,通过CAS自旋插入到队尾,在插入队尾之后判定该节点对应的前节点是否为head,若是此时可以尝试获取下锁(若成功则说明前head已经释放过锁),否则判定前节点是否是一个正常节点(不为被取消的节点类型),若是通过LockSupport.park()进行阻塞,(此时若当前线程被interrupt中断,这里也不会进行处理,只有当该线程获取到锁之后才会进行interrupt中断,也就是说线程一旦进入锁排队中就无法被nterrupt中断),只有若不是则向上循环寻找到正常的节点。如果为非公平锁,那么该线程有两次机会和排在FIFO中head节点去抢占state,如果抢占失败那么后面的流程与上述公平锁一致了
- 当一个锁被线程释放了,例如node1被释放了,那么就会通过LockSupport.unpark()唤醒它下一个节点中的线程去占有锁。这里唤醒的节点需要判定它的状态是否为正常的状态,有些锁可能因为设置超时时间而导致锁被取消了,那么这种节点需要被舍弃,那么就要向下需要到第一个正常状态节点进行唤醒
3:Semaphore(共享式)
使用方式如下所示:
//定义信号量中最大共享锁数
Semaphore semaphore = new Semaphore(10);
try {
//使用信号量同一时间只有指定数量的线程可以访问 可以做到线程级别的限流
//获取共享锁
semaphore.acquire();
//执行的业务代码
//释放锁
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
与ReentrantLock一样,Semaphore也有两种锁模式,公平锁与非公平锁,默认是一个非公平锁实现
//permits 设定最大许可证数
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
//permits 设定最大许可证数 fair true公平锁 false非公平锁
public Semaphore(int permits, boolean fair) {
sync = fair ? new FairSync(permits) : new NonfairSync(permits);
}
当执行semaphore.acquire();
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
//判定当前线程是否被中断 中断抛出异常
if (Thread.interrupted())
throw new InterruptedException();
//tryAcquireShared()AQS中方法与tryAcquire()对应用于获取共享锁 由具体子类实现
//返回值值为负值未获取到许可证
if (tryAcquireShared(arg) < 0)
//共享可中断模式进行阻塞
doAcquireSharedInterruptibly(arg);
}
//非公平锁
static final class NonfairSync extends Sync {
private static final long serialVersionUID = -2694183684443567898L;
NonfairSync(int permits) {
super(permits);
}
//调用Sync中nonfairTryAcquireShared
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
}
final int nonfairTryAcquireShared(int acquires) {
//自旋
for (;;) {
//获取目前许可证数量
int available = getState();
//占有一份 许可证-1
int remaining = available - acquires;
//通过CAS自旋将剩余许可证数量写入 并返回
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
//公平锁
static final class FairSync extends Sync {
private static final long serialVersionUID = 2014338818796000944L;
FairSync(int permits) {
super(permits);
}
protected int tryAcquireShar(int acquires) {
//CAS自旋写入
for (;;) {
//hasQueuedPredecessors()判断是否存在FIFO队列并且head不为空 AQS中方法 因为是公平锁 所以需要判定是否已有线程等待 严格按照FIFO
if (hasQueuedPredecessors())
return -1;
//通过CAS自旋将剩余许可证数量写入 并返回 与非公平锁动作一致
int available = getState();
int remaining = available - acquires;
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
}
当前线程若未获取到许可证,通过AQS中doAcquireSharedInterruptibly()以共享可中断的方式进行阻塞,源码如下所示:
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
//创建类型为共享模式的节点插入队尾
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
//自旋
for (;;) {
//获取当前节点前节点
final Node p = node.predecessor();
if (p == head) {
//前节点为head时再次参数获取许可证
int r = tryAcquireShared(arg);
//成功获取 变更当前节点为head 并返回
if (r >= 0) {
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
//判定当前节点与之前对应前一节点必须状态为正常
if (shouldParkAfterFailedAcquire(p, node) &&
//阻塞当前线程 在被幻想后判断是否被中断 若中断抛出异常
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
//说明获取许可证失败
if (failed)
//做些清除动作
cancelAcquire(node);
}
}
释放锁动作:
//释放锁
public void release() {
sync.releaseShared(1);
}
//释放共享锁
public final boolean releaseShared(int arg) {
//释放锁 -实际上就是对许可证+1操作
if (tryReleaseShared(arg)) {
//唤醒FIFO下一个中等待的阻塞线程
doReleaseShared();
return true;
}
return false;
}
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryReleaseShared(int releases) {
for (;;) {
//获取最新的许可证数量
int current = getState();
//并更后的许可证数量
int next = current + releases;
//校验合法性
if (next < current) // overflow
throw new Error("Maximum permit count exceeded");
//乐观锁 CAS自旋将最新的值写入
if (compareAndSetState(current, next))
return true;
}
}
}
以上述ReentrantLock与Semaphore源码简单分析就已经可以看出AQS的作用,它通过volatile state+FIFO双端队列+CAS自旋+LockSupport线程阻塞唤醒等机制结合一起来实现sync锁的效果(功能比sync锁更加丰富),而使用者只需要去实现具体的获取锁与释放锁的逻辑即可,它隐藏了复杂的线程阻塞排队机制。
4:ReentrantLock与Synchronized性能对比
ReentrantLock对比之前描述的Synchronized的性能,ReentrantLock对于锁的获取与释放,已经在如何因对锁冲突是通过CAS自旋,也就是悲观锁的实现来处理的,而Synchronized在高并发下会升级为重量级锁通过monitorenter和monitorexit这两个字节码指令来保证临界区的安全。个人感觉在大量并发下Synchronized的性能应该会高于ReentrantLock的。为此做了一个小实验如下所示:
最终的结果:
后面的100,300代表使用线程数,前面代表执行完所有操作后对于的时间,从数据来看并发越大 synchronized要比ReentrantLock性能要高点。
以上只代表个人观点,如有错误希望大佬批评指正。