同步工具、锁与 JUC
synchronized
synchronized 通过对象监视器(Monitor)实现互斥。
- 修饰实例方法:锁是当前对象
this; - 修饰静态方法:锁是当前类的
Class对象; - 修饰代码块:锁是括号中指定的对象。
它不仅保证互斥(原子性),也能保证临界区内共享变量的可见性和有序性。
volatile
volatile 适合状态标志、配置刷新等场景:
- 保证变量的可见性;
- 禁止相关读写发生特定重排序,保证一定的有序性;
- 不保证复合操作原子性,如
count++。
CAS 与原子类
CAS(Compare And Swap):比较内存值是否等于预期值;相等则更新,否则失败。它是乐观并发控制的基础,Java 原子类广泛使用 CAS。
优缺点:
- 优点:非阻塞,竞争较低时性能好;
- 缺点:可能发生 ABA、自旋重试消耗 CPU、单次通常只能原子更新一个位置。
ABA 问题
变量从 A 变为 B 又变回 A,CAS 只比较值时会误判“未变化”。可用版本号解决,例如 AtomicStampedReference。
保证原子性的常见方式
synchronized;ReentrantLock;AtomicInteger、AtomicLong、AtomicReference等原子类;- 数据库原子条件更新、事务或分布式协调机制(跨进程场景)。
乐观锁与悲观锁
- 悲观锁:假设冲突经常发生,访问前先加锁,典型如
synchronized、ReentrantLock、数据库SELECT ... FOR UPDATE。 - 乐观锁:假设冲突较少,更新时校验版本或条件;冲突后失败或重试,典型如 CAS、数据库版本号更新。
选择依据是冲突率、操作时长、重试成本和一致性要求。
死锁
死锁是多个线程互相等待对方释放资源,且无法继续推进的状态。
死锁的四个必要条件:
- 互斥:资源一次只能被一个线程占用;
- 持有并等待:线程持有资源的同时继续等待其他资源;
- 不可抢占:资源不能被强制剥夺;
- 循环等待:线程间形成循环等待链。
常见预防措施:统一加锁顺序、缩短持锁时间、避免嵌套锁、使用 tryLock 超时获取锁、必要时进行死锁检测。
活锁:线程一直在响应对方但没有实际进展;饥饿:线程长期得不到 CPU 或锁。这两者不同于死锁。
ThreadLocal
ThreadLocal<T> 为每个线程保存独立变量副本,适合跨方法传递线程上下文,而非线程间共享数据。
private static final ThreadLocal<String> TRACE_ID = new ThreadLocal<>();
TRACE_ID.set("trace-001");
String traceId = TRACE_ID.get();
TRACE_ID.remove();
常见场景:链路追踪 ID、当前用户上下文、租户信息、日期格式化器(旧代码)等。
使用注意
在线程池中线程会复用。ThreadLocal 的值如果未清理,后续请求可能读到脏数据,也可能造成内存泄漏。因此必须在 finally 中调用 remove()。
try {
TRACE_ID.set(traceId);
// 业务逻辑
} finally {
TRACE_ID.remove();
}
不建议将数据库连接长期直接放入 ThreadLocal;更推荐使用连接池和框架管理事务边界。
单例的线程安全
饿汉式
类加载时初始化,天然线程安全,但无法延迟加载:
public class Singleton {
private static final Singleton INSTANCE = new Singleton();
private Singleton() {}
public static Singleton getInstance() { return INSTANCE; }
}
双重检查锁(DCL)
实现懒加载时,实例字段必须加 volatile,以避免对象初始化重排序:
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
更推荐枚举单例或静态内部类单例,代码更简洁且安全。
JUC 与 ConcurrentHashMap
JUC 即 java.util.concurrent,提供线程池、锁、原子类、并发容器、同步器等工具。
为什么 HashMap 线程不安全?
多个线程并发 put、扩容或修改时可能发生数据覆盖、丢失、结构不一致等问题。Java 7 扩容并发场景下曾可能形成环形链表并导致查询死循环;Java 8 虽修复了该具体问题,但 HashMap 仍然不是线程安全容器。
ConcurrentHashMap
它是线程安全的哈希表:Java 8 主要采用 CAS + synchronized 对桶级节点进行细粒度协调,支持较高并发读写。
注意:单个操作如 put 是线程安全的,但“先 get 再 put”的复合业务逻辑未必原子,应使用 putIfAbsent、computeIfAbsent、merge 等原子方法。