文章目录
什么是 AQS?
AQS(AbstractQueuedSynchronizer),位于 java.util.concurrent.locks 包下的一个类,它是 jdk 1.5 提供的一套 基于 CAS 理论 + 阻塞队列(链表)+ LockSupport.park()/LockSupport.unpark() 开发的锁机制框架。是除了 Java 自带的 synchronized 关键字之外的锁机制。
我们常用的 ReentrantLock、CountDownLatch、CyclicBarrier 等并发类均是基于 AQS 来实现的,具体用法是通过继承 AQS 并实现其模版方法,来达到同步状态的管理。
AQS 的功能在使用中可以分为两种:独占锁和共享锁。
-
独占锁:每次只能有一个线程持有锁(ReentrantLock 就是独占锁)
-
共享锁:允许多个线程同时获得锁,并发访问共享资源(ReentrantReadWriteLock 的读操作)
AQS 核心思想
AQS 的核心思想是,如果被请求的共享资源 state 空闲(state 是一个标志位),则将当前请求资源的线程设置为有效的工作线程(可以认为线程持锁了),并且将共享资源设置为锁定状态。如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制 AQS 是用阻塞队列(链表)实现的,即将暂时获取不到锁的线程加入到队列中。
简单理解就是,AQS 内部会使用 CAS 判断 state 是否锁定(假设 state = 0 是没锁,state = 1 是有锁),如果没锁就将当前线程设置为持锁的工作线程,并将 state 修改为 1;如果有锁,就将拿不到锁的线程加入阻塞等待队列,等待锁分配。
自定义 AQS
AQS 设计是基于模版方法模式,一般的使用方法是:
-
继承 AbstractQueuedSynchronizer 并重写指定的方法(这些重写方法很简单,无非是对于共享资源 state 的获取和释放)
-
将 AQS 组合在自定义同步组件的实现中,并调用其模板方法,而这些模板方法会调用使用者重写的方法
public class MyLock implements Lock {
private final MySync sync = new MySync();
// 加锁(不成功会进入等待队列)
@Override
public void lock() {
// 独占式获取同步状态,如果获取失败则插入同步队列进行等待
sync.acquire(1);
}
// 加锁(可打断)
@Override
public void lockInterruptibly() throws InterruptedException {
// 与 acquire() 相同,但在同步队列中进行等待的时候可以检测中断
sync.acquireInterruptibly(1);
}
// 尝试加锁(一次)
@Override
public boolean tryLock() {
// 独占式获取同步状态,试着获取如果成功返回 true,否则返回 false
return sync.tryAcquire(1);
}
// 尝试加锁(带超时)
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
// 在 acquireInterruptibly() 基础上增加了超时等待功能,在超时时间内没有获得同步状态返回 false
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
// 解锁
@Override
public void unlock() {
// 释放同步状态,该方法会唤醒在同步队列中的下一个节点
sync.release(1);
}
// 创建条件变量
@Override
public Condition newCondition() {
return sync.newCondition();
}
private static class MySync extends AbstractQueuedSynchronizer {
// 独占式获取同步状态,试着获取如果成功返回 true,否则返回 false
@Override
protected boolean tryAcquire(int arg) {
// 设定 state = 0 时没锁,state = 1 时有锁
if (compareAndSetState(0, 1)) {
// 通过 CAS 能修改预期值从 0 到 1,将当前线程设置为工作线程持有锁
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
return false;
}
// 独占式释放同步状态,等待中的其他线程此时将有机会获取同步状态
@Override
protected boolean tryRelease(int arg) {
setState(0); // state = 0 修改为没锁
setExclusiveOwnerThread(null); // 持有锁的线程重置
return true;
}
// 是否在独占模式下被线程占用
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
public Condition newCondition() {
return new ConditionObject();
}
}
}
AQS 可以重写的方法:
方法声明 | 方法描述 |
---|---|
boolean tryAcquire(int arg) | 独占式获取同步状态,试着获取如果成功返回 true,否则返回 false |
boolean tryRelease(int arg) | 独占式释放同步状态,等待中的其他线程此时将有机会获取同步状态 |
int tryAcquireShared(int arg) | 共享式获取同步状态,返回值 >= 0 代表获取成功,否则失败 |
boolean tryReleaseShared(int arg) | 共享式释放同步状态,成功为 true,否则为 false |
boolean isHeldExclusively() | 是否在独占模式下被线程占用 |
独占锁:
方法声明 | 方法描述 |
---|---|
void acquire(int arg) | 独占式获取同步状态,如果获取失败则插入同步队列进行等待 |
void acquireInterruptibly(int arg) | 与 acquire() 相同,但在同步队列中进行等待的时候可以检测中断 |
boolean tryAcquireNanos(int arg, long nanosTimeout) | 在 acquireInterruptibly() 基础上增加了超时等待功能,在超时时间内没有获得同步状态返回 false |
boolean release(int arg) | 释放同步状态,该方法会唤醒在同步队列中的下一个节点 |
共享锁:
方法声明 | 方法描述 |
---|---|
void acquireShared(int arg) | 共享式获取同步状态,与独占式的区别在于同一时刻有多个线程获取同步状态 |
void acquireSharedInterruptibly(int arg) | 在 acquireShared() 基础上增加了能响应中断的功能 |
boolean tryAcquireSharedNanos(int arg, long nanosTimeout) | 在 acquireSharedInterruptibly() 基础上增加了超时等待功能 |
boolean releaseShared(int arg) | 共享式释放同步状态 |
总结下自定义 AQS 的步骤:
-
继承 AbstractQueuedSynchronizer,根据我们的需求重写相应的方法,比如要实现一个独占锁就重写 tryAcquire()、tryRelease(),要实现共享锁就重写 tryAcquireShared()、tryReleaseShared()
-
在我们的组件中调用 AQS 中的模板方法就可以了,这些模板方法会调用我们之前重写的那些方法
我们只需要很小的工作量就可以实现自己的同步组件,重写的那些方法,仅仅是一些简单的对于共享资源 state 的获取和释放操作,至于像获取资源失败、线程需要阻塞之类的操作,AQS 已经帮我们完成了。
编写上面的自定义 AQS 只是为了从框架使用层面上更好的理解它,但一般我们不需要自己自定义(除非有特定业务场景需要),因为 jdk 已经提供了 ReentrantLock 等针对业务场景的 AQS 实现,我们可以直接使用它。
ReentrantLock 是怎么实现锁机制的?或者说 AQS 是怎么运行的?
AQS 源码分析
ReentrantLock 是基于 AQS 实现的,所以会通过它具体分析 AQS 的运行机制。先简单了解下 ReentrantLock 的整体框架,方便后面讲解原理时知道它们是干什么的:
ReentrantLock 内部提供了一个 Sync 继承了 AQS,并且提供了两个子类 NoFairSync 和 FairSync 分别是非公平锁和公平锁的实现类。
AQS 中 ConditionObject 是条件变量,Node 是队列。
CAS 尝试获得锁及锁重入处理
ReentrantLock.java
public class ReentrantLock implements Lock, Serializable {
private final Sync sync;
public ReentrantLock() {
sync = new NofairSync(); // 默认非公平锁,非公平锁其实就是让线程抢锁
}
public ReentrantLock(boolean fair) {
sync = fair ? new NoFairSync() : new NoFairSync();
}
public void lock() {
// NonfairSync.lock()
sync.lock();
}
static final class NonfairSync extends Sync {
private static final long serialVersionUID = 7316153563782823691L;
final void lock() {
// CAS,预期值为 0,新值为 1,如果能修改为 1,设置当前线程持锁
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1); // 调用 AbstractQueuedSynchronizer.acquire()
}
// AbstractQueuedSynchronizer.acquire(1) 调用时会调用该方法
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
abstract static class Sync extends AbstractQueuedSynchronizer {
private static final long serialVersionUID = -5179523762034025860L;
abstract void lock();
final boolean nonfairTryAcquire(int acquires) { // acquires = 1
final Thread current = Thread.currentThread(); // 当前线程
int c = getState(); // 获取锁状态
if (c == 0) {
// 在 NonfairSync.lock() 已经尝试过 CAS 失败,所以这里是重试操作
if (compareAndSetState(0, acquires)) {
setExclusiveOwnerThread(current);
return true;
}
}
// 锁重入操作,本来抢到锁的线程又来抢占
// 相当于 lock.lock() 调用了两遍
// lock.lock();
// lock.lock();
// 该操作主要规避死锁问题
else if (current == getExclusiveOwnerThread()) {
int nextc = c + acquires;
if (nextc < 0) // overflow
throw new Error("Maximum lock count exceeded");
setState(nextc); // 修改 state 避免死锁
return true;
}
return false;
}
...
}
}
AbstractQueuedSynchronizer.java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
// 调用 tryAcquire(),实际调用 NofairSync.tryAcquire(1)
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
...
}
上面的代码是在尝试持锁及锁重入的处理,可以用下图理解上面的逻辑:
调用 lock() 时,此时 Thread-0 持有锁 state = 1,Thread-1 尝试 CAS 获取锁失败(尝试两次获取锁,第一次是调用 lock() 时,第二次是调用 acquire() 间接调用了 tryAcquire() 又尝试了一次)。
线程入队处理
AbstractQueuedSynchronizer.java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
private Node addWaiter(Node mode) {
// 将当前线程转换为节点
Node node = new Node(Thread.currentThread(), mode);
// Try the fast path of enq; backup to full enq on failure
Node pred = tail;
if (pred != null) {
node.prev = pred;
if (compareAndSetTail(pred, node)) {
pred.next = node;
return node;
}
}
enq(node);
return node;
}
// 自旋操作,模拟线程锁抢占,用的数据结构是链表
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;
}
}
}
}
}
我们上面说到 ReentrantLock 默认是非公平锁,也就是线程是会抢锁的,那怎么抢?enq() 就是模拟抢锁的操作。
enq() 通过自旋的方式(链表节点不断的轮换变动实现随机的方式)让线程在无限循环中不断的轮换位置,最终当持有锁的线程 Thread-0 释放锁时,拿到锁的线程就是随机的了。
调用 addWaiter(),将 Thread-1 转换为链表节点构造 Node 队列。图中黄色三角表示该 Node 的 waitStatus 状态,waitStatus = 0 表示默认正常状态,可以去抢占锁 state 修改为 1;其中第一个 Node 称为 Dummy 哨兵(链表中也有称为哑节点),用来占位,并不关联线程。
LockSupport.park() 阻塞
AbstractQueuedSynchronizer.java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
final boolean acquireQueued(final Node node, int arg) {
try {
boolean interrupted = false;
for (;;) {
final Node p = node.predecessor();
// 两次拿锁都失败后,还在尝试拿锁调用 tryAcquire(1)
if (p == head && tryAcquire(arg)) {
setHead(node);
p.next = null; // help GC
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt()) // 最主要看这句代码
interrupted = true;
}
} catch (Throwable t) {
cancelAcquire(node);
throw t;
}
}
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// Node.SIGNAL = -1
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.
*/
pred.compareAndSetWaitStatus(ws, Node.SIGNAL); // waitStatus = -1
}
return false;
}
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this); // 实际做阻塞的操作
return Thread.interrupted();
}
}
-
acquireQueued() 会在一个死循环中不断尝试获得锁,还是失败后调用 LockSupport.park() 阻塞
-
如果自己是紧邻着 head(排第二位,图中的 Thread-1),那么再次 tryAcquire() 取锁,当然这时 state 仍为 1 因为 Thread-0 还持锁,Thread-1 获取锁失败
-
进入 shouldParkAfterFailedAcquire() 逻辑,将前驱 node (Dummy)的 waitStatus 改为 -1,这次返回 false(-1标识:有责任去唤醒后继节点,即 Dummy 有责任去唤醒 Thread-1)
-
shouldParkAfterFailedAcquire() 执行完毕回到 acquireQueued() 再次 tryAcquire() 尝试获取锁,当然这时 state 仍为 1 失败
-
当再次进入 shouldParkAfterFailedAcquire() 时,这时因为其 node 的 waitStatus 已经是 -1,这次返回 true
-
进入 parkAndCheckInterrupt(),Thread-1 进入阻塞状态(灰色表示阻塞)
多个线程竞争失败如下:
LockSupport.unpark() 唤醒
ReentrantLock.java
public class ReentrantLock implements Lock, Serializable {
private final Sync sync;
public void unlock() {
sync.release(1);
}
abstract static class Sync extends AbstractQueuedSynchronizer {
protected final boolean tryRelease(int releases) {
int c = getState() - releases;
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null); // 持有锁线程重置
}
setState(c); // 重置共享资源 state
return free;
}
}
}
AbstractQueuedSynchronizer.java
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
private void unparkSuccessor(Node node) {
/*
* If status is negative (i.e., possibly needing signal) try
* to clear in anticipation of signalling. It is OK if this
* fails or if status is changed by waiting thread.
*/
int ws = node.waitStatus;
if (ws < 0)
node.compareAndSetWaitStatus(ws, 0);
/*
* Thread to unpark is held in successor, which is normally
* just the next node. But if cancelled or apparently null,
* traverse backwards from tail to find the actual
* non-cancelled successor.
*/
Node s = node.next; // 取下一个节点(自旋操作后,队列拿下一个线程是随机的)
if (s == null || s.waitStatus > 0) {
s = null;
for (Node p = tail; p != node && p != null; p = p.prev)
if (p.waitStatus <= 0)
s = p;
}
if (s != null)
LockSupport.unpark(s.thread); // 从队列中唤醒下一个节点的线程可以开始抢锁了
}
}
上面的代码最主要的就是调用了 LockSupport.unpark() 唤醒队列中的线程可以抢锁了。
AQS 运行机制总结
ReentrantLock 的实现主要的几个方法:
加锁 lock():
-
tryAcquire():尝试加锁与锁重入处理
-
addWaiter():用来入队,往最后一个节点插入
-
acquireQueued():将 addWaiter() 的节点进行 park 等待唤醒 (park 等同于 synchronized 锁机制的 wait() 等待)
解锁 unlock():
- release():unpark 唤醒队列中的线程(unpark 等同于 synchronized 锁机制的 notify() 唤醒)
根据上面分析的流程简单分析下 AQS 的基本实现:
AQS 维护一个共享资源 state,通过内置的队列来完成获取资源线程的排队工作(内置的同步队列称为 CLH 阻塞队列,实际上它是一个链表)。该队列由一个一个的 Node 节点组成,每个 Node 节点维护一个 prev 引用和 next 引用,分别指向自己的前驱和后继节点。AQS 维护两个指针,分别指向队列头部 head 和尾部 tail。
当线程取锁失败(比如 tryAcquire() 试图设置 state 状态失败),会被构造成一个节点加入 CLH 队列中,同时当前线程会被阻塞在队列中(通过 LockSupport.park() 实现阻塞),当持有同步状态的线程释放同步状态时,会唤醒后继节点,然后此节点线程继续加入到对同步状态的争夺中。
整体流程如下图(若图片无法打开,可以点击该链接 AQS 运行机制):
Condition 条件变量
Condition 是一个多线程协调通信的工具类,可以让某些线程一起等待某个条件,只有满足条件时,线程才会被唤醒。
当我们使用 ReentrantLock 时,一般也会创建条件变量:
ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newCondition();
try {
lock.lock();
condition.await(); // 阻塞
} finally {
lock.unlock();
}
try {
lock.lock();
condition.signal(); // 唤醒
} finally {
lock.unlock();
}
Condition 作为条件,主要的操作就是条件不满足时进入阻塞等待。那它是怎么阻塞的?又是什么时候知道要释放?
public abstract class AbstractQueuedSynchronizer
extends AbstractOwnableSynchronizer
implements java.io.Serializable {
...
static final class Node {
volatile Node prev;
volatile Node next;
volatile Thread thread;
Node nextWaiter;
}
public class ConditionObject implements Condition, java.io.Serializable {
private transient Node firstWaiter;
private transient Node lastWaiter;
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
Node node = addConditionWaiter();
int savedState = fullyRelease(node);
int interruptMode = 0;
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
interruptMode = REINTERRUPT;
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// If lastWaiter is cancelled, clean out.
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
t = lastWaiter;
}
Node node = new Node(Node.CONDITION); // 修改 watiStatus = -2
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node; // 只操作 nextWaiter
lastWaiter = node;
return node;
}
}
final int fullyRelease(Node node) {
try {
int savedState = getState();
if (release(savedState))
return savedState;
throw new IllegalMonitorStateException();
} catch (Throwable t) {
node.waitStatus = Node.CANCELLED;
throw t;
}
}
public final boolean release(int arg) {
if (tryRelease(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h); // LockSupport.unpark()
return true;
}
return false;
}
}
可以看到,ConditionObject 继承自 Condition,内部是在操作 Node。每个 ConditionObject 其实就对应着一个等待队列。
Node 从物理结构上是双链表(持有 prev 和 next 变量),但从在 Condition 的应用角度来讲它是单链表,因为 Node 只操作 nextWaiter 变量专门用于做等待处理,prev 和 next 变量是用于做类似 synchronized 机制的 entryList 队列容器。
Condition 条件变量内部还是用的 park/unpark 实际处理阻塞和唤醒。
await() 的流程可以用下图说明:
图中一开始 Thread-0 持有锁,调用 await() 时会进入 ConditionObject 的 addConditionWaiter() 流程创建新的 Node 并且状态为 -2(Node.CONDITION),关联 Thread-0,加入等待队列尾部。
wait()/notify()、await()/signal()、park/unpark 对比
-
wait()/notify():依托于 synchronized,基于 JVM 底层对于阻塞的实现,Monitor 使用 waitSet 作为等待机制,线程随机被唤醒其实就是用的 Set 提取算法,notifyAll() 会唤醒所有
-
await()/signal():依赖于 ReentrantLock 条件变量,已经用条件变量与 AQS 体系作为唤醒机制,本质上底层实现是 park/unpark 实现阻塞
-
park/unpark:以 thread 为操作对象,操作更精准,可以准确地唤醒某一个线程(notify() 随机唤醒一个线程,notifyAll() 唤醒所有),增加了灵活性
其实 park/unpark 的设计原理核心是 “许可”,park 是等待一个许可,unpark 是为某线程提供一个许可。但这个许可是不能叠加的,许可是一次性的。
比如线程 B 连续调用了三次 unpark,当线程 A 调用 park 就使用掉这个许可,如果线程 A 再次调用 park,则进入等待状态。
AQS 和 synchronized 锁机制类比
虽然 synchronized 和 AQS 是两种不同的锁机制实现方案,不过为了方便记忆,是可以将 AQS 和 synchronized 的实现方式做类比的。关于 synchronized 锁机制详细可以查看另一篇文章:Java synchronized 与 CAS。
上图是重量级锁 Monitor 的实现原理图,Monitor 分别由 Owner、entryList 和 waitSet 组成,Object 是锁,Owner 就是持有锁的工作线程,entryList 是等待队列,waitSet 是等待唤醒的线程。
在 AQS 中同样也有可以对应的地方,compareAnSetState() 自己控制锁状态,setExclusiveOwnerThread() 就是 Monitor 的 Owner,等待队列(其实是一个链表,Node 节点持有 prev 和 next 引用)就是 Monitor 的 entryList,Condition 就是 Monitor 的 waitSet。
AQS 的其他业务场景
ReentrantReadWriteLock
ReentrantReadWriteLock 读写锁指一个资源能够被多个读线程访问,或者被一个写线程访问,但是不能同时存在读写线程。
读写锁有两个静态内部类:ReadLock 读锁和 WriteLock 写锁,这两个锁实现了 Lock 接口。
-
写锁的获取和释放:写锁是支持重入的排他锁,如果当前线程已经获取了写锁,则增加写状态。如果当前线程在获取读锁时,读锁已经被获取或者该线程不是已获取写锁的线程,则当前线程进入等待状态。读写锁确保写锁的操作对读锁可见。写锁释放每次减少写状态,当写状态为 0 时表示写锁已经释放
-
读锁的获取和释放:读锁时支持重入的共享锁(共享锁为 shared 节点,对于 shared 节点会进行一连串的唤醒,直到遇到一个读节点),它能够被多个线程同时获取,在没有其他写线程访问(写状态为 0)时,读锁总是能够被成功地获取,而所做的也只是增加读状态(线程安全)。如果当前线程已经获取了读锁,则增加读状态。如果当前线程在获取读锁时,写锁已经被获取,则进入等待状态。
public static void main(String[] args) {
DataContainer container = new DataContainer();
new Thread(() -> {
container.read();
}, "t1").start();
new Thread(() -> {
// container.read();
container.write();
}).start();
}
static class DataContainer {
private Object data;
private ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
private ReentrantReadWriteLock.ReadLock r = rw.readLock();
private ReentrantReadWriteLock.WriteLock w = rw.writeLock();
public Object read() {
System.out.println("获取读锁...");
r.lock();
try {
System.out.println("读取");
Thread.sleep(1000);
return data;
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放读锁...");
r.unlock();
}
return null;
}
public void write() {
System.out.println("获取写锁...");
w.lock();
try {
System.out.println("写入");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println("释放写锁...");
w.unlock();
}
}
}
// 两个线程都在读
执行结果:
获取读锁... // 线程 1 拿到锁
读取
获取读锁... // 线程 2 也拿到锁
读取
释放写锁...
释放写锁...
// 一个线程读一个线程写
执行结果:
获取读锁...
读取
获取写锁...
释放读锁... // 拿到写锁时,写入前会先释放读锁
写入
释放写锁...
读写锁适合缓存的场景:
public class CacheDemo {
Object data;
// 是否有效,如果失效需要重新计算 data
volatile boolean cacheValid;
final ReentrantReadWriteLock rwl = new ReentrantReadWriteLock();
void processCacheData() {
rwl.readLock().lock();
if (!cacheValid) {
// 获取写锁前必须先释放读锁
rwl.readLock().unlock();
rwl.writeLock().lock();
try {
// 判断是否有其他线程已经获取了写锁、更新了缓存,避免重复更新
if (!cacheValid) {
data = null;
cacheValid = true;
}
// 降级为读锁,释放写锁,这样能够让其他线程读取缓存
rwl.readLock().lock();
} finally {
// 自己用完数据,释放写锁
rwl.writeLock().unlock();
}
}
}
}
Semaphore
Semaphore 也就是我们常说的信号灯,Semaphore 可以控制同时访问的线程个数,通过 acquire 获取一个许可,如果没有就等待,通过 release 释放一个许可。它的作用有点类似于限流的作用。
public static void main(String[] args) {
// 创建一个无界线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 配置只能3个线程同时访问
final Semaphore semaphore = new Semaphore(3);
// 模拟5个客户端访问
for (int i = 0; i < 5; i++) {
int num = i;
Runnable task = (() -> {
try {
// 获得许可
semaphore.acquire();
System.out.println("获得许可:" + num);
// 随机休眠
TimeUnit.SECONDS.sleep((int)(Math.random() * 10 + 1));
// 访问完后,释放许可
semaphore.release();
System.out.println("----当前还有多少个许可:" + semaphore.availablePermits());
} catch (InterruptedException e) {
e.printStackTrace();
}
});
exec.execute(task);
}
// 退出线程池
exec.shutdown();
}
执行结果:
获得许可:1
获得许可:2
获得许可:0
----当前还有多少个许可:1
获得许可:3
----当前还有多少个许可:1
获得许可:4
----当前还有多少个许可:1
----当前还有多少个许可:2
----当前还有多少个许可:3
CountDownLatch
CoutDownLatch 是一个同步工具类,它允许一个或多个线程一直等待,直到其他线程的操作执行完毕再执行。
从命名可以解读到是一个递减的计数器,类似于我们倒计时的概念。
public static void main(String[] args) {
CountDownLatch latch = new CountDownLatch(3); // 设定计数器数量
ExecutorService service = Executors.newFixedThreadPool(4);
service.submit(() -> {
System.out.println("t1 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); // 计数器-1
System.out.println("t1 end..." + latch.getCount());
}, "t1");
service.submit(() -> {
System.out.println("t2 begin...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); // 计数器-1
System.out.println("t2 end..." + latch.getCount());
}, "t2");
service.submit(() -> {
System.out.println("t3 begin...");
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
latch.countDown(); // 计数器-1
System.out.println("t3 end..." + latch.getCount());
}, "t3");
service.submit(() -> {
try {
System.out.println("t4 waiting...");
latch.await(); // 等待其他线程执行完后再执行
System.out.println("t4 wait end...");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "t4");
}
执行结果:
t1 begin...
t2 begin...
t3 begin...
t4 waiting...
t1 end...2
t2 end...1
t3 end...0
t4 wait end...
CyclicBarrier
CyclicBarrier 字面意思是可循环的屏障。它主要做的事情是,让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,屏障才会打开,所有被屏障拦截的线程才会继续工作。
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(5);
CyclicBarrier barrier = new CyclicBarrier(4, () -> {
System.out.println("task1, task2, task3, task4 finish...");
});
service.submit(() -> {
System.out.println("task1 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
barrier.await();
System.out.println("task1 execute...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
System.out.println("task2 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
barrier.await();
System.out.println("task2 execute...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
System.out.println("task3 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
barrier.await();
System.out.println("task3 execute...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
service.submit(() -> {
System.out.println("task4 begin...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
try {
barrier.await();
System.out.println("task4 execute...");
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
});
}
执行结果:
task1 begin...
task2 begin...
task3 begin...
task4 begin...
task1, task2, task3, task4 finish...
task2 execute...
task4 execute...
task3 execute...
task1 execute...
...
AQS 和 synchronized 的选择
或许你会有疑问:AQS 和 synchronized 用哪种好?
其实 AQS 和 synchronized 没有可比性。
AQS 的底层原理应用的是 CAS 原子变量 + 队列 + park/unpark 的支持来添加业务去具体实现锁的定义,它是并发工具。而 synchronized 是基于 JVM 的锁机制实现。
所以应该根据具体的业务场景具体选择。