臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

Java 锁不是把一段代码永久“锁死”,而是让多个线程访问同一份共享数据时,先竞争同一把锁:拿到锁的线程进入临界区,其他线程等待。Java 锁只在同一个 JVM 内有效,跨 Pod 或跨 JVM 的互斥需要使用 Redis 分布式锁 等分布式协调手段。

如果对线程和线程池还不熟悉,可以先阅读 线程。

二、🧭 理论:锁到底在解决什么问题

2.1 前提:多个线程共享同一份可变数据

下面三个条件同时出现时,才需要考虑线程安全:

  1. 有多个线程。
  2. 这些线程访问同一个对象或变量。
  3. 至少一个线程会修改它。

例如,两个线程同时操作同一个计数器:

count++;

这行代码看起来只有一步,实际可以理解为:

读取 count
  → 加 1
  → 把新值写回 count

假设 count 原来是 0,线程 A 和线程 B 都在对它加一:

线程 A 读取 0
线程 B 读取 0
线程 A 写入 1
线程 B 写入 1

两个线程总共执行了两次加一,结果却是 1。线程 A 的结果被线程 B 覆盖了,这叫线程干扰(thread interference),也常被称为丢失更新。

2.2 临界区是什么

操作共享数据、不能被多个线程交错执行的代码区域,叫作临界区(critical section)。

// 这段“读取 → 修改 → 写回”应该作为一个整体执行。
count++;

锁的任务,就是保护临界区:

线程 A 获得锁 → 执行临界区 → 释放锁
线程 B 等待   → 获得锁       → 执行临界区 → 释放锁

这种“同一时间只允许一个线程进入”的性质叫作互斥(mutual exclusion)。

2.3 锁住的到底是什么

经常听到“锁住方法”“锁住代码”,但 Java 真正竞争的是一个锁对象。

private final Object lock = new Object();
 
public void increment() {
    // 多个线程只有竞争同一个 lock 对象,才能形成互斥。
    synchronized (lock) {
        count++;
    }
}

synchronized 后面的对象可以理解为会议室钥匙。代码块是会议室,锁对象是钥匙。两个线程使用不同的钥匙,就算它们执行的是同一段代码,也不会互相等待。

锁必须一致

保护同一份共享数据的所有读写操作,都要遵守同一套加锁规则并竞争同一把锁。只给写方法加锁、读取时完全不加锁,仍可能读到不一致或不可见的数据。

2.4 锁同时解决互斥和可见性

Java 锁不只让线程排队,还建立内存可见性保证:一个线程释放某把锁之前的修改,对之后成功获得同一把锁的线程可见。

Java 内存模型把这种先后保证称为 happens-before。初学时可以先记住:

线程 A 修改数据并释放锁
  happens-before
线程 B 随后获得同一把锁并读取数据

如果没有这样的同步关系,不同线程可能对同一份数据看到不同的值,这叫内存一致性错误(memory consistency error)。

三、⚙️ 理论:Java 中常见的同步方式

3.1 synchronized:使用对象自带的内置锁

每个 Java 对象都关联着一个内置锁(intrinsic lock),也叫监视器锁(monitor lock)。synchronized 会自动获得并释放这把锁。

3.1.1 同步代码块

private int count;
private final Object lock = new Object();
 
public void increment() {
    // 进入代码块前获得 lock 的内置锁。
    synchronized (lock) {
        count++;
    }
    // 正常结束或抛出异常离开代码块时,锁都会自动释放。
}

同步代码块可以准确控制临界区大小。没有访问共享状态的耗时操作,通常不必放进锁里。

3.1.2 实例同步方法

public synchronized void increment() {
    count++;
}

实例同步方法竞争的是当前对象,也就是 this 的内置锁。下面两种写法的锁对象相同:

public synchronized void increment() {
    count++;
}
 
public void incrementEquivalent() {
    synchronized (this) {
        count++;
    }
}

如果创建了两个不同实例,它们各自拥有自己的锁,彼此不会阻塞:

Counter first = new Counter();
Counter second = new Counter();
 
// first 和 second 不是同一个对象,因此不是同一把锁。

3.1.3 静态同步方法

public static synchronized void incrementGlobalCount() {
    globalCount++;
}

静态同步方法竞争的是 当前类.class 对象的内置锁,不是某个业务实例的锁:

