1. 并发安全问题的起源与挑战
1996年Java刚诞生时,面向对象编程正在兴起,但多线程编程还处于初级阶段。当时主流CPU都是单核设计,线程调度更多是模拟并行而非真正的并行执行。我最早在Solaris系统上使用Java 1.0时,即使写了所谓的"多线程"代码,本质上还是通过时间片轮转实现的伪并发。
随着2000年后多核CPU的普及,真正的并行计算成为可能。这时开发者突然发现,以前在单核环境下运行良好的多线程程序开始出现各种诡异问题。最典型的就是银行转账案例:
java复制class Account {
private int balance;
void transfer(Account target, int amount) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
}
}
}
这段代码在单线程下完美运行,但在多线程环境下会出现竞态条件(Race Condition)。假设账户A有100元,账户B有0元,两个线程同时执行A.transfer(B, 100),最终可能出现A=-100而B=200的荒谬结果。
关键点:竞态条件的本质是多个线程对共享数据的非原子性操作。在Java内存模型(JMM)中,每个线程有自己的工作内存,与主内存的同步存在延迟。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 互斥锁时代(JDK 1.0-1.4)
2.1 synchronized的诞生
Java最初的解决方案是synchronized关键字,它提供了三种使用方式:
- 同步实例方法:锁是当前实例对象
- 同步静态方法:锁是当前类的Class对象
- 同步代码块:显式指定锁对象
java复制// 同步方法示例
public synchronized void transfer(Account target, int amount) {
if (this.balance >= amount) {
this.balance -= amount;
target.balance += amount;
}
}
// 同步块示例
public void transfer(Account target, int amount) {
synchronized(this) {
if (this.balance >= amount) {
this.balance -= amount;
synchronized(target) {
target.balance += amount;
}
}
}
}
2.2 锁的局限性
在实际项目中,我们发现synchronized有几个严重问题:
-
死锁风险:如上例中的嵌套synchronized,如果线程1持有A锁请求B锁,同时线程2持有B锁请求A锁,就会形成死锁。我在电商系统开发中就遇到过因为订单锁和库存锁顺序不一致导致的死锁。
-
性能瓶颈:synchronized是重量级锁,在JDK1.6之前会直接引发操作系统级别的线程挂起和唤醒。在高并发场景下,这会导致大量线程在锁上排队。
-
不可中断:一旦线程进入synchronized阻塞状态,就无法被中断,只能一直等待。
3. JUC并发包的革命(JDK 5.0)
3.1 Lock接口的引入
JDK5.0推出的java.util.concurrent(JUC)包带来了全新的并发控制方式。核心接口Lock提供了比synchronized更灵活的特性:
java复制Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
ReentrantLock的优势:
- 可尝试获取锁(tryLock)
- 可中断的获取锁(lockInterruptibly)
- 公平锁与非公平锁选择
- 支持多个条件变量(Condition)
3.2 原子类与CAS
JUC还引入了基于CAS(Compare-And-Swap)的原子类,如AtomicInteger:
java复制AtomicInteger counter = new AtomicInteger(0);
// 线程安全的递增
counter.incrementAndGet();
CAS原理:比较当前值与预期值,如果相等则更新,否则重试。现代CPU都提供CAS指令(如x86的CMPXCHG)。
实战经验:在统计PV的系统中,用AtomicLong比synchronized性能提升近10倍。但要注意ABA问题,必要时使用AtomicStampedReference。
3.3 并发容器
JUC提供了一系列线程安全的容器:
- ConcurrentHashMap:分段锁实现的哈希表
- CopyOnWriteArrayList:写时复制的List
- BlockingQueue:各种阻塞队列实现
java复制// 高并发计数器的最佳实践
private final ConcurrentMap<String, AtomicLong> counters = new ConcurrentHashMap<>();
public void increment(String key) {
counters.computeIfAbsent(key, k -> new AtomicLong(0)).incrementAndGet();
}
4. 无锁编程的兴起(JDK 8+)
4.1 LongAdder的优化
JDK8引入了LongAdder,针对高并发计数场景做了特殊优化:
java复制LongAdder adder = new LongAdder();
adder.increment(); // 线程安全
// 实际实现采用分段累加,最后汇总结果
long sum = adder.sum();
在100个线程各执行100万次递增的测试中:
- AtomicLong耗时:2.3秒
- LongAdder耗时:0.4秒
4.2 CompletableFuture的异步编程
JDK8的CompletableFuture提供了更强大的异步编程能力:
java复制CompletableFuture.supplyAsync(() -> queryFromDB())
.thenApplyAsync(data -> processData(data))
.thenAcceptAsync(result -> sendResult(result))
.exceptionally(ex -> handleError(ex));
4.3 VarHandle与内存屏障
JDK9引入的VarHandle提供了更精细的内存访问控制:
java复制class Point {
private volatile int x, y;
private static final VarHandle X;
static {
try {
X = MethodHandles.lookup()
.findVarHandle(Point.class, "x", int.class);
} catch (Exception e) { ... }
}
void atomicIncrement() {
X.getAndAdd(this, 1); // 原子操作
}
}
5. 现代Java并发最佳实践
5.1 锁的选择策略
根据场景选择合适并发控制:
- 低竞争:synchronized(JVM已优化)
- 高竞争:ReentrantLock
- 读多写少:ReadWriteLock
- 纯计数:LongAdder
- 简单状态:原子类
5.2 避免常见陷阱
-
锁粒度:我曾在日志系统中错误地使用全局锁,导致性能瓶颈。应该根据数据范围确定最小必要锁粒度。
-
线程泄漏:ExecutorService必须正确关闭,否则会导致线程堆积。建议使用try-with-resources:
java复制try (ExecutorService es = Executors.newVirtualThreadPerTaskExecutor()) {
es.submit(task);
}
- 上下文切换成本:线程数不是越多越好。在IO密集型场景,考虑使用虚拟线程(JDK19+):
java复制Thread.startVirtualThread(() -> {
// 任务代码
});
5.3 性能优化技巧
- 伪共享(False Sharing):多个原子变量位于同一缓存行会导致性能下降。使用@Contended注解(JDK8+):
java复制@Contended
class Counter {
volatile long x;
volatile long y;
}
-
缓存一致性:频繁修改的变量应该独立缓存,不频繁修改的变量可以组合在一起。
-
基准测试:使用JMH进行可靠的并发性能测试,避免手工测试的误差。
6. 并发调试与问题排查
6.1 线程转储分析
通过jstack获取线程转储:
bash复制jstack <pid> > thread_dump.txt
分析重点:
- 死锁(deadlock)
- 锁竞争(contended lock)
- 线程阻塞(parked)
6.2 JFR监控
Java Flight Recorder提供低开销的运行时监控:
bash复制jcmd <pid> JFR.start duration=60s filename=recording.jfr
关键事件:
- jdk.LockContention
- jdk.ThreadPark
- jdk.JavaMonitorEnter
6.3 可视化工具
- JConsole:基础监控
- VisualVM:插件扩展
- Arthas:线上诊断
我在实际项目中常用Arthas的monitor命令统计方法调用:
bash复制monitor -c 5 com.example.Service methodName
7. 并发安全的未来趋势
7.1 虚拟线程(协程)
JDK19引入的虚拟线程大幅降低了线程创建和切换成本:
java复制try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
IntStream.range(0, 10_000).forEach(i -> {
executor.submit(() -> {
Thread.sleep(Duration.ofSeconds(1));
return i;
});
});
}
7.2 结构化并发
JDK21的StructuredTaskScope使并发任务生命周期管理更安全:
java复制try (var scope = new StructuredTaskScope<String>()) {
Future<String> user = scope.fork(() -> findUser());
Future<String> order = scope.fork(() -> findOrder());
scope.join();
return new Response(user.resultNow(), order.resultNow());
}
7.3 值对象与不可变性
记录类(Record)和不可变集合有助于减少并发问题:
java复制record Point(int x, int y) {}
var path = List.of(new Point(1,1), new Point(2,2));
// 天然线程安全
在20年的Java开发生涯中,我见证了并发编程从简单的synchronized到如今丰富工具集的演进。现代Java开发者应该根据具体场景选择合适的并发工具,而不是一味追求最新技术。对于大多数业务系统,synchronized和并发容器已经足够;只有在极端高并发场景,才需要考虑无锁编程等高级技术。
