从互斥锁到无锁,Java 20年并发安全进化史

Java自1996年诞生以来,其并发安全方面演进史,就是一部从"悲观互斥"向"乐观并行"持续进化的历史。本文将沿着JDK版本发布的时间线,梳理Java在解决线程安全问题时,在锁机制、同步工具以及无锁算法上的关键变革。

第一章:混沌初开------JDK 1.0的原始线程模型(1996年)

1996年1月,JDK 1.0的发布奠定了Java最基础的线程模型。这个版本虽然功能原始,但确立了一个具有传承性的设计------Java采用了协作式的线程通信方式。

1.1 基础原语:synchronized与wait/notify

从第一个版本开始,Java就引入了synchronized关键字作为内置锁(Intrinsic Lock),以及与之配套的wait()/notify()方法进行线程间通信。其设计核心非常简洁:

  • 非静态方法 使用synchronized修饰,相当于锁住当前实例对象(synchronized(this))。
  • 静态方法 使用synchronized修饰,相当于锁住该类的Class对象(synchronized(XXX.class))。

这是Java并发安全的起点------悲观锁思想的体现:线程进入同步块前必须获得锁,其他线程只能阻塞等待。在当时的技术背景下,这种设计简单直接,足以应对基础的多线程需求。

1.2 早期设计的遗憾:被废弃的"暴力"方法

JDK 1.0初期还提供了stop()suspend()resume()三个方法。但由于它们本质上是不安全的------stop()直接终止线程会导致对象状态被破坏,suspend()挂起线程时持有锁极易引发死锁------这些方法在JDK 1.2中就被正式废弃了。这一事件给Java开发者上了一课:并发控制不能靠"暴力中断",而需要线程间的自觉配合

第二章:理论奠基------JDK 1.2到1.4的修修补补(1998-2002年)

在Java诞生的前六年,开发者们饱受并发问题的困扰,却往往归咎于代码逻辑,而忽视了更深层的原因:Java内存模型(JMM)的定义存在缺陷

2.1 可见性难题

早期的JMM缺乏严谨的"先行发生"(happens-before)规则。一个线程修改了共享变量,另一个线程可能永远看不到这个修改,这就是可见性问题。

当时volatile关键字虽然被设计用来解决这个问题,但在旧的JMM下,volatile的语义并不足够强,不能完全解决这个问题。

2.2 民间力量的酝酿:Doug Lea的贡献

在这一时期,并发编程领域的泰斗Doug Lea开始撰写一系列并发工具类,包括后来成为JUC核心的ReentrantLockConcurrentHashMap等。这些代码虽然没有被纳入JDK,但其设计思想和实现方案为后来的标准化奠定了基础。

这个时期的Java处于一个尴尬的境地:硬件已经进入多核时代,但语言层面的并发模型还停留在单核时代

第三章:革命前夜------JDK 1.5的里程碑式突破(2004年)

2004年,JDK 1.5(即Java 5)的发布堪称Java并发编程史上最重要的里程碑。这一年,JSR 133规范为Java带来了全新的内存模型,JSR 166引入了划时代的java.util.concurrent包。

3.1 JSR 133的救赎:内存模型的重新定义

JSR 133重新定义了volatilesynchronized的语义,明确了happens-before关系

对一个volatile变量的写操作,happens-before于后续对这个变量的读操作。

这意味着只要写入volatile变量,就相当于向内存发出"刷新"信号;读取volatile变量,则相当于从内存中"加载"最新值。这一修订,为后来高性能并发工具的发展扫清了理论障碍。

3.2 JSR 166的降临:JUC包的诞生

由Doug Lea主导的JSR 166专家组正式将一系列并发工具纳入JDK标准库,包括:

  • 显式锁ReentrantLockReadWriteLock
  • 同步器SemaphoreCountDownLatchCyclicBarrier
  • 并发容器ConcurrentHashMapCopyOnWriteArrayList
  • 线程池ThreadPoolExecutorExecutors

3.3 CAS的正式登场:原子变量类的革命

