1. Java并发编程的核心价值与挑战
在当今多核处理器成为标配的时代,有效利用硬件并发能力已成为Java开发者必须掌握的技能。我经历过太多因为并发处理不当导致的线上事故——从简单的界面卡顿到可怕的资金差错,这些教训让我深刻认识到:理解Java并发机制不是选修课,而是生存技能。
Java内存模型(JMM)就像交通规则,它定义了多线程环境下变量的访问规则。没有它,线程间的操作就会像没有红绿灯的十字路口,随时可能发生"撞车"事故。而真正掌握并发编程,需要从三个维度入手:
- 可见性:一个线程的修改何时对其它线程可见
- 有序性:指令重排序会带来哪些意想不到的结果
- 原子性:哪些操作是不可分割的整体
2. Java并发特性深度解析
2.1 线程基础与生命周期管理
创建线程的三种经典方式各有利弊:
java复制// 方式1:继承Thread类
class MyThread extends Thread {
public void run() {
System.out.println("Thread running");
}
}
// 方式2:实现Runnable接口
class MyRunnable implements Runnable {
public void run() {
System.out.println("Runnable running");
}
}
// 方式3:使用Lambda表达式
new Thread(() -> System.out.println("Lambda thread")).start();
关键经验:优先选择Runnable方式,它更灵活且符合面向接口编程原则。线程池只能接收Runnable/Callable任务。
线程状态转换是个容易踩坑的知识点。我曾遇到一个BUG:线程调用wait()后没有及时被notify(),导致线程永远停留在WAITING状态。正确的状态管理应该包括:
- 通过interrupt()优雅终止线程
- 使用标志位控制循环退出
- 避免使用已废弃的stop()方法
2.2 synchronized的底层实现
这个关键字背后的monitor机制非常精妙。通过javap反编译可以看到,synchronized会在代码块前后插入monitorenter和monitorexit指令:
code复制public void syncMethod();
Code:
0: aload_0
1: dup
2: astore_1
3: monitorenter // 获取锁
4: aload_1
5: monitorexit // 正常释放锁
6: goto 14
9: astore_2
10: aload_1
11: monitorexit // 异常时释放锁
12: aload_2
13: athrow
14: return
锁升级过程是另一个面试常考点:
- 无锁状态:新创建的对象
- 偏向锁:第一个线程访问时,记录线程ID
- 轻量级锁:有竞争时升级为CAS自旋
- 重量级锁:自旋超过阈值后进入阻塞
性能提示:对于明确存在高竞争的场景,直接使用ReentrantLock可能比synchronized更高效。
3. Java内存模型(JMM)详解
3.1 happens-before原则
这个原则定义了操作间的可见性关系,就像事件的时间戳。以下是一些典型规则:
- 程序顺序规则:同一线程中的操作按程序顺序执行
- 锁规则:解锁操作先于后续的加锁操作
- volatile规则:写操作先于后续的读操作
- 线程启动规则:Thread.start()先于线程中的任何操作
我曾遇到一个典型的可见性问题案例:
java复制class VisibilityProblem {
boolean ready = false;
void writer() {
ready = true; // 操作1
}
void reader() {
while(!ready); // 操作2
System.out.println("Visible now");
}
}
在没有同步措施的情况下,操作2可能永远看不到操作1的修改。
3.2 volatile关键字原理
这个关键字做了两件事:
- 禁止指令重排序
- 保证可见性
其底层通过内存屏障(Memory Barrier)实现:
- 写操作前插入StoreStore屏障
- 写操作后插入StoreLoad屏障
- 读操作前插入LoadLoad屏障
- 读操作后插入LoadStore屏障
典型应用场景包括:
- 状态标志位(如shutdown请求)
- 一次性安全发布(如单例模式的double-check)
- 独立观察(定期更新的统计值)
4. 并发工具类实战
4.1 ConcurrentHashMap设计精妙
JDK1.8中的实现采用了更细粒度的锁机制:
- 使用Node数组+链表/红黑树结构
- 读操作完全无锁
- 写操作只锁住单个桶
- 扩容时采用多线程协同
与HashTable的简单粗暴锁整个表相比,性能差异可以达到几个数量级。在我的压力测试中,16线程环境下ConcurrentHashMap的吞吐量是HashTable的20倍以上。
4.2 ThreadLocal的内存泄漏防范
这个类虽然好用,但容易引发内存泄漏。正确的使用姿势应该包括:
- 声明为static final
- 实现initialValue()方法
- 使用后及时调用remove()
典型错误案例:
java复制void processRequest(HttpRequest request) {
ThreadLocal<User> userHolder = new ThreadLocal<>();
userHolder.set(getCurrentUser());
// 处理业务...
// 忘记调用userHolder.remove()
}
在Tomcat等线程池环境下,这个User对象会一直存在于线程的threadLocals中无法回收。
5. 原子类与CAS原理
5.1 Unsafe类的魔法操作
这个后门类提供了直接操作内存的能力,典型操作包括:
- allocateMemory/freeMemory:堆外内存分配
- park/unpark:线程阻塞/唤醒
- compareAndSwap:CAS原子操作
虽然Java9开始限制了它的使用,但理解其原理仍然很重要。CAS操作的伪代码如下:
code复制boolean compareAndSwap(int expectedValue, int newValue) {
if(currentValue == expectedValue) {
currentValue = newValue;
return true;
}
return false;
}
5.2 LongAdder性能优化
在高并发计数场景下,AtomicLong可能成为瓶颈,因为所有线程都在竞争同一个变量。LongAdder的解决方案很巧妙:
- 维护一个Cell数组
- 线程哈希到不同的Cell上进行累加
- 最终结果通过sum()合并计算
基准测试显示,在20个线程同时递增的情况下,LongAdder的吞吐量是AtomicLong的6倍。
6. 线程池最佳实践
6.1 参数配置黄金法则
根据我的经验,线程池参数应该这样设置:
- 核心线程数 = CPU核心数 * (1 + 等待时间/计算时间)
- 最大线程数 = 核心线程数 * 2
- 队列容量 = 最大线程数 * 10
- 拒绝策略 = 记录日志后由调用线程执行
典型的IO密集型配置示例:
java复制int poolSize = Runtime.getRuntime().availableProcessors() * 3;
ExecutorService executor = new ThreadPoolExecutor(
poolSize,
poolSize * 2,
60L, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000),
new CustomThreadFactory(),
new ThreadPoolExecutor.CallerRunsPolicy());
6.2 异常处理要点
线程池中的异常如果不捕获会导致线程提前终止。推荐的处理方式:
- 实现UncaughtExceptionHandler
- 对于Runnable任务使用try-catch包裹
- 对于Callable任务检查Future.get()抛出的异常
我曾经遇到一个线上问题:由于某个定时任务抛出NPE又没有捕获,导致整个调度线程退出,业务监控完全失效。
7. 死锁诊断与预防
7.1 死锁四要素
- 互斥条件:资源一次只能一个线程占用
- 占有且等待:持有资源的同时等待其他资源
- 不可抢占:资源只能由持有者释放
- 循环等待:多个线程形成环形等待链
诊断死锁的几种方法:
- jstack查看线程dump
- JConsole的线程检测功能
- VisualVM的死锁检测
7.2 破解死锁的实践方案
- 锁排序:按照固定顺序获取锁
- 锁超时:使用tryLock设置超时时间
- 开放调用:不在持有锁时调用外部方法
- 使用更高级的并发工具
一个典型的锁排序示例:
java复制void transfer(Account from, Account to, int amount) {
Account first = from.id < to.id ? from : to;
Account second = from.id < to.id ? to : from;
synchronized(first) {
synchronized(second) {
// 转账逻辑
}
}
}
8. 性能优化实战技巧
8.1 减少锁竞争的方法
- 缩小同步块范围:只锁必要的代码
- 降低锁粒度:如ConcurrentHashMap的分段锁
- 使用读写锁:ReadWriteLock适合读多写少场景
- 无锁算法:如CAS操作
我曾经优化过一个日志服务,将synchronized方法改为使用StampedLock的乐观读后,QPS从2000提升到了15000。
8.2 伪共享问题解决
CPU缓存系统中,当多个线程修改同一个缓存行中的不同变量时,会导致性能下降。解决方案:
- 填充缓存行(JDK8之前)
java复制class Value {
volatile long value;
long p1, p2, p3, p4, p5, p6; // 填充
}
- 使用@Contended注解(JDK8+)
java复制@Contended
class Counter {
volatile long count;
}
在基准测试中,解决伪共享后计数器性能提升了近3倍。