public static void incrementGlobalCountEquivalent() {
    synchronized (Counter.class) {
        globalCount++;
    }
}

因此,实例同步方法和静态同步方法通常不是同一把锁,不能互相保护同一份共享状态。

3.2 可重入是什么意思

synchronized 和 ReentrantLock 都是可重入锁(reentrant lock)。同一个线程已经持有某把锁时,可以再次获得它,而不会把自己阻塞住。

public synchronized void outer() {
    // 当前线程已经持有 this 的锁。
    inner();
}
 
public synchronized void inner() {
    // 仍然是同一个线程、同一个 this 锁,因此可以再次进入。
}

可重入解决的是“线程调用了另一个也需要同一把锁的方法”这种常见场景。它不代表别的线程可以一起进入。

3.3 ReentrantLock:需要更多控制时使用

ReentrantLock 与 synchronized 一样可以提供互斥和内存可见性,但它还支持:

  • tryLock():拿不到锁就立即返回,不一直等待。
  • 带超时的 tryLock(...):只等待指定时间。
  • lockInterruptibly():等待锁时可以响应线程中断。
  • 可选公平策略:等待较久的线程更优先,但通常会降低吞吐量。
  • Condition:实现更灵活的条件等待。

基本写法:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class LockCounter {
    private int count;
 
    // final 防止程序运行中把 lock 换成另一个对象。
    private final Lock lock = new ReentrantLock();
 
    public void increment() {
        // lock:获得锁;如果其他线程持有锁,当前线程在这里等待。
        lock.lock();
        try {
            count++;
        } finally {
            // unlock:即使业务代码抛出异常,也必须释放锁。
            lock.unlock();
        }
    }
}

必须在 finally 中解锁

ReentrantLock 不会像 synchronized 那样自动释放。如果获得锁后发生异常而没有执行 unlock(),其他线程可能一直等下去。

3.4 volatile、原子类和锁怎么选

它们解决的问题有重叠,但不是一回事:

工具适合解决的问题不适合的情况
volatile一个线程修改简单状态,其他线程需要及时看见,例如停止标记count++、先判断再修改等复合操作
AtomicInteger对单个数值做原子加减、比较并更新同时维护多个变量之间的业务约束
synchronized保护一段临界区,语法简单且自动释放锁需要超时获取、可中断等待等精细控制
ReentrantLock保护一段临界区,并需要 tryLock、超时或条件队列简单场景中可能增加忘记解锁的风险
并发容器多线程共享集合,例如 ConcurrentHashMap一个容器操作线程安全,不代表多步业务流程整体原子

volatile int count 仍不能让 count++ 变成原子操作,因为它只能保证读写的可见性,不能把“读取、加一、写回”合并成不可分割的一步。

简单计数可以继续阅读 7;共享 Map 可以继续阅读 2。

3.5 常见错误:锁对象不同

下面的代码看起来加了锁,实际没有互斥:

public void increment() {
    // 每次调用都创建一把新锁,线程之间不会竞争同一个对象。
    Object lock = new Object();
 
    synchronized (lock) {
        count++;
    }
}

锁对象通常应是稳定的成员变量:

private final Object lock = new Object();

也不要使用可能在业务代码中被替换或被外部代码持有的对象作为锁。专用的 private final 锁对象更容易控制。

3.6 常见风险:死锁

两个线程分别拿着对方需要的锁,并且都不释放,就形成死锁(deadlock):

线程 A:持有锁 1,等待锁 2
线程 B:持有锁 2,等待锁 1

最基础的规避方式是:程序中需要同时获得多把锁时,始终使用统一的加锁顺序。

统一规定:先获得锁 1,再获得锁 2

此外,应尽量缩小临界区,避免持锁期间执行无法控制耗时的网络调用。

3.7 Java 锁为什么不能代替分布式锁

Java 锁对象只存在于当前 JVM 的内存中:

Pod A / JVM A:lock 对象 A
Pod B / JVM B:lock 对象 B

即使两边的变量名和代码完全相同,它们也不是同一个对象。Pod A 获得 Java 锁,不会阻止 Pod B 执行业务。

因此要先判断并发发生在哪里:

