选对工具,事半功倍
在日常开发中,面对高并发场景时,我们常常面临这样的困惑:synchronized 和 ReentrantLock 到底该用哪个?ThreadLocal 会不会导致内存泄漏?CAS真的比锁快吗?今天,我们就来系统梳理解决并发问题的两大类、六种核心方案,帮你建立清晰的决策框架。
一、从一个工作场景说起
想象一下这个常见场景:你需要实现一个简单的任务计数器,多个线程会同时增加这个计数。
如果直接使用 int count = 0; 然后多个线程执行 count++,结果会怎样?你会发现最终计数值经常少于实际执行的任务数。这是因为 count++ 这个操作不是原子操作,它在执行时可能被其他线程打断。
这就是典型的线程安全问题:当多个线程访问共享资源时,如果没有正确的同步,程序的行为将变得不可预测。
二、解决方案全景图
所有并发问题的解决方案,本质上围绕两种思路展开:避免共享和安全共享。基于这两种思路,我们可以将技术方案分为两大类、六种具体实现:

三、无锁方案详解
1. 线程封闭:最好的同步就是不同步
线程封闭的核心思想是:如果数据不共享,自然就不需要同步。
局部变量(栈封闭)
publicvoidprocessTask(){
// 局部变量,每个线程调用时都会创建自己的副本
int localCounter = 0;
localCounter++; // 绝对线程安全
// ... 其他操作
}
这是最简单、最高效的线程安全方案。局部变量存储在每个线程私有的栈帧中,其他线程根本无法访问。

ThreadLocal(线程级变量副本)
privatestaticfinal ThreadLocal<Integer> userCounter =
ThreadLocal.withInitial(() -> 0);
publicvoidprocessUserRequest(){
// 每个线程获取自己独立的副本
Integer count = userCounter.get();
userCounter.set(count + 1);
// 使用完后必须清理,防止内存泄漏
userCounter.remove();
}
ThreadLocal 为每个线程创建变量的独立副本,适用于需要在多个方法间传递但不想同步的上下文信息(如用户ID、事务上下文)。

注意:使用 ThreadLocal 必须记得在适当的时候调用 remove(),尤其是在线程池场景中,否则可能导致内存泄漏。
2. 不可变对象:安全的共享
// 使用final修饰类和所有字段
publicfinalclassImmutableConfig{
privatefinal String appName;
privatefinalint maxConnections;
publicImmutableConfig(String appName, int maxConnections){
this.appName = appName;
this.maxConnections = maxConnections;
}
// 只有getter,没有setter
public String getAppName(){ return appName; }
publicintgetMaxConnections(){ return maxConnections; }
}
不可变对象一旦创建,其状态就永远不变。因为不能修改,所以可以安全地在多线程间共享,无需任何同步。这是函数式编程和String类线程安全的基础。

3. CAS原子类:无锁的线程安全
import java.util.concurrent.atomic.AtomicInteger;
publicclassCounter{
private AtomicInteger count = new AtomicInteger(0);
publicvoidsafeIncrement(){
// 基于CAS操作,保证原子性
count.incrementAndGet();
}
publicintgetCount(){
return count.get();
}
}
CAS(Compare-And-Swap)是一种乐观锁策略。它包含三个操作数:内存值(V)、预期值(A)和新值(B)。当且仅当V等于A时,才将V更新为B,否则重试或失败。
原子类的优势在于无锁,在低竞争环境下性能极高。但高竞争时可能导致大量自旋,消耗CPU资源。

四、有锁方案详解
1. synchronized:JVM内置锁
publicclassSynchronizedCounter{
privateint count = 0;
// 同步方法
publicsynchronizedvoidincrement(){
count++;
}
// 同步代码块(更细粒度控制)
publicvoidincrementWithBlock(){
synchronized(this) {
count++;
}
// 这里不需要同步
}
}
synchronized 是Java最基础的同步机制,由JVM实现和管理。JDK 1.6后进行了大量优化(偏向锁、轻量级锁、锁升级等),性能已大幅提升。

2. ReentrantLock:更灵活的显式锁
import java.util.concurrent.locks.ReentrantLock;
publicclassLockCounter{
privateint count = 0;
privatefinal ReentrantLock lock = new ReentrantLock();
publicvoidincrement(){
lock.lock(); // 手动获取锁
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
publicbooleantryIncrement(){
// 尝试获取锁,最多等待100毫秒
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
count++;
returntrue;
} finally {
lock.unlock();
}
}
returnfalse;
}
}
ReentrantLock 比 synchronized 提供更多高级功能:
- 可中断的锁等待:
lockInterruptibly() - 公平锁选项:
new ReentrantLock(true)
五、实战选择指南
面对具体场景,如何选择?参考下面的决策流程:

场景示例:
- Web应用的请求计数器:竞争不激烈 → 选择
AtomicInteger - 用户会话信息存储:线程隔离数据 → 选择
ThreadLocal - 简单的服务方法同步:无特殊需求 → 选择
synchronized - 复杂的交易系统:需要尝试锁、超时、公平性 → 选择
ReentrantLock
六、总结与最佳实践
优先考虑“避免共享”:这是并发编程的最高境界。无状态设计、线程封闭、不可变对象应该成为你的首选方案。
理解锁的代价:锁不仅是性能开销,更是复杂度开销。synchronized 在大多数情况下足够好,除非你需要 ReentrantLock 的高级特性。
CAS的适用场景:在低到中度竞争下,CAS原子类性能优异。但要注意ABA问题和自旋开销。
避免过度设计:不要为了用新技术而用。简单的 synchronized 往往比复杂的 ReentrantLock 加条件变量更可靠。
工具组合使用:优秀的并发程序通常组合多种技术。比如 ConcurrentHashMap 就结合了分段锁、CAS和 volatile。
并发编程没有“银弹”,只有对原理的深入理解和对场景的准确判断。掌握这六种核心武器,结合实际场景灵活运用,你就能写出既安全又高效的多线程程序。
最好的并发代码,往往是那些看起来最简单、最不像并发代码的代码。