JDK 1.5最具有前瞻性的设计,是引入了java.util.concurrent.atomic包,提供了AtomicIntegerAtomicLongAtomicReference等一系列原子类。这些类的底层实现,正是CAS(Compare-And-Swap,比较并交换)------一种源自硬件层面的乐观并发控制技术。

3.3.1 硬件级的乐观锁

CAS是一种乐观锁 策略。它包含三个操作数:内存位置(V)、期望值(A)、新值(B) 。只有当V的值等于A时,才将V更新为B,且整个操作是原子的。

CAS依赖于现代CPU提供的特定指令(如x86架构的cmpxchg),由硬件保证其原子性,避免了用户态锁的介入。它的核心思想是:假设没有冲突,能改就改;如果改的时候发现被动了,那就重试

3.3.2 无锁编程的实践

AtomicInteger为例,其incrementAndGet()方法的内部实现是一个典型的CAS循环:

java 复制代码
public final int incrementAndGet() {
    for (;;) {
        int current = get();
        int next = current + 1;
        if (compareAndSet(current, next))
            return next;
    }
}

这种"循环重试 "的模式,就是无锁编程的雏形。虽然它会占用CPU循环,但在低冲突场景下,远比挂起线程高效。这正是乐观锁思想的精髓:用CPU周期换取线程的存活,用自旋替代阻塞

3.3.3 ReentrantLock的CAS内核

值得注意的是,即便是ReentrantLock这样的显式锁,其底层实现也大量依赖CAS。AbstractQueuedSynchronizer(AQS)作为JUC锁的基石,其核心状态(state)就是一个被CAS操作的volatile变量。获取锁、释放锁的成败,都取决于能否通过CAS成功修改这个状态。可以说,CAS是JUC框架的发动机

3.4 Unsafe:CAS的底层支撑

JDK 1.5中CAS的广泛应用,离不开一个核心底层工具类------sun.misc.Unsafe。该类提供了直接操作内存、执行原子操作的底层能力,是Java并发工具(如原子类、AQS)实现的核心依赖,也是CAS操作能够落地的关键。

Unsafe的设计初衷是为JDK内部类(如java.util.concurrent.atomicjava.util.concurrent.locks)提供底层支持,并非面向普通开发者。它的核心能力包括:

  • 原子操作 :提供compareAndSwapIntcompareAndSwapLongcompareAndSwapObject等方法,直接封装CPU的CAS指令,是AtomicInteger等原子类的底层实现。例如,AtomicIntegercompareAndSet方法,本质就是调用UnsafecompareAndSwapInt方法。

  • 内存操作:可直接分配、释放内存,操作对象的字段(无需通过反射),突破了Java的安全限制,能直接操作堆外内存,为高性能并发提供了可能。

  • 线程调度 :提供parkunpark方法,用于线程的挂起和唤醒,是AQS实现线程等待队列的核心底层方法。

需要注意的是,Unsafe具有极高的危险性:它绕过了Java的安全检查,直接操作内存和底层资源,一旦使用不当,极易引发内存泄漏、数据错乱甚至JVM崩溃。

因此,JDK官方一直未将其公开暴露,普通开发者无法直接通过正常方式获取其实例(需通过反射),且该类在后续JDK版本中逐渐被更安全的API替代。

3.5 JDK 1.5的历史定位

JDK 1.5标志着Java并发编程从"单一路径"走向"多元化":

并发模型 代表技术 适用场景
悲观阻塞 synchronized 简单同步,低并发
显式锁 ReentrantLock 需要超时、中断等高级功能
无锁并发 Atomic* 简单计数器,状态标志
并发容器 ConcurrentHashMap 高并发数据结构

开发者第一次拥有了选择权:可以根据场景在互斥阻塞和乐观自旋之间做出权衡。

第四章:双雄并立------JDK 1.6的锁优化与性能之争(2006年)

就在开发者们逐渐转向ReentrantLock和原子类时,JVM团队没有放弃synchronized

从JDK 1.6开始,HotSpot VM对synchronized进行了大刀阔斧的优化,引入了锁升级 机制,使其性能在某些场景下甚至超越了ReentrantLock

