
线程安全不取决于代码行数(如单行读取 size()),而取决于共享变量的读写是否建立正确的“先行发生”(happens-before)关系;未同步的跨线程读写必然导致数据不可见、结果错乱,即使只是调用一个方法。
线程安全不取决于代码行数(如单行读取 size()),而取决于共享变量的读写是否建立正确的“先行发生”(happens-before)关系;未同步的跨线程读写必然导致数据不可见、结果错乱,即使只是调用一个方法。
在Java多线程开发中,一个常见却危险的误解是:“单行代码天然线程安全”——例如 return map.size(); 或 map.get(key); 看似简洁无害,便认为无需同步。但事实恰恰相反:线程安全性与代码长度无关,而由内存可见性、操作原子性及执行顺序约束共同决定。只要多个线程访问同一共享可变状态(如 HashMap 实例的字段),且至少有一个线程执行写操作(如 put()、clear()、remove()),那么所有对该对象的访问——无论读或写、单行或多行——都必须通过明确的同步机制保障“先行发生”关系,否则程序行为即为未定义(undefined behavior)。
为什么 size() 这样的单行调用也不安全?
以 HashMap 为例,其 size() 方法本身是原子的(仅返回一个 int 字段),但它返回的值可能早已过期。原因在于:
- size 字段并非 volatile,JVM 不保证其修改对其他线程立即可见;
- 若线程A刚执行完 map.put(...),线程B紧接着调用 map.size(),B可能仍读到旧值(甚至因指令重排序看到中间态);
- 更严重的是,若 HashMap 正在扩容(rehash),其内部结构处于不一致状态,此时并发调用 size() 可能触发 ConcurrentModificationException(尽管非迭代场景下不总抛出),或返回完全错误的数值。
下面是一个典型风险示例:
// 共享非线程安全Map
private final Map<integer connection> networkView = new HashMap();
// 写操作(需同步)
public void addConnection(int id, Connection conn) {
synchronized (networkView) {
networkView.put(id, conn);
}
}
// ❌ 危险!看似安全的单行读,实则无HB保障
public int getNetworkSize() {
return networkView.size(); // 可能返回陈旧值!
}
// ✅ 安全:读操作也纳入同一锁保护,建立HB关系
public int getNetworkSizeSafe() {
synchronized (networkView) {
return networkView.size();
}
}</integer>
⚠️ 注意:synchronized(networkView) 的关键作用不仅是互斥,更是建立 Happens-Before —— 任一退出该同步块的操作(如 put 结束),对之后进入同一锁的任何线程(如 size() 调用)都可见。
JavaStoryDevelop下载你接手一个需求时,是不是经常这样:"改一个小字段 → 结果把不相关的代码也改了"、"需求说得很模糊 → 做着做着发现跑偏了"、"数据库加个字段 → 上线才发现漏了数据兼容"、"做了个新功能 → 一个月后自己都忘了当时怎么设计的"、"写到一半被中断 → 再回来不知道从哪继续";这个 Skill 就是专门解决这些问题的。 它会自动探测你的项目环境(JDK、Spring Boot、ORM、数据库、前端框架),记在
何时可以不用 synchronized?替代方案更优
若你仅进行只读访问且能确保写操作已彻底结束且无后续修改(如初始化后冻结),可考虑 Collections.unmodifiableMap() 配合 final 引用。但绝大多数运行时动态更新场景不适用。
更推荐的现代化解法是选用线程安全的数据结构,而非手动加锁:
| 场景 | 推荐方案 | 说明 |
|---|---|---|
| 高频读 + 低频写,需强一致性 | ConcurrentHashMap | size()、get()、put() 均线程安全;size() 返回近似实时值(JDK8+ 保证强一致性) |
| 迭代中需强一致性(如你的 for-each 场景) | ConcurrentHashMap + forEach() 或 newKeySet().forEach() | 避免 ConcurrentModificationException,且迭代期间允许并发修改 |
| 写操作极少,读极多,可接受弱一致性 | CopyOnWriteArrayList(适用于 List) | 读无锁,写时复制,适合监听器列表等场景 |
✅ 正确重构你的迭代逻辑(替代原始 synchronized 块):
private final ConcurrentHashMap<integer connection> networkView
= new ConcurrentHashMap();
public void broadcastClose() {
// 安全:ConcurrentHashMap 的 forEach 是线程安全的
networkView.forEach((id, conn) -> {
conn.send(new CloseConnectionMessage(ID));
conn.close();
try { conn.join(); } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
});
}</integer>
关键结论与最佳实践
- ❌ 错误认知:“单行 = 安全”、“只读 = 无需同步”。
- ✅ 黄金法则:任何被多个线程访问的可变共享状态,读写均需同步(显式锁、volatile、原子类、线程安全容器)。
- ? 同步粒度建议:优先使用线程安全集合(如 ConcurrentHashMap),避免粗粒度 synchronized(this);若必须手写锁,确保读写共用同一锁对象。
- ? 性能提示:ConcurrentHashMap 的 size() 在 JDK8+ 中通过 sumCount() 实现,比手动同步块更高效且语义清晰;其分段锁设计使高并发读写吞吐远超全局同步的 HashMap。
记住:线程安全不是“加不加锁”的技术选择,而是对内存模型契约的主动履行。每一次未同步的共享访问,都是在和JVM的优化机制博弈——而你永远不该赌它“恰好”按你预期工作。
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南











