JUC之并发编程
JUC之并发编程
JUC 的原理可以概括为:它提供了一套建立在 volatile、CAS和 AQS三大基石之上的高级工具集,用于替代底层的 synchronized和 wait()/notify()机制,实现更精细、性能更高、功能更强大的线程控制。
下面我们分点详细解析其核心原理。
- 三大基石
1.1 volatile 关键字
作用:保证变量的可见性和有序性(禁止指令重排序),但不保证原子性。
原理:
可见性:当一个线程修改了 volatile修饰的变量,新值会立即被强制刷新到主内存。同时,其他线程中关于该变量的缓存行(Cache Line)会失效,下次读取时必须从主内存重新加载。
有序性:通过插入内存屏障(Memory Barrier)禁止编译器和处理器对指令进行重排序,从而遵循 happens-before原则。
典型应用:volatile常用于作为状态标志位(如 isRunning),或者用于实现一些“一次性发布”的安全发布模式(如双重检查锁单例模式)。
1.2 CAS (Compare-And-Swap)
作用:一种无锁算法的核心,它保证了对一个变量进行“读取-计算-写入”这三个操作时的原子性,而无需使用重量级锁。
原理:
操作包含三个操作数:内存位置(V)、预期原值(A)、新值(B)。
当且仅当内存位置 V 的值等于预期原值 A 时,处理器才会自动将位置 V 的值更新为新值 B,否则不执行任何操作。整个操作是一个原子指令。
通常配合 自旋(循环) 使用,即如果 CAS 失败,线程会循环重试,直到成功。
实现:
底层依赖硬件指令实现(如 x86 架构的 CMPXCHG指令)。
Java 中通过 Unsafe类提供的本地方法(Native Method)来调用这些指令。AtomicInteger、AtomicLong等原子类是其最直接的体现。
缺点:
ABA 问题:
一个值从 A 变成了 B,又变回了 A,CAS 检查时会认为它没变,但实际上可能已经发生了其他操作。
解决方案是:增加版本概念,使用带有版本的原子引用,如 AtomicStampedReference。
循环时间长时 CPU 开销大:如果竞争激烈,线程会长时间自旋,消耗 CPU。
只能保证一个共享变量的原子操作。可以使用 AtomicReference将多个变量封装成一个对象来进行 CAS 操作。
1.3 AQS (AbstractQueuedSynchronizer)
作用:JUC 包中锁和同步器的核心框架和基石。它是一个抽象类,定义了构建锁和同步器的模板方法。ReentrantLock、CountDownLatch、Semaphore、ReentrantReadWriteLock等都是基于 AQS 构建的。
核心思想:
它维护了一个 volatile int state(代表共享资源的状态)和一个 FIFO 线程等待队列(CLH 队列的变体,用于管理获取资源失败的线程)。
子类通过继承 AQS 并实现其模板方法(主要是 tryAcquire、tryRelease、tryAcquireShared、tryReleaseShared)来定义如何获取和释放资源,从而实现不同的同步语义(如独占锁、共享锁)。
工作流程(以独占锁为例):
获取锁:线程调用 acquire(int arg)方法。
tryAcquire尝试直接获取资源(由子类实现逻辑),如果成功则返回。
如果失败,则将当前线程封装成一个节点(Node)并通过 CAS 操作加入到等待队列的尾部,然后进入自旋或阻塞状态。
队列中的前驱节点释放资源后,会唤醒(unpark)后继节点,后继节点再次尝试获取资源。
释放锁:线程调用 release(int arg)方法。
tryRelease尝试释放资源(由子类实现),如果成功,则唤醒等待队列中下一个有效的节点。
AQS 通过一个框架就解决了大部分同步工具的实现问题,极大地简化了并发工具的开发。
- 核心组件及其原理
基于上述三大基石,JUC 提供了丰富的组件:
2.1 锁(Locks)
ReentrantLock:
原理:基于 AQS 实现的可重入独占锁。state为 0 表示锁空闲,>0 表示被持有,数值代表重入次数。
特点:相比 synchronized,它提供了可中断的锁获取、超时获取锁、公平/非公平模式选择等高级功能。
ReentrantReadWriteLock:
原理:将 AQS 的 state变量拆解使用:高 16 位表示读锁状态(共享计数),低 16 位表示写锁状态(独占重入计数)。
特点:实现了读写分离,允许多个读线程同时访问,但写线程独占,提高了读多写少场景的性能。
2.2 原子类(Atomic Classes)
原理:如 AtomicInteger,其内部使用 volatile int value保证可见性,所有修改操作(如 incrementAndGet)都基于 CAS 实现,保证了原子性而无须加锁,性能极高。
2.3 并发容器(Concurrent Collections)
ConcurrentHashMap:
原理(JDK8+):摒弃了 JDK7 中的分段锁(Segment),采用 Node数组 + 链表 + 红黑树的结构。锁的粒度更细,直接对数组的每个桶(Bucket)的头节点(Node)进行 synchronized 锁定(JDK 对 synchronized做了大量优化,性能已很高)和 CAS 操作。
高并发读:因为 Node的 val和 next指针都用 volatile修饰,保证了可见性,所以读操作通常不需要加锁。
CopyOnWriteArrayList:
原理:写时复制。所有写操作(add, set, remove)都会在底层创建一个新的数组副本,在新副本上操作,完成后再将数组引用指向新副本。读操作则在旧数组上进行,因此读操作非常快且无需同步,但写操作开销大,适合读多写少的场景。
2.4 同步工具(Synchronizers)
CountDownLatch:
原理:基于 AQS 的共享模式。初始化时设置状态值 state = count。await()方法会阻塞,直到其他线程调用 countDown()方法(tryReleaseShared)使 state减为 0,然后唤醒所有等待线程。
CyclicBarrier:
原理:让一组线程互相等待,直到所有线程都到达一个屏障点(Barrier)后才继续执行。基于 ReentrantLock和 Condition实现。可以循环使用(reset)。
Semaphore:
原理:基于 AQS 的共享模式。state值表示许可证(Permit)数量。acquire()获取一个许可(state减 1),release()释放一个许可(state加 1),用于控制同时访问特定资源的线程数量。
2.5 线程池(Executor Framework)
原理:核心是 ThreadPoolExecutor 类,其内部维护了一个工作线程集合(Worker Set) 和一个阻塞任务队列(BlockingQueue)。
核心参数:corePoolSize, maximumPoolSize, workQueue, RejectedExecutionHandler。
工作流程:
提交任务(execute)。
如果运行线程数 < corePoolSize,创建新线程执行。
否则,将任务放入工作队列。
如果队列已满且运行线程数 < maximumPoolSize,创建新线程执行。
否则,触发拒绝策略。
优点:通过复用线程,避免了频繁创建和销毁线程的开销;可以有效地控制并发度和资源消耗。
总结
组件/概念 核心原理 解决的问题
volatile 内存屏障,缓存一致性协议 可见性,有序性
CAS 硬件原子指令,无锁自旋 原子操作,避免重量级锁开销
AQS volatile state+ FIFO队列 + 模板方法 构建锁和同步器的核心框架
ReentrantLock 基于AQS实现独占锁,可重入 更灵活的互斥同步,支持公平/非公平
ConcurrentHashMap synchronized锁桶 + volatile读 高并发下的线程安全Map
CountDownLatch 基于AQS共享模式,state作计数器 多个线程等待直到一个事件发生
线程池 线程复用,工作队列,资源管控 降低线程创建销毁开销,控制资源
JUC 的强大之处在于它将复杂的并发控制抽象成了一个个易于使用的工具,而其底层则巧妙地结合了 Java 内存模型规范、硬件特性和精妙的算法设计。理解 volatile、CAS和 AQS,是理解整个 JUC 体系的关键。