跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客我的书AI学习GitHub 精选镜像AI 生图工具UI配色美学关于
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava算法

Java 多线程进阶:锁策略、CAS 与 JUC 组件详解

Java 多线程进阶涵盖常见锁策略对比,包括悲观乐观、重量级轻量级、挂起自旋、读写互斥、可重入及公平性分析。重点解析 synchronized 锁升级机制(偏向锁至重量级锁)及优化措施如锁消除与粗化。深入探讨 CAS 原理、应用场景及 ABA 问题解决方案。最后介绍 JUC 核心组件 Callable、ReentrantLock、Semaphore 和 CountDownLatch 的使用与区别。

链路追踪发布于 2026/3/16更新于 2026/10/1598 浏览
Java 多线程进阶:锁策略、CAS 与 JUC 组件详解

一、常见的锁策略

1.1 悲观锁 vs 乐观锁

悲观和乐观是指锁竞争的激烈情况。

  1. 悲观锁:加锁的时候预测接下来的锁竞争会非常激烈,就需要针对这样的激烈情况额外做工作;
  2. 乐观锁:加锁的时候预测接下来的锁竞争不激烈,就不需要额外做工作去处理锁竞争情况;

1.2 重量级锁 vs 轻量级锁

这两种锁就对应上述的悲观乐观情况下的处理机制。

  1. 重量级锁:应对上面的锁竞争激烈的悲观情况,效率更低;
  2. 轻量级锁:应对上面的锁竞争不激烈的情况,效率更高。

1.3 挂起等待锁 vs 自旋锁

这又是对上面的重量级与轻量级锁的典型实现。

  1. 挂起等待锁:重量级锁的典型实现,是操作系统内核级别的,加锁发生竞争,线程进入阻塞后,就需要内核进行唤醒。获取锁的周期会变长,但是这期间不会消耗 CPU 资源。
  2. 自旋锁:轻量级锁的典型实现,是应用程序级别的,加锁时发生竞争,一般不进行阻塞,而是通过忙等,等待后续程序唤醒。获取锁的周期很短,可以及时获取到锁,但是这期间会一直消耗 CPU 资源。

1.4 普通互斥锁 vs 读写锁

这两种锁是针对加锁解锁时的线程安全问题。

  • 普通互斥锁只有加锁,解锁操作,并且读操作不会出现线程安全问题;
  • 而读写锁有读加锁,写加锁,和解锁操作, 读锁与读锁之间不互斥; 读锁与写锁之间存在互斥; 写锁与写锁之间也存在互斥。 读写锁主要是针对读操作多,写操作少的情况服务。

1.5 可重入锁 vs 不可重入锁

这组锁就是针对同一个线程多次嵌套获取同一把锁。

  • 可重入锁:字面意思是'可以重新进入的锁',即允许同一个线程多次获取同一把锁。
  • 不可重入锁:字面意思是'不可以重新进入的锁',即同一个线程多次获取同一把锁,会报错。

1.6 不公平锁 vs 公平锁

这组锁是针对线程获取锁的概率设置的。

  • 不公平锁:在 Java 中不公平锁是概率均等的随机分配锁;
  • 公平锁:在 Java 中公平锁是按照等待的时间先来后到分配锁。因为操作系统是随机调度,为了实现公平锁就需要数据结构来记录先后调度顺序。

二、synchronized 特性

2.1 synchronized 的锁策略

锁策略如下:

  • synchronized 是一个自适应的锁,开始时是乐观锁,如果锁冲突频繁,就转换为悲观锁。
  • 开始是轻量级锁实现,如果锁被持有的时间较长,就转换成重量级锁。
  • 是一种普通互斥锁。
  • 是一种不公平锁。
  • 是一种可重入锁。

2.2 synchronized 加锁过程

