1、前置知识点
1.1、volatile
volatile是 Java 中的一个关键字,用于修饰变量,主要解决多线程环境下的可见性和有序性问题。1、volatile不能保证复合操作的原子性(如 i++) 2、过度是用volatile可能会影响性能,因为它阻止了缓存优化 3、volatile的可见性保障仅限于它修饰的变量本身,不适用于该遍历引用的对象内部字段 (简单概括)
1.1.1、主要特性
-
可见性保障
- 当一个线程修改了volatile变量的值,新值会立即被写入主内存
- 其他线程读取该遍历时,会直接从主内存读取最新值,而不是是用本地缓存
-
禁止指令重排序
- volatile修饰的变量会禁止 JVM和吹起对其进行指令重排序优化
- 确保代码的执行顺序与程序顺序是一致的
1.2、CAS
CAS 是并发编程中的一种无锁原子操作,全称为 Compare-And-Swap(比较并交换),它是现代 CPU 提供的一种原子指令,用于实现多线程同步。
在JVM层面,CAS操作最终会转换为CPU指令(如x86的cmpxchg指令),现代CPU都提供了类似的原子指令支持。 (简单概括)
- 核心原理 三个操作数
- 内存位置 V
- 预期值 A
- 新增 B
- CAS的特点
- 原子性:整个操作是不可分割的
- 无锁不需要传统的锁机制
- 乐观锁并发控制 先尝试更新,失败责重试
- 自旋 spin:通常配合循环实现重试机制
- ABA问题
- 问题现象
- 线程1读取遍历值为A,
- 线程2将值从A改为B然后又改回A,
- 线程1执行CAS操作时仍然认为值没有变化
- 解决方法:
- 使用版本号或时间戳
- Java提供了AtomicMarkableReference 和 AtomicStampedReference
- 问题现象
1.3、LockSupport
LockSupport 是 Java 并发包 (java.util.concurrent.locks) 中的一个工具类,提供了线程阻塞和唤醒的基本操作,它是构建更高级同步工具(如锁和同步器)的基础。 (简单概括)
-
核心功能
- 阻塞线程
- park() 阻塞当前线程
- parkNanos() 阻塞指定纳秒的时间
- parkUntile() 阻塞制动指定的时间戳
- 唤醒线程
- unpark() 唤醒指定指定线程
- 阻塞线程
-
特点优势
-
精准控制:可以指定要唤醒的特点线程
-
无竞态天剑:unpark 可以先于 park调用
-
底层实现:依赖于unsafe 类直接操作线程
-
2、ReentrantLock
2.1、核心方法
// 内部类 重要的类 实现了 AQS
private final Sync sync;
abstract static class Sync extends AbstractQueuedSynchronizer {
// lock 加锁方法
final boolean nonfairTryAcquire(int acquires) {}
// unlock最终调用的是 该方法
protected final boolean tryRelease(int releases) {}
}
// 非公平锁实现类
static final class NonfairSync extends Sync {
protected final boolean tryAcquire(int acquires) {}
}
// 公平锁实现类
static final class FairSync extends Sync {
protected final boolean tryAcquire(int acquires) {
}
// ReentrantLock 无惨构造方法 默认使用的是非公平锁
public ReentrantLock() {
sync = new NonfairSync();
}
// ReentrantLock 有参构造方法,根据传参决定使用哪种锁
public ReentrantLock(boolean fair) {
sync = fair ? new FairSync() : new NonfairSync();
}
//加锁方法
public void lock() {
sync.acquire(1);
}
//释放锁方法
public void unlock() {
sync.release(1);
}
ReentrantLock 的核心功能都委托给内部 Sync 类实现,而 Sync 继承自 AQS。它也就是AQS的具体实现
3、AbstractQueuedSynchronizer
3.1、核心变量&类&方法
// 链表中的节点
static final class Node{
}
// 链表头节点
private transient volatile Node head;
// 链表 尾结点
private transient volatile Node tail;
// 锁状态 锁的获取释放都是通过该状态来确定的
private volatile int state;
// 如果获取不到锁的线程通过此方法创建链表或者添加到链表最后
private Node addWaiter(Node mode) {}
// 链表添加成功后在该方法中会再次尝试获取一次,
// 如果还是获取不到责阻塞住当前线程,等待被唤醒
// 通过LockSupport.park(this);
final boolean acquireQueued(final Node node, int arg) {}
// 判断前驱节点的状态 来确定当前节点是阻塞,还是可以获取到锁
private static boolean shouldParkAfterFailedAcquire(Node pred, Node node) {
// 释放锁成功后,调用该方法来唤醒被阻塞的下一个节点
private void unparkSuccessor(Node node) {}
3.2、AQS中的Node
static final class Node {
//标记节点为共享模式
static final Node SHARED = new Node();
//标记节点为独占模式
static final Node EXCLUSIVE = null;
//下面的4个常量是给waitStatus用的
//当前节点被取消 这种状态的节点是异常的,无法被唤醒,也无法唤醒后继节点
static final int CANCELLED = 1;
//表示前继节点释放锁之后,需要对新节点进行唤醒操作
//如果唤醒signal状态的后续节点,会将signal状态更新为0
static final int SIGNAL = -1;
//当前节点进行等待队列中
static final int CONDITION = -2;
//表示下一次共享式同步状态获取将无条件传播下去
static final int PROPAGATE = -3;
/*
* 标记当前节点的信号量状态 (1,0,-1,-2,-3)5种状态
* 使用CAS更改状态,volatile保证线程可见性,高并发场景下,
* 即被一个线程修改后,状态会立马让其他线程可见。
* 新增初始化的节点状态为:0
*/
volatile int waitStatus;
//当前节点的前驱节点
volatile Node prev;
//当前节点的后节点
volatile Node next;
//当前节点的线程
volatile Thread thread;
}
3.2、AQS的核心思想
aqs的核心思想是,如果被请求的共享资源空闲,则将当前请求资源的线程设置为有效的工作线程,并且将共享资源设置为锁定状态,如果被请求的共享资源被占用,那么久需要一套线程阻塞等待以及被唤醒时锁分配机制,这个机制AQS是基于CLH锁(Craig Landin and Hagersten logcks)进一步优化实现。 AQS的CLH变体队列是一个双向队列,会暂时获取不到锁的线程将被加入到该队列中,CLH变体队列和原本的CLH队列区别主要有两点
- 由自旋优化为【自旋+ 阻塞】:自旋操作的性能很高,但大量的自旋操作比较占用CPU资源,因此CLH变体队列中会先通过自旋尝试获取,如果失败再进行阻塞等待
- 由单向队列优化为【双向队列】:在CLH变体队列中,会对等待的线程进行阻塞操作,当队列前边的线程释放锁之后,需要对后边的线程进行唤醒,因此增加了next指针,成为了双向队列
AQS是用state变量表示同步状态,通过内置的FIFO线程等待/等待队列来完成获取资源线程的排队工作
3.3、JUC中AQS框架的具体实现
| 实现类 | 使用和关联关系 |
|---|---|
| ReentrantLock | 使用AQS保存锁重复持有的次数。当一个线程获取锁时ReentrantLock记录当前获得锁的线程标识 |
| Semaphore | 使用AQS同步状态来保存信号量的当前计数 |
| CountDownLatch | 使用AQS同步状态来表示计数,计数为0时,所有的Acquire操作才可以通过 |
| ReentrantReadWriteLock | 使用AQS同步状态中的16位保存写锁持有的次数,剩下的16为用于保存读锁的持有次数 |
| ThreadPoolExecutor | worker利用AQS同步状态实现对独占线程变量的设置 |
4、以ReentrantLock加锁和解锁来进行画图理解
4.1、简易版加锁解锁流程
开始 加锁
│
├─ 尝试获取锁 (lock())
│ │
│ ├─ 如果锁未被占用 (state == 0)
│ │ ├─ CAS操作设置state为1
│ │ ├─ 设置当前线程为独占线程
│ │ └─ 获取锁成功 → 结束
│ │
│ ├─ 如果当前线程是独占线程 (重入)
│ │ ├─ state增加1
│ │ └─ 获取锁成功 → 结束
│ │
│ └─ 如果锁已被其他线程占用
│ ├─ 将当前线程加入等待队列
│ ├─ 阻塞当前线程
│ └─ 等待被唤醒
│
└─ 尝试非阻塞获取锁 (tryLock())
│
├─ 如果锁可用 → 立即获取锁并返回true
└─ 如果锁不可用 → 立即返回false
开始调用unlock() 解锁
│
├─ 调用sync.release(1) [AQS方法]
│ │
│ ├─ 尝试释放锁 tryRelease(1) [Sync实现]
│ │ │
│ │ ├─ 获取当前state值
│ │ ├─ 计算释放后的state: c = state - releases
│ │ │
│ │ ├─ 如果当前线程不是锁持有者 → 抛出IllegalMonitorStateException
│ │ │
│ │ ├─ 如果c == 0 (完全释放)
│ │ │ ├─ 设置独占线程为null
│ │ │ ├─ 设置state为0
│ │ │ └─ 返回true
│ │ │
│ │ └─ 如果c > 0 (部分释放,重入情况)
│ │ ├─ 设置state为c
│ │ └─ 返回false
│ │
│ ├─ 如果tryRelease返回true (完全释放)
│ │ ├─ 检查等待队列是否有等待线程
│ │ ├─ 如果有 → 唤醒后继节点线程
│ │ └─ 返回true
│ │
│ └─ 如果tryRelease返回false (部分释放)
│ └─ 返回false
│
└─ 流程结束