i++不是线程安全的,因其被拆分为读取、加1、写回三步,中间可能被其他线程打断导致结果错误;应使用atomicinteger.incrementandget()或synchronized块解决,volatile无法修复该问题。

原子性问题:i++ 为什么不是线程安全的
因为 i++ 实际拆成三步:读取 i、加 1、写回 i,中间可能被其他线程打断。两个线程同时执行,最终结果可能只加了 1 而不是 2。
- 用
AtomicInteger.incrementAndGet()替代i++,底层靠 CAS 保证原子性 - 简单临界区可用
synchronized块包裹读-改-写逻辑,但注意锁对象要唯一且不可变 - 别用
volatile修饰i来“修复”i++——它只保可见性,不保原子性,错误依旧
可见性陷阱:volatile 能做什么、不能做什么
volatile 强制每次读都从主内存取,每次写都立刻刷回主内存,解决的是“一个线程改了,另一个线程看不到”的问题。
- 适合单次读或单次写场景,比如状态标志:
volatile boolean isRunning = true; - 不适合复合操作,如
if (flag) { doSomething(); flag = false; }——判断和赋值之间仍可能被重排序或穿插执行 - 在 JDK 5+ 才真正可靠;JDK 4 及之前有严重重排序漏洞,旧代码迁移时要特别核对
有序性干扰:new Object() 的指令重排怎么影响线程安全
构造函数执行完毕前,对象引用就可能被赋值给共享变量,导致其他线程拿到未初始化完成的对象(如字段为 null 或默认值)。
- 典型表现是
NullPointerException出现在看似“已初始化”的对象上 - 在构造器内避免将
this引用逸出(如注册监听、启动线程、存入静态集合) - 双重检查锁单例中,必须用
volatile修饰实例字段,否则重排序可能让其他线程看到半初始化对象
同步机制选型:什么时候该用 synchronized,什么时候该用 ReentrantLock
两者语义一致,但行为和开销不同。别只看“更高级”就换,容易引入死锁或忽略 unlock。
立即学习“Java免费学习笔记(深入)”;
-
synchronized自动加锁/解锁,不会忘记释放,适合简单同步块;JVM 层优化好,轻量级锁在无竞争时开销极小 -
ReentrantLock支持超时获取(tryLock(long, TimeUnit))、可中断等待(lockInterruptibly())、公平锁等,但必须配对unlock(),建议写在finally块里 - 别混用:一个用
synchronized锁 A,另一个用ReentrantLock锁 A,毫无互斥效果
HashMap 在多线程 put 时可能死循环,SimpleDateFormat 多次 parse 可能返回错误日期——这些都不是靠加锁就能随便救回来的,得换线程安全版本,或者干脆隔离实例。










