1. 为什么高并发是Java面试的核心战场
在Java技术栈的面试中,高并发问题就像一场无法回避的硬仗。我经历过上百场技术面试,发现90%的中高级Java岗位都会深入考察并发编程能力。这不是没有原因的——当今互联网服务的日均请求量动辄上亿,而Java作为企业级应用的主力语言,其并发处理能力直接决定了系统能否扛住真实业务场景的流量冲击。
去年我参与设计的一个电商秒杀系统,在压力测试阶段就暴露了典型的并发问题:当3000个请求同时涌入时,库存扣减出现了超卖现象。通过分析线程堆栈发现,问题根源在于没有正确使用锁机制,导致多个线程同时读取到相同的库存值。这个案例让我深刻认识到,并发编程绝不是纸上谈兵的理论,而是直接影响系统稳定性的实战技能。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 线程安全:从理论到实践的深度解析
2.1 线程安全的本质特征
线程安全的核心在于状态的可控性。当一个类的实例被多个线程同时访问时,如果不需要额外的同步措施就能保持行为的正确性,我们才称它是线程安全的。Java中经典的线程安全类如String、Integer都是不可变对象的代表,它们的内部状态在构造完成后就不能被修改,自然规避了并发问题。
但现实中的业务场景往往需要可变状态。比如银行账户的余额变更,这时就需要通过同步机制来保证原子性。我曾见过一个典型的反例:开发者在Account类的transfer方法上加了synchronized,却在查询余额的getBalance方法上漏掉了同步,结果在高并发时出现了"余额闪现"的诡异现象。
2.2 synchronized的实战要点
synchronized关键字是Java最基础的同步工具,但它的使用有很多门道:
java复制// 正确的对象锁用法
public synchronized void transfer(Account target, int amount) {
this.balance -= amount;
target.balance += amount;
}
// 更细粒度的锁控制
private final Object lock = new Object();
public void transferBetter(Account target, int amount) {
synchronized(lock) {
this.balance -= amount;
target.balance += amount;
}
}
特别注意锁对象的选择:使用专门创建的lock对象比直接锁this更安全,可以避免外部恶意锁定的风险。在分布式系统中,我曾遇到过一个性能问题——某个被频繁调用的服务方法锁住了整个类对象,导致系统吞吐量急剧下降。通过拆锁优化,将类锁改为更细粒度的实例锁后,QPS提升了近8倍。
3. JUC工具库的实战应用场景
3.1 ConcurrentHashMap的底层智慧
JDK中的ConcurrentHashMap是面试必问的高频考点。与Hashtable的全表锁不同,它采用分段锁技术(JDK7)或CAS+synchronized(JDK8)实现高并发读写。有个容易忽略的细节:它的size()方法返回值是近似值,因为在统计过程中可能有线程在修改表。
实际项目中,我常用它来构建缓存系统。比如最近开发的一个配置中心,使用ConcurrentHashMap存储动态配置时,特别需要注意初始容量的设置:
java复制// 预估2000个配置项,并发更新频繁
ConcurrentHashMap<String, Config> cache =
new ConcurrentHashMap<>(2048, 0.75f, 32);
这里的第三个参数concurrencyLevel(并发级别)需要根据更新线程数合理设置,过高会造成内存浪费,过低则导致锁竞争。
3.2 线程池的七个核心参数
线程池配置不当是生产环境常见的性能杀手。来看一个真实的故障案例:某次大促期间,我们的订单服务突然出现大量超时。排查发现是因为使用了无界队列的FixedThreadPool,当下游系统响应变慢时,积压的请求吃光了堆内存。
正确的做法是使用ThreadPoolExecutor并明确所有参数:
java复制ThreadPoolExecutor executor = new ThreadPoolExecutor(
5, // 核心线程数
10, // 最大线程数
60, // 空闲线程存活时间
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // 有界队列
new NamedThreadFactory("order-pool"), // 自定义线程工厂
new CallerRunsPolicy() // 饱和策略
);
特别强调拒绝策略的选择:在订单系统中我们采用CallerRunsPolicy,让提交任务的线程自己执行任务,虽然会降低提交速度,但保证了不会丢失请求。而在对实时性要求不高的日志处理场景,则可以使用DiscardPolicy。
4. 原子类与CAS的ABA问题解决方案
4.1 AtomicInteger的底层实现
原子类利用CPU的CAS指令实现无锁编程,比synchronized性能更高。但很多人不知道的是,在ARM架构的安卓设备上,CAS的实现可能与x86有所不同。我曾遇到一个诡异的bug:同样的原子计数器代码在服务器上运行正常,但在安卓平板上会出现计数偏差,最终发现是因为ARM的弱内存模型导致。
使用AtomicInteger时要注意自增操作的原子性:
java复制// 不安全的自增方式
int unsafe = atomicInt.get() + 1;
// 正确的原子自增
int safe = atomicInt.incrementAndGet();
4.2 ABA问题的工程解决方案
经典的ABA问题是指:线程1读取值为A,线程2将值改为B又改回A,此时线程1的CAS操作仍然成功,但可能不符合业务预期。在金融系统中,这种隐蔽的问题可能导致严重后果。
JDK提供了AtomicStampedReference来解决:
java复制AtomicStampedReference<Integer> money = new AtomicStampedReference<>(100, 0);
// 存款时检查版本戳
int[] stamp = new int[1];
int current = money.get(stamp);
if(money.compareAndSet(current, current + 50, stamp[0], stamp[0] + 1)) {
// 更新成功
}
在实际的账户系统中,我们结合版本号和时间戳设计了更完善的防ABA机制,关键是要确保状态变化的可追溯性。
5. 锁优化与性能调优实战
5.1 死锁的排查与预防
死锁是并发程序中最令人头痛的问题之一。去年我们线上系统曾出现过一个典型的死锁场景:转账服务中,线程A先锁账户X再锁Y,而线程B先锁Y再锁X,在高峰期形成了死锁闭环。
通过jstack抓取的线程转储可以清晰看到死锁链:
code复制"Thread-1" waiting to lock 0x000000076ab2c4e8 (a Account)
which is held by "Thread-2"
"Thread-2" waiting to lock 0x000000076ab2c4f0 (a Account)
which is held by "Thread-1"
解决方案是引入全局锁排序规则:对所有账户按ID排序后统一按顺序加锁。这个案例让我养成了在代码审查时特别关注锁获取顺序的习惯。
5.2 偏向锁与JVM调优
现代JVM的锁优化技术非常精妙。在压测一个高并发服务时,我们注意到当线程数超过CPU核心数时,性能会出现断崖式下跌。通过-XX:+PrintFlagsFinal参数发现,JVM默认在启动后几秒就会禁用偏向锁(BiasedLocking)。
对于明确知道会有高竞争的场景,可以通过参数调整:
code复制-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
但要注意,在Java 15之后偏向锁已被标记为废弃,因为维护偏向锁的开销在现代CPU架构上可能已经超过了收益。这个变化提醒我们,技术选型需要与时俱进。
6. 并发编程的避坑指南
6.1 volatile的常见误解
很多面试者认为volatile能保证原子性,这是危险的认知偏差。volatile只能保证可见性和有序性,对复合操作(如i++)仍然需要同步。我在代码审查中经常发现这样的错误用法:
java复制// 错误用法:volatile不能保证count++的原子性
private volatile int count = 0;
public void unsafeIncrement() {
count++;
}
正确的做法是要么使用原子类,要么用synchronized保护。在最近的一个性能监控系统中,我们通过将volatile与AtomicLong配合使用,既保证了性能又确保了正确性。
6.2 线程局部变量的内存泄漏
ThreadLocal是解决线程安全问题的利器,但也可能成为内存泄漏的源头。我们线上曾有个服务运行两周后就会OOM,最终发现是因为没有及时清理ThreadLocal中存储的用户会话信息。
正确的使用模式:
java复制private static final ThreadLocal<User> currentUser = new ThreadLocal<>();
try {
currentUser.set(user);
// 执行业务逻辑
} finally {
currentUser.remove(); // 必须清理
}
在Tomcat等容器环境中,这个问题会更加突出,因为工作线程通常会复用。建议为ThreadLocal变量添加@Cleanup注解或使用try-finally块确保清理。
7. 高并发设计模式实战
7.1 生产者-消费者模式的工程实现
消息队列是解耦生产者与消费者的经典模式。在自研轻量级队列时,我们对比了多种实现方案:
- 使用BlockingQueue最简单但性能一般
- Disruptor框架性能极高但学习曲线陡峭
- 基于CAS的自研环形缓冲区平衡了复杂度与性能
最终选择方案3的实现核心:
java复制class RingBuffer<E> {
private final AtomicLong producerIndex = new AtomicLong();
private final AtomicLong consumerIndex = new AtomicLong();
private final E[] buffer;
public void put(E item) {
long pi = producerIndex.get();
while (!producerIndex.compareAndSet(pi, pi + 1)) {
pi = producerIndex.get();
}
buffer[(int)(pi % buffer.length)] = item;
}
}
这个设计在百万级QPS的压力下依然保持稳定,关键是通过缓存行填充避免了伪共享问题。
7.2 ForkJoinPool的适用场景
对于计算密集型任务,ForkJoinPool比普通线程池更高效。在数据分析系统中,我们用它来处理大型矩阵运算:
java复制class MatrixTask extends RecursiveTask<Double> {
private final double[][] matrix;
private final int start, end;
protected Double compute() {
if (end - start < THRESHOLD) {
return computeDirectly();
}
int mid = (start + end) >>> 1;
MatrixTask left = new MatrixTask(matrix, start, mid);
MatrixTask right = new MatrixTask(matrix, mid, end);
left.fork();
return right.compute() + left.join();
}
}
但要注意,ForkJoinPool不适合IO密集型任务,因为工作窃取机制在阻塞场景下效果不佳。我们曾错误地用它来处理文件上传,结果性能反而不如普通线程池。
