一、线程安全问题的本质
并发编程的核心挑战:当多个线程同时访问共享资源时,由于操作系统的抢占式调度特性,可能导致不可预期的结果。这种因非原子操作和竞态条件引发的数据不一致问题,称为线程安全问题。
二、经典线程安全问题案例
1. 非原子操作示例
class Counter {
private int count = 0;
public void add() {
count++; // 非原子操作
}
}
public class Main {
public static void main(String[] args) throws InterruptedException {
Counter counter = new Counter();
Thread t1 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 50000; i++) {
counter.add();
}
});
t1.start();
t2.start();
t1.join();
t2.join();
System.out.println(counter.getCount()); // 预期10_0000,实际可能得到随机值
}
}
2. 问题根源分析
count++
操作在JVM层面实际对应三条指令:
ILOAD // 从内存加载值到寄存器
IADD 1 // 寄存器值+1
ISTORE // 将结果写回内存
线程调度时序图:
三、线程安全问题的四大成因
成因 | 描述 | 典型场景 |
---|---|---|
原子性破坏 | 操作被拆分为多个不可分割的步骤 | count++ 、复合操作 |
内存可见性 | 线程缓存导致数据不一致 | 多核CPU架构下的共享变量 |
指令重排序 | 编译器优化打乱执行顺序 | 单例模式的双重检查锁 |
竞态条件 | 执行结果依赖时序 | 先检查后操作 |
四、synchronized解决方案
1. 同步方法
public synchronized void add() { // 锁对象为this
count++;
}
2. 同步代码块
public void add() {
synchronized(this) { // 显式指定锁对象
count++;
}
}
3. 静态方法同步
public static synchronized void staticAdd() { // 锁对象为Class对象
staticCount++;
}
五、锁机制工作原理
1. 锁对象的选择原则
锁类型 | 作用域 | 适用场景 |
---|---|---|
实例锁 | 对象级别 | 保护非静态成员 |
类锁 | 全局级别 | 保护静态成员 |
2. 可重入锁特性
public synchronized void methodA() {
methodB(); // 可重入调用
}
public synchronized void methodB() {
// 操作共享资源
}
3. 锁竞争流程图
六、最佳实践与性能优化
-
最小化同步范围
// 不推荐 public synchronized void process() { // 大量非共享操作... count++; } // 推荐 public void process() { // 非共享操作... synchronized(this) { count++; } }
-
锁分离技术
class SeparateLock { private final Object readLock = new Object(); private final Object writeLock = new Object(); public void read() { synchronized(readLock) { // 读操作 } } public void write() { synchronized(writeLock) { // 写操作 } } }
-
并发工具替代方案
AtomicInteger atomicCount = new AtomicInteger(); atomicCount.incrementAndGet(); // 原子操作
七、常见误区与避坑指南
-
错误:同步无关对象
private final Object lock = new Object(); public void add() { synchronized(lock) { count++; // 正确 } } public void print() { synchronized(new Object()) { // 错误!每次创建新对象 System.out.println(count); } }
-
错误:忽略可见性
private boolean flag = false; // 需要volatile修饰 public void run() { while(!flag) { // 可能死循环 // ... } }
-
错误:滥用类锁
public static synchronized void globalLock() { // 影响所有实例的性能 }
八、线程安全等级评估
安全等级 | 描述 | 示例 |
---|---|---|
不可变 | 对象状态永不改变 | String、包装类 |
绝对安全 | 所有操作原子化 | AtomicInteger |
相对安全 | 需正确调用接口 | Vector集合 |
线程兼容 | 依赖外部同步 | ArrayList |
线程对立 | 无法安全使用 | 废弃的API |
九、现代并发工具推荐
-
Lock API
ReentrantLock lock = new ReentrantLock(); lock.lock(); try { // 临界区代码 } finally { lock.unlock(); }
-
并发集合
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>(); CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
-
线程池框架
ExecutorService executor = Executors.newFixedThreadPool(4); executor.submit(() -> processTask());
总结
解决线程安全问题的关键在于理解并发执行的底层机制:
-
识别共享资源的访问点
-
通过锁机制保证原子性
-
结合volatile保证可见性
-
使用并发工具简化开发
黄金法则:
-
能不用锁尽量不用
-
必须用锁时保持最小粒度
-
优先选择并发工具类
-
多测试多验证