1. 线程安全的核心概念解析
在Java开发中,线程安全就像是在拥挤的餐厅里确保每位顾客都能有序用餐。当多个线程同时访问共享资源时,如果没有适当的保护措施,就会出现数据混乱的情况。我见过太多因为线程安全问题导致的线上事故,比如订单重复扣款、库存超卖等。
线程安全的本质在于解决三个核心问题:
- 原子性:像银行转账操作,必须保证"扣款"和"加款"两个步骤不可分割
- 可见性:一个线程修改了共享变量,其他线程能立即看到最新值
- 有序性:程序执行的顺序要符合预期,避免指令重排序带来的问题
重要提示:即使是在单核CPU上,由于线程切换的存在,线程安全问题依然可能出现。不要认为"我的服务器配置低"就可以忽视线程安全。
2. Java内存模型与线程安全
2.1 JMM的核心机制
Java内存模型(JMM)定义了线程如何与内存交互。每个线程都有自己的工作内存,这就像给每个服务员配了一个记事本。问题在于,当主内存的数据发生变化时,各个线程的"记事本"可能还记录着旧值。
典型的可见性问题案例:
java复制public class VisibilityProblem {
private static boolean flag = true;
public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
while(flag) {} // 可能永远循环
System.out.println("线程结束");
}).start();
Thread.sleep(1000);
flag = false; // 主线程修改标志位
}
}
2.2 happens-before原则
这是理解线程安全的关键规则,主要包括:
- 程序顺序规则:同一线程中的操作按代码顺序执行
- 锁规则:解锁操作先于后续的加锁操作
- volatile规则:volatile变量的写操作先于后续读操作
- 线程启动规则:Thread.start()先于线程内的任何操作
- 线程终止规则:线程中的所有操作先于线程终止检测
3. 线程安全的实现方案
3.1 synchronized关键字
这是最基础的线程安全解决方案,就像给餐厅的某个座位加上"使用中"的牌子。我总结了几种使用方式:
- 实例方法同步:
java复制public synchronized void transfer(Account target, int amount) {
this.balance -= amount;
target.balance += amount;
}
- 静态方法同步:
java复制public static synchronized void staticMethod() {
// 锁的是Class对象
}
- 同步代码块:
java复制public void method() {
// 非同步代码
synchronized(this) {
// 同步代码
}
}
实际经验:避免在同步块中调用外部方法,这可能导致死锁。我曾经遇到过在同步方法中调用第三方服务回调导致的死锁问题。
3.2 volatile关键字
volatile就像是给变量加上了一个广播喇叭,任何修改都会立即通知所有线程。但它只能保证可见性,不能保证原子性。
适用场景:
- 状态标志位(如开关控制)
- 单例模式的双重检查锁定
java复制public class Singleton {
private static volatile Singleton instance;
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
3.3 原子类(Atomic)
Java.util.concurrent.atomic包下的类,如AtomicInteger,使用CAS(Compare-And-Swap)机制实现线程安全。这就像是用自动售货机代替人工售货 - 完全由硬件保证原子性。
典型用法:
java复制private AtomicInteger counter = new AtomicInteger(0);
public void increment() {
counter.incrementAndGet(); // 线程安全的自增
}
性能对比(基于我的压力测试):
- 无锁情况:约1000万次/秒
- synchronized:约300万次/秒
- AtomicInteger:约800万次/秒
3.4 Lock接口
相比synchronized,Lock接口提供了更灵活的锁操作,就像是可以设置超时的餐厅预约:
java复制Lock lock = new ReentrantLock();
try {
lock.lock();
// 临界区代码
} finally {
lock.unlock(); // 必须手动释放
}
高级特性:
- 尝试获取锁:tryLock()
- 可中断锁:lockInterruptibly()
- 公平锁:new ReentrantLock(true)
4. 线程安全容器详解
4.1 ConcurrentHashMap设计精妙
这是我见过最优秀的并发容器实现。JDK8中的实现采用了:
- 数组+链表+红黑树结构
- CAS+synchronized细粒度锁
- 扩容时多线程协助
与Hashtable的对比:
| 特性 | Hashtable | ConcurrentHashMap |
|---|---|---|
| 锁粒度 | 整个表 | 单个桶(bucket) |
| 并发度 | 1 | 默认16 |
| 迭代器 | 强一致性 | 弱一致性 |
4.2 CopyOnWrite容器
适用于读多写少的场景,原理是在修改时创建新副本:
java复制List<String> list = new CopyOnWriteArrayList<>();
list.add("item"); // 底层会复制整个数组
注意事项:
- 内存占用大,不适合大数据量
- 写操作性能较差
- 迭代器访问的是快照数据
5. 线程池与线程安全
5.1 正确使用线程池
我推荐使用ThreadPoolExecutor而不是Executors工具类,因为后者可能隐藏了关键参数:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
4, // 核心线程数
8, // 最大线程数
60, // 空闲时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100), // 任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
5.2 常见陷阱
- 线程局部变量未清理:
java复制ThreadLocal<User> userHolder = new ThreadLocal<>();
try {
userHolder.set(currentUser);
// 业务逻辑
} finally {
userHolder.remove(); // 必须清理!
}
- 死锁的四个必要条件:
- 互斥条件
- 请求与保持
- 不剥夺条件
- 循环等待
诊断技巧:jstack查看线程dump,寻找"BLOCKED"状态和持有锁的信息。
6. 实战案例分析
6.1 计数器实现方案对比
需求:实现一个高并发环境下的计数器
方案一:synchronized
java复制private int count;
public synchronized void increment() { count++; }
方案二:AtomicLong
java复制private AtomicLong count = new AtomicLong();
public void increment() { count.incrementAndGet(); }
方案三:LongAdder(JDK8+)
java复制private LongAdder count = new LongAdder();
public void increment() { count.increment(); }
性能测试结果(8线程,1000万次操作):
- synchronized:2.8秒
- AtomicLong:1.2秒
- LongAdder:0.4秒
6.2 订单库存扣减方案
典型错误实现:
java复制public boolean deductStock(Long productId, int num) {
Product product = getById(productId); // 1.查询库存
if (product.getStock() >= num) { // 2.判断库存
product.setStock(product.getStock() - num); // 3.扣减库存
update(product); // 4.更新数据库
return true;
}
return false;
}
正确实现(使用数据库乐观锁):
java复制public boolean deductStock(Long productId, int num) {
int rows = productMapper.updateStock(productId, num);
return rows > 0;
}
对应的SQL:
sql复制UPDATE product
SET stock = stock - #{num},
version = version + 1
WHERE id = #{productId}
AND stock >= #{num}
AND version = #{version}
7. 高级话题与性能优化
7.1 无锁编程技巧
- CAS自旋优化:
java复制public class SpinLock {
private AtomicBoolean locked = new AtomicBoolean(false);
public void lock() {
while (!locked.compareAndSet(false, true)) {
// 自旋等待
Thread.yield(); // 避免过度消耗CPU
}
}
}
- 消除伪共享:使用@Contended注解(JDK8+)
java复制@Contended
public class VolatileLong {
public volatile long value = 0L;
}
7.2 并发设计模式
- 不变模式(Immutable):
java复制public final class ImmutableValue {
private final int value;
public ImmutableValue(int value) {
this.value = value;
}
public int getValue() {
return value;
}
}
- 线程特定存储模式:
java复制public class UserContext {
private static final ThreadLocal<User> holder = new ThreadLocal<>();
public static void set(User user) {
holder.set(user);
}
public static User get() {
return holder.get();
}
}
8. 常见问题排查指南
8.1 死锁诊断
典型死锁代码:
java复制// 线程1
synchronized (lockA) {
synchronized (lockB) { ... }
}
// 线程2
synchronized (lockB) {
synchronized (lockA) { ... }
}
诊断步骤:
- jps获取Java进程ID
- jstack
查看线程栈 - 查找"deadlock"关键词和BLOCKED状态线程
8.2 内存泄漏排查
ThreadLocal导致的内存泄漏场景:
java复制public class ThreadLocalLeak {
private static ThreadLocal<byte[]> local = new ThreadLocal<>();
public static void main(String[] args) {
new Thread(() -> {
local.set(new byte[10 * 1024 * 1024]); // 10MB
// 忘记调用local.remove()
}).start();
}
}
解决方案:
- 使用try-finally确保remove()被调用
- 使用ThreadLocal.withInitial()方法
9. 最佳实践总结
经过多年的实践,我总结了以下线程安全黄金法则:
- 优先使用不可变对象
- 缩小同步范围,降低锁粒度
- 读写分离,CopyOnWrite是个好选择
- 使用并发工具类而不是自己造轮子
- 对共享变量的访问都要有防御措施
- 注意锁的顺序,避免死锁
- 考虑使用线程封闭技术(如ThreadLocal)
- 异步处理时注意任务的有序性
在最近的一个电商项目中,我们通过以下优化将并发性能提升了5倍:
- 将synchronized替换为ReentrantLock
- 使用ConcurrentHashMap代替Collections.synchronizedMap
- 对热点数据采用LongAdder计数
- 使用@Contended注解消除伪共享