ARTICLE DETAIL

资讯详情

深耕编程入门与网站建设的一线实战洞察。

AI全栈实战 | 1.7-02 并发基石:为什么禁止用 Executors 创建线程池?锁升级全过程拆解

AI全栈实战 | 1.7-02 并发基石:为什么禁止用 Executors 创建线程池?锁升级全过程拆解 上篇回顾1.7-01 把 JVM 内存模型、GC 算法、收集器演进、排查工具链一次讲透。本篇进入并发编程——这是 Java 高阶最硬核的部分也是线上事故的高发区。并发 bug 的特点是「本地测不出来、上线偶发出现、复现极难」根因往往是对 JMM、锁机制、线程池理解不到位。一、开篇一次诡异的线上死锁某团队线上服务每隔几小时就卡死重启后恢复。日志没有异常线程全部正常。运维用jstackdump 线程栈一看transfer-thread-1 prio5 BLOCKED - waiting to lock 0x000000076b4a0c80 - locked 0x000000076b4a0c90 transfer-thread-2 prio5 BLOCKED - waiting to lock 0x000000076b4a0c90 - locked 0x000000076b4a0c80经典死锁线程 1 持有锁 A 等锁 B线程 2 持有锁 B 等锁 A。根因是两个方法以相反顺序加锁——转账方法 A 先锁 from 再锁 to方法 B 先锁 to 再锁 from。修复方案统一锁顺序按 accountId 排序小的先锁。但这个 bug 在本地跑了 10 万次都没出现——并发问题的可怕在于概率性。本篇把 JMM、锁机制、线程池三大基石讲透让你写出不翻车的并发代码。二、JMMJava 内存模型2.1 为什么需要 JMMCPU 有多级缓存每个核心看到的变量值可能不一致。Java 为了跨平台统一行为定义了 JMM——规定线程何时能看到其他线程的修改。线程 1 线程 2 ↓ ↓ 工作内存 工作内存 ↕ ↕ └───── 主内存 ───────┘JMM 规定线程不能直接操作主内存必须先读到工作内存修改后写回。2.2 三大特性特性含义保证手段原子性操作不可分割synchronized / Lock / AtomicInteger可见性修改后其他线程立即可见volatile / synchronized / final有序性防止指令重排volatile / happens-before2.3 volatile 的语义边界volatile 做两件事可见性写后立刻刷主内存读前从主内存刷新2禁止指令重排插入内存屏障volatile不保证原子性。i即使 i 是 volatile 也不是线程安全的——i是「读加写」三步中间可能被打断。// volatile 的正确用法状态标志privatevolatilebooleanrunningtrue;publicvoidstop(){runningfalse;}publicvoidrun(){while(running){// 工作循环}}2.4 happens-before 规则happens-before 是 JMM 的核心——如果 A happens-before B那么 A 的操作对 B 可见。规则说明程序顺序同一线程内前操作 happens-before 后操作锁规则unlock happens-before 后续 lockvolatile 规则volatile 写 happens-before 后续 volatile 读线程启动start() happens-before 线程内任意操作线程终止线程内操作 happens-before 其他线程检测到终止传递性A→B 且 B→C 则 A→C三、synchronized 锁升级全过程JDK 6 后 synchronized 不再是「重量级锁」一棍子打死而是根据竞争情况自动升级。无锁 → 偏向锁 → 轻量级锁 → 重量级锁3.1 偏向锁第一个线程访问时对象头记录该线程 ID。下次该线程进入不需要 CAS 操作直接进入。适合单线程反复进入同步块的场景。3.2 轻量级锁第二个线程来竞争时偏向锁撤销升级为轻量级锁。用 CAS 操作把对象头指向线程栈中的锁记录。适合交替执行、无真实竞争的场景。3.3 重量级锁CAS 自旋超过阈值默认 10 次仍没拿到锁升级为重量级锁——进入 monitor线程挂起等待。适合竞争激烈的场景。3.4 锁升级不可逆一旦升级到重量级锁就不会降级。所以不要在低竞争场景用 synchronized 反复触发升级。3.5 synchronized vs ReentrantLock维度synchronizedReentrantLock实现JVM 内置java.util.concurrent 类锁释放自动手动 unlock必须 finally可中断不可lockInterruptibly()超时不可tryLock(timeout)公平锁非公平可配公平/非公平条件变量1 个wait/notify多个 Condition推荐简单场景用 synchronized语法简洁、不会忘记 unlock复杂场景超时、中断、多条件用 ReentrantLock。四、AQSJUC 的核心框架AbstractQueuedSynchronizer 是 ReentrantLock、CountDownLatch、Semaphore、CyclicBarrier 的底层框架。4.1 核心思想1. state 变量表示同步状态volatile int 2. CLH 队列管理等待线程 3. 模板方法模式子类实现 tryAcquire/tryRelease4.2 CLH 队列head → [线程1] ⇄ [线程2] ⇄ [线程3] ← tail ↑ 已获取锁 ↑ 自旋等待 ↑ 自旋等待等待线程在队列里自旋轻量级锁阶段或挂起重量级锁阶段前驱节点释放后唤醒后继。4.3 ReentrantLock 的 tryAcquire// 简化版finalbooleannonfairTryAcquire(intacquires){finalThreadcurrentThread.currentThread();intcgetState();if(c0){// 无锁状态CAS 抢锁if(compareAndSetState(0,acquires)){setExclusiveOwnerThread(current);returntrue;}}elseif(currentgetExclusiveOwnerThread()){// 重入state 加 1setState(cacquires);returntrue;}returnfalse;}五、CAS 与 ABA 问题5.1 CAS 原理Compare And Swap比较当前值是否等于预期值相等则更新为新值。是乐观锁的核心。AtomicIntegerainewAtomicInteger(0);ai.compareAndSet(0,1);// 预期0更新为1 → trueai.compareAndSet(0,2);// 预期0但已是1 → false5.2 ABA 问题线程1 读到值 A 线程2 把 A 改成 B又改回 A 线程1 CAS(A→C) 成功值虽然还是 A但中间已经被改过。对纯数值无影响对引用类型可能有问题对象字段被改了但引用没变。5.3 解决方案AtomicStampedReference给值加版本号CAS 时同时比较值和版本号。AtomicStampedReferenceIntegerrefnewAtomicStampedReference(100,0);// 初始值100版本0int[]stampnewint[1];Integervalref.get(stamp);// val100, stamp[0]0ref.compareAndSet(100,200,0,1);// 预期值100版本0 → 更新为200版本1六、线程池为什么阿里禁止 Executors6.1 Executors 的隐患// 阿里规约禁止这两种Executors.newFixedThreadPool(10);// 队列 LinkedBlockingQueue 无上限 → OOMExecutors.newCachedThreadPool();// 线程数 Integer.MAX_VALUE → OOMnewFixedThreadPool 的队列是LinkedBlockingQueue无界任务堆积导致 OOM。newCachedThreadPool 的最大线程数是Integer.MAX_VALUE创建过多线程导致 OOM。6.2 正确做法手动配置 ThreadPoolExecutorThreadPoolExecutorexecutornewThreadPoolExecutor(corePoolSize,// 核心线程数maxPoolSize,// 最大线程数keepAliveTime,// 空闲存活时间TimeUnit.SECONDS,newArrayBlockingQueue(1000),// 有界队列newThreadFactoryBuilder().setNameFormat(biz-pool-%d).build(),// 命名便于排查newThreadPoolExecutor.CallerRunsPolicy()// 拒绝策略);6.3 七参数详解参数说明corePoolSize常驻线程数maxPoolSize队列满后扩到的最大线程数keepAliveTime非核心线程空闲存活时间unit时间单位workQueue任务队列threadFactory线程工厂命名很重要handler拒绝策略6.4 线程数设置公式CPU 密集型计算为主线程数 CPU 核数 1IO 密集型网络/DB/文件为主线程数 CPU 核数 * (1 IO等待时间/CPU时间)经验值IO 密集型通常 2~10 倍 CPU 核数。具体靠压测调。6.5 四种拒绝策略策略行为适用场景AbortPolicy抛 RejectedExecutionException默认要求严格CallerRunsPolicy由提交线程执行不丢任务、降级DiscardPolicy静默丢弃可容忍丢任务DiscardOldestPolicy丢最早未处理任务最新优先生产推荐CallerRunsPolicy——不丢任务用提交线程做背压。七、CompletableFuture现代异步编程CompletableFutureOrderfutureCompletableFuture.supplyAsync(()-userService.getById(userId),executor).thenApply(user-{OrderordernewOrder(user);returnorder;}).thenCombine(CompletableFuture.supplyAsync(()-couponService.get(userId),executor),(order,coupon)-{order.setCoupon(coupon);returnorder;}).exceptionally(ex-{log.error(创建订单失败,ex);returnOrder.failed(ex.getMessage());});Orderorderfuture.get(3,TimeUnit.SECONDS);优势链式编排、超时控制、异常处理、多任务组合。比 Future 灵活得多。八、小结表模块关键点易踩坑JMM可见性/原子性/有序性volatile 不保证原子性synchronized锁升级不可逆低竞争反复触发升级AQSstate CLH 队列自旋过多浪费 CPUCAS乐观锁ABA 问题线程池手动配七参数Executors 无界队列 OOMCompletableFuture异步编排必须传自定义线程池
返回列表