← 返回因果链

🔗 Java 并发因果链

因为…所以… — 从串行到多线程到异步的完整因果推导

线程安全synchronizedvolatile Lock线程池CompletableFuture ThreadLocalJMM

📊 因果链总览图

❓ 串行执行
效率低
💡 多线程 ⚡ 共享变量
不可见
💡 volatile
happens-before
⚡ 竞态条件
数据错乱
💡 synchronized ⚡ 性能瓶颈
锁太粗
💡 Lock框架
读写锁/StampedLock
⚡ 线程创建
销毁开销
💡 线程池 ⚡ 阻塞等待
效率低
💡 CompletableFuture
⚡ 线程间
变量隔离
💡 ThreadLocal ⚡ 内存泄漏 💡 remove()

① 因为:串行执行效率低 → 所以多线程

❓ 问题:一件一件事做,CPU利用率低
// ❌ 串行:3个任务各1秒 = 总共3秒 public void serialExecute() { queryFromDB(); // 1秒 callRemoteAPI(); // 1秒 sendEmail(); // 1秒 // 总耗时:3秒 }
💡 所以:多线程并发执行
// ✅ 并行:3个任务同时执行 = 总共1秒 @Autowired private ExecutorService executor; public void parallelExecute() { CompletableFuture.runAsync(() -> queryFromDB(), executor); CompletableFuture.runAsync(() -> callRemoteAPI(), executor); CompletableFuture.runAsync(() -> sendEmail(), executor); // 总耗时:1秒(最慢的那个) }
⚡ 但是:多线程共享变量 → 线程安全问题
// ❌ 两个线程同时 count++,结果不对 private int count = 0; public void increment() { count++; // 不是原子操作!= 读取 → +1 → 写回 } // Thread1: 读取count=0 → +1 → 写回1 // Thread2: 读取count=0 → +1 → 写回1 ← 应该是2!

② 因为:共享变量不可见 → 所以volatile

❓ 问题:一个线程改了变量,另一个线程看不到

每个线程有自己的工作内存(CPU缓存),不一定及时写回主内存。另一个线程读的还是旧值。

线程1 工作内存
flag=true
还没写回↓ 主内存
flag=false
↑读到旧值 线程2 工作内存
flag=false
// ❌ 没有volatile:线程2可能永远看不到stop=true private boolean stop = false; // 普通变量 public void thread1() { while (!stop) { // 可能永远循环! doSomething(); } } public void thread2() { stop = true; // 线程1可能看不到这个修改 }
💡 所以:volatile——强制读写主内存 + 禁止指令重排
// ✅ volatile:保证可见性 private volatile boolean stop = false; // volatile 做了两件事: // 1. 写操作:立刻刷新到主内存(不是只写工作内存) // 2. 读操作:每次都从主内存读(不用工作内存的缓存) // 3. 禁止指令重排序(Memory Barrier) // ⚠️ volatile 不保证原子性! private volatile int count = 0; public void increment() { count++; // ❌ 还是不安全!count++不是原子操作 } // volatile只保证"看到最新值",不保证"没人同时改"
⚡ 但是:volatile只保证可见性,不保证原子性 → 竞态条件还在

两个线程同时count++,虽然都能读到最新值,但++操作本身不是原子的(读→加→写三步),还是会出错 → 引出synchronized

③ 因为:竞态条件 → 所以synchronized

❓ 问题:多线程同时修改共享变量 → 数据错乱
💡 所以:synchronized——同一时刻只有一个线程能执行
// 三种用法 // 1. 同步方法(锁的是this对象) public synchronized void increment() { count++; // ✅ 安全 } // 2. 同步静态方法(锁的是Class对象) public static synchronized void staticIncrement() { ... } // 3. 同步代码块(锁指定对象,粒度更细) private final Object lock = new Object(); public void increment() { synchronized (lock) { count++; } } // 底层原理: // - 同步代码块:monitorenter / monitorexit 字节码指令 // - 同步方法:ACC_SYNCHRONIZED 标志位 // - 锁升级:无锁 → 偏向锁 → 轻量级锁(CAS) → 重量级锁(OS互斥)
⚡ 但是:synchronized性能瓶颈