并发范围常见手段
同一个 JVM 中的线程synchronized、ReentrantLock、原子类、并发容器
多个 JVM 或多个 PodRedis、ZooKeeper 等分布式协调机制
数据库记录并发修改事务、行锁、乐观锁、唯一约束、条件更新

不要看到多 Pod 就立即加分布式锁

如果数据库事务、唯一约束、乐观锁或幂等设计已经能保证正确性,就不需要额外增加 Redis 锁。锁的选择取决于共享资源在哪里,而不是服务有多少个 Pod。

3.8 乐观锁与悲观锁

乐观锁和悲观锁首先是两种并发控制思想,不是专指某两个 Java 类。

悲观锁:先锁住,再操作。
乐观锁:先操作,提交修改时再检查有没有冲突。

3.8.1 悲观锁:先认为冲突会发生

悲观锁认为其他线程或事务很可能同时修改数据,因此先获得锁,成功后才继续操作。

synchronized、ReentrantLock 和 Redis 分布式锁都体现了这种思路:

获得锁
  → 读取或修改共享资源
  → 释放锁

在数据库中,常见例子是在事务内使用 SELECT ... FOR UPDATE:

BEGIN;
 
-- 查询账号并尝试获得相应的数据库锁。
SELECT id, nickname
FROM account
WHERE id = 1001
FOR UPDATE;
 
-- 当前事务持有锁期间再修改账号。
UPDATE account
SET nickname = '小明'
WHERE id = 1001;
 
COMMIT;

事务 A 获得锁后,其他事务如果也要修改受保护的数据,通常需要等待事务 A 提交或回滚。具体锁定范围和等待行为由数据库、索引、隔离级别与执行计划共同决定,不能只凭 SQL 外观断定。

悲观锁适合并发冲突较多、修改过程较短,而且不能接受频繁失败重试的场景。代价是等待和阻塞;同时持有多把锁时,还要注意死锁。

3.8.2 乐观锁:更新时检查数据有没有变化

乐观锁认为并发冲突通常不多,因此读取时不长期占住数据库锁,而是在更新时检查数据是否仍是自己读到的版本。

最常见的实现是增加 version 字段。假设账号当前版本是 5:

-- 只有数据库中的版本仍然是 5,才能更新成功。
UPDATE account
SET nickname = '小明',
    version = version + 1
WHERE id = 1001
  AND version = 5;

执行结果有两种:

  • 影响行数为 1:版本没有变化,本次更新成功,版本变为 6。
  • 影响行数为 0:读取后已有其他事务修改过数据,本次更新发生冲突。

发生冲突后,程序需要根据业务选择重新读取、有限次数重试,或者提示用户“数据已经被修改”。不能忽略影响行数,否则乐观锁就没有真正生效。

在 Jakarta Persistence 中,可以使用 @Version 让持久化框架维护版本并检测冲突:

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import jakarta.persistence.Version;
 
@Entity
public class Account {
    @Id
    private Long id;
 
    private String nickname;
 
    // @Version:持久化框架更新实体时检查并递增版本。
    // 如果数据库版本已经变化,会报告乐观锁冲突。
    @Version
    private Long version;
}

具体抛出异常的时机可能在刷新或提交事务时。检测到冲突后,Jakarta Persistence 会抛出 OptimisticLockException,相应事务需要回滚。

3.8.3 两种策略怎么选

对比项悲观锁乐观锁
基本思路先占住资源,再执行操作不提前长期占锁,更新时检查版本
冲突结果其他线程或事务通常等待当前更新失败,由程序处理冲突
常见实现synchronized、ReentrantLock、数据库行锁、Redis 锁version 字段、条件更新、CAS
适合场景冲突频繁,操作较短读多写少,冲突较少
主要风险阻塞、吞吐量下降、死锁冲突后需要重试或提示失败

AtomicInteger 常用的 CAS(比较并交换)也体现了乐观思想:先比较当前值是不是预期旧值,相同才更新;不相同说明其他线程已经修改过。

选择关键

不要只比较哪个锁“性能更好”。冲突很少时,乐观锁可以避免长期阻塞;冲突非常频繁时,大量乐观锁失败和重试也可能更昂贵。

四、🚀 实践:从错误计数到正确加锁

4.1 前置准备

本例不需要 Maven、Spring 或第三方依赖,只需要 JDK 17 或更高版本。