synchronized 的自适应过程称为锁升级:无锁 -> 偏向锁 -> 自旋锁 -> 重量级锁。

  • 偏向锁:偏向锁不是真的'加锁',只是给对象头中做一个'偏向锁的标记',记录这个锁属于哪个线程。 如果后续没有其他线程来竞争该锁,那么就不用进行其他同步操作了 (避免了加锁解锁的开销) 如果后续有其他线程来竞争该锁 (刚才已经在锁对象中记录了当前锁属于哪个线程了,很容易识别 当前申请锁的线程是不是之前记录的线程),那就取消原来的偏向锁状态,进入一般的轻量级锁状态。 就像跟你搞暧昧,但是不正式跟你确认关系,如果没人来追求你就一直不确定关系,如果有人追求你就确认关系。
  • 无锁 -> 偏向锁:这个阶段是进入 synchronized 代码块;
  • 偏向锁 -> 自旋锁:这个阶段是拿到偏向锁后,遇到其他线程来竞争这个锁;
  • 自旋锁 -> 重量级锁:这个阶段是 JVM 发现当前的锁竞争非常激烈。

2.3 其它优化措施

除了上面的 synchronized 锁升级以外,还有以下的优化措施:

  • 锁消除:编译器优化措施,编译器会对加锁的代码进行判断(但这种判断是非常保守的,只有 100% 确定当前是单线程),如果当前逻辑不需要加锁,编译器就会自动去除 synchronized。
  • 锁的粒度:加锁和解锁之间,包含的代码执行的逻辑/时间越多,则锁的粒度就越粗,反之越细。

锁粗化:一个代码对细粒度的代码反复加锁解锁,就会将这个步骤优化为更粗粒度的加锁解锁。

文章配图

三、CAS

3.1 CAS 概念

CAS: 全称 Compare and swap,字面意思:'比较并交换',一个 CAS 涉及到以下操作:

我们假设内存中的原数据 V,旧的预期值 A,需要修改的新值 B。比较 A 与 V 是否相等。(比较)如果比较相等,将 B 写入 V。(交换)返回操作是否成功。

伪代码表示如下:

boolean CAS(address, expectValue, swapValue) {
    if (&address == expectedValue) {
        &address = swapValue;
        return true;
    }
    return false;
}

3.2 CAS 应用场景

3.2.1 实现原子类

原子类:特指 java.util.concurrent.atomic 包下的类,这些类中的操作都是原子的。

例如执行下面这段代码:结果就一直是 100000。

import java.util.concurrent.atomic.AtomicInteger;

public class Demo {
    private static AtomicInteger count = new AtomicInteger(0);

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.incrementAndGet();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 50000; i++) {
                count.incrementAndGet();
            }
        });
        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();
        System.out.println(count.get());
    }
}

原子类线程安全的原理:

假设两个线程同时调用 getAndIncrement:两个线程都读取 value 的值到 oldValue 中。(oldValue 是一个局部变量,在栈上,每个线程有自己的栈) 线程 1 先执行 CAS 操作。由于 oldValue 和 value 的值相同,直接进行对 value 赋值。 线程 2 再执行 CAS 操作,第一次 CAS 的时候发现 oldValue 和 value 不相等,不能进行赋值。因此需要进入循环。在循环里重新读取 value 的值赋给 oldValue。 线程 2 接下来第二次执行 CAS,此时 oldValue 和 value 相同,于是直接执行赋值操作。

线程 1 和 线程 2 返回各自的 oldValue 的值即可。

文章配图

3.2.2 实现自旋锁

基于 CAS 实现更灵活的自旋锁,获取到更多的控制权。

主要逻辑如下:

  • 通过 CAS 看当前锁是否被某个线程持有。
  • 如果这个锁已经被别的线程持有,那么就自旋等待。
  • 如果这个锁没有被别的线程持有,那么就把 owner 设为当前尝试加锁的线程。

伪代码描述如下:

public class SpinLock {
    private Thread owner = null;

    public void lock() {
        while (!CAS(this.owner, null, Thread.currentThread())) {
        }
    }

    public void unlock() {
        this.owner = null;
    }
}

3.3 CAS 的 ABA 问题

3.3.1 ABA 问题简介

