优先选 synchronized 方法;仅当需可中断、超时或尝试获取锁时才用 reentrantlock,因其需手动 unlock 且易出错,而 synchronized 更安全高效。

用 synchronized 方法还是 ReentrantLock?看状态变更是否需要可中断或超时
线程安全的核心不是“加锁”,而是“状态变更的原子性”。如果类只有一两个简单字段,且所有读写都走同一把锁,synchronized 方法最直接、JVM 优化好、不易出错。但一旦需要尝试获取锁(比如避免死等)、响应中断、或在多个方法间共享锁逻辑,就得换 ReentrantLock。
-
synchronized无法判断是否成功抢到锁,也不能被中断;lock.tryLock(1, TimeUnit.SECONDS)可控得多 - 嵌套调用时,
synchronized可重入,ReentrantLock也支持,但必须配对unlock(),漏写会卡死其他线程 - 别在
synchronized块里做 I/O 或长耗时操作——锁持有时间越长,竞争越激烈
volatile 能不能替代锁?只适用于「纯赋值 + 无依赖读写」场景
volatile 不是万能的线程安全开关。它只保证变量的可见性和禁止重排序,不保证复合操作的原子性。比如 counter++ 是读-改-写三步,volatile int counter 完全挡不住竞态。
- 适合:开关标志位(
volatile boolean shutdownRequested),初始化后只读的引用(volatile List<string> cache</string>) - 不适合:计数器、集合增删、依赖前值的判断(如
if (count > 0) count--) - 注意:
volatile对long和double的 64 位写是原子的,但别靠这个设计逻辑——可读性差,且 JDK 9+ 对非final字段的语义更严格
构造函数里传入可变对象?立刻复制或包装成不可变
封装失效常发生在构造阶段。如果构造函数接收 ArrayList、HashMap 或自定义可变对象,外部保留引用后随时能绕过你的锁修改内部状态。
- 接收集合参数时,用
new ArrayList(inputList)或new CopyOnWriteArrayList(inputList),别用Collections.unmodifiableList——那是运行时防护,不解决源头污染 - 接收数组参数,必须
Arrays.copyOf,因为数组是协变的,Object[] a = new String[1]合法,但后续可能被塞进类型不符元素 - 如果类本身要暴露状态(比如
getState()),返回新副本或不可变视图(Collections.unmodifiableMap),而不是原始引用
状态校验和不变量维护,别只靠 getter/setter 加锁
加锁只是手段,真正难的是定义清楚“什么是合法状态”。比如一个订单类有 status 和 paidAt,你得确保 status == PAID 时 paidAt != null。这种约束光靠单个方法加锁拦不住,得在关键路径上显式检查。
立即学习“Java免费学习笔记(深入)”;
- 在
setState(Status s)里加if (s == PAID && paidAt == null) throw new IllegalStateException(...) - 避免在
synchronized块里调用外部传入的回调函数——对方可能反过来调你的方法,引发死锁或状态不一致 - 考虑用
record或ImmutableList封装内部状态快照,比手写防御性拷贝更可靠
真正麻烦的从来不是“怎么加锁”,而是“哪些字段算一个状态单元”“谁有权改它”“改完之后必须满足什么条件”。这些得在设计类的第一天就想清楚,代码只是把它写实而已。









