
本文详解 java 多线程环境下对静态共享集合(如 static list)的同步策略,指出仅使用 synchronized 方法无法保证线程安全,并通过对比分析、代码示例与关键注意事项,阐明必须显式锁定共享对象本身的原因。
在 Java 并发编程中,synchronized 是最基础且常用的同步机制,但其作用范围和锁对象的选择极易被误解。以 Account 类为例,初看之下,将 deposit() 和 withdraw() 声明为 synchronized 方法似乎已确保了线程安全——毕竟所有对 balance 和 log 的修改都被“串行化”了。然而,这种理解只在锁对象与被保护资源严格对应时才成立;而本例中,锁对象(this 实例)与被保护资源(static List
问题根源:锁对象错配
public synchronized void deposit(...) 等价于:
public void deposit(double val) {
synchronized (this) { // ← 锁的是当前 Account 实例!
balance = balance + val;
log.add(val); // ← 但 log 是 static,被所有实例共享!
}
}这意味着:
- 若有多个 Account 实例(如 a1、a2),线程 T1 调用 a1.deposit() 会锁住 a1;
- 线程 T2 同时调用 a2.withdraw() 会锁住 a2;
- 二者互不阻塞,却同时操作同一个静态 log 列表 → ArrayList 非线程安全,add() 操作可能引发 ConcurrentModificationException、数据丢失或内部结构损坏(如 size 字段未正确更新)。
这正是原方案不安全的根本原因:方法级同步保护的是实例状态(balance),而非跨实例共享的静态资源(log)。
立即学习“Java免费学习笔记(深入)”;
正确解法:按资源粒度精准加锁
要保护静态 log,必须让所有访问它的代码都竞争同一把锁。最佳实践是直接以 log 对象自身为锁:
public class Account {
private static final List log = new ArrayList<>(); // ✅ final 保证引用不可变
private double balance;
public Account() { balance = 0.0; }
public synchronized void deposit(double val) {
balance += val;
synchronized (log) { // ✅ 所有线程均竞争 log 这一共享锁
log.add(val);
}
}
public synchronized void withdraw(double val) {
balance -= val;
synchronized (log) {
log.add(-val); // 注意:原题 withdraw 日志应记录负值,体现资金流出
}
}
// 安全的静态访问器(可选)
public static List getLog() {
synchronized (log) {
return new ArrayList<>(log); // ✅ 返回副本,避免外部直接修改
}
}
} ? 关键点说明:log 声明为 final:防止意外重新赋值导致锁失效(如 log = new ArrayList() 后,新旧引用锁对象不一致);双重锁嵌套可行:synchronized(this) 保护 balance,synchronized(log) 保护 log,互不干扰;避免锁 Class 对象(如 synchronized(Account.class)):虽能保护静态资源,但粒度过大,易造成不必要的线程阻塞。
更优替代方案:使用线程安全集合
对于日志等追加场景,推荐直接选用 java.util.concurrent 包中的线程安全集合,语义更清晰、性能通常更优:
private static final Listlog = Collections.synchronizedList(new ArrayList<>()); // 或更现代的选择(JDK 14+): // private static final List log = new CopyOnWriteArrayList<>();
此时 log.add() 本身已线程安全,无需额外 synchronized(log) 块(但注意:Collections.synchronizedList 的迭代仍需手动同步)。
重要补充:业务逻辑陷阱
文中代码还存在一个严重设计缺陷:使用 double 表示货币金额。浮点数精度误差会导致不可接受的财务偏差(例如 0.1 + 0.2 != 0.3)。生产环境必须改用 BigDecimal 或整数(以分为单位):
private BigDecimal balance = BigDecimal.ZERO;
public void deposit(BigDecimal val) {
balance = balance.add(val);
synchronized (log) {
log.add(val.doubleValue()); // 若日志仅作调试,可保留 double;否则也应用 BigDecimal
}
}总结
- ✅ 同步目标决定锁对象:保护实例变量 → 锁 this;保护静态变量 → 锁该静态变量(或 Class 对象);
- ✅ 静态集合必须显式同步:synchronized 方法无法覆盖 static 资源;
- ✅ 优先选用并发集合:CopyOnWriteArrayList、ConcurrentLinkedQueue 等比手写同步更可靠;
- ⚠️ 永远避免 double/float 处理金钱:这是金融系统硬性规范。
正确的同步不是“加锁越多越好”,而是“锁得恰到好处”——精准匹配临界资源与锁粒度,方为高并发程序的稳健基石。