假设存在两个线程 t1 和 t2。有一个共享变量 num,初始值为 A。 接下来,线程 t1 想使用 CAS 把 num 值改成 Z,那么就需要先读取 num 的值,记录到 oldNum 变量中。 使用 CAS 判定当前 num 的值是否为 A,如果为 A,就修改成 Z。 但是,在 t1 执行这两个操作之间,t2 线程可能把 num 的值从 A 改成了 B,又从 B 改成了 A。 线程 t1 的 CAS 是期望 num 不变就修改。但是 num 的值已经被 t2 给改了。只不过又改成 A 了。这 个时候 t1 究竟是否要更新 num 的值为 Z。就不符合预期出现 Bug。

过程图:

Bug 例子:

假如你去取钱有 1000,取 500,不小心按了两次取款,我们只想扣一次 500,第一次按相当于 T2,第二次按相当于 T1,先执行 T2 读取操作修改为 500,这时又正好有人给你汇款了 500,内存中与寄存器中又相等了,T1 扣款的操作也会执行成功。

造成 ABA 问题的原因就是内存值既有加又有减,所以我们的解决 ABA 的方式如下。

3.3.2 解决方案

给要修改的值,引入版本号。在 CAS 比较数据当前值和旧值的同时,也要比较版本号是否符合预期。 CAS 操作在读取旧值的同时,也要读取版本号。 真正修改的时候:

  • 如果当前版本号和读到的版本号相同,则修改数据,并把版本号 + 1。
  • 如果当前版本号高于读到的版本号。就操作失败 (认为数据已经被修改过了)。

四、JUC 组件

JUC 组件就是指 java.util.concurrent 这个包下面的类。

4.1 Callable 接口

Callable 接口就和 java.lang 包下的 Runnable 接口的定位差不多,只不过给了我们一个返回值,在 Runnable 接口要重写 run 方法,而在 Callable 接口中要重写 call 方法。

文章配图

在使用 Callable 接口传入线程时需要借助 JUC 下的另一个类 FutureTask。

文章配图

使用例子一般如下:

public class Demo {
    public static void main(String[] args) {
        //创建实现了 Callable 接口的方法
        Callable<Integer> callable = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                return null;
            }
        };
        //将实现 Callable 接口的对象作为参数传入 FutureTask,泛型参数要一致
        FutureTask<Integer> futureTask = new FutureTask<>(callable);
        //将 FutureTask 对象作为参数传入 Thread
        Thread thread = new Thread(futureTask);
    }
}

4.2 ReentrantLock 类

ReentrantLock 类可重入互斥锁,和 synchronized 的定位是差不多的。

文章配图

ReentrantLock 的用法:

  • public void lock(): 加锁,如果获取不到锁就死等。
  • public boolean trylock(超时时间): 加锁,如果获取不到锁,等待一定的时间之后就放弃加锁。
  • public void unlock(): 解锁。 使用这个类加锁需要我们手动解锁,为避免解锁执行不到的情况,我们一般将 unlock 放在 finally 代码块中。
ReentrantLock locker = new ReentrantLock();
try {
    locker.lock();
} finally {
    locker.unlock();
}

ReentrantLock 与 synchronized 的区别:

  • ReentrantLock 是类,是 Java 代码实现的,synchronized 是关键字由 JVM 使用 c++ 代码实现的。
  • ReentrantLock 是需要 lock() 方法加锁,unlock() 方法解锁。synchronized 是通过进出代码块加锁解锁。
  • ReentrantLock 还提供了 tryLock() 方法可以设置超时时间,不会一直阻塞,加锁成功返回 true,调用者可以根据返回值自己操作。
  • ReentrantLock 搭配的通知机制是 Condition 类,可以更精确控制唤醒某个指定的线程,相比 wait / notify 功能更强大。

ReentrantLock 还提供了公平锁的实现,默认是非公平锁,但是构造方法传参 true 实现公平锁。

文章配图

4.3 Semaphore 类

信号量,:用来表示'可用资源的个数'。本质上就是一个计数器,能够协调多个线程之间的资源调配。 最主要的就是申请资源(P 操作,调用 acquire 方法)释放资源(V 操作,调用 release 方法)

