开发基础
JAVA
JUC
多线程
Lock锁
并发容器类
JUC辅助类
JUC底层知识
JVM
本文档使用 MrDoc 发布
-
+
首页
JUC底层知识
## java内存模型(JMM)  JMM规定了内存主要划分为**主内存**和**工作内存**两种。 > **主内存**:保存了所有的变量。 > **共享变量**:如果一个变量被多个线程使用,那么这个变量会在每个线程的工作内存中保有一个副本,这种变量就是共享变量。 > **工作内存**:每个线程都有自己的工作内存,线程独享,保存了线程用到的变量副本(主内存共享变量的一份拷贝)。工作内存负责与线程交互,也负责与主内存交互。 此处的主内存和工作内存跟JVM内存划分(堆、栈、方法区)是在不同的维度上进行的,如果非要对应起来,主内存对应的是Java堆中的对象实例部分,工作内存对应的是栈中的部分区域,从更底层的来说,**主内存对应的是硬件的物理内存,工作内存对应的是寄存器和高速缓存**。 JMM对共享内存的操作做出了如下两条规定: > - 线程对共享内存的所有操作都必须在自己的工作内存中进行,不能直接从主内存中读写; > - 不同线程无法直接访问其他线程工作内存中的变量,因此共享变量的值传递需要通过主内存完成。 内存模型的三大特性: - **原子性:**即不可分割性。比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作是原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要**使用同步技术(sychronized)或者锁(Lock)来让它变成一个原子操作**。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,我们可以通过阅读API来了解这些原子类的用法。比如:**AtomicInteger、AtomicLong、AtomicReference**等。 - **可见性:**每个线程都有自己的工作内存,所以当某个线程修改完某个变量之后,在其他的线程中,未必能观察到该变量已经被修改。**在 Java 中 volatile、synchronized 和 final 实现可见性。**volatile只能让被他修饰内容具有可见性,但不能保证它具有原子性。 - **有序性:**java的有序性跟线程相关。一个线程内部所有操作都是有序的,如果是多个线程所有操作都是无序的。因为JMM的工作内存和主内存之间存在延迟,而且java会对一些指令进行重新排序。volatile和synchronized可以保证程序的有序性,很多程序员只理解这两个关键字的执行互斥,而没有很好的理解到volatile和synchronized也能保证指令不进行重排序。 ## volatile关键字 ### volatile关键字的关键特性 * **可见性** 保证变量的更新操作能够立即被其他线程看到。 普通变量的修改可能不会立即对其他线程可见。 * **有序性** 禁止指令重排序,确保代码的执行顺序与代码的书写顺序一致。 普通变量的操作可能会被编译器或处理器进行指令重排序。 * **不具备原子性** 无法保证复杂的操作(如自增)的原子性。 对于简单的读写操作,volatile可以保证原子性。 ## CAS CAS(Compare And Swap)即比较并交换,是一种原子操作,用于实现无锁并发算法。它核心思想是将某位置的值与预期值比较,若相等则更新为新值,否则不更新并重试。在Java中,CAS通过java.util.concurrent.atomic包中的原子类实现,如AtomicInteger。CAS操作包含内存位置V、期望原值A、新值B,只有当V的值等于A时,才将V设为B。CAS常用于实现乐观锁,适用于读多写少的场景,但存在ABA问题,可通过版本号或时间戳解决。CAS使用不当可能导致自旋开销大、无法保证多个变量原子性等问题。 ### 使用AtomicInteger实现CAS操作 ``` import java.util.concurrent.atomic.AtomicInteger; public class CasDemo { public static void main(String[] args) { AtomicInteger i = new AtomicInteger(1); System.out.println("第一次更新:" + i.compareAndSet(1, 200)); System.out.println("第一次更新后i的值:" + i.get()); System.out.println("第二次更新:" + i.compareAndSet(1, 300)); System.out.println("第二次更新后i的值:" + i.get()); System.out.println("第三次更新:" + i.compareAndSet(200, 300)); System.out.println("第三次更新后i的值:" + i.get()); } } ``` >s **输出结果** > >第一次更新:true 第一次更新后i的值:200 第二次更新:false 第二次更新后i的值:200 第三次更新:true 第三次更新后i的值:300 ### 验证原子性 ``` import java.util.concurrent.atomic.AtomicInteger; public class CasAtomicDemo { private static AtomicInteger count = new AtomicInteger(0); public static void main(String[] args) throws InterruptedException { Thread thread1 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); } }); Thread thread2 = new Thread(() -> { for (int i = 0; i < 1000; i++) { count.incrementAndGet(); } }); thread1.start(); thread2.start(); thread1.join(); thread2.join(); System.out.println("Count: " + count.get()); } } ``` >s **输出结果** > > Count: 2000 ### 缺点 * 开销大 在并发量比较高的情况下,如果反复尝试更新某个变量,却又一直更新不成功,会给CPU带来较大的压力。 * ABA问题 当变量从A修改为B再修改回A时,变量值等于期望值A,但是无法判断是否修改,CAS操作在ABA修改后依然成功。 * 不能保证代码块的原子性 CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。 * 解决ABA问题 为了解决ABA问题,可以使用版本号或时间戳。每次更新变量时,不仅更新变量的值,还更新一个版本号或时间戳。这样,即使变量的值恢复到原来的值,版本号或时间戳也会不同,从而避免ABA问题。 ### 总结 * CAS:一种无锁并发算法,通过比较并交换实现原子操作。 * 优点:避免了锁的开销,提高了并发性能。 * 缺点:可能存在自旋开销,无法保证多个变量的原子性,存在ABA问题。 * 应用场景:适用于读多写少的场景,如计数器、标志位等。 ### 原子操作的类 | 类名 | 描述 | |----------------------|-----------------| | AtomicInteger | 提供对整数的原子操作。 | | AtomicLong | 提供对长整数的原子操作。 | | AtomicBoolean | 提供对布尔值的原子操作。 | | AtomicReference | 提供对对象引用的原子操作。 | | AtomicIntegerArray | 提供对整数数组的原子操作。 | | AtomicLongArray | 提供对长整数数组的原子操作。 | | AtomicReferenceArray | 提供对对象引用数组的原子操作。 | ## AQS AQS(AbstractQueuedSynchronizer)是Java并发包中最重要的基础组件之一,它是构建锁和其他同步工具的核心框架。ReentrantLock、Semaphore、CountDownLatch 等工具都是基于 AQS 实现的。 ### 框架结构 AQS框架结构如下:  AQS维护了一个volatile语义(支持多线程下的可见性)的共享资源变量**state**和一个FIFO(first-in-first-out)**线程等待队列**(多线程竞争state资源被阻塞时,会进入此队列)。 ### 核心思想 AQS的核心思想是: * 通过一个共享的 state 变量来表示同步状态。 * 通过一个 FIFO 队列(CLH 队列)来管理等待线程。 * 通过 CAS 操作来实现线程安全的 state 更新。 AQS 的设计采用了模板方法模式,开发者只需要实现 tryAcquire、tryRelease 等方法,AQS 会自动处理线程的排队和唤醒 ### 核心数据结构 #### 核心属性 * **state(同步状态)** * 类型:volatile int * 作用:表示共享资源的可用状态,是 AQS 实现同步的核心。 在 ReentrantLock 中,state 表示锁的重入次数。 在 Semaphore 中,state 表示剩余许可数量。 在 CountDownLatch 中,state 表示倒计数的初始值。 * **head 和 tail(等待队列头尾指针)** * 类型:Node 类型指针。 * 作用:维护一个 CLH 变体的 FIFO 双向队列,用于管理等待获取资源的线程。 head:指向队列的虚拟头节点(哨兵节点)。 tail:指向队列的最后一个节点。 #### CLH 队列 * CLH 队列:一个双向链表,用于管理等待线程。每个节点(Node)代表一个等待线程。 * Node 结构: ``` static final class Node { volatile int waitStatus; // 等待状态 volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 等待线程 Node nextWaiter; // 条件队列的后继节点 } ``` #### Node 内部类的关键属性 AQS 通过 Node 内部类封装等待线程的信息,每个节点代表一个等待线程: * **waitStatus(节点状态)** * 类型:int * 作用:表示线程的等待状态,包含以下值: CANCELLED (1):线程已取消等待(如超时或中断)。 SIGNAL (-1):当前节点的后继节点需要被唤醒。 CONDITION (-2):节点在条件队列中等待(如 Condition 的 await())。 PROPAGATE (-3):共享模式下,释放资源时需要传播唤醒后续节点。 * **prev 和 next(前驱和后继指针)** * 类型:Node 类型指针。 * 作用:维护双向队列的结构,用于快速定位前驱和后继节点。 * **thread(绑定的线程)** * 类型:Thread * 作用:记录当前节点关联的线程。 * **nextWaiter(条件队列指针)** * 类型:Node 类型指针。 * 作用:在条件队列(如 Condition)中,指向下一个等待节点。 #### AbstractOwnableSynchronizer 的属性 AQS 继承自 AbstractOwnableSynchronizer,包含以下属性: * **exclusiveOwnerThread(独占模式持有者线程)** * 类型:Thread * 作用:记录当前持有独占锁(如 ReentrantLock)的线程,用于实现可重入锁机制。 ### 工作原理 #### 获取锁 * 调用 acquire(int arg) 方法。 * 尝试获取锁(tryAcquire)。 成功:直接返回。 失败:将线程加入队列并挂起。 * 线程被唤醒后,重新尝试获取锁。 ``` public final void acquire(int arg) { if (!tryAcquire(arg) && acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) selfInterrupt(); } ``` #### 释放锁 * 调用 release(int arg) 方法。 * 尝试释放锁(tryRelease)。 * 唤醒队列中的下一个线程。 ``` public final boolean release(int arg) { if (tryRelease(arg)) { Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); return true; } return false; } ``` ### 基于AQS实现锁的思路 AQS将大部分的同步逻辑均已经实现好,继承的自定义同步器只需要实现state的获取(acquire)和释放(release)的逻辑代码就可以,主要包括以下方法: * 独占模式(如 ReentrantLock): * 通过 tryAcquire/tryRelease 修改 state,控制单个线程的加锁与释放。 * tryAcquire(int):独占方式。尝试获取资源,成功则返回true,失败则返回false。 * tryRelease(int):独占方式。尝试释放资源,成功则返回true,失败则返回false。 * 共享模式(如 Semaphore): * 通过 tryAcquireShared/tryReleaseShared 修改 state,允许多个线程同时获取资源。 * tryAcquireShared(int):共享方式。尝试获取资源。负数表示失败;0表示成功,但没有剩余可用资源;正数表示成功,且有剩余资源。 * tryReleaseShared(int):共享方式。尝试释放资源,如果释放后允许唤醒后续等待结点返回true,否则返回false。 * 条件队列(如 Condition): * 通过 ConditionObject 管理条件等待队列,与主同步队列协作实现线程等待与唤醒。 * isHeldExclusively():该线程是否正在独占资源。只有用到Condition才需要去实现它。 ### ReentrantLock底层原理  在ReentrantLock类中包含了3个AQS的实现类: 1. 抽象类Sync 2. 非公平锁实现类NonfaireSync 3. 公平锁实现类FairSync #### Sync抽象类 ``` /** * 自定义方法:为非公平锁的实现提供快捷路径 */ abstract void lock(); /** * 自定义通用方法,两个子类的tryAcquire方法都需要使用非公平的tryLock方法 */ final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 如果当前没有线程获取到锁 if (compareAndSetState(0, acquires)) { // 则CAS获取锁 setExclusiveOwnerThread(current); // 如果获取锁成功,把当前线程设置为有锁线程 return true; } } else if (current == getExclusiveOwnerThread()) { // 如果当前线程已经拥有锁,则重入 int nextc = c + acquires; // 每重入一次stat累加acquires if (nextc < 0) // overflow throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; } /** * 实现AQS的释放锁方法 */ protected final boolean tryRelease(int releases) { int c = getState() - releases; // 每释放一次stat就减releases if (Thread.currentThread() != getExclusiveOwnerThread()) // 当前线程不是有锁线程抛异常 throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // stat减为0则释放锁 free = true; setExclusiveOwnerThread(null); } setState(c); return free; } protected final boolean isHeldExclusively() { // While we must in general read state before owner, // we don't need to do so to check if current thread is owner return getExclusiveOwnerThread() == Thread.currentThread(); } ``` #### NonfairSync ``` /** * Sync object for non-fair locks */ static final class NonfairSync extends Sync { private static final long serialVersionUID = 7316153563782823691L; /** * Performs lock. Try immediate barge, backing up to normal * acquire on failure. */ final void lock() { if (compareAndSetState(0, 1)) // CAS把stat设置为1 setExclusiveOwnerThread(Thread.currentThread()); // 获取到锁 else acquire(1); } protected final boolean tryAcquire(int acquires) { return nonfairTryAcquire(acquires); // 使用了Sync抽象类的nonfairTryAcquire方法 } } ``` #### FairSync ``` /** * Sync object for fair locks */ static final class FairSync extends Sync { private static final long serialVersionUID = -3000897897090466540L; final void lock() { acquire(1); } /** * Fair version of tryAcquire. Don't grant access unless * recursive call or no waiters or is first. */ 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; } } ``` 其中`hasQueuedPredecessors` 1. 当等待队列只有一个线程时,直接获取到锁。 2. 如果队列不止一个线程,并且下一个线程就是当前申请锁的线程,则获取锁。 ### AQS的优化 AQS在CLH锁的基础上进行了优化,形成了其内部的CLH队列变体。主要改进点有以下两方面: * 自旋 + 阻塞:CLH锁使用纯自旋方式等待锁的释放,但大量的自旋操作会占用过多的CPU资源。AQS引入了自旋 + 阻塞的混合机制: * 如果线程获取锁失败,会先短暂自旋尝试获取锁; * 如果仍然失败,则线程会进入阻塞状态,等待被唤醒,从而减少CPU的浪费。 * 单向队列改为双向队列:CLH锁使用单向队列,节点只知道前驱节点的状态,而当某个节点释放锁时,需要通过队列唤醒后续节点。AQS将队列改为双向队列,新增了next指针,使得节点不仅知道前驱节点,也可以直接唤醒后继节点,从而简化了队列操作,提高了唤醒效率。 ### AQS的应用场景 * AQS广泛应用于以下并发工具类: * ReentrantLock:基于AQS实现的可重入锁。 * Semaphore:基于AQS实现的信号量。 * CountDownLatch:基于AQS实现的倒计时门闩。 * ReentrantReadWriteLock:基于AQS实现的读写锁。 * CyclicBarrier:基于AQS实现的循环栅栏。
admin
2025年3月9日 09:24
转发文档
收藏文档
上一篇
下一篇
手机扫码
复制链接
手机扫一扫转发分享
复制链接
Rancher
Jenkins
ADMIN-UI
VBEN-ADMIN-UI
RUST-FS
MinIO
mindoc
Markdown文件
PDF文档(打印)
分享
链接
类型
密码
更新密码