一、线程安全问题的本质
在多线程环境下,当多个线程同时访问共享资源时,如果没有正确的同步机制,就可能导致数据不一致或程序行为异常。举个生活中的例子:就像多个收银员同时操作同一个收银台,如果不加控制,很容易算错账。
示例1:经典的银行转账问题(Java技术栈)
public class UnsafeBank {
private static int balance = 1000; // 共享账户余额
public static void withdraw(int amount) {
if (balance >= amount) {
try {
Thread.sleep(10); // 模拟处理延迟
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额:" + balance);
}
}
public static void main(String[] args) {
// 两个线程同时取款
new Thread(() -> withdraw(800)).start();
new Thread(() -> withdraw(800)).start();
}
}
输出可能结果:
Thread-0取款800,余额:200
Thread-1取款800,余额:-600 // 余额异常!
问题分析:
由于balance的检查和修改不是原子操作,两个线程可能同时通过if判断,导致超额取款。
二、快速解决方案
1. 同步代码块(synchronized)
最直接的解决方案,通过锁机制保证同一时间只有一个线程执行关键代码。
示例2:使用synchronized修复银行问题
public class SafeBank {
private static int balance = 1000;
private static final Object lock = new Object(); // 专用锁对象
public static void withdraw(int amount) {
synchronized (lock) { // 加锁
if (balance >= amount) {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance -= amount;
System.out.println(Thread.currentThread().getName() + "取款" + amount + ",余额:" + balance);
}
}
}
}
关键点:
- 锁对象建议使用专用
Object而非this,避免与外部锁冲突 - 锁粒度不宜过大,否则会降低并发性能
2. 原子类(AtomicInteger等)
适合简单变量的原子操作,底层基于CAS(Compare-And-Swap)实现。
示例3:使用AtomicInteger优化计数器
import java.util.concurrent.atomic.AtomicInteger;
public class Counter {
private AtomicInteger count = new AtomicInteger(0);
public void increment() {
count.incrementAndGet(); // 原子操作
}
public int getCount() {
return count.get();
}
}
优势:
- 无锁竞争,性能高于
synchronized - 适合高频读写的简单场景
三、进阶解决方案
1. ReentrantLock显式锁
比synchronized更灵活,支持超时、公平锁等特性。
示例4:ReentrantLock实现限时等待
import java.util.concurrent.locks.ReentrantLock;
public class TimeoutLockDemo {
private final ReentrantLock lock = new ReentrantLock();
public void performTask() {
try {
if (lock.tryLock(1, TimeUnit.SECONDS)) { // 尝试获取锁,最多等待1秒
try {
System.out.println("执行关键操作");
Thread.sleep(2000); // 模拟耗时操作
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时,放弃操作");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
适用场景:
- 需要尝试获取锁或中断响应的场景
- 需要实现公平锁(按申请顺序获取锁)
2. ThreadLocal线程封闭
彻底避免共享,每个线程维护独立副本。
示例5:SimpleDateFormat的线程安全方案
public class DateUtil {
private static final ThreadLocal<SimpleDateFormat> threadLocal =
ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
public static String format(Date date) {
return threadLocal.get().format(date); // 每个线程独立实例
}
}
注意事项:
- 使用后需调用
threadLocal.remove()防止内存泄漏(尤其在线程池中)
四、实战经验与陷阱规避
- 死锁预防:按固定顺序获取多个锁,或使用
tryLock超时机制 - 性能权衡:
- 读多写少场景用
ReadWriteLock - 低竞争场景用原子类,高竞争场景用
ConcurrentHashMap
- 读多写少场景用
- JUC工具类:优先使用
ConcurrentHashMap、CopyOnWriteArrayList等线程安全容器
终极建议:
- 优先考虑无锁设计(如不可变对象)
- 使用
final字段避免意外修改 - 通过
Collections.unmodifiableXXX返回防御性副本
评论