Java多线程并发安全问题典型表现为共享变量读写不一致,如未加锁的count++操作因“读-改-写”非原子性导致结果小于预期。

Java 中多线程并发安全问题,最典型的就是多个线程同时读写共享变量时出现数据不一致——比如计数器没加锁,最终结果远小于预期值。下面用简洁、可运行的代码直接模拟这个过程。
1. 模拟非线程安全的计数器
定义一个普通 int 变量 count,启动多个线程反复执行 count++(本质是读-改-写三步操作),不加任何同步手段:
public class UnsafeCounter {
public static int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[100];
for (int i = 0; i < 100; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // 非原子操作:读取、+1、写回,可能被中断
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Expected: 100000, Actual: " + count); // 通常远小于 100000
}
}
运行多次会发现输出不稳定,比如 99823、99511 等——这就是典型的竞态条件(Race Condition)。
2. 模拟可见性问题(volatile 不保证原子性)
仅用 volatile 修饰变量,能保证可见性,但无法解决 count++ 的原子性问题:
立即学习“Java免费学习笔记(深入)”;
public class VolatileCounter {
public static volatile int count = 0;
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[10];
for (int i = 0; i < 10; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
count++; // volatile 不阻止指令交错,结果仍不准
}
});
threads[i].start();
}
for (Thread t : threads) t.join();
System.out.println("Volatile result: " + count); // 依然出错
}
}
3. 模拟指令重排序(双重检查锁 DCL 的经典陷阱)
在单例模式中,若未用 volatile 修饰 instance,可能导致线程拿到未初始化完成的对象:
public class DCLSingleton {
private static DCLSingleton instance;
private DCLSingleton() {
// 模拟耗时初始化(如加载配置)
try { Thread.sleep(1); } catch (InterruptedException e) {}
}
public static DCLSingleton getInstance() {
if (instance == null) {
synchronized (DCLSingleton.class) {
if (instance == null) {
instance = new DCLSingleton(); // 编译器/JVM 可能重排序:先赋值引用,再调构造
}
}
}
return instance;
}
}
// 测试类中启动多线程反复调用 getInstance(),可能触发 NPE 或部分初始化对象
虽然实际复现需特定 JVM 和压力,但该代码结构已暴露重排序风险——这也是并发安全中“正确性”容易被忽略的一环。
4. 快速修复对比(验证问题根源)
把上面任一例子加上同步机制,结果立刻稳定:
- 加 synchronized:方法或代码块加锁,确保同一时刻只有一个线程执行 count++
- 用 AtomicInteger:count = new AtomicInteger(0); 替换 count++ 为 count.incrementAndGet()
- volatile + final 修饰 instance:在 DCL 中给 instance 加 volatile,禁止重排序
修复后每次运行都输出预期值,反向印证了原始代码的问题所在。


