4.1 对象头与Mark Word

synchronized的优化依赖于对象在内存中的布局。每个Java对象的对象头中都有一块称为Mark Word 的区域,它记录了对象的哈希码、GC年龄以及锁状态标志。锁升级的过程,就是Mark Word中标志位变化的过程。

4.2 锁升级路径:无锁 → 偏向锁 → 轻量级锁 → 重量级锁

  • 偏向锁(Biased Locking):针对锁大多由同一线程获得的场景。当线程第一次进入同步块,JVM会将Mark Word中的线程ID设为当前线程。此后该线程再次进入时,只需比对ID,几乎零开销。如果其他线程来竞争,偏向锁即撤销。

  • 轻量级锁(Lightweight Locking) :当竞争出现时,锁升级为轻量级锁。线程会在自己的栈帧中创建锁记录(Lock Record),通过CAS自旋尝试修改Mark Word指向自己的锁记录。注意,这里的CAS正是JDK 1.5引入的原子操作在JVM内部的运用------JVM自身也开始用CAS优化锁的获取。

  • 重量级锁(Heavyweight Locking) :当自旋超过一定阈值,或等待线程数过多,锁会膨胀为重量级锁。此时Mark Word指向一个操作系统级别的互斥量(mutex)。未获取锁的线程进入阻塞队列,由操作系统调度,涉及用户态与内核态的切换,开销最大。

4.3 CAS的双重角色

从这个阶段开始,CAS在Java并发体系中扮演着双重角色:

  1. 应用层面 :开发者通过AtomicInteger等类直接使用CAS,实现无锁并发。
  2. JVM层面 :虚拟机内部用CAS优化synchronized的轻量级锁获取,用CAS实现偏向锁的撤销。

4.4 两种路径的对比

到了JDK 1.6,Java实际上形成了两条并行的并发技术路线:

技术路线 代表 核心机制 优势
内置锁路线 synchronized 锁升级 + JVM级CAS 简单易用,自动优化
显式锁路线 ReentrantLock + Atomic* 应用级CAS + 自旋 灵活可控,功能丰富

第五章:巅峰对决------JDK 1.8的革命性重构(2014年)

JDK 1.8的发布,标志着Java并发技术走向成熟。这一年,ConcurrentHashMap经历了革命性重构,将CAS和synchronized的配合发挥到了极致。

5.1 ConcurrentHashMap的两代变迁

JDK 7时代:分段锁(Segment)

在JDK 7及之前,ConcurrentHashMap采用了分段锁设计。它将整个哈希表分成多个Segment(段),每个Segment独立持有一把锁。当多个线程操作不同Segment中的数据时,可以真正并行执行,大大提高了并发度。

但是,分段锁也存在固有缺陷:内存开销大(需要维护多个锁);段数量固定,扩容困难;且某些操作(如size())需要依次锁住所有段,性能较差。

JDK 8的革新:synchronized + CAS

JDK 8完全摒弃了分段锁,采用了更精细的设计:

  • 数据结构 :采用Node数组 + 链表/红黑树
  • 并发控制:写入数据时,根据桶位(bucket)的状态采取不同策略:
  • 如果桶位为空 ,则通过CAS直接插入节点------这是无锁并发的实践。
  • 如果桶位非空 ,则对链表头节点树根节点 使用synchronized加锁------这是细粒度锁的应用。

这种设计实现了"锁粒度细化到单个桶 ",且巧妙利用了synchronized在JDK 8中已优化的性能。写线程只需锁住自己正在操作的桶,其他桶的访问完全不受影响。

这是"CAS+局部锁"的完美结合,也是两条并发技术路线从竞争走向融合的标志。

5.2 LongAdder的登场

JDK 8还引入了LongAdder,这是对AtomicLong的进一步优化。在高并发场景下,大量线程同时CAS竞争同一个变量会导致大量重试和缓存抖动。

LongAdder的核心思想是分而治之 :它将一个计数变量拆分成多个单元(Cell),每个线程只更新自己对应的单元,最后求和时再汇总。这实际上是将CAS的竞争从"单点"分散到"多点",进一步提升了吞吐量