1. 锁太粗:整个方法加锁,其实只需要锁一小段
2. 不可中断:拿到锁的线程不释放,其他线程只能死等
3. 不可公平:新来的线程可能抢到锁,等最久的线程反而拿不到
4. 读写不分:读操作不修改变量,完全可以并行读,但synchronized不分读写

④ 因为:synchronized不够灵活 → 所以Lock框架

💡 所以:ReentrantLock——因为需要更灵活的锁控制
// ReentrantLock 比 synchronized 多了什么? ReentrantLock lock = new ReentrantLock(true); // true=公平锁 try { if (lock.tryLock(5, TimeUnit.SECONDS)) { // ✅ 可超时 try { // 业务逻辑 } finally { lock.unlock(); // ✅ 必须手动释放 } } else { log.info("5秒内没拿到锁,走降级逻辑"); // ✅ 可中断 } } catch (InterruptedException e) { Thread.currentThread().interrupt(); } // 因为读操作可以并行 → 所以ReadWriteLock ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock(); rwLock.readLock().lock(); // 多个线程可同时读 rwLock.writeLock().lock(); // 写独占 // 因为读写锁写锁会阻塞所有读 → 所以StampedLock(乐观读) StampedLock stampedLock = new StampedLock(); long stamp = stampedLock.tryOptimisticRead(); // 乐观读,不加锁! // 读取数据... if (!stampedLock.validate(stamp)) { // 检查期间有没有写 stamp = stampedLock.readLock(); // 有人写了,升级为悲观读锁 try { // 重新读 } finally { stampedLock.unlockRead(stamp); } }
类比:synchronized vs Lock vs StampedLock
synchronized = 公共厕所(谁先进谁用,不能预约,不能加塞)
ReentrantLock = 银行取号(可以公平取号,可以放弃等,可以限时)
ReadWriteLock = 图书馆(可以很多人同时看书,但有人改书时别人不能看)
StampedLock = 图书馆+版本号(借书时记个版本号,还书时看有没有人改过,没有就不用加锁)

⑤ 因为:线程创建销毁开销大 → 所以线程池

❓ 问题:每次new Thread() 都要创建线程+分配栈内存+OS调度

创建一个线程约1ms,如果每个请求都new一个线程,1000个请求 = 1秒只花在线程创建上。

💡 所以:线程池——提前创建好线程,重复使用
// 线程池核心参数(7个) ThreadPoolExecutor executor = new ThreadPoolExecutor( 10, // corePoolSize: 核心线程数(常驻) 50, // maximumPoolSize: 最大线程数 60, TimeUnit.SECONDS, // keepAliveTime: 非核心线程空闲存活时间 TimeUnit.SECONDS, // 存活时间单位 new LinkedBlockingQueue<>(200), // workQueue: 任务队列 new ThreadFactory() { // threadFactory: 线程工厂 private final AtomicInteger count = new AtomicInteger(1); @Override public Thread newThread(Runnable r) { return new Thread(r, "biz-thread-" + count.getAndIncrement()); } }, new ThreadPoolExecutor.CallerRunsPolicy() // rejectedHandler: 拒绝策略 ); // 任务提交流程(因果链): // 提交任务 → 核心线程没满?→ 创建核心线程执行 // ↓ 已满 // 队列没满?→ 放入队列等待 // ↓ 已满 // 最大线程没满?→ 创建非核心线程执行 // ↓ 已满 // 执行拒绝策略!

因为队列满了才创建非核心线程 → 所以拒绝策略怎么选?

拒绝策略行为因为…所以选
AbortPolicy抛异常因为要感知到任务丢了 → 所以用默认策略
CallerRunsPolicy调用者线程自己跑因为不想丢任务 → 所以让提交者自己跑,顺便限流
DiscardPolicy默默丢弃因为日志等可丢 → 所以直接扔
DiscardOldestPolicy丢弃最老的任务因为新任务更重要 → 所以扔老的

⑥ 因为:同步阻塞效率低 → 所以CompletableFuture

