文章目录
一、AQS原理
1.1 AQS简介
AQS全称为AbstractQueuedSynchronizer(抽象队列同步器)。AQS是一个用来构建锁和其他同步组件的基础框架。
使用AQS可以简单且高效地构造出应用广泛的同步器,例如ReentrantLock、Semaphore、ReentrantReadWriteLock和FutureTask等等。
1.2 AQS原理
AQS核心思想是:如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态。
如果被请求的共享资源被占用,那么就需要一套线程阻塞等待以及被唤醒时锁分配的机制,这个机制AQS使用CLH队列锁实现的。
即将暂时获取不到锁的线程加入到队列中。
AQS 用状态属性来表示资源的状态(分独占模式和共享模式),子类需要定义如何维护这个状态,控制如何获取锁和释放锁
- 独占模式是只有一个线程能够访问资源,如 ReentrantLock
- 共享模式允许多个线程访问资源,如 Semaphore,ReentrantReadWriteLock 是组合式
1.3 CLH队列
CLH(Craig,Landin,and Hagersten)队列是一个虚拟的双向队列(虚拟的双向队列即不存在队列实例,仅存在结点之间的关联关系)。
AQS是将每条请求共享资源的线程封装成一个CLH锁队列的一个结点(Node)来实现锁的分配。
1.4 ASQ的设计
1.4.1 设计原理
获取锁的逻辑:
while(state 状态不允许获取){ // tryAcquire(arg)
if(队列里还没有此线程){
入队并阻塞 (park)
}
}
当前线程出队
释放锁的逻辑:
if(state 状态允许了)// // tryRelease(arg)
{
恢复阻塞的线程(s) unpark
}
关键点:
- 原子维护state状态
- 阻塞及恢复线程
- 维护队列
1.4.2 state 设计
- state 使用了 32bit int 来维护同步状态,独占模式 0 表示未加锁状态,大于 0 表示已经加锁状态。
/**
* The synchronization state.
*/
private volatile int state;
- state 使用 volatile 修饰配合 cas 保证其修改时的原子性
- state 表示线程重入的次数(独占模式)或者剩余许可数(共享模式)
- state API:
protected final int getState()
:获取 state 状态protected final void setState(int newState)
:设置 state 状态protected final boolean compareAndSetState(int expect,int update)
:CAS 安全设置 state
具体来说,所有可以基于一个int值来维护线程对共享资源的持有状态都可以基于AQS实现,比如:
- 对于
ReentrantLock
中实现的AQS子类Sync
,state
表示加锁次数(0表示为加锁,否则表示被同一个线程枷锁了多少次) - 对于
ReentrantReadWriteLock
来说,state
的高16位表示线程对读锁的加锁次数,低16位标识线程对写锁的加锁次数。(当然读锁、写锁也都是可重入的,并且读写锁互斥) - 对于
Semaphore
来说,state
表示其可用信号量,简单来说:state的数值标识其还可以被线程获取的次数,0标识信号量已经被耗尽(占不可用) - 对于
CountDownLatch
来说,state
表示锁需要被解锁的次数,可以实现多个线程执行释放操作后state
变为0标识该锁被打开(对应阻塞的线程此时才能被释放执行),进而通过计数器实现多个线程前置动作全部完成后才能执行后续动作的作用。
1.4.3 封装线程的 Node 节点中 waitStatus 设计
- 使用 volatile 修饰配合 CAS 保证其修改时的原子性。
根据这个属性我们就可以知道这个线程在等待队列中的状态。
// 默认为 0
volatile int waitStatus;
// 由于超时或中断,此节点被取消,不会再改变状态
static final int CANCELLED = 1;
// 此节点后面的节点已(或即将)被阻止(通过park),【当前节点在释放或取消时必须唤醒后面的节点】
static final int SIGNAL = -1;
// 此节点当前在条件队列中
static final int CONDITION = -2;
// 将releaseShared传播到其他节点
static final int PROPAGATE = -3;
1.4.4 阻塞恢复设计
- 使用 park & unpark 来实现线程的暂停和恢复,因为命令的先后顺序不影响结果
- park & unpark 是针对线程的,而不是针对同步器的,因此控制粒度更为精细
- park 线程可以通过 interrupt 打断
1.4.5 队列设计
使用了 FIFO 先入先出队列,并不支持优先级队列,同步队列是双向链表,便于出队入队
// 头结点,指向哑元节点
private transient volatile Node head;
// 阻塞队列的尾节点,阻塞队列不包含头结点,从 head.next → tail 认为是阻塞队列
private transient volatile Node tail;
static final class Node {
// 枚举:共享模式
static final Node SHARED = new Node();
// 枚举:独占模式
static final Node EXCLUSIVE = null;
// node 需要构建成 FIFO 队列,prev 指向前继节点
volatile Node prev;
// next 指向后继节点
volatile Node next;
// 当前 node 封装的线程
volatile Thread thread;
// 条件队列是单向链表,只有后继指针,条件队列使用该属性
Node nextWaiter;
}
条件变量来实现等待、唤醒机制,支持多个条件变量,类似于 Monitor 的 WaitSet,条件队列是单向链表
public class ConditionObject implements Condition, java.io.Serializable {
// 指向条件队列的第一个 node 节点
private transient Node firstWaiter;
// 指向条件队列的最后一个 node 节点
private transient Node lastWaiter;
}
为什么队列要设计成双向链表?
1.5 自定义同步器
AbstractQueuedSynchronizer是一个用于构建锁和相关同步组件的基础框架。那我们来使用一下这个基础框架。
我们定义一个类,来继承AQS这个类
子类主要实现这些方法,如果不重写,就会抛出异常。
- tryAcquire 尝试获取锁
- tryRelease 尝试释放锁
- tryAcquireShared
- tryReleaseShared
- isHeldExclusively
public class demo14 {
public static void main(String[] args) throws InterruptedException {
MyLock1 lock = new MyLock1();
new Thread(() -> {
lock.lock();
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread() + "locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread() + "unlocking");
lock.unlock();
}
}, "t1").start();
new Thread(() -> {
lock.lock();
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread() + "locking");
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread() + "unlocking");
lock.unlock();
}
}, "t2").start();
}
}
class MySync extends AbstractQueuedSynchronizer {
protected MySync() {
super();
}
@Override
protected boolean tryAcquire(int arg) {
if (getState() == 0) {
if (compareAndSetState(0, 1)) {
setExclusiveOwnerThread(Thread.currentThread());
return true;
}
}
return false;
}
@Override
protected boolean tryRelease(int arg) {
if (arg == 1) {
if (getState() == 0) {
throw new IllegalMonitorStateException();
}
setExclusiveOwnerThread(null);
setState(0);
return true;
}
return false;
}
@Override
protected int tryAcquireShared(int arg) {
return super.tryAcquireShared(arg);
}
@Override
protected boolean tryReleaseShared(int arg) {
return super.tryReleaseShared(arg);
}
@Override
protected boolean isHeldExclusively() {
return getState() == 1;
}
@Override
public String toString() {
return super.toString();
}
public Condition newCondition() {
return new ConditionObject();
}
}
class MyLock1 implements Lock {
static MySync sync = new MySync();
@Override
public void lock() {
sync.acquire(1);
}
@Override
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
@Override
public boolean tryLock() {
return sync.tryAcquire(1);
}
@Override
public boolean tryLock(long time, TimeUnit unit) throws InterruptedException {
return sync.tryAcquireNanos(1, unit.toNanos(time));
}
@Override
public void unlock() {
sync.release(1);
}
@Override
public Condition newCondition() {
return sync.newCondition();
}
}
2024-08-08T21:15:38.677 Thread[t1,5,main]locking
2024-08-08T21:15:39.677 Thread[t1,5,main]unlocking
2024-08-08T21:15:39.677 Thread[t2,5,main]locking
2024-08-08T21:15:40.678 Thread[t2,5,main]unlocking
我们自己实现了一个锁,真棒。
二、ReentrantLock 原理
2.1 ReentrantLock 和 synchronized 的区别
- 锁的实现:synchronized 是JVM实现的,而** reentrantLock 是JDK实现的**
- 性能:新版本 Java 对 synchronized 进行了很多优化,synchronized 与 ReentrantLock 大致相同
- 使用:ReentrantLock 需要手动解锁,synchronized 执行完代码块自动解锁
- 可中断:ReentrantLock 可中断,而 synchronized 不行
- 公平锁:公平锁是指多个线程在等待同一个锁时,必须按照申请锁的时间顺序来依次获得锁
- ReentrantLock 可以设置公平锁,synchronized 中的锁是非公平的
- 不公平锁的含义是阻塞队列内公平,队列外非公平
- 锁超时:尝试获取锁,超时获取不到直接放弃,不进入阻塞队列
- ReentrantLock 可以设置超时时间,synchronized 会一直等待
- 锁绑定多个条件:一个 ReentrantLock 可以同时绑定多个 Condition 对象,更细粒度的唤醒线程
- 两者都是可重入锁
2.2 非公平锁实现原理
2.2.1 加锁流程
- 创建锁对象调用lock方法
public static void main(String[] args) throws InterruptedException {
ReentrantLock reentrantLock = new ReentrantLock; // 默认创建非公平锁
reentrantLock.lock(); // 加锁
reentrantLock.unlock(); // 解锁
}
- 进入lock方法
public void lock() {
sync.lock();
}
- 我们这里选择公平锁的lock实现方法
/**
* 尝试获取锁
*/
final void lock() {
// 如果当前状态为0,则尝试获取锁,CAS加锁,设置状态为1,加锁成功
if (compareAndSetState(0, 1)) {
// 设置当前线程为获取锁的线程
setExclusiveOwnerThread(Thread.currentThread());
} else {
// 获取锁失败,则调用acquire方法
acquire(1);
}
}
- 加锁失败,表示出现了竞争,那么我们就需要尝试让这个线程进入等待队列
public final void acquire(int arg) {
// 1. 先再一次尝试获取锁
// 2.如果获取锁失败尝试将该线程加入等待队列中
// 2.1 如果加入等待队列中成功了,那么就打断该线程
// 3.如果获取锁成功,则直接返回。
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
- 尝试获取锁
/**
* 执行不公平的尝试锁定。
* tryAcquire在子类中实现,但两者都需要对trylock方法进行非公平尝试。
* 所以直接在这里写这个方法,方便重用
*
* @param acquires
*/
final boolean nonfairTryAcquire(int acquires) {
// 当前线程
final Thread current = Thread.currentThread();
// 获取当前状态 0表示没有被获取,>0 表示被获取了
int c = getState();
if (c == 0) {
// CAS 加锁, 这里只加一次
if (compareAndSetState(0, acquires)) {
// 设置当前线程为获取锁的线程
setExclusiveOwnerThread(current);
return true;
}
}
// 如果该锁已经被当前线程获取,说明这里是锁重入
else if (current == getExclusiveOwnerThread()) {
// 锁重入 nextc表示锁重入了几次
int nextc = c + acquires;
// 判断锁重入次数是否超过最大值,超过最大值报错异常
if (nextc < 0)
throw new Error("Maximum lock count exceeded");
// 设置当前锁的状态
setState(nextc);
return true;
}
return false;
}
- 由于锁没有被释放,所有加锁肯定失败。接下来进入addWaiter,创建一个节点加入到等待队列中。
- Node 的创建是懒惰的,其中第一个 Node 称为 Dummy(哑元)或哨兵,用来占位,并不关联线程
暂时无法在飞书文档外展示此内容
private Node addWaiter(Node mode) {
// mode == null
// 创建一个新节点,存储该线程
Node node = new Node(Thread.currentThread(), mode);
// pred指向尾节点
Node pred = tail;
// 如果尾节点不为空,说明哨兵或者队列中有线程已经创建过了。
if (pred != null) {
// 这里使用的是尾插法
// 新节点的前驱指向尾节点
node.prev = pred;
// 尾节点指向该线程
if (compareAndSetTail(pred, node)) {
// 尾节点的后继指向该线程
pred.next = node;
// 直接返回
return node;
}
}
// 如果刚才创建失败或者尾节点为空
// 如果尾节点为空那就需要先创建一个哨兵
// 如果是因为CAS加锁失败,说明有线程竞争
// 需要重新加锁尝试,所以就都来到了enq方法
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;
}
}
}
}
- 线程加入到等待队列中之后,然后再次尝试阻塞线程
- acquireQueued 会在一个自选中不断尝试获得锁,失败后进入park阻塞
- 如果当前线程是在head节点后,会再次尝试获取锁,但这里仍然会失败。
final boolean acquireQueued(final Node node, int arg) {
// 抢占锁是否成功,true表示失败,false表示成功
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);
}
}
- 进入shouldParkAfterFailedAcquire逻辑,将前驱节点的waitStatus改为-1,返回false,waitStatus 为 -1 的节点用来唤醒下一个节点
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
int ws = pred.waitStatus;
if (ws == Node.SIGNAL)
// 前一个节点状态是等待唤醒状态(-1)
// 说明还需要排队,那就需要先暂停。
// 后面的节点可以安全暂停(park)
return true;
if (ws > 0) {
// 前置节点已经被取消(ws=1>0),跳过前置节点,并且重新设置前置节点
// 这里的思路是如果前置节点已经是取消状态,就跳过当前的前置节点继续向前找有效的前置节点
// 直接把已经被取消的节点都删除掉。
do {
node.prev = pred = pred.prev;
} while (pred.waitStatus > 0);
pred.next = node;
} else {
// 状态是0或者-3,的情况下线,设置前置节点为 -1 需要唤醒状态
compareAndSetWaitStatus(pred, ws, Node.SIGNAL);
}
// 返回false会产生自旋,在下一次进入当前函数的时候就会返回true
return false;
}
- shouldParkAfterFailedAcquire 执行完毕回到 acquireQueued ,再次 tryAcquire 尝试获取锁,这时 state 仍为 1 获取失败(第四次)
- 当再次进入 shouldParkAfterFailedAcquire 时,这时其前驱 node 的 waitStatus 已经是 -1 了,返回 true
- 进入 parkAndCheckInterrupt, Thread-1 park(灰色表示)
/**
* 该方法让线程去休息,真正进入等待状态。
* park()会让当前线程进入waiting状态。
* 在此状态下,有两种途径可以唤醒该线程:1)被unpark();2)被interrupt()。
* 需要注意的是,Thread.interrupted()会清除当前线程的中断标记位。
* 预备知识补充=======================
* 2.预备知识
* 2.1.park、unpark、interrupt、isInterrupted、interrupted方法的理解
* 一:park、unpark
* 1.park、unpark它不是Thread中的方法,而是LockSupport.park(),LockSupport是JUC中的对象;
* 2.park可以让线程暂停 (只有在isInterrupted状态为false的情况下才有效),unpark可以让暂停的线程继续执行;
* <p>
* 二:interrupt、isInterrupted、interrupted
* 1.interrupt、isInterrupted、interrupted 它是Thread中的方法;
* 2.interrupt 设置一个中断标记,interrupt()方法可以让暂停的方法继续执行,通俗直观的理解是,设置打断标记后,处于暂停状态的线程就会被唤醒,如果是睡眠的就抛出中断异常;
* 3.isInterrupted 这个好理解就是查看当前线程的中断标记;
* 4.interrupted 这是一个静态方法,只能这样调用Thread.interrupted(),这个方法会返回当前interrupt状态,如果interrupt=true会将其改变为 false,反之则不成立;
*/
private final boolean parkAndCheckInterrupt() {
LockSupport.park(this);
// // Thread.interrupted() 返回当前的interrupted状态,如果之前是true,即有中断标记,修改为false,即清除中断标记
return Thread.interrupted();
}
再有多个线程经历竞争失败后:
2.2.2 锁释放
- 释放锁
@Override
public void unlock() {
sync.release(1);
}
- 进入release流程
- 进入 tryRelease,设置 exclusiveOwnerThread 为 null,state = 0。再看是否有锁重入,如果有锁重入,那么释放锁就失败
// 锁释放,并唤醒下一个节点
public final boolean release(int arg) {
// 先尝试释放锁
if (tryRelease(arg)) {
// 如果释放锁成功,看哨兵节点的waitStatus是否等于-1,这样就可以去唤醒第一个线程节点
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
/**
* 释放锁
* protected:为什么要加这个
*/
protected final boolean tryRelease(int releases) {
// 获取当前线程状态 - 1 表示释放了一个锁(可能有锁重入)
int c = getState() - releases;
// 判断当前线程是否是获取锁的线程,如果不是,报错
if (Thread.currentThread() != getExclusiveOwnerThread())
throw new IllegalMonitorStateException();
boolean free = false;
if (c == 0) {
// 完全释放锁
free = true;
// 设置锁的线程为null
setExclusiveOwnerThread(null);
}
// 设置当前状态
setState(c);
return free;
}
- unparkSuccessor 尝试唤醒节点
private void unparkSuccessor(Node node) {
// 检查当前哨兵节点的状态
int ws = node.waitStatus;
// 如果小于0,表示可以释放掉该哨兵节点了,设置它的状态为0,
// 但这里为啥要设置为0,并且用CAS加锁的方式,会有线程跟释放锁的线程竞争嘛?
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)
s = t;
}
// 找到了就去唤醒它。
if (s != null)
LockSupport.unpark(s.thread);
}
- 唤醒的节点会从park位置醒来,也就是我们的acquireQueued方法,继续执行。然后又开始尝试获取锁。
- 设置锁的拥有者为当前线程,state=1
- 然后开始修改表头,将当前节点设置为表头,之前的表头给释放掉。然后就可以被垃圾回收。
- 但是有趣的来了,如果当前线程获取锁之前,又来了一个线程获取锁并且成功了,那么当前线程获取锁会失败,并且继续阻塞。
所以这就是不公平锁,那怎么让它公平呢?
答案就是获取锁之前先看队列中有没有节点在阻塞。
protected final boolean tryAcquire(int acquires) {
final Thread current = Thread.currentThread();
int c = getState();
if (c == 0) {
// 判断当前锁中的线程队列是否有线程在等待
// 这样就实现了公平等待,不会被插队
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;
}
- 这里的判断不知道为啥是这样的
public final boolean hasQueuedPredecessors() {
Node t = tail; // Read fields in reverse initialization order
Node h = head;
Node s;
// 1.判断队列是否为空,如果等于空直接返回false
// 2.队列不为空,那就看除了哨兵还没有没有节点,如果没有就直接返回true
return h != t &&
((s = h.next) == null || s.thread != Thread.currentThread());
}
2.2.3 可重入
可重入锁是指同一个线程如果首次获得了这把锁,那么它是这把锁的拥有者,因此有权利再次获得这把锁。如果是不可重入锁,那么第二次获得锁时,自己也会被锁挡住,直接造成死锁。
源码解析参考:nonfairTryAcquire(int acquires))
和 tryRelease(int releases)
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
method1();
}
public static void method1() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " execute method1");
method2();
} finally {
lock.unlock();
}
}
public static void method2() {
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " execute method2");
} finally {
lock.unlock();
}
}
在lock方法方法家两把锁会是什么情况呢?
- 加锁两次解锁两次:正常执行
static ReentrantLock lock = new ReentrantLock();
public static void main(String[] args) {
lock.lock();
lock.lock();
try {
System.out.println(Thread.currentThread().getName() + " execute main");
} finally {
lock.unlock();
lock.unlock();
}
}
- 加锁两次解锁一次:程序直接卡死,线程不能出来
- 加锁两次解锁一次:程序直接卡死,线程不能出来,也就说明申请几把锁,最后需要解除几把锁
- 加锁一次解锁两次:运行程序会直接报错
2.2.4 可打断
不可打断模式:lock()方法是不可打断的,即使它被打断(只会有一个打断标记),仍会驻留在 AQS 队列中,一直要等到获得锁后才能得知自己被打断了。
看源代码,即使被打断了,那也会继续执行,然后再次尝试获取锁,直到获取锁成功,然后将打断标记返回为true,表示在阻塞的过程中被其他线程打断了。原线程会产生一次中断,将打断标记记为true,这样就后面再执行LockSupport.park(this);就不会阻塞了;
// 这是ReentrantLock中的lock()方法
public void lock() {
// 调用非公平锁同步器中的lock()方法
sync.lock();
}
// 这是ReentrantLock中非公平锁的同步器
static final class NonfairSync extends Sync {
// 1.执行lock()
final void lock() {
if (compareAndSetState(0, 1))
setExclusiveOwnerThread(Thread.currentThread());
else
// 2.CAS失败后,执行acquire(1)
acquire(1);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
}
// ====================以下都是AQS中的方法=======================
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
// 3.tryAcquire(1)再次失败,执行acquireQueued()
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
// 7.执行到这里说明:经历过中断并获取到锁,进入selfInterrupt方法再执行一次中断
selfInterrupt();
}
static void selfInterrupt() {
// 7.1 重新产生一次中断,将打断标记置为true,后面再执行LockSupport.park(this);就不会阻塞了
Thread.currentThread().interrupt();
}
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;
// 6.再次进入循环获得锁后,返回打断状态true
return interrupted;
}
if (shouldParkAfterFailedAcquire(p, node) &&
// 4.tryAcquire(1)又一次失败,并且满足线程挂起条件,执行parkAndCheckInterrupt()
parkAndCheckInterrupt())
// 5.如果是因为被打断而唤醒, 记录打断状态为true,再次进入循环进行tryAcquire获得锁后, 才能返回打断状态
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
private final boolean parkAndCheckInterrupt() {
// 4.1 park后会在此处阻塞
// 如果打断标记已经是 true, 则 park 会失效
LockSupport.park(this);
// 4.2 park时,如果被打断,则返回true并清除打断标记(将打断标记置为false)
return Thread.interrupted();
}
可打断模式:
public void lockInterruptibly()
:获得可打断的锁
- 如果没有竞争此方法就会获取 lock 对象锁
- 如果有竞争就进入阻塞队列,可以被其他线程用 interrupt 打断,直接抛出异常
public static void main(String[] args) throws InterruptedException {
ReentrantLock lock = new ReentrantLock();
Thread t1 = new Thread(() -> {
try {
System.out.println("尝试获取锁");
lock.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("获取锁失败");
return;
}
try {
System.out.println("获取锁成功");
} finally {
lock.unlock();
}
}, "t1");
lock.lock();
t1.start();
Thread.sleep(1000);
System.out.println("中断t1线程");
t1.interrupt();
}
代码:
public void lockInterruptibly() throws InterruptedException {
sync.acquireInterruptibly(1);
}
public final void acquireInterruptibly(int arg)
throws InterruptedException {
// 如果这个线程被打断过,直接抛出异常
if (Thread.interrupted())
throw new InterruptedException();
// 先尝试获取锁,如果没获取到,
if (!tryAcquire(arg))
doAcquireInterruptibly(arg);
}
private void doAcquireInterruptibly(int arg)
throws InterruptedException {
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;
}
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
// 如果发现被打断了,直接抛出异常,不会再次进入循环尝试获取锁
throw new InterruptedException();
}
} finally {
// 最后发现获取锁失败,那么就将这个队列出队
if (failed)
cancelAcquire(node);
}
}
private void cancelAcquire(Node node) {
// 判断是否为空
if (node == null)
return;
// 将节点的线程设置为空
node.thread = null;
// 这个节点总共又三种可能性
// 1.尾节点
// 2.头结点的下一个节点
// 3.中间某个节点
// 找到它的前驱,并且跳过那些已经取消的节点
Node pred = node.prev;
while (pred.waitStatus > 0)
node.prev = pred = pred.prev;
// 前驱节点的后继节点,可能是当前node,也可能是已取消的节点
Node predNext = pred.next;
// 设置当前节点为取消状态
node.waitStatus = Node.CANCELLED;
// 如果是尾节点,需要将尾节点改为前驱节点
if (node == tail && compareAndSetTail(node, pred)) {
// 将前驱节点的后继节点设置为空,也就是将所有取消节点出队了。
compareAndSetNext(pred, predNext, null);
} else {
int ws;
// 当前节点是中间节点
if (pred != head &&
// 前驱节点状态是-1,如果不是那就让它变成-1
((ws = pred.waitStatus) == Node.SIGNAL ||
(ws <= 0 && compareAndSetWaitStatus(pred, ws, Node.SIGNAL))) &&
pred.thread != null) {
// 当前节点的下一个节点
Node next = node.next;
if (next != null && next.waitStatus <= 0)
compareAndSetNext(pred, predNext, next);
} else {
// 当前节点是头节点的下一个节点,唤醒当前节点的下一个节点
unparkSuccessor(node);
}
node.next = node; // help GC
}
}
2.2.5 锁超时
public boolean tryLock()
:尝试获取锁,获取到返回 true,获取不到直接放弃,不进入阻塞队列
public boolean tryLock(long timeout, TimeUnit unit)
:在给定时间内获取锁,获取不到就退出
实现原理
- 成员变量:指定超时限制的阈值,小于该值的线程不会被挂机。
static final long spinForTimeoutThreshold = 1000L;
超时时间设置的小于该值,就会被禁止挂起,因为阻塞在唤醒的成本太高,不如选择自选空转。
- tryLock()
public boolean tryLock() {
// 只尝试一次
return sync.nonfairTryAcquire(1);
}
- tryLock(long timeout, TimeUnit unit)
public final boolean tryAcquireNanos(int arg, long nanosTimeout) {
if (Thread.interrupted())
throw new InterruptedException();
// tryAcquire 尝试一次
// 如果尝试一次失败后才执行doAcquireNanos
return tryAcquire(arg) || doAcquireNanos(arg, nanosTimeout);
}
protected final boolean tryAcquire(int acquires) {
return nonfairTryAcquire(acquires);
}
private boolean doAcquireNanos(int arg, long nanosTimeout) {
if (nanosTimeout <= 0L)
return false;
// 获取最后期限的时间戳
final long deadline = System.nanoTime() + nanosTimeout;
//
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;
if (shouldParkAfterFailedAcquire(p, node) &&
// 如果 nanosTimeout 大于该值,才有阻塞的意义,否则直接自旋会好点
nanosTimeout > spinForTimeoutThreshold)
LockSupport.parkNanos(this, nanosTimeout);
// 【被打断会报异常】
if (Thread.interrupted())
throw new InterruptedException();
}
}
}
2.2.6 条件变量
基本使用:
synchronized的条件变量,适当条件不满足时进入 WaitSet等待;ReentrantLock的条件变量比 synchronized 强大之处在于支持多个条件变量。
ReentrantLock 类获取Condition 对象:public Condition newCondition()
Condition 类 API:
void await()
:当前线程从运行状态进入等待状态,释放锁。void signal()
:唤醒一个等待在 Condition 上的线程,但是必须获得与该 Condition 相关的锁。
使用流程:
- await/signal 前需要获得锁。
- await执行后,会释放锁进入 ConditionObject 等待。
- await 的线程被唤醒去重新竞争 lock 锁。
- 线程在条件队列被打断会抛出中断异常。
- 竞争lock锁成功后,会从await后继续执行。
public class demo18 {
static ReentrantLock lock = new ReentrantLock();
static Condition A = lock.newCondition();
static Condition B = lock.newCondition();
static boolean hasA = false;
static boolean hasB = false;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
try {
lock.lock();
while (!hasA) {
System.out.println("A等待");
A.await();
}
System.out.println("获取到了A");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
new Thread(() -> {
try {
lock.lock();
while (!hasB) {
System.out.println("B等待");
B.await();
}
System.out.println("获取到了B");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
}).start();
Thread.sleep(1000);
ProductA();
Thread.sleep(1000);
ProductB();
}
private static void ProductA() {
lock.lock();
try {
System.out.println("A来了");
hasA = true;
A.signal();
} finally {
lock.unlock();
}
}
private static void ProductB() {
lock.lock();
try {
System.out.println("B来了");
hasB = true;
B.signal();
} finally {
lock.unlock();
}
}
}
A等待
B等待
A来了
获取到了A
B来了
获取到了B
实现原理
总体流程是将 await 线程包装成 node 节点放入 ConditionObject 的条件队列,如果被唤醒就将 node 转移到 AQS 的执行阻塞队列,等待获取锁,每个 Condition 对象都包含一个等待队列
- 加入等待队列
- 释放锁
- 阻塞线程
- 被唤醒
- 加入阻塞队列
- 获取锁
- 从等待队列中删除
- 判断是正常执行还是抛出异常
// 打断模式 - 在退出等待时重新设置打断状态
private static final int REINTERRUPT = 1;
// 打断模式 - 在退出等待时抛出异常
private static final int THROW_IE = -1;
public final void await() throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 添加当条件队列中
Node node = addConditionWaiter();
// 释放掉当前线程获取的所有的锁
int savedState = fullyRelease(node);
// 有没有被打断
int interruptMode = 0;
// 该节点是否在AQS阻塞队列中,如果不在就进行阻塞
while (!isOnSyncQueue(node)) {
LockSupport.park(this);
// 如果正常唤醒,就尝试加入阻塞队列中,然后看能不能加入
// 如果是被打断唤醒,看看是因为什么
if ((interruptMode = checkInterruptWhileWaiting(node)) != 0)
break;
}
// 即使是因为在等待队列中被打断,也是需要拿到锁之后才抛异常
// 所以即使在等待队列中被打断了,也需要加入到阻塞队列中等待获取锁
// 走到这里说明两种情况:
// 1.正常唤醒,该线程已经退出等待队列,加入到阻塞i队列中,interruptMode = 0
// 2.异常唤醒,被打断,interruptMode = REINTERRUPT / THROW_IE
// 尝试获取锁,返回true表示在获取锁的过程中被打断了
// 如果被打断了,判断是不是在等待队列中打断的
if (acquireQueued(node, savedState) && interruptMode != THROW_IE)
// 如果是在阻塞队列中发生的异常
// 1.在acquireQueued发生打断
// 2.在checkInterruptWhileWaiting发生打断
// 这俩都是在阻塞队列中打断
interruptMode = REINTERRUPT;
// 等待队列中的节点被唤醒的时候,是不会主动从等待队列中删除,需要手动删除
if (node.nextWaiter != null) // clean up if cancelled
unlinkCancelledWaiters();
// 如果被打断过
// 条件队列中被打断需要抛异常
// 阻塞队列中打断需要再次打断,表示已经被打断过了
if (interruptMode != 0)
reportInterruptAfterWait(interruptMode);
}
private Node addConditionWaiter() {
Node t = lastWaiter;
// 当前队列不为空,并且切点的状态不是 -2,说明尾节点不在等待队列中,将他们清除掉
if (t != null && t.waitStatus != Node.CONDITION) {
unlinkCancelledWaiters();
// 重新获取最新的尾节点
t = lastWaiter;
}
// 创建一个当前线程的新节点,设置状态为CONDITION,并添加到队列尾部。
Node node = new Node(Thread.currentThread(), Node.CONDITION);
if (t == null)
firstWaiter = node;
else
t.nextWaiter = node;
lastWaiter = node;
return node;
}
private void unlinkCancelledWaiters() {
// 头节点
Node t = firstWaiter;
// 指向上一个正常的节点
Node trail = null;
while (t != null) {
// 当前节点的后继节点
Node next = t.nextWaiter;
// 不是正常等待节点
if (t.waitStatus != Node.CONDITION) {
// 将这个节点与下一个节点断开,以便回收
t.nextWaiter = null;
// 如果还没有遇到正常节点,将头节点指向下一个节点,但是这里下一个节点也不一定是正常的
// 如果下一个不是正常的,那头节点继续赋值给下下个节点
// 如果下一个是正常的,那这里赋值完就ok了,不用在对头节点赋值了
// 如果遇到了正常节点,那么trail就不等于null,头节点也不需要更新
if (trail == null)
firstWaiter = next;
else
// 上一个正常节点的下一个节点指向next节点,但这里next节点也不一定是正常的
// 如果下一个节点不是正常的,那么会继续赋值给下下个节点
// 但如果下一个节点是正常的,那这里就赋值正确
trail.nextWaiter = next;
// 如果下一个节点为空,那么就是尾节点了,让尾节点指向最后一个正常节点trail;
if (next == null)
lastWaiter = trail;
}
else
// trail指向最近的一个正常节点
trail = t;
// 循环遍历
t = next;
}
}
我感觉这段代码写的不好,我写了一个改进版的:
这里我直接让trail初始化一个节点,但不赋值,只是用来当一个哨兵。
只要遇到正常节点,就将trail的下一个节点指向当前节点,然后更新trail为当前节点
如果遇到不正常的,直接将它的下一个几点赋值为空。
最后,将头节点指向temp的下一个节点,尾节点指向trail。
private void unlinkCancelledWaiters() {
Node t = firstWaiter;
Node trail = new Node();
Node temp = trail;
while (t != null) {
Node next = t.nextWaiter;
if (t.waitStatus != Node.CONDITION) {
t.nextWaiter = null;
} else {
trail.nextWaiter = t;
trail = t;
}
t = next;
}
firstWaiter = temp.nextWaiter;
lastWaiter = trail;
}
释放锁
// 线程可能有锁重入,获得了多把锁,需要全部释放
final int fullyRelease(Node node) {
// 释放是否失败
boolean failed = true;
try {
// 获取锁重入的个数
int savedState = getState();
// 全部释放掉
if (release(savedState)) {
// 释放成功,返回锁重入的个数
failed = false;
return savedState;
} else {
// 释放锁失败,抛出异常
throw new IllegalMonitorStateException();
}
} finally {
// 如果失败了,直接标记该节点为取消节点
if (failed)
node.waitStatus = Node.CANCELLED;
}
}
public final boolean release(int arg) {
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;
if (c == 0) {
free = true;
setExclusiveOwnerThread(null);
}
setState(c);
return free;
}
final boolean isOnSyncQueue(Node node) {
// 1.表示当前节点在条件队列中
// 2.表示不在条件队列中,但也没有加入阻塞队列中
if (node.waitStatus == Node.CONDITION || node.prev == null)
return false;
// 表示不在条件队列中,而且加入到了阻塞队列中,并且是中间某个节点,因为尾节点的next等于null
if (node.next != null) // If has successor, it must be on queue
return true;
// 说明这个节点是尾节点
// 从阻塞队列的尾节点开始向前【遍历查找 node】,如果查找到返回 true,查找不到返回 false
// 为啥要从后往前呢?因为加入的队列肯定在队尾后面几个,从后开始找更快
return findNodeFromTail(node);
}
/**
* Returns true if node is on sync queue by searching backwards from tail.
* Called only when needed by isOnSyncQueue.
* @return true if present
*/
private boolean findNodeFromTail(Node node) {
Node t = tail;
for (;;) {
if (t == node)
return true;
if (t == null)
return false;
t = t.prev;
}
}
await 线程 park 后如果被 unpark 或者被打断,都会进入 checkInterruptWhileWaiting 判断线程是否被打断:在条件队列被打断的线程需要抛出异常
检查在节点等待过程中是否被打断
private int checkInterruptWhileWaiting(Node node) {
// 如果被打断了,检查打断过程中是否被外部线程唤醒了
return Thread.interrupted() ?
(transferAfterCancelledWait(node) ? THROW_IE : REINTERRUPT) :
0;
}
final boolean transferAfterCancelledWait(Node node) {
// 如果设置成功说明当前node一定在条件队列中
// 因为如果时因为signal唤醒的话就会设置节点状态为0,这里时并发的,
// 有可能是打断线程,也有可能是唤醒线程。
if (compareAndSetWaitStatus(node, Node.CONDITION, 0)) {
enq(node); // 加入到AQS阻塞队列中,等待获取锁
return true;
}
// 走到这里说明当前node已经被外部线程调用 signal 方法将其迁移到阻塞队列了(有可能正在迁移)
// 如果还没有迁移成功的话,当前线程让步,释放CPU给外部线程使用。
while (!isOnSyncQueue(node))
Thread.yield();
// 表示是在条件队列外发生的中断
return false;
}
private void reportInterruptAfterWait(int interruptMode) throws InterruptedException {
// 条件成立说明【在条件队列内发生过中断,此时 await 方法抛出中断异常】
if (interruptMode == THROW_IE)
throw new InterruptedException();
// 条件成立说明【在条件队列外发生的中断,此时设置当前线程的中断标记位为 true】
else if (interruptMode == REINTERRUPT)
// 进行一次自己打断,产生中断的效果
selfInterrupt();
}
Signal:
public final void signal() {
// 判断调用 signal 方法的线程是否是当前锁占有的线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
Node first = firstWaiter;
// 如果等待队列不为空,就将该节点放入AQS迁移队列中
if (first != null)
doSignal(first);
}
private void doSignal(Node first) {
do {
// 将firstWaiter指向firstWaiter的下一个节点
// 如果这个节点为空表明队列中没有节点了,尾节点也设置为空
if ( (firstWaiter = first.nextWaiter) == null)
lastWaiter = null;
// 释放掉该节点
first.nextWaiter = null;
} while (!transferForSignal(first) &&
(first = firstWaiter) != null);
}
// 将该节点转移到阻塞队列中
final boolean transferForSignal(Node node) {
// 先设置节点状态
// 如果失败说明线程被取消或者被中断
if (!compareAndSetWaitStatus(node, Node.CONDITION, 0))
return false;
// 加入到阻塞队列中,p是当前节点的前驱节点
Node p = enq(node);
int ws = p.waitStatus;
// 如果前驱节点被取消或者不能设置状态为SIGNAL,就取消当前节点的阻塞状态。
if (ws > 0 || !compareAndSetWaitStatus(p, ws, Node.SIGNAL))
// 这唤醒了,
LockSupport.unpark(node.thread);
return true;
}
三、ReadWrite
3.1 读写锁
独占锁:指该锁一次只能被一个线程所持有,对 ReentrantLock 和 Sunchronized 而言都是独占锁
共享锁:指该锁可以被多个线程锁持有。
ReentrantReadWriteLock 的读锁是共享锁,写锁是独占锁
作用:多个线程同时读一个资源类没有任何问题,为了满足并发量,读取共享资源应该同时进行,但是如果一个线程想去写共享资源,就不应该再有其他线程可以对该资源进行读或写。
使用规则:
加锁解锁格式:
r.lock();
try {
// 临界区
} finally {
r.unlock();
}
- 读-读能共存、读-写不能共存、写-写不能共存
- 读锁不支持条件变量
- 重入时升级不支持:持有读锁的情况下去获取写锁会导致获取写锁永久等待,需要先释放读,再去获得写
- 重入时降级支持:持有写锁的情况下去获取读锁,造成只有当前线程会持有读锁,因为写锁会互斥其他的锁
构造方法:
public ReentrantReadWriteLock()
:默认构造方法,非公平锁public ReentrantReadWriteLock(boolean fair)
:true 为公平锁
常用API:
public ReentrantReadWriteLock.ReadLock readLock()
:返回读锁public ReentrantReadWriteLock.WriteLock writeLock()
:返回写锁public void lock()
:加锁public void unlock()
:解锁public boolean tryLock()
:尝试获取锁
举个例子:
读读可以并发
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
new Thread(() -> {
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 尝试获取读锁");
readLock.lock();
Thread.sleep(1000);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 获取读锁成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 释放读锁");
}
}, "t1").start();
new Thread(() -> {
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 尝试获取读锁");
readLock.lock();
Thread.sleep(1000);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 获取读锁成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 释放读锁");
}
}, "t2").start();
}
2024-08-13T22:28:04.438 t1 尝试获取读锁
2024-08-13T22:28:04.438 t2 尝试获取读锁
2024-08-13T22:28:05.439 t1 获取读锁成功
2024-08-13T22:28:05.439 t2 获取读锁成功
2024-08-13T22:28:05.439 t1 释放读锁
2024-08-13T22:28:05.439 t2 释放读锁
读写会互斥:
public static void main(String[] args) {
ReentrantReadWriteLock readWriteLock = new ReentrantReadWriteLock();
ReentrantReadWriteLock.ReadLock readLock = readWriteLock.readLock();
ReentrantReadWriteLock.WriteLock writeLock = readWriteLock.writeLock();
new Thread(() -> {
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 尝试获取读锁");
readLock.lock();
Thread.sleep(1000);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 获取读锁成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
readLock.unlock();
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 释放读锁");
}
}, "t1").start();
new Thread(() -> {
try {
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 尝试获取写锁");
writeLock.lock();
Thread.sleep(1000);
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 获取写锁成功");
} catch (Exception e) {
e.printStackTrace();
} finally {
writeLock.unlock();
System.out.println(LocalDateTime.now() + " " + Thread.currentThread().getName() + " 释放写锁");
}
}, "t2").start();
}
2024-08-13T22:27:00.094 t1 尝试获取读锁
2024-08-13T22:27:00.094 t2 尝试获取写锁
2024-08-13T22:27:01.095 t1 获取读锁成功
2024-08-13T22:27:01.095 t1 释放读锁
2024-08-13T22:27:02.096 t2 获取写锁成功
2024-08-13T22:27:02.096 t2 释放写锁
3.2 实现原理
3.2.1 成员属性
读写锁用的是同一个Sync
同步器,因此等待队列、state等也是同一个,原理与ReentrantLock
加锁没有特别之处,不同时写锁状态占了state
的低16位,读锁占用了state
的高16位。
- 读写锁:
private final ReentrantReadWriteLock.ReadLock readerLock;
private final ReentrantReadWriteLock.WriteLock writerLock;
- 构造方法
public ReentrantReadWriteLock(boolean fair) {
// true 为公平锁
sync = fair ? new FairSync() : new NonfairSync();
// 这两个 lock 共享同一个 sync 实例,都是由 ReentrantReadWriteLock 的 sync 提供同步实现
readerLock = new ReadLock(this);
writerLock = new WriteLock(this);
}
Sync类的属性:
- 统计变量:
// 用来移位
static final int SHARED_SHIFT = 16;
// 高16位的1
static final int SHARED_UNIT = (1 << SHARED_SHIFT);
// 65535,16个1,代表写锁的最大重入次数
static final int MAX_COUNT = (1 << SHARED_SHIFT) - 1;
// 低16位掩码:0b 1111 1111 1111 1111,用来获取写锁重入的次数
static final int EXCLUSIVE_MASK = (1 << SHARED_SHIFT) - 1;
- 获取读写锁的次数
// 获取读写锁的读锁分配的总次数
static int sharedCount(int c) { return c >>> SHARED_SHIFT; }
// 写锁(独占)锁的重入次数
static int exclusiveCount(int c) { return c & EXCLUSIVE_MASK; }
- 内部类
// 记录读锁线程自己的持有读锁的数量(重入次数),因为 state 高16位记录的是全局范围内所有的读线程获取读锁的总量
static final class HoldCounter {
int count = 0;
// 这是弱引用嘛
// Use id, not reference, to avoid garbage retention
final long tid = getThreadId(Thread.currentThread());
}
// 线程安全的存放线程各自的 HoldCounter 对象
static final class ThreadLocalHoldCounter extends ThreadLocal<HoldCounter> {
public HoldCounter initialValue() {
return new HoldCounter();
}
}
- 内部类实例
// 当前线程持有的可重入读锁的数量,计数为 0 时删除
private transient ThreadLocalHoldCounter readHolds;
// 记录最后一个获取【读锁】线程的 HoldCounter 对象
private transient HoldCounter cachedHoldCounter;
- 首次获得锁:
// 第一个获取读锁的线程
private transient Thread firstReader = null;
// 记录该线程持有的读锁次数(读锁重入次数)
private transient int firstReaderHoldCount;
- Sync 构造方法:
Sync() {
readHolds = new ThreadLocalHoldCounter();
// 确保其他线程的数据可见性,state 是 volatile 修饰的变量,重写该值会将线程本地缓存数据【同步至主存】
setState(getState());
}
3.2.2 图解流程
- t1线程:w.lock
t1成功加锁
public void lock() {
sync.acquire(1);
}
public final void acquire(int arg) {
if (!tryAcquire(arg) &&
acquireQueued(addWaiter(Node.EXCLUSIVE), arg))
selfInterrupt();
}
protected final boolean tryAcquire(int acquires) {
Thread current = Thread.currentThread();
int c = getState();
// 获取低16位,表示写锁的个数
int w = exclusiveCount(c);
// 如果有读锁或者写锁
if (c != 0) {
// 1.W==0,表示没有写锁,但是c != 0,说明有读锁,获取锁失败
// 2.w != 0,说明有写锁,判断持有线程的锁是不是自己的
if (w == 0 || current != getExclusiveOwnerThread())
return false;
// 走到这里,说明有写锁,并且写锁是自己的,说明发生了写锁重入,并且此时没有并发
// 如果超过最大值,直接抛异常
if (w + exclusiveCount(acquires) > MAX_COUNT)
throw new Error("Maximum lock count exceeded");
// 更新state
setState(c + acquires);
return true;
}
// 走到这里说明没有写锁也没有读锁
// 判断写锁是否该阻塞,这里分为公平锁和非公平锁
// 如果不该阻塞,尝试CAS加锁
if (writerShouldBlock() ||
!compareAndSetState(c, c + acquires))
// 加锁失败
return false;
// 成功,设置锁的持有线程为当前线程。
setExclusiveOwnerThread(current);
return true;
}
// 非公平锁 writerShouldBlock 总是返回 false, 无需阻塞
final boolean writerShouldBlock() {
return false;
}
// 公平锁会检查 AQS 队列中是否有前驱节点, 没有(false)才去竞争
final boolean writerShouldBlock() {
return hasQueuedPredecessors();
}
- t2线程:r.lock
public void lock() {
sync.acquireShared(1);
}
public final void acquireShared(int arg) {
// tryAcquireShared 返回
// -1 :表示获取锁失败
// 0:表示成功
// 正数:表示还有多少后继节点支持共享模式,读写锁为-1
if (tryAcquireShared(arg) < 0)
doAcquireShared(arg);
}
protected final int tryAcquireShared(int unused) {
Thread current = Thread.currentThread();
int c = getState();
// exclusiveCount:低16为,写锁
// 说明有写锁并且写锁不是自己的返回失败,加入阻塞队列中
// 如果有写锁,并且是自己的,那可以写锁降级为读锁
if (exclusiveCount(c) != 0 &&
getExclusiveOwnerThread() != current)
return -1;
// sharedCount 高16位:读锁个数
int r = sharedCount(c);
// 走到这里,有两种情况
// 1.有写锁,但写锁是自己的
// 2.没写锁
// 读锁应该阻塞吗
if (!readerShouldBlock() &&
r < MAX_COUNT &&
compareAndSetState(c, c + SHARED_UNIT)) { // 修改读锁个数
// 这里修改成功,接下来就没有并发了
// 如果加锁之前读锁为0,说明当前线程是第一个读锁线程
if (r == 0) {
firstReader = current;
firstReaderHoldCount = 1;
// 第一个读锁线程是自己就发生了读锁重入
} else if (firstReader == current) {
firstReaderHoldCount++;
} else {
// 当前线程不是第一个读线程,那就设置为最后一个获取读锁的线程
HoldCounter rh = cachedHoldCounter;
// 如果最后一个获取读锁的线程不为空
if (rh == null || rh.tid != getThreadId(current))
cachedHoldCounter = rh = readHolds.get();
// 到这里说明,cachedHoldCounter不为null,且是当前线程
// 如果没有重入
else if (rh.count == 0)
readHolds.set(rh);
// 重入次数+1
rh.count++;
}
return 1;
}
// 到这里,说明要么应该阻塞,要么CAS加锁失败
// 那就会不断尝试获取读锁,
return fullTryAcquireShared(current);
}
// 不公平锁
final boolean readerShouldBlock() {
return apparentlyFirstQueuedIsExclusive();
}
// AQS,看队列中第一个是否是独占锁(写锁)
// 如果有写锁,那就需要阻塞
// 如果没有那就不需要阻塞
// 偏向写锁,避免写锁一直饥饿。如果阻塞队列中的第一个是写锁,那当前读锁就阻塞。
final boolean apparentlyFirstQueuedIsExclusive() {
Node h, s;
return (h = head) != null &&
(s = h.next) != null &&
!s.isShared() &&
s.thread != null;
}
// 公平锁
final boolean readerShouldBlock() {
return hasQueuedPredecessors();
}
final int fullTryAcquireShared(Thread current) {
/*
* This code is in part redundant with that in
* tryAcquireShared but is simpler overall by not
* complicating tryAcquireShared with interactions between
* retries and lazily reading hold counts.
*/
//
HoldCounter rh = null;
for (;;) {
int c = getState();
// 如果有写锁
if (exclusiveCount(c) != 0) {
//写锁不是自己的,那直接返回
if (getExclusiveOwnerThread() != current)
return -1;
// else we hold the exclusive lock; blocking here
// would cause deadlock.
// 是否应该阻塞
} else if (readerShouldBlock()) {
// Make sure we're not acquiring read lock reentrantly
// 如果当前线程是firstReader ,那说明会读锁重入
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");
// 1.没写锁
// 2.有写锁并且是自己的
// 我想明白为啥可以死循环了?一开始觉得如果加锁失败了应该阻塞来着?
// 是这样的,只要没写锁,那么读锁不是互斥的,大家都尝试加锁,总会成功的
// 只有写锁存在的时候,读锁才需要阻塞,否则就一直尝试获取读锁
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;
}
}
}
获取读锁失败,就需要加入阻塞队列阻塞,
private void doAcquireShared(int arg) {
// 加入到阻塞队列中,节点是共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
if (p == head) {
// 如果前驱节点是头节点就再次尝试获取锁
int r = tryAcquireShared(arg);
// 获取锁成功
if (r >= 0) {
// 设置自己为头节点,并且唤醒相连的后续的共享节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果发生打断就自我打断一下
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 是否在获取锁失败后阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
如果没有成功,在 doAcquireShared 内 for (;😉 循环一次,shouldParkAfterFailedAcquire 内把前驱节点的 waitStatus 改为 -1,再 for (;😉 循环一次尝试 tryAcquireShared,不成功在 parkAndCheckInterrupt() 处 park
这种状态下,假设又有 t3 r.lock,t4 w.lock,这期间 t1 仍然持有锁,就变成了下面的样子
t1:w.unlock,写锁解锁的代码
public void unlock() {
// 释放锁
sync.release(1);
}
public final boolean release(int arg) {
// 尝试释放锁
if (tryRelease(arg)) {
Node h = head;
// 唤醒后继节点
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
protected final boolean tryRelease(int releases) {
// 锁的持有线程不是当前线程
if (!isHeldExclusively())
throw new IllegalMonitorStateException();
// 变化后的state值
int nextc = getState() - releases;
// 写锁全部释放的时候才算释放成功
boolean free = exclusiveCount(nextc) == 0;
if (free)
setExclusiveOwnerThread(null);
setState(nextc);
return free;
}
- 唤醒流程 sync.unparkSuccessor,这时 t2 在 doAcquireShared 的 parkAndCheckInterrupt() 处恢复运行,继续循环,执行 tryAcquireShared 成功则让读锁计数加一
- 接下来 t2 调用 setHeadAndPropagate(node, 1),它原本所在节点被置为头节点;还会检查下一个节点是否是 shared,如果是则调用 doReleaseShared() 将 head 的状态从 -1 改为 0 并唤醒下一个节点,这时 t3 在 doAcquireShared 内 parkAndCheckInterrupt() 处恢复运行,唤醒连续的所有的共享节点
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置当前节点为头节点
setHead(node);
// propagate 表示有共享资源
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
if (s == null || s.isShared())
// 如果下一个节点是空或是共享的,就唤醒它
doReleaseShared();
}
}
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
private void doReleaseShared() {
// 读锁是共享锁,可能会有多个线程同时释放读锁
// 所以说这里是并发的
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// SIGNAL 唤醒后继
if (ws == Node.SIGNAL) {
// 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果h==head,说明后继线程还没有更新头节点
// 那么留着下一个节点去唤醒下下个节点
//如果不等于,说明后继线程已经更改了头节点,那它俩一块继续并发唤醒后继节点
if (h == head) // loop if head changed
break;
}
}
- 下一个节点不是 shared 了,因此不会继续唤醒 t4 所在节点
- t2 读锁解锁,进入 sync.releaseShared(1) 中,调用 tryReleaseShared(1) 让计数减一,但计数还不为零,t3 同样让计数减一,计数为零,进入doReleaseShared() 将头节点从 -1 改为 0 并唤醒下一个节点
public void unlock() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected final boolean tryReleaseShared(int unused) {
Thread current = Thread.currentThread();
// 更新 firstReader
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();
int nextc = c - SHARED_UNIT;
// 读锁的计数不会影响其它获取读锁线程, 但会影响其它获取写锁线程,计数为 0 才是真正释放
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
t4 在 acquireQueued 中 parkAndCheckInterrupt 处恢复运行,再次 for (;😉 这次自己是头节点的临节点,并且没有其他节点竞争,tryAcquire(1) 成功,修改头结点,流程结束
四:Semaphore
4.1 基本使用
synchronized
可以起到锁的作用,但在某个时间段内,只能由一个线程允许执行
Semaphore
(信号量)可以用来限制能同时访问共享资源的线程上限,非重入锁。
构造方法:
public Semaphore(int permits)
:permits 表示许可线程的数量(state)public Semaphore(int permits, boolean fair)
:fair 表示公平性,如果设为 true,下次执行的线程会是等待最久的线程
常用API:
public void acquire()
:表示获取许可public void release()
:表示释放许可,acquire() 和 release() 方法之间的代码为同步代码。
public static void main(String[] args) {
// 1.创建Semaphore对象
Semaphore semaphore = new Semaphore(3);
// 2. 10个线程同时运行
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
// 3. 获取许可
semaphore.acquire();
sout(Thread.currentThread().getName() + " running...");
Thread.sleep(1000);
sout(Thread.currentThread().getName() + " end...");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 4. 释放许可
semaphore.release();
}
}).start();
}
}
Thread-0 running...
Thread-2 running...
Thread-3 running...
Thread-3 end...
Thread-2 end...
Thread-0 end...
Thread-1 running...
Thread-9 running...
Thread-5 running...
Thread-5 end...
Thread-6 running...
Thread-9 end...
Thread-1 end...
Thread-7 running...
Thread-8 running...
Thread-6 end...
Thread-4 running...
Thread-8 end...
Thread-7 end...
Thread-4 end...
4.2 实现原理
加锁流程:
Semaphore
有点像一个停车场,permits
就好像停车数量,当线程获得了permits
就像是获得了停车位,然后停车场现实空余车位减一。
public Semaphore(int permits) {
sync = new NonfairSync(permits);
}
NonfairSync(int permits) {
super(permits);
}
Sync(int permits) {
setState(permits);
}
刚开始,perimits为3,这事5个线程来获取资源。
假设其中 Thread-1,Thread-2,Thread-4 CAS 竞争成功,permits 变为 0,而 Thread-0 和 Thread-3 竞争失败,进入 AQS 队列park 阻塞。
加锁:
public void acquire() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
// 可中断
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
if (tryAcquireShared(arg) < 0)
// 获取锁失败就进行阻塞
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
return nonfairTryAcquireShared(acquires);
}
// 非公平锁,公平锁会在循环内 hasQueuedPredecessors()方法判断阻塞队列是否有阻塞的线程节点
final int nonfairTryAcquireShared(int acquires) {
// 死循环获取锁
for (;;) {
// 剩余可以利用的车位个数
int available = getState();
// 减去需要的车位个数
int remaining = available - acquires;
// 如果没有车位了,就返回负数
// 如果还有车位,并且CAS加锁成功才返回正数
// 如果CAS加锁失败会再次尝试加锁
if (remaining < 0 ||
compareAndSetState(available, remaining))
return remaining;
}
}
获取锁失败,就将该线程加入阻塞队列中,并阻塞该线程
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将该线程包装成node节点加入阻塞队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 如果前驱节点是头节点,表示可以再次尝试加锁
if (p == head) {
// r:剩余的车位个数
int r = tryAcquireShared(arg);
// 加锁成功
if (r >= 0) {
// 尝试唤醒后面r个线程
setHeadAndPropagate(node, r);
p.next = null; // help GC
failed = false;
return;
}
}
// 判断是否应该阻塞
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
throw new InterruptedException();
}
} finally {
if (failed)
cancelAcquire(node);
}
}
这时 Thread-4 释放了 permits,状态如下
解锁流程:
public void release() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
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加锁成功,返回true
if (compareAndSetState(current, next))
return true;
}
}
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
private void doReleaseShared() {
// 读锁是共享锁,可能会有多个线程同时释放读锁
// 所以说这里是并发的
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
// SIGNAL 唤醒后继
if (ws == Node.SIGNAL) {
// 因为读锁共享,如果其它线程也在释放读锁,那么需要将 waitStatus 先改为 0
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue; // loop to recheck cases
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 &&
!compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue; // loop on failed CAS
}
// 如果h==head,说明后继线程还没有更新头节点
// 那么留着下一个节点去唤醒下下个节点
//如果不等于,说明后继线程已经更改了头节点,那它俩一块继续并发唤醒后继节点
if (h == head) // loop if head changed
break;
}
}
接下来 Thread-0 竞争成功,permits 再次设置为 0,设置自己为 head 节点,断开原来的 head 节点,unpark 接
下来的 Thread-3 节点,但由于 permits 是 0,因此 Thread-3 在尝试不成功后再次进入 park 状态
4.3 PROPAGATE
为什么需要PROPAGATE?
早期有bug
- releaseShared 方法
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
Node h = head;
if (h != null && h.waitStatus != 0)
unparkSuccessor(h);
return true;
}
return false;
}
- doAcquireShared 方法
private void doAcquireShared(int arg) {
// 加入到阻塞队列中,节点是共享模式
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
boolean interrupted = false;
for (;;) {
// 获取前驱节点
final Node p = node.predecessor();
if (p == head) {
// 如果前驱节点是头节点就再次尝试获取锁
int r = tryAcquireShared(arg);
// 获取锁成功
if (r >= 0) {
// 这里有空挡
// 设置自己为头节点,并且唤醒相连的后续的共享节点
setHeadAndPropagate(node, r);
p.next = null; // help GC
// 如果发生打断就自我打断一下
if (interrupted)
selfInterrupt();
failed = false;
return;
}
}
// 是否在获取锁失败后阻塞线程
if (shouldParkAfterFailedAcquire(p, node) &&
parkAndCheckInterrupt())
interrupted = true;
}
} finally {
if (failed)
cancelAcquire(node);
}
}
- setHeadAndPropagate方法
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head; // Record old head for check below
// 设置当前节点为头节点
setHead(node);
// propagate 表示有共享资源
if (propagate > 0 && h.waitStatus != 0) {
Node s = node.next;
if (s == null || s.isShared())
// 如果下一个节点是空或是共享的,就唤醒它
unparkSuccessor(h);
}
}
假设存在某次循环中队列里排队的结点情况为 head(-1)->t1(-1)->t2(-1)
假设存在将要信号量释放的 T3 和 T4,释放顺序为先 T3 后 T4
正常流程:
- T3 调用 releaseShared(1),直接调用了 unparkSuccessorr(head),head.waitStatus 从-1变成 0
- T1 由于T3释放信号量被唤醒,然后T4释放,唤醒了T2.
BUG流程:
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从-1变成 0
- T1由于T3释放信号量被唤醒,调用tryAcquireShared,返回值为0(获取锁失败,但没有剩余资源量)
- T1还没有调用 setHeadAndPropagate 方法,T4调用 releaseShared(1),此时 head.Status为0(此时独到的head和步骤1中为同一个head),所以不满足条件,不会调用 unpardSuccessor(head)
- T1获取信号量成功,调用setHeadAndPropagate(t1.node,0),因为不满足 propagate > 0(剩余资源量等于0),从而不回唤醒后继节点,T2线程得不到唤醒。
修复后流程:
- T3 调用 releaseShared(1),直接调用了 unparkSuccessor(head),head.waitStatus 从-1变成 0
- T1由于T3释放信号量被唤醒,调用tryAcquireShared,返回值为0(获取锁失败,但没有剩余资源量)
- T1还没有调用 setHeadAndPropagate 方法,T4调用 releaseShared(1),此时 head.Status为0(此时读到的head和步骤1中为同一个head),调用 doReleaseShared() 将等待状态置为 PROPAGATE(-3)
- T1获取信号量成功,调用setHeadAndPropagate(t1.node,0),读到 h.waitStatus < 0,从而调用 doReleaseShared() 唤醒 T2
bug修复后的代码:
private void setHeadAndPropagate(Node node, int propagate) {
Node h = head;
// 设置自己为 head 节点
setHead(node);
// propagate 表示有共享资源(例如共享读锁或信号量)
// head waitStatus == Node.SIGNAL 或 Node.PROPAGATE
if (propagate > 0 || h == null || h.waitStatus < 0 ||
(h = head) == null || h.waitStatus < 0) {
Node s = node.next;
// 如果是最后一个节点或者是等待共享读锁的节点,做一次唤醒
if (s == null || s.isShared())
doReleaseShared();
}
}
// 唤醒
private void doReleaseShared() {
// 如果 head.waitStatus == Node.SIGNAL ==> 0 成功, 下一个节点 unpark
// 如果 head.waitStatus == 0 ==> Node.PROPAGATE
for (;;) {
Node h = head;
if (h != null && h != tail) {
int ws = h.waitStatus;
if (ws == Node.SIGNAL) {
// 防止 unparkSuccessor 被多次执行
if (!compareAndSetWaitStatus(h, Node.SIGNAL, 0))
continue;
// 唤醒后继节点
unparkSuccessor(h);
}
// 如果已经是 0 了,改为 -3,用来解决传播性
else if (ws == 0 && !compareAndSetWaitStatus(h, 0, Node.PROPAGATE))
continue;
}
if (h == head)
break;
}
}
五、CountDownLatch
5.1 基本使用
CountDownLatch:计数器,用来进行线程同步协作,等待所有线程完成
构造器:
public CountDown(int count)
:初始化唤醒需要down几步
常用API:
public void await()
:让当前线程等待,等待计数归零才可以被唤醒,否则进入无限等待。public void countDown()
:计数器进行减1(donw 1)
应用:同步等待多线程准备完毕
// LOL 10人进入游戏倒计时
public static void main(String[] args) throws InterruptedException {
CountDownLatch countDownLatch = new CountDownLatch(10);
ExecutorService service = Executors.newFixedThreadPool(10);
String[] all = new String[10];
Random random = new Random();
for (int i = 0; i < 10; i++) {
int x = i;
service.submit(() -> {
for (int j = 0; j <= 100; j++) {
try {
Thread.sleep(random.nextInt(100));
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
all[x] = j + "%";
System.out.print("\r" + Arrays.toString(all)); // \r 回到行首
}
countDownLatch.countDown();
});
}
countDownLatch.await();
service.shutdown();
}
// [100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%, 100%]
// 就跟进度条一样,慢慢增长到100%
5.2 实现原理
阻塞等待:
- 线程调用 await() 等待其他线程完成任务(支持打断)
public void await() throws InterruptedException {
sync.acquireSharedInterruptibly(1);
}
public final void acquireSharedInterruptibly(int arg)
throws InterruptedException {
if (Thread.interrupted())
throw new InterruptedException();
// 当 state > 0,表示计数器不等于0,需要阻塞等待,等待其他线程释放资源
// state == 0,此时不需要阻塞线程,直接返回
if (tryAcquireShared(arg) < 0)
doAcquireSharedInterruptibly(arg);
}
protected int tryAcquireShared(int acquires) {
// 当计数器为0后,表示不用等待了
return (getState() == 0) ? 1 : -1;
}
private void doAcquireSharedInterruptibly(int arg)
throws InterruptedException {
// 将该线程包装成node节点加入阻塞队列中
final Node node = addWaiter(Node.SHARED);
boolean failed = true;
try {
for (;;) {
final Node p = node.predecessor();
// 如果前驱节点是头节点,表示可以再次尝试加锁
if (p == head) {
// r:剩余的车位个数
int r = tryAcquireShared(arg);
// 加锁成功
if (r >= 0) {
// 尝试唤醒后面r个线程
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 countDown() {
sync.releaseShared(1);
}
public final boolean releaseShared(int arg) {
if (tryReleaseShared(arg)) {
doReleaseShared();
return true;
}
return false;
}
protected boolean tryReleaseShared(int releases) {
// Decrement count; signal when transition to zero
for (;;) {
int c = getState();
if (c == 0)
return false;
int nextc = c-1;
// 计数器减1,当减到0时就可以唤醒阻塞队列中的阻塞线程
if (compareAndSetState(c, nextc))
return nextc == 0;
}
}
六、CyclicBarrier
6.1 基本使用
CyclicBarrier:循环屏障,用来进行线程协作,等待线程满足某个计数,才能触发自己执行。
常用方法:
public CyclicBarrier(int parties, Runnable barrierAction)
:用于在线程到达屏障 parties 时,执行 barrierAction- parties:代表多少个线程到达屏障开始触发线程任务
- barrierAction:线程任务
public int await()
:线程调用 await 方法通知 CyclicBarrier 本线程已经到达屏障。
public static void main(String[] args) {
CyclicBarrier cb = new CyclicBarrier(2, new Runnable() {
@Override
public void run() {
System.out.println("到达屏障");
}
});
new Thread(() -> {
System.out.println("线程1开始.." + new Date());
try {
cb.await(); // 当前线程到达栅栏,等待其他线程到达栅栏
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
System.out.println("线程1继续执行..." + new Date());
}).start();
new Thread(() -> {
System.out.println("线程2开始.." + new Date());
try {
cb.await(); // 当前线程到达栅栏,等待其他线程到达栅栏
} catch (InterruptedException | BrokenBarrierException e) {
throw new RuntimeException(e);
}
System.out.println("线程2继续执行..." + new Date());
}).start();
}
线程1开始..Mon Sep 02 10:40:25 CST 2024
线程2开始..Mon Sep 02 10:40:25 CST 2024
到达屏障
线程2继续执行...Mon Sep 02 10:40:25 CST 2024
线程1继续执行...Mon Sep 02 10:40:25 CST 2024
6.2 实现原理
成员属性:
- 全局锁:利用可重入锁实现的工具类
// barrier 实现是依赖于 Condition条件队列,condition 条件队列必须依赖lock才能使用
// 为什么?因为线程是因为没有达到条件需要阻塞,条件成立后就需要唤醒,所以需要等待唤醒机制,所以就使用了条件队列
private final ReentrantLock lock = new ReentrantLock();
// 线程挂起实现使用的 condition 队列,当前代所有线程到位,这个条件队列内的线程才会被唤醒
private final Condition trip = lock.newCondition();
- 线程数量:
private final int parties; // 代表多少个线程到达屏障开始触发线程任务
private int count; // 表示当前“代”还有多少个线程未到位,初始值为 parties
- 当前代中最后一个线程到位后要执行的事件:
private final Runnable barrierCommand;
- 代:就是说CyclicBarrier可以使用多次,当前要是触发成功了,就接着继续等待下一次触发
// 表示 barrier 对象当前 代
private Generation generation = new Generation();
private static class Generation {
// 表示当前“代”是否被打破,如果被打破再来到这一代的线程 就会直接抛出 BrokenException 异常
// 且在这一代挂起的线程都会被唤醒,然后抛出 BrokerException 异常。
boolean broken = false;
}
- 构造方法:
public CyclicBarrie(int parties, Runnable barrierAction) {
// 因为小于等于 0 的 barrier 没有任何意义
if (parties <= 0) throw new IllegalArgumentException();
this.parties = parties;
this.count = parties;
// 可以为 null
this.barrierCommand = barrierAction;
}
成员方法:
- await():阻塞等待,直到所有线程都到达屏障
public int await() throws InterruptedException, BrokenBarrierException {
try {
return dowait(false, 0L);
} catch (TimeoutException toe) {
throw new Error(toe); // cannot happen
}
}
// timed:表示当前调用await方法的线程是否指定了超时时长,如果 true 表示线程是响应超时的
// nanos:线程等待超时时长,单位是纳秒
private int dowait(boolean timed, long nanos)
throws InterruptedException, BrokenBarrierException,
TimeoutException {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 获取当前代
final Generation g = generation;
// 如果当前代已经是打破状态,直接抛出异常
if (g.broken)
throw new BrokenBarrierException();
// 当前线程被中断了,则直接打破当前代,唤醒队列中的阻塞的线程,最后抛出异常
if (Thread.interrupted()) {
breakBarrier();
throw new InterruptedException();
}
// 计数到达屏障的线程个数
int index = --count;
// 等于0时表示所有线程都到达屏障了。
if (index == 0) { // tripped
boolean ranAction = false;
try {
// 触发自己的任务
final Runnable command = barrierCommand;
if (command != null)
command.run();
// 如果run()方法未出现异常,则标记成功
ranAction = true;
// 开启下一代,并唤醒所有阻塞的线程
nextGeneration();
return 0;
} finally {
// 如果run()方法抛出异常,直接打破当前代
if (!ranAction)
breakBarrier();
}
}
// 自旋:直到条件满足、当前代被打破、线程被中断,等待超时
for (;;) {
try {
// 当前线程加入条件队列中等待,释放掉锁
if (!timed)
trip.await();
else if (nanos > 0L)
nanos = trip.awaitNanos(nanos);
} catch (InterruptedException ie) {
// 如果等待过程中被打断
// 当前代没有被打破
if (g == generation && ! g.broken) {
// 打破屏障
breakBarrier();
throw ie;
} else {
// 如果当前代变化了,完成自我打断
Thread.currentThread().interrupt();
}
}
// 此时被唤醒
// 如果当前代被打破,抛出异常
if (g.broken)
throw new BrokenBarrierException();
// 当前代没有被打破,但是变化了,
if (g != generation)
return index;
if (timed && nanos <= 0L) {
breakBarrier();
throw new TimeoutException();
}
}
} finally {
lock.unlock();
}
}
- breakBarrier():打破 Barrier 屏障
private void breakBarrier() {
// 将代中的 broken 设置为 true,表示这一代是被打破了,再来到这一代的线程,直接抛出异常
generation.broken = true;
// 重置 count 为 parties
count = parties;
// 将在trip条件队列内挂起的线程全部唤醒,唤醒后的线程会检查当前是否是打破的,然后抛出异常
trip.signalAll();
}
- nextGeneration():开启新的下一代
private void nextGeneration() {
// 将在 trip 条件队列内挂起的线程全部唤醒
trip.signalAll();
// 重置 count 为 parties
count = parties;
// 开启新的一代,使用一个新的generation对象,表示新的一代,新的一代和上一代【没有任何关系】
generation = new Generation();
}
七、线程安全集合类
线程安全集合类可以分为三大类:
- 遗留的线程安全集合如:Hashtable、Vector
- 使用 Collections 装饰的线程安全集合,如:
- Collections.synchronizedCollection
- Collections.synchronizedList
- Collections.synchronizedMap
- Collections.synchronizedSet
- Collections.synchronizedNavigableMap
- Collections.synchronizedNavigableSet
- Collections.synchronizedSortedMap
- Collections.synchronizedSortedSet
java.util.concurrent.*
下的集合安全类
我们重点关注java.util.concurrent.*
下的集合安全类,可以发现它们有规律,里面包含三类关键词:Blocking、CopyOnWrite、Concurrent
- Blocking 大部分实现基于锁,并提供用来阻塞的方法
- CopyOnWrite 之类容器修改开销相对较重
- Concurrent 类型的容器
- 内部很多操作使用CAS优化,一般可以提高较高吞吐量
- 弱一致性:
- 遍历时弱一致性,例如,当利用迭代器遍历时,如果容易发生修改,迭代器仍然可以继续进行遍历,这时内容是旧的。
- 求大小弱一致性,size操作未必是100%准备
- 读取时弱一致性
八、ConcurrentHashMap 原理
8.1 集合对比
三种集合:
- HashMap 是线程不安全的,性能好
- Hashtable 线程安全基于 synchronized,综合性能差,已经被淘汰
- ConcurrentHashMap 保证了线程安全,综合性能好,而且效率高,性能好
集合对比:
- Hashtable 继承 Dictionary类,HashMap、ConcurrentHashMap继承 abstractMap,均实现Map接口
- Hashtable底层是数组+链表,JDK8以后HashMap和ConcurrentHashMap底层是数组+链表+红黑树
- HashMap线程非安全,Hashtable线程安全,Hashtable的方法都加了 synchronized 来确保线程安全
- ConcurrentHashMap、Hashtable不允许null值,HashMap允许 null值
- ConcurrentHashMap、HashMap 的初始容量为 16,Hashtable 初始容量为11,填充因子默认都是 0.75,两种 Map 扩容是当前容量翻倍:capacity * 2,Hashtable 扩容时是容量翻倍 + 1:capacity*2 + 1
8.2 JDK 8 ConcurrentHashMap
8.2.1 重要属性和内部类
// 默认为 0
// 当初始化时, 为 -1
// 当扩容时, 为 -(1 + 扩容线程数)
// 当初始化或扩容完成后,为 下一次的扩容的阈值大小
private transient volatile int sizeCtl;
// 整个 ConcurrentHashMap 就是一个 Node[]
static class Node<K,V> implements Map.Entry<K,V> {}
// hash 表
transient volatile Node<K,V>[] table;
// 扩容时的 新 hash 表
private transient volatile Node<K,V>[] nextTable;
// 扩容时如果某个 bin 迁移完毕, 用 ForwardingNode 作为旧 table bin 的头结点
static final class ForwardingNode<K,V> extends Node<K,V> {}
// 用在 compute 以及 computeIfAbsent 时, 用来占位, 计算完成后替换为普通 Node
static final class ReservationNode<K,V> extends Node<K,V> {}
// 作为 treebin 的头节点, 存储 root 和 first
static final class TreeBin<K,V> extends Node<K,V> {}
// 作为 treebin 的节点, 存储 parent, left, right
static final class TreeNode<K,V> extends Node<K,V> {
8.2.2 构造器分析
可以看到实现了懒惰初始化,在构造方法中仅仅计算了 table 的大小,以后在第一次使用时才会真正创建
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
throw new IllegalArgumentException();
// 如果初始容量小于并发级别,把并发级别赋值给初始容量
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
// 计算初始容量
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
get 流程
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// spread 方法确保返回结果是正数
int h = spread(key.hashCode());
// 如果哈希表不为空,且哈希表有大小,并且该key所在的哈希桶上有元素
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
// 如果头节点就是要查找的key,就直接返回value
if ((eh = e.hash) == h) {
// 引用比较 || 值比较
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
// 如果hash值为负说明该bin在扩容中或是 treebin,这时调用 find 方法来查找
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
// 遍历链表
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
put流程
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// ConcurrentHashMap 不能存放 null 值
if (key == null || value == null) throw new NullPointerException();
// 得到hash值
int hash = spread(key.hashCode());
// 表示当前 k-v 封装成 node 后插入到指定桶位后,在桶位中的所属链表的下标位置
int binCount = 0;
// 自旋
// 添加数据,肯定会出现线程不安全,这里使用CAS的方式加锁,所以需要自旋
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 哈希表为null,还未初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 找到该key对应的哈希表中的桶下标的头节点,如果为空
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// CAS加锁创建一个新的节点
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
// 走到这里说明该key对应的桶有元素
// 如果当前桶正在扩容的话,帮助扩容
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 走到这里表示该key对应的桶有元素,并且哈希表没有在扩容
else {
V oldVal = null;
// 因为这里是并发操作,而且判断修改的流程很多,比较复杂,所以直接对头节点加锁
// 使用synchronized 更优
synchronized (f) {
// 这里重新获取一下桶的头节点有没有被修改,因为可能被其他线程修改过,这里是线程安全的获取
if (tabAt(tab, i) == f) {
// 头节点的哈希值大于 0 说明当前桶位是普通的链表节点
if (fh >= 0) {
binCount = 1;
// 循环遍历链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 添加到链表尾部
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key,
value, null);
break;
}
}
}
// 如果修改过,判断是不是红黑树节点
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
// putTreeVal 会看 key 是否已经在树中,如果是的话,就返回对应的TreeNode
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) {
// 更新旧值
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
}
}
// 计算该桶中元素的个数
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 增加size计数
addCount(1L, binCount);
return null;
}
initTable 流程
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
// 初始化的时候存在并发的情况,因为初始化是懒加载,创建的时候不会初始化
while ((tab = table) == null || tab.length == 0) {
// sizeCtl < 0 表示别的线程在进行初始化,当前线程让步
if ((sc = sizeCtl) < 0)
Thread.yield(); // lost initialization race; just spin
// 加锁。将sizeCtl设置成-1.表示要进行初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
// 获得锁,创建table,这时其他线程会在 while() 循环中 yield 直至table创建
try {
// 如果table还未创建
if ((tab = table) == null || tab.length == 0) {
// sc表示容量,大于0就是用sc为指定大小,否则使用16默认值
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
// 创建哈希表
@SuppressWarnings("unchecked")
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
// 扩容阈值:sc = 0.75 * n
sc = n - (n >>> 2);
}
} finally {
// 加锁,把下一次扩容的阈值赋值给 sizeCtl
sizeCtl = sc;
}
break;
}
}
return tab;
}
addCount流程
private final void addCount(long x, int check) {
// 没有竞争发生,向 baseCount 累加计数
// 有竞争发生,新建 counterCells,向其中的一个 cell 累加计数
// counterCells 初始有两个 cell 如果计数竞争比较激烈,会创建新的 cell 来累加计数
CounterCell[] as; long b, s;
// 判断累加数组 cells 是否初始化,没有就去累加 base 域,累加失败进入条件内逻辑
// 累加失败表示有并发
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
// true表示未竞争,false发生竞争
boolean uncontended = true;
// as == null || (m = as.length - 1) < 0 : 表示判断as是否初始化,如果没有初始化,进入fullAddCount流程
// 如果已经初始化了,那么通过hash寻址判断对应的槽位有没有初始化,如果没有初始化,进入fullAddCount流程
// 如果对应的槽位已经初始化了,尝试在该槽位进行累加,如果累加失败进入fullAddcount流程
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
// 创建累加单元数组和累加单元,进行累加重试
fullAddCount(x, uncontended);
return;
}
// 走到这里说明,尝试累加成功了
if (check <= 1)
return;
// 获取元素个数
s = sumCount();
}
// check==-1表示是删除节点
// >=0 表示是增加节点
// 主要是帮助扩容
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
8.2 JDK 7 ConcurrentHashMap
ConcurrentHashMap 对锁粒度进行了优化,使用了分段锁技术,讲整张表分成了多个数组(Segment),每个Segment对应一把锁。每个Segment又是一个类似HashMap数组的结构。这样就可以允许多个修改操作并发进行,Segment 是一种可重入锁,继承 ReentrantLock,并发时锁住的是每个 Segment,其他 Segment 还是可以操作的,这样不同 Segment 之间就可以实现并发,大大提高效率。
底层结构:Segment数组+HashEntry数组+链表
- 优点:如果多个线程访问不同的segment,实际是没有冲突的,这与JDK8是类似的。
- 缺点:Segment 数组默认大小为16,这个容量初始化指定后就不能改变了,并且不是懒惰初始化。默认就最多只支持16个线程并发。
8.3 LinkedBlockingQueue
LinkedBlockingQueue 是Java 中的一个阻塞队列实现,它基于链表结构,它是线程安全的,底层使用了ReentrantLock锁来保证线程安全。
8.3.1 成员属性
// 队列的大小
private final int capacity;
/** Current number of elements */
private final AtomicInteger count = new AtomicInteger();
/**
* Head of linked list.
* Invariant: head.item == null
*/
transient Node<E> head;
/**
* Tail of linked list.
* Invariant: last.next == null
*/
private transient Node<E> last;
/** Lock held by take, poll, etc */
private final ReentrantLock takeLock = new ReentrantLock();
/** Wait queue for waiting takes */
private final Condition notEmpty = takeLock.newCondition();
/** Lock held by put, offer, etc */
private final ReentrantLock putLock = new ReentrantLock();
/** Wait queue for waiting puts */
private final Condition notFull = putLock.newCondition();
8.3.2 入队
static class Node<E> {
E item;
/**
* One of:
* - the real successor Node:真实的后继节点
* - this Node, meaning the successor is head.next:指向当前这个节点,出队时垃圾回收
* - null, meaning there is no successor (this is the last node):尾节点,后继节点为null
*/
Node<E> next;
Node(E x) { item = x; }
}
private void enqueue(Node<E> node) {
// assert putLock.isHeldByCurrentThread();
// assert last.next == null;
last = last.next = node;
}
8.3.3 出队
private E dequeue() {
// assert takeLock.isHeldByCurrentThread();
// assert head.item == null;
Node<E> h = head;
Node<E> first = h.next;
h.next = h; // help GC
head = first;
E x = first.item;
first.item = null;
return x;
}
8.3.4 Put
public void put(E e) throws InterruptedException {
if (e == null) throw new NullPointerException();
// Note: convention in all put/take/etc is to preset local var
// holding count negative to indicate failure unless set.
// c 表示当前队列中有几个元素
int c = -1;
Node<E> node = new Node<E>(e);
// put锁
final ReentrantLock putLock = this.putLock;
// 当前队列中元素个数
final AtomicInteger count = this.count;
putLock.lockInterruptibly();
try {
// 当队列满的时候等待,加入到等待队列中
while (count.get() == capacity) {
notFull.await();
}
// 加入队列中
enqueue(node);
// 队列个数+1
c = count.getAndIncrement();
// 如果队列中还有空位,唤醒其他put线程
if (c + 1 < capacity)
notFull.signal();
} finally {
// 释放put锁
putLock.unlock();
}
// 如果之前队列中元素为null,现在加入一个元素之后,就可以唤醒take线程去读取
if (c == 0)
signalNotEmpty();
}
private void signalNotEmpty() {
final ReentrantLock takeLock = this.takeLock;
takeLock.lock();
try {
notEmpty.signal();
} finally {
takeLock.unlock();
}
}
8.3.5 Take
public E take() throws InterruptedException {
E x;
int c = -1;
final AtomicInteger count = this.count;
final ReentrantLock takeLock = this.takeLock;
takeLock.lockInterruptibly();
try {
// 当队列中没有元素的时候,加入等待队列等待
while (count.get() == 0) {
notEmpty.await();
}
// 出队
x = dequeue();
c = count.getAndDecrement();
// 如果队列中还有元素,唤醒其他take线程
if (c > 1)
notEmpty.signal();
} finally {
takeLock.unlock();
}
// 如果之前队列是满的,现在有空位了,就唤醒put线程。
if (c == capacity)
signalNotFull();
return x;
}
private void signalNotFull() {
final ReentrantLock putLock = this.putLock;
putLock.lock();
try {
notFull.signal();
} finally {
putLock.unlock();
}
}
8.3.6 加锁分析
高明之处:用了两把锁和 dummy 节点
- 用一把锁,同一时刻,最多只允许有一个线程(生产者或者消费者)执行
- 用两把锁,同一时刻,可以允许两个线程(一个生产者和一个消费者)执行
- 消费者和消费者之间串行
- 生产者和生产者之间串行
线程安全分析:
- 当节点总数大于2时(包括 dummy节点),putLock保证的是last节点的线程安全,takeLock保证的是head节点的线程安全,亮啊不所保证了入队和出队没有竞争
- 当节点总数等于2时(即一个dummy节点,一个正常节点)这时候,仍然是两把锁锁两个对象,不会有竞争。
- 当节点总数等于1时(即一个dummy节点)这时take线程会被notEmpty阻塞i,有竞争,会阻塞。
8.3.7 性能比较
主要列举 LinkedBlockingQueue 与 ArrayBlockingQueue 的性能比较
- Linked支持有界,Array强制有界
- Linked实现是链表,Array实现是数组
- Linked是懒惰的,而Array需要提前初始化Node数组
- Linked每次入队会生成新的Node,而Array的Node是提前创建好的
- Linked两把锁,而Array只有一把锁
8.4 ConcurrentLinkedQueue
ConcurrentLinkedQueue 的设计与 LinkedBlockingQueue 非常像,也是
- 两把锁,同一时刻,可以允许两个线程(一个生产者和一个消费者)同时执行
- dummy节点的引入让两把锁将来锁住的是不同对象,避免竞争。
- 只是这把锁使用了CAS来实现。
ConcurrentLinkedDeque 是双向链表结构的无界并发队列
- 不允许 null 入列
- 队列中所有未删除的节点的 item 都不能为 null 且都能从 head 节点遍历到
- 删除节点是将 item 设置为 null,队列迭代时跳过 item 为 null 节点
- head 节点跟 tail 不一定指向头节点或尾节点,可能存在滞后性
private static class Node<E> {
volatile E item;
volatile Node<E> next;
}
8.4.1 构造方法
- 无参构造方法:
public ConcurrentLinkedQueue() {
// 初始化一个dummy节点
head = tail = new Node<E>(null);
}
- 有参构造方法
public ConcurrentLinkedQueue(Collection<? extends E> c) {
Node<E> h = null, t = null;
for (E e : c) {
checkNotNull(e);
// 讲集合中的元素全部加入队列中
Node<E> newNode = new Node<E>(e);
if (h == null)
h = t = newNode;
else {
// CAS的方式加载
t.lazySetNext(newNode);
t = newNode;
}
}
if (h == null)
h = t = new Node<E>(null);
head = h;
tail = t;
}
void lazySetNext(Node<E> val) {
UNSAFE.putOrderedObject(this, nextOffset, val);
}
8.4.2 入队
这样设计的原因是修改尾节点的next指针和修改尾节点是两个操作,如果只是用CAS,并且两个操作相继执行的话,可能有线程安全问题。
public boolean offer(E e) {
checkNotNull(e);
final Node<E> newNode = new Node<E>(e);
// 循环CAS直到入队成功
for (Node<E> t = tail, p = t;;) {
// 尾节点的下一个节点
Node<E> q = p.next;
// 如果队列为null,表示可以竞争加锁
if (q == null) {
// 尝试CAS加锁
if (p.casNext(null, newNode)) {
// 如果成功了,那么
if (p != t) // hop two nodes at a time
casTail(t, newNode); // Failure is OK.
return true;
}
// Lost CAS race to another thread; re-read next
}
else if (p == q)
// We have fallen off list. If tail is unchanged, it
// will also be off-list, in which case we need to
// jump to head, from which all live nodes are always
// reachable. Else the new tail is a better bet.
p = (t != (t = tail)) ? t : head;
else
// Check for tail updates after two hops.
p = (p != t && t != (t = tail)) ? t : q;
}
}
8.5 CopyOnWriteArrayList
CopyOnWriteArraySet 底层实现采用了 写入时拷贝 的思想,增删改操作会将底层数组拷贝一份,修改操作在新数组上执行,这时不影响其它线程的并发读,读写分离。 以新增为例:
- 存储结构
private transient volatile Object[] array; // volatile 保证了读写线程之间的可见性
- 全局锁:保证线程的执行安全
final transient ReentrantLock lock = new ReentrantLock();
- 新增数据,需要加锁,创建新数组
public boolean add(E e) {
final ReentrantLock lock = this.lock;
// 加锁,保证线程安全
lock.lock();
try {
// 获取旧的数组
Object[] elements = getArray();
int len = elements.length;
// 【拷贝新的数组(这里是比较耗时的操作,但不影响其它读线程)】
Object[] newElements = Arrays.copyOf(elements, len + 1);
// 添加新元素
newElements[len] = e;
// 替换旧的数组,【这个操作以后,其他线程获取数组就是获取的新数组了】
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
- 读数据:不加锁,在原数组上操作
public E get(int index) {
return get(getArray(), index);
}
private E get(Object[] a, int index) {
return (E) a[index];
}
适合读多写少的应用场景
- 迭代器:CopyOnWriteArrayList 在返回迭代器时,创建一个内部数组当前的快照(引用),即使其他线程替换了原始数组,迭代器遍历的快照依然引用的是创建快照时的数组,所以这种实现方式也存在一定的数据延迟性,对其他线程并行添加的数据不可见
public Iterator<E> iterator() {
// 获取到数组引用,整个遍历的过程该数组都不会变,一直引用的都是老数组,
return new COWIterator<E>(getArray(), 0);
}
// 迭代器会创建一个底层array的快照,故主类的修改不影响该快照
static final class COWIterator<E> implements ListIterator<E> {
// 内部数组快照
private final Object[] snapshot;
private COWIterator(Object[] elements, int initialCursor) {
cursor = initialCursor;
// 数组的引用在迭代过程不会改变
snapshot = elements;
}
// 【不支持写操作】,因为是在快照上操作,无法同步回去
public void remove() {
throw new UnsupportedOperationException();
}
}
8.5.1 弱一致性
数据一致性就是读到最新更新的数据:
- 强一致性:当更新操作完成之后,任何多个后续进程或者线程的访问都会返回最新的更新过的值
- 弱一致性:系统并不保证进程或者线程的访问都会返回最新的更新过的值,也不会承诺多久之后可以读到
线程0读的还是之前的数组,而线程0读之前数组已经被改变了。
不一定弱一致性就不好
- 数据库的事务隔离级别就是弱一致性的表现
- 并发高和一致性是矛盾的,需要权衡