检查环境:

java -version
javac -version

下面的代码可以保存为任意临时目录中的 JavaLockDemo.java。它会让多个线程分别测试:

  1. 不加锁的计数器。
  2. 使用 synchronized 的计数器。
  3. 使用 ReentrantLock 的计数器。

4.2 可以拿来干什么

4.2.1 保护同一个 JVM 内的共享对象

例如,一个 Spring 单例 Bean 中保存了会变化的内存状态。多个请求线程可能同时调用它,就需要使用线程安全的设计。

public class Inventory {
    private int stock = 10;
 
    public synchronized boolean deduct() {
        // 判断和扣减必须作为一个整体执行。
        if (stock <= 0) {
            return false;
        }
 
        stock--;
        return true;
    }
}

输入是多个线程对同一个 Inventory 实例的调用;处理过程是在同一把 this 锁内完成“判断库存和扣减”;输出是最多只有 10 次调用返回 true。

不过,实际库存通常保存在数据库中,而且服务可能有多个 Pod。这时不能只依赖 Java 锁,应优先使用数据库条件更新、乐观锁或其他跨进程方案。

4.2.2 获取不到锁时快速放弃

当业务不希望线程无限等待时,可以使用 tryLock():

import java.util.concurrent.locks.ReentrantLock;
 
public class ReportGenerator {
    private final ReentrantLock lock = new ReentrantLock();
 
    public boolean generate() {
        // tryLock:只尝试一次;锁正被占用时立即返回 false。
        if (!lock.tryLock()) {
            return false;
        }
 
        try {
            // 当前线程已获得锁,在这里生成报告。
            return true;
        } finally {
            // 只有成功获得锁后,才执行 unlock。
            lock.unlock();
        }
    }
}

输入是多个线程同时调用同一个 ReportGenerator;第一个线程获得锁并执行,其他线程立即得到 false,适合“任务已在执行,请稍后再试”这类单 JVM 场景。

4.3 完整实践:代码、运行和验证

新建 JavaLockDemo.java,写入以下完整代码:

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
 
public class JavaLockDemo {
 
    // 每一种计数器都提供相同的方法,便于使用同一套并发测试代码。
    interface Counter {
        void increment();
 
        int get();
    }
 
    static class UnsafeCounter implements Counter {
        private int count;
 
        @Override
        public void increment() {
            // count++ 包含“读取、加一、写回”,不是原子操作。
            count++;
        }
 
        @Override
        public int get() {
            return count;
        }
    }
 
    static class SynchronizedCounter implements Counter {
        private int count;
 
        @Override
        public synchronized void increment() {
            // 实例同步方法获得当前 SynchronizedCounter 对象的内置锁。
            count++;
        }
 
        @Override
        public synchronized int get() {
            // 读取和写入都使用同一把 this 锁。
            return count;
        }
    }
 
    static class ReentrantLockCounter implements Counter {
        private int count;
        private final Lock lock = new ReentrantLock();
 
        @Override
        public void increment() {
            // lock:获得显式锁;锁被占用时,当前线程等待。
            lock.lock();
            try {
                count++;
            } finally {
                // unlock:放在 finally 中,防止异常导致锁无法释放。
                lock.unlock();
            }
        }
 
        @Override
        public int get() {
            lock.lock();
            try {
                return count;
            } finally {
                lock.unlock();
            }
        }
    }
 
    public static void main(String[] args) throws InterruptedException {
        runTest("不加锁", new UnsafeCounter());
        runTest("synchronized", new SynchronizedCounter());
        runTest("ReentrantLock", new ReentrantLockCounter());
    }
 