❓ 问题:Future.get()会阻塞当前线程
// ❌ Future:阻塞等待结果 Future<String> future1 = executor.submit(() -> queryUser()); Future<String> future2 = executor.submit(() -> queryOrder()); String user = future1.get(); // 阻塞!等1秒 String order = future2.get(); // 又阻塞!等1秒 // 即使两个任务并行跑,get()也要按顺序等
💡 所以:CompletableFuture——异步编排,不阻塞
// ✅ CompletableFuture:异步执行,回调处理 CompletableFuture<String> userFuture = CompletableFuture .supplyAsync(() -> queryUser(), executor); CompletableFuture<String> orderFuture = CompletableFuture .supplyAsync(() -> queryOrder(), executor); // 两个都完成后合并 CompletableFuture<String> result = userFuture .thenCombine(orderFuture, (user, order) -> user + " | " + order); // 异常处理 CompletableFuture<String> safeResult = result .exceptionally(ex -> "降级数据"); // 出错时返回默认值 // 典型场景:外卖下单 // 因为下单涉及多个独立操作 → 所以用CF并行执行 CompletableFuture.allOf( CompletableFuture.runAsync(() -> createOrder(), executor), CompletableFuture.runAsync(() -> notifyRestaurant(), executor), CompletableFuture.runAsync(() -> assignRider(), executor) ).join(); // 三个操作并行,1秒内完成
类比:CompletableFuture = 外卖配送链
Future = 点了外卖,然后站在门口等,什么都不干
CompletableFuture = 点了外卖,去干别的事,骑手到了打电话通知你(回调)
thenCombine = 外卖和饮料分别配送,两个都到了才一起吃
exceptionally = 饮料没了,换一瓶默认的

⑦ 因为:线程间变量隔离 → 所以ThreadLocal

❓ 问题:每个线程需要自己的独立变量副本

比如:每个请求需要独立的UserContext、数据库连接、SimpleDateFormat(不是线程安全的)

💡 所以:ThreadLocal——每个线程自己的变量盒子
// ThreadLocal 典型场景:用户上下文 public class UserContext { private static final ThreadLocal<User> currentUser = new ThreadLocal<>(); public static void set(User user) { currentUser.set(user); } public static User get() { return currentUser.get(); } public static void clear() { currentUser.remove(); // ⚠️ 必须清理! } } // 拦截器中设置 @Component public class AuthInterceptor implements HandlerInterceptor { @Override public boolean preHandle(...) { User user = parseToken(request); UserContext.set(user); return true; } @Override public void afterCompletion(...) { UserContext.clear(); // ⚠️ 必须清理!否则内存泄漏 } }
⚡ 但是:ThreadLocal内存泄漏

因果链:因为ThreadLocal存在ThreadLocalMap中 → Map的Key是弱引用 → GC回收Key后Value还在 → 但Key=null → Value永远无法访问 → 内存泄漏

Thread ThreadLocalMap Key=null(被GC)
Value=大对象(还在!)
⚠️ 线程池 + ThreadLocal = 危险!
线程池的线程会复用 → 上一个请求的ThreadLocal数据还在 → 下一个请求拿到了错误的用户信息!
必须在finally中remove()清理!

🎯 面试点映射:因果链视角

面试题因果链
volatile原理因为工作内存和主内存不一致 → 所以volatile强制读写主内存 + 禁止重排序
synchronized原理因为竞态条件 → 所以需要互斥 → monitorenter/monitorexit + 锁升级
锁升级过程因为大部分锁其实没竞争 → 所以偏向锁(标记线程ID) → 有竞争CAS升级轻量级锁 → 竞争激烈升级重量级锁
ReentrantLock vs synchronized因为synchronized不可中断/不可公平/不可读写分离 → 所以Lock框架提供更多控制
线程池参数因为线程创建销毁开销大 → 所以线程池复用 → 因为资源有限 → 所以需要核心数/最大数/队列/拒绝策略
CompletableFuture因为Future.get()阻塞 → 所以CF异步编排 + 回调
ThreadLocal内存泄漏因为Key弱引用被GC → Value强引用还在 → 所以必须remove()
happens-before因为多线程执行顺序不可预测 → 所以JMM定义happens-before规则 → 保证特定顺序的可见性
AQS原理因为Lock需要排队等锁 → 所以AQS用CLH队列 + state变量 + CAS实现公平/非公平锁

📌 因果链总结

串行
效率低
多线程 变量
不可见
volatile 竞态
条件
synchronized 性能
瓶颈
Lock
读写锁
创建
开销
线程池 阻塞
等待
CF异步 变量
隔离
ThreadLocal
全栈资料库 · 因果链板块 · Java并发因果链