5.3 JDK 8的历史地位

JDK 8标志着Java并发技术走向成熟:

  • 锁不再是唯一选择:大量场景可以用无锁数据结构替代锁。
  • 悲观与乐观的融合ConcurrentHashMap证明了CAS和synchronized可以协同工作。
  • 细粒度成为主流:无论是锁粒度还是数据分片粒度,都走向精细化。

第六章:未来已来------JDK 9到21的持续演进(2017-2023年)

从JDK 9开始,Java进入模块化和快速迭代的时代。并发领域虽然没有革命性变革,但持续进行着优化和调整。

6.1 偏向锁的谢幕

JDK 15 中,偏向锁被标记为废弃;JDK 21 中,偏向锁默认被禁用。原因在于:在高并发应用中,锁通常由不同线程竞争,偏向锁的撤销成本反而成为负担。JVM团队根据实际场景数据,做出了务实的选择------这体现了并发技术演进中"实践检验真理"的原则。

6.2 VarHandle:标准化的内存访问工具

JDK 9引入了java.lang.invoke.VarHandle,作为Unsafe的标准化替代方案,解决了Unsafe安全性差、API不规范的问题,同时保留了底层内存操作和原子操作的能力,成为Java并发底层编程的新选择。

VarHandle的核心优势的在于"安全、标准化、高效",其核心能力与Unsafe对应,但更具规范性:

  • 原子操作支持 :提供compareAndSetgetAndAddgetAndSet等原子方法,与Unsafe的原子操作功能一致,且支持更丰富的类型(包括基本类型和引用类型),底层同样依赖CPU的CAS指令,性能与Unsafe相当。

  • 内存语义控制 :支持通过memoryOrder参数指定内存访问顺序(如volatile、acquire、release等),灵活控制内存可见性和有序性,比volatile关键字更精细,也比Unsafe的操作更规范。

  • 安全性提升 :VarHandle是公开的标准化API,无需通过反射获取,且操作被严格限制在合法范围内,避免了Unsafe直接操作内存可能引发的风险,同时提供了类型安全检查,减少了编程错误。

  • 替代Unsafe的核心场景 :在JDK后续版本中,大量原依赖Unsafe的类(如原子类、并发容器)逐渐迁移到VarHandle实现,例如AtomicInteger的部分方法在JDK 9+中已通过VarHandle实现,进一步提升了代码的安全性和可维护性。

VarHandle的出现,标志着Java底层并发工具的规范化演进------不再依赖非公开的危险API,而是通过标准化接口提供底层能力,兼顾了高性能和安全性,为后续并发技术的优化奠定了基础。

结语

纵观这二十多年的发展,Java并发安全的演进,从最初粗暴的全局锁,到精细化的读写锁,再到基于CAS的无锁并发,每一次技术跃迁,都是为了在保证线程安全的前提下,将硬件的并行效能发挥到极致

相关推荐
悟空码字2 小时前
别再让你的SpringBoot包"虚胖"了!这份瘦身攻略请收好
java·spring boot·后端
szm02252 小时前
操作系统-
java·linux·服务器
哆啦A梦15882 小时前
java项目在后端做跨域配置
java·vue3
盐水冰2 小时前
【烘焙坊项目】后端搭建(13)- 数据统计--图形报表
java·后端·学习·spring
易雪寒2 小时前
Java List 根据List中对象的属性值是否相同作为同一组,分割成多个连续的子List
java·数据结构·list·分组切割
小王不爱笑1322 小时前
Kubernetes(K8s)核心知识点
java
桑榆肖物2 小时前
.NET 10 Native AOT 在 Linux 嵌入式设备上的实战
java·linux·.net·aot
墨着染霜华2 小时前
Java实战:封装Redis非阻塞分布式锁,彻底解决表单重复提交主键冲突
java·redis·分布式
启山智软2 小时前
【使用 Java(JSP)实现的简单商城页面前端示例】
java·前端·商城开发