    private static void runTest(String name, Counter counter)
            throws InterruptedException {
        int taskCount = 100;
        int incrementsPerTask = 10_000;
        int expected = taskCount * incrementsPerTask;
 
        // 创建固定大小的线程池,让多个工作线程并发处理任务。
        ExecutorService pool = Executors.newFixedThreadPool(8);
 
        // CountDownLatch:初始值等于任务数;每完成一个任务就减一。
        CountDownLatch done = new CountDownLatch(taskCount);
 
        for (int task = 0; task < taskCount; task++) {
            // submit:把任务交给线程池,任务可能由不同线程同时执行。
            pool.submit(() -> {
                try {
                    for (int i = 0; i < incrementsPerTask; i++) {
                        counter.increment();
                    }
                } finally {
                    // countDown:无论任务正常结束还是抛出异常,都标记一次完成。
                    done.countDown();
                }
            });
        }
 
        // await:主线程等待所有计数任务结束,再读取最终结果。
        done.await();
 
        // shutdown:不再接收新任务,已提交的任务仍会执行完。
        pool.shutdown();
 
        // awaitTermination:等待线程池真正结束,避免残留工作线程。
        if (!pool.awaitTermination(10, TimeUnit.SECONDS)) {
            pool.shutdownNow();
        }
 
        System.out.printf(
                "%-15s 预期值:%d,实际值:%d%n",
                name,
                expected,
                counter.get()
        );
    }
}

在文件所在目录编译并运行:

javac JavaLockDemo.java
java JavaLockDemo

一次可能的结果:

不加锁             预期值:1000000,实际值:721364
synchronized       预期值:1000000,实际值:1000000
ReentrantLock      预期值:1000000,实际值:1000000

结果解释:

  • 不加锁时,多个线程的“读取、加一、写回”相互覆盖,实际值通常小于预期值。
  • synchronized 让同一时间只有一个线程执行 count++。
  • ReentrantLock 实现了相同的互斥效果,但锁的获得和释放由代码显式控制。
  • 不加锁的结果具有不确定性,偶尔也可能刚好等于预期值;这不能证明代码线程安全。

4.4 关键报错与排查

现象常见原因排查方式
加锁后结果仍不正确不同方法使用了不同锁对象检查所有共享数据访问是否竞争同一个稳定的锁对象
程序一直不结束ReentrantLock 获得后没有释放,或发生死锁检查 unlock() 是否在 finally 中,以及多把锁的获取顺序
实例方法之间没有互斥实际操作的是不同对象实例打印或检查对象来源,确认线程共享同一个实例
单机正常,多 Pod 仍重复执行Java 锁只能作用于当前 JVM改用数据库约束、幂等设计或合适的分布式协调方案
使用 volatile 后计数仍丢失volatile 不保证复合操作原子性改用 AtomicInteger 或用锁保护完整操作

五、🧾 如何选择

遇到并发问题时,可以按下面的顺序判断:

是否真的有共享的可变数据?
  ├─ 没有 → 通常不需要锁
  └─ 有
      ↓
共享范围是否只在一个 JVM?
  ├─ 是 → 简单临界区优先 synchronized
  │        需要超时、可中断等控制时使用 ReentrantLock
  │        单变量简单更新可考虑原子类
  └─ 否
      ↓
共享资源是否是数据库记录?
  ├─ 是 → 冲突少时考虑 version 乐观锁或条件更新
  │        冲突多且必须等待时考虑事务与悲观锁
  └─ 否 → 检查幂等设计或合适的分布式锁

不要先问“应该使用哪一种锁”,而要先问:

  1. 哪些线程会访问哪一份共享数据?
  2. 哪几步操作必须作为一个整体?
  3. 所有访问是否真的竞争同一把锁?
  4. 这些线程是否处于同一个 JVM?
  5. 能否使用数据库约束或无共享设计直接消除问题?

六、📌 总结

  • 锁保护的是对共享状态的一段完整操作,也就是临界区。
  • 多个线程必须竞争同一个锁对象,互斥才会生效。
  • synchronized 使用对象的内置锁,离开代码块时自动释放。
  • ReentrantLock 支持更多控制,但必须在 finally 中调用 unlock()。
  • volatile 主要解决可见性,不能让 count++ 变成原子操作。
  • Java 锁只在同一个 JVM 内有效,不能直接协调多个 Pod。
  • 悲观锁是“先锁再操作”,乐观锁是“更新时检查版本”。
  • 乐观锁冲突后必须检查结果,并选择重试、重新读取或提示失败。

记忆句:先找共享数据,再圈出临界区;悲观锁先占座,乐观锁结账时对版本。

七、📚 官方资料

版本说明

Oracle 的并发教程以 JDK 8 编写,本文用它确定“线程干扰 → 内存一致性 → synchronized → 内置锁”的学习顺序;API 行为同时参考 Java SE 25 文档。本文使用的基础概念和示例适用于现代 JDK。