因为…所以… — 从串行到多线程到异步的完整因果推导
每个线程有自己的工作内存(CPU缓存),不一定及时写回主内存。另一个线程读的还是旧值。
两个线程同时count++,虽然都能读到最新值,但++操作本身不是原子的(读→加→写三步),还是会出错 → 引出synchronized
1. 锁太粗:整个方法加锁,其实只需要锁一小段
2. 不可中断:拿到锁的线程不释放,其他线程只能死等
3. 不可公平:新来的线程可能抢到锁,等最久的线程反而拿不到
4. 读写不分:读操作不修改变量,完全可以并行读,但synchronized不分读写
创建一个线程约1ms,如果每个请求都new一个线程,1000个请求 = 1秒只花在线程创建上。
因为队列满了才创建非核心线程 → 所以拒绝策略怎么选?
| 拒绝策略 | 行为 | 因为…所以选 |
|---|---|---|
| AbortPolicy | 抛异常 | 因为要感知到任务丢了 → 所以用默认策略 |
| CallerRunsPolicy | 调用者线程自己跑 | 因为不想丢任务 → 所以让提交者自己跑,顺便限流 |
| DiscardPolicy | 默默丢弃 | 因为日志等可丢 → 所以直接扔 |
| DiscardOldestPolicy | 丢弃最老的任务 | 因为新任务更重要 → 所以扔老的 |
比如:每个请求需要独立的UserContext、数据库连接、SimpleDateFormat(不是线程安全的)
因果链:因为ThreadLocal存在ThreadLocalMap中 → Map的Key是弱引用 → GC回收Key后Value还在 → 但Key=null → Value永远无法访问 → 内存泄漏
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实现公平/非公平锁 |