可以把信号量想象成是停车场的展示牌:当前有车位 100 个。表示有 100 个可用资源。当有车开进去的时候,就相当于申请一个可用资源,可用车位就 -1 (这个称为信号量的 P 操作) 当有车开出来的时候,就相当于释放一个可用资源,可用车位就 +1 (这个称为信号量的 V 操作) 如果计数器的值已经为 0 了,还尝试申请资源,就会阻塞等待,直到有其他线程释放资源.

文章配图

4.4 CountDownLatch 类

CountDownLatch 类的作用:同时等待 N 个任务执行结束。(就像跑步比赛,10 个选手依次就位,哨声响才同时出发;所有选手都通过终点,才能公布成绩。)

使用多线程的时候,经常会把一个大任务拆分为多个子任务,使用 CountDownLatch 衡量子任务完成,让整个任务完成。

  • 构造 CountDownLatch 实例,参数传入任务个数,初始化 10 表示有 10 个任务需要完成。
  • 每个任务执行完毕,都调用 countDown() 方法。在 CountDownLatch 内部的计数器同时自减。

主线程中使用 await() 方法; 阻塞等待所有任务执行完毕后 wait 结束。相当于计数器为 0 了。

文章配图

目录

  1. 一、常见的锁策略
  2. 1.1 悲观锁 vs 乐观锁
  3. 1.2 重量级锁 vs 轻量级锁
  4. 1.3 挂起等待锁 vs 自旋锁
  5. 1.4 普通互斥锁 vs 读写锁
  6. 1.5 可重入锁 vs 不可重入锁
  7. 1.6 不公平锁 vs 公平锁
  8. 二、synchronized 特性
  9. 2.1 synchronized 的锁策略
  10. 2.2 synchronized 加锁过程
  11. 2.3 其它优化措施
  12. 三、CAS
  13. 3.1 CAS 概念
  14. 3.2 CAS 应用场景
  15. 3.2.1 实现原子类
  16. 3.2.2 实现自旋锁
  17. 3.3 CAS 的 ABA 问题
  18. 3.3.1 ABA 问题简介
  19. 3.3.2 解决方案
  20. 四、JUC 组件
  21. 4.1 Callable 接口
  22. 4.2 ReentrantLock 类
  23. 4.3 Semaphore 类
  24. 4.4 CountDownLatch 类

更多推荐文章

查看全部
  • Java 多线程:线程池原理与 ThreadPoolExecutor 详解
  • OpenCode 开源 AI 编码代理工具完全指南
  • Leetcode Editor 插件在 IntelliJ IDEA 中的安装与配置指南
  • 无线联邦学习:保护隐私的无线网络 AI 协同进化
  • 30 岁转行 Python 程序员的职业路径与技术成长经验分享
  • 原生 HTML/CSS/JS 实现网页井字棋游戏
  • ZXing-C++:现代C++二维码识别库入门指南
  • 2025 无人机四大顶会精选 16 篇前沿论文:IROS ICRA RSS CoRL
  • Python+AI 零基础变现指南:3 天开发职场文案 App 实战
  • FPGA低延迟库在高频交易中的实战优化与避坑指南
  • GESP C++二级编程题:小杨的 H 字矩阵
  • VSCode 中 Git 推送与拉取的详细操作指南
  • FastAPI:Python 高性能 Web 框架的优雅之选
  • Flutter+OpenHarmony 智能家居开发:多设备验证、BUG 修复与打包发布流程
  • LLaMA-Factory 微调 GPT-OSS-20B 模型教程(AutoDL+LoRA)
  • C++ 堆数据结构原理与实现详解
  • RISC-V 开源处理器实战:从 Verilog RTL 设计到 FPGA 原型验证
  • 在线图书借阅平台的设计与实现
  • 2025 年 AI 办公工具评测:PPT 生成与远程控制
  • 《Agent Runtime 工程化》第十一章 Agent Runtime 进阶学习路线

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online