02 并发与线程池
这一块在面试里是干什么的
主要判断你:
- 会不会写并发代码
- 是否知道线程安全风险
- 出问题时会不会分析死锁、竞争、性能瓶颈
进程和线程
- 进程:资源分配的基本单位
- 线程:CPU 调度的基本单位
一句话:
“一个进程里可以有多个线程,线程共享进程资源。”
并发和并行
- 并发:看起来同时执行,宏观同时
- 并行:真正同时执行,微观同时
线程创建方式
常见说法:
- 继承
Thread - 实现
Runnable - 实现
Callable - 线程池
面试里推荐强调:
“实际开发优先使用线程池,而不是手动频繁创建线程。”
为什么要用线程池
它解决的问题:
- 减少线程创建销毁开销
- 控制并发数量
- 避免系统被打爆
- 统一管理线程生命周期
线程池核心参数
corePoolSize:核心线程数maximumPoolSize:最大线程数keepAliveTime:非核心线程空闲存活时间workQueue:任务队列RejectedExecutionHandler:拒绝策略
高频面试题:
线程池工作流程
- 先看核心线程是否满
- 没满就创建核心线程执行
- 满了就进队列
- 队列满了再尝试创建非核心线程
- 还满就触发拒绝策略
常见拒绝策略
- 直接抛异常
- 调用者自己执行
- 丢弃任务
- 丢弃最旧任务
什么是线程安全
多个线程同时访问同一份数据,结果仍然正确。
synchronized
最基础的线程同步手段。
它的作用:
- 保证同一时间只有一个线程进入临界区
- 保证可见性
- 保证有序性的一部分
volatile
它保证:
- 可见性
- 一定程度上的有序性
它不保证:
- 原子性
高频快答:
“volatile 适合一个线程写、多个线程读的状态标记,不适合 i++ 这种复合操作。”
什么是原子性
一个操作要么全部完成,要么全部不完成,中间不会被打断。
什么是可见性
一个线程修改变量后,其他线程能立刻看到最新值。
什么是有序性
程序执行顺序在单线程看起来正常,但多线程里可能因为指令重排出现问题。
Lock 和 synchronized 区别
先记简单版:
synchronized:关键字,使用简单,自动释放锁Lock:接口,更灵活,可手动加锁解锁,支持更多特性
ReentrantLock
可重入锁。
可重入的意思:
“同一个线程拿到锁后,可以再次进入同一把锁保护的代码。”
什么是死锁
两个或多个线程互相等待对方释放资源,导致都无法继续。
死锁四个条件先知道名字:
- 互斥
- 请求并持有
- 不可剥夺
- 循环等待
如何避免死锁
- 固定加锁顺序
- 缩小锁范围
- 尽量避免嵌套锁
- 使用超时机制
ThreadLocal
不是用来解决共享,而是给每个线程一份自己的变量副本。
常见场景:
- 用户上下文
- traceId
- 数据库连接上下文
常见坑:
- 线程池复用线程,如果不清理可能脏数据泄漏
sleep() 和 wait()
sleep():Thread的方法,不释放锁wait():Object的方法,释放锁,需要配合synchronized
Runnable 和 Callable
Runnable:没有返回值Callable:有返回值,可以抛异常
并发容器先会说几个
ConcurrentHashMapCopyOnWriteArrayListBlockingQueue
一句话:
“并发容器适合多线程场景,比自己手写锁更安全省事。”
高频快答
为什么不建议直接 new 线程池
因为默认参数可能不合理,容易导致资源耗尽。实际开发更建议显式指定核心参数。
ConcurrentHashMap 为什么比 HashMap 更适合并发
因为它是线程安全的,并发控制更细,性能更好。
i++ 为什么不是线程安全
因为它不是一个原子操作,包含读、改、写三个步骤。
这一章最小记忆包
- 生产代码优先线程池
- 线程池重点记 5 个参数和工作流程
synchronized保证同步,volatile不保证原子性- 线程安全核心是原子性、可见性、有序性
ThreadLocal是线程私有,不是共享- 死锁要会定义、原因、规避方式