Java 并发知识点
1. 线程状态
- 新建(NEW):创建线程对象后进入此状态,无法获取 CPU 资源。
- 就绪(RUNNABLE):调用
start()
方法后进入,等待获取 CPU 资源以运行。 - 运行:获得 CPU 资源后执行代码。
- 阻塞:因某些事件触发,如锁阻塞、等待唤醒、超时等待、IO 操作等,阻塞解除后回到就绪状态。
- BLOCKED:获取同步锁失败,进入锁池等待。
- WAITING:调用
wait()
方法后等待,需notify()
或notifyAll()
唤醒。 - TIMED_WAITING:调用
wait(long)
或sleep(long)
方法,时间到或被唤醒后就绪。 - IO 阻塞:进行 IO 操作时让出 CPU 资源,操作结束后就绪。
- 消亡(TERMINATED):线程代码执行结束,被销毁。
2. 线程池
核心参数
- corePoolSize:核心线程数,闲时从阻塞队列取任务执行,不使用时不释放。
- maximumPoolSize:最大线程数,核心线程和队列满后,可创建非核心线程,但总数不超此值。
- keepAliveTime:非核心线程空闲时的存活时间。
- unit:存活时间的时间单位。
- workQueue:阻塞队列,核心线程满后,新任务存入队列。
- threadFactory:使用工厂模式创建线程对象。
- handler:拒绝策略,核心线程、队列和非核心线程都满时,处理新任务的策略。
拒绝策略
- AbortPolicy:抛出
RejectedExecutionException
异常。 - CallerRunsPolicy:提交任务的线程自行执行该任务。
- DiscardOldestPolicy:用新任务替换队列中等待最久的任务。
- DiscardPolicy:丢弃新提交的任务。
最大线程数设置
需综合考虑系统资源、任务性质等因素,一般可根据 CPU 核心数、IO 密集程度等进行估算。
3. 阻塞队列
- 当核心线程满时,新任务存入的队列,避免内存溢出,常选用有界队列。
- 常见阻塞队列:
ArrayBlockingQueue
、LinkedBlockingQueue
等。
4. wait()
和 sleep()
的区别
- 共同点:都使当前线程放弃 CPU 资源,进入阻塞状态。
- 归属不同:
sleep()
是Thread
类的静态方法,wait()
是Object
类的实例方法。 - 唤醒时机:
sleep(long)
和wait(long)
时间到自动苏醒,wait()
需notify()
或notifyAll()
唤醒。 - 锁持有情况:
sleep()
阻塞时持有锁,wait()
阻塞时释放锁。
5. 线程安全的集合类
- Vector:使用
Synchronized
锁实现。 - CopyOnWriteArrayList 和 CopyOnWriteArraySet:使用写入复制算法,用
lock
锁实现。 - ConcurrentHashMap:使用写入复制算法,用
lock
锁实现。
6. Lock
锁
- 比
Synchronized
更灵活,是对其的补充,需手动加锁和解锁。 - 实现类有
ReentrantLock
、ReentrantReadWriteLock
。
7. synchronized
和 Lock
的区别
- 实现方式:
synchronized
是 Java 内置关键字,由 JVM 控制;Lock
是接口,有丰富 API。 - 锁状态判断:
synchronized
无法判断,Lock
可以。 - 锁释放:
synchronized
自动释放,Lock
手动释放。 - 特性:两者都是可重入锁,
synchronized
不可中断、非公平,Lock
可判断锁、可设置公平或非公平。 - 适用场景:
synchronized
适合少量代码同步,Lock
适合大量同步代码。
8. Fail - Fast
- 概念:对线程不安全集合迭代时,若其他线程修改集合结构或元素数量,抛出
ConcurrentModificationException
。 - 原因:集合修改操作使
modCount
和expectedModCount
不一致,遍历检测到就抛异常。
9. 取消正在执行的线程
可通过设置标志位、调用 interrupt()
方法等方式。
10. ReadWriteLock
- 读写锁,分为读锁(共享锁)和写锁(独占锁)。
- 读锁允许多线程同时访问,提高读取效率;写锁只允许一个线程访问,保证数据修改安全。
11. 千万级计算任务处理
使用分支合并计算(ForkJoin),将大任务拆分成小任务并行计算,最后合并结果。可使用 ForkJoinPool
实现。
12. ForkJoinPool
- 特点是分叉合并和工作窃取,为每个线程分配独立队列,线程可从其他线程队列取任务执行。
- 与
ThreadPoolExecutor
的区别:都实现相关接口,可设置线程数;ForkJoinPool
有独立队列和工作窃取机制,适合处理大数据量任务。
13. 工作窃取
线程完成自己队列任务后,从其他未完成线程的队列尾部获取任务计算,减少竞争。
14. 双端队列
两端都可进行插入和获取操作的队列。
- 非线程安全:
LinkedList
、ArrayDeque
。 - 线程安全:
ConcurrentLinkedDeque
、LinkedBlockingDeque
。
15. 函数式接口
- 接口中只有一个抽象方法,可用
@FunctionalInterface
注解强制规定。 - 好处是简化编程模型,可结合
lambda
表达式使用。
16. 常用函数式接口
- Function<T, R>:函数型接口,有
R apply(T)
方法。 - Predicate:判定型接口,有
boolean test(T)
方法。 - Supplier:生产型接口,有
T get()
方法。 - Consumer:消费型接口,有
void accept(T)
方法。
17. 锁的分类
- 公平锁和非公平锁:公平锁按顺序获取锁,非公平锁支持抢占,
Synchronized
和ReentrantLock
是非公平锁。 - 独占锁和共享锁:独占锁一次只能被一个线程持有,如
Synchronized
等;共享锁可被多个线程持有,如ReentrantReadWriteLock.ReadLock
。 - 悲观锁和乐观锁:悲观锁读写数据都上锁,如
Synchronized
等;乐观锁读时不上锁,更新时判断数据是否被修改,Java 中常用 CAS 实现。
18. 读写锁
读锁共享,写锁独占,特点是读读不互斥、读写互斥、写写互斥。
19. 自旋锁
轻量级锁,线程未获得资源时自旋,减少资源消耗,自旋一定次数后进入阻塞状态,原子类使用自旋锁。
20. 偏向锁
认为多数情况下只有一个线程访问,出现竞争时升级为轻量级锁,JDK 1.6 后 Synchronized
和 Lock
底层使用。
21. CAS
- 比较并交换,保证变量原子操作,包含要修改的变量、预期值和新值。
- 存在 CPU 开销大、不能保证代码块原子性和 ABA 问题。
22. ABA 问题及解决
- 问题:CAS 操作中,值被修改后又改回原值,原线程认为值未变而继续操作。
- 解决方法:使用原子引用(如
AtomicStampedReference
)添加版本号判断。
23. Volatile
- 修饰属性,底层通过内存屏障实现。
- 作用是保证变量可见性和避免指令重排,但不能保证原子性,不能保证线程安全。
24. 指令重排
编译器或 CPU 为优化性能对指令重新排序,多线程中会带来可见性问题。
25. 内存屏障
- 限制编译器和处理器指令重排,保证可见性和有序性。
- 写屏障保证共享变量改动同步到主存,读屏障保证读取主存最新数据。
26. ThreadLocal
- 为线程绑定变量,解决多线程访问共享变量问题,各线程变量隔离。
- 实现原理是每个线程有
ThreadLocalMap
,将ThreadLocal
对象作为 key,变量作为 value 存储。 - 可能出现内存泄漏和脏数据问题,使用后需调用
remove()
方法。
27. 并发、并行、串行
- 串行:任务按顺序依次执行。
- 并行:同一时刻多个任务同时执行,需多 CPU 或单 CPU 多核心支持。
- 并发:多个任务同时执行,但同一时刻只有一个任务执行,任务抢占 CPU 资源。
28. 对象的组成
- 对象头:包含
Mark Word
(存储运行时数据)、Klass Word
(指向类地址)和数组长度(数组对象有)。 - 实例数据:对象的属性和值。
- 对齐填充字节:使对象占用内存为 8 字节倍数。
29. 上下文切换
多线程编程中,线程时间片用完后保存状态,让给其他线程,下次切换回来再加载状态的过程,消耗 CPU 时间。
30. JVM 加锁过程
- 无锁:无线程访问。
- 偏向锁:一个线程加锁,无竞争,开启则写入线程 ID,未开启则进入轻量级锁。
- 轻量级锁:多个线程竞争,偏向锁升级,用 CAS 维护原子性,竞争严重时膨胀为重量级锁。
- 重量级锁:直接加锁,未获资源线程进入阻塞状态。