Java多线程基础核心考点
在Java开发领域,多线程是衡量开发者并发编程能力的核心指标。无论是银行系统的高并发交易,还是电商平台的秒杀场景,多线程技术都是不可或缺的基石。本站整理的Java多线程面试50题,并非简单的知识点罗列,而是基于真实面试场景,从基础概念到高阶应用的全方位解析。
① 进程与线程的区别
进程是资源分配的最小单位,而线程是CPU调度的最小单位。每个进程拥有独立的内存空间,而同一进程内的线程共享堆和方法区,但拥有独立的栈空间。理解这一区别是掌握Java多线程的前提。
② 线程的生命周期
Java线程有六种状态:NEW(新建)、RUNNABLE(运行)、BLOCKED(阻塞)、WAITING(等待)、TIMED_WAITING(超时等待)、TERMINATED(终止)。面试中常考状态转换的条件,如wait()进入等待,notify()唤醒等。
③ start()与run()的区别
调用start()方法会启动新线程并执行run()方法,而直接调用run()方法只是普通的方法调用,不会启动新线程。这是多线程面试中最基础也最容易混淆的考点之一。
线程创建的四种方式
在Java中,创建线程主要有以下几种方式,每种方式都有其适用场景:
- 继承Thread类:简单直接,但Java单继承限制使其灵活性较差。
- 实现Runnable接口:推荐方式,避免了单继承限制,且便于共享资源。
- 实现Callable接口:支持返回值和异常抛出,需配合FutureTask使用。
- 线程池创建:通过ExecutorService创建,复用线程,降低资源消耗,是生产环境首选。
// 示例:实现Runnable接口创建线程
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程执行中...");
}
public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start(); // 启动线程
}
}
JUC包深入解析
java.util.concurrent(JUC)包是Java并发编程的核心,提供了丰富的并发工具类。在Java多线程面试50题中,JUC相关知识点占据了相当大的比重,尤其是ReentrantLock、ReadWriteLock、Semaphore等类的使用场景和底层原理。
ReentrantLock详解
ReentrantLock是可重入锁,实现了Lock接口。与synchronized相比,它提供了更灵活的锁机制,如尝试获取锁、定时获取锁、公平锁等。其底层基于AQS(AbstractQueuedSynchronizer)实现。
- 可重入性:同一个线程可以多次获取同一把锁,不会发生死锁。
- 公平锁与非公平锁:公平锁按等待时间分配,非公平锁可能插队,性能更高。
- 条件变量:通过newCondition()创建,实现线程间的精确通知。
ReadWriteLock读写锁
ReadWriteLock维护了一对锁,一个读锁和一个写锁。读锁是共享锁,写锁是独占锁。在读多写少的场景下,读写锁能显著提升性能。
常见实现类:ReentrantReadWriteLock。面试中常问:读锁和写锁的兼容性?答:读-读不互斥,读-写互斥,写-写互斥。
Semaphore信号量
Semaphore用于控制同时访问特定资源的线程数量。常用于限流、资源池管理等场景。例如,限制数据库连接池的最大连接数。
Semaphore semaphore = new Semaphore(5); // 允许5个线程同时访问
semaphore.acquire(); // 获取许可
try {
// 访问资源
} finally {
semaphore.release(); // 释放许可
}
synchronized锁机制深度剖析
synchronized是Java内置的同步关键字,其底层实现基于JVM的monitor对象。在Java多线程面试50题中,synchronized的锁升级过程、性能优化及与ReentrantLock的区别是高频考点。
锁升级过程
JDK 1.6之后,synchronized进行了大量优化,引入了锁升级机制:
① 无锁状态
对象刚创建时,处于无锁状态,Mark Word中存储对象的哈希码、分代年龄等信息。
② 偏向锁
当第一个线程获取锁时,对象Mark Word中记录线程ID,后续该线程再次获取锁无需同步。
③ 轻量级锁
当有其他线程尝试获取锁时,偏向锁升级为轻量级锁,通过CAS操作竞争锁,自旋等待。
④ 重量级锁
当竞争激烈时,轻量级锁升级为重量级锁,线程阻塞,等待OS调度,性能开销较大。
synchronized与ReentrantLock的区别
| 特性 | synchronized | ReentrantLock |
|---|---|---|
| 实现层面 | JVM层面 | JDK层面(AQS) |
| 锁释放 | 自动释放 | 需手动释放(finally中) |
| 公平锁 | 非公平锁 | 可配置公平/非公平 |
| 条件变量 | wait/notify | Condition接口 |
| 中断响应 | 不支持 | 支持 |
线程池核心参数与最佳实践
线程池是Java多线程面试50题中的重中之重。合理配置线程池能显著提升系统性能和稳定性。ThreadPoolExecutor是核心类,其构造函数包含7个关键参数。
线程池七大参数
- corePoolSize:核心线程数,线程池中长期存活的线程数量。
- maximumPoolSize:最大线程数,线程池允许创建的最大线程数。
- keepAliveTime:空闲线程存活时间,非核心线程空闲超过此时间将被回收。
- unit:keepAliveTime的时间单位。
- workQueue:任务队列,存放等待执行的任务。
- threadFactory:线程工厂,用于创建新线程,可自定义线程名等。
- handler:拒绝策略,当队列满且线程数达最大值时,如何处理新任务。
常见拒绝策略
- AbortPolicy:默认策略,直接抛出RejectedExecutionException异常。
- CallerRunsPolicy:由调用线程处理任务,降低提交速度。
- DiscardPolicy:直接丢弃任务,不抛异常。
- DiscardOldestPolicy:丢弃队列中最老的任务,尝试重新提交。
线程池配置建议
线程池大小的配置需根据任务类型决定:
- CPU密集型任务:核心线程数 = CPU核数 + 1,减少上下文切换。
- I/O密集型任务:核心线程数 = CPU核数 2 或 CPU核数 / (1 - 阻塞系数),充分利用I/O等待时间。
- 混合型任务:可根据I/O耗时将任务拆分为CPU和I/O两部分,分别配置线程池。
volatile关键字与内存模型
volatile是Java虚拟机提供的轻量级同步机制,在Java多线程面试50题中,volatile的可见性和有序性保证是核心考点。
volatile的三大特性
① 可见性
当一个线程修改了volatile变量的值,新值对其他线程立即可见。实现原理是通过内存屏障和缓存一致性协议(MESI)。
② 有序性
volatile禁止指令重排序,保证代码执行顺序与代码书写顺序一致。通过插入内存屏障实现。
③ 不保证原子性
volatile不保证复合操作的原子性,如i++。需结合synchronized或AtomicInteger使用。
内存屏障
volatile通过插入内存屏障禁止指令重排序:
- LoadLoad屏障:确保所有Load操作在LoadLoad屏障之前完成。
- StoreStore屏障:确保所有Store操作在StoreStore屏障之前完成。
- LoadStore屏障:确保所有Load操作在LoadStore屏障之前完成。
- StoreLoad屏障:确保所有Store操作在StoreLoad屏障之前完成,并触发其他处理器缓存刷新。
死锁的产生与排查
死锁是多线程开发中的常见陷阱,在Java多线程面试50题中,死锁的产生条件、预防和排查方法是必考内容。
死锁产生的四个必要条件
- 互斥条件:资源一次只能被一个线程占用。
- 占有并等待:线程持有资源的同时等待其他资源。
- 不可抢占:资源只能由持有者主动释放。
- 循环等待:线程形成头尾相接的循环等待资源关系。
死锁排查工具
- jstack:JDK自带工具,可生成线程dump,分析线程状态和锁持有情况。
- JVisualVM:图形化工具,实时监控线程和内存使用情况。
- Arthas:阿里巴巴开源的Java诊断工具,支持动态追踪线程和锁信息。
预防死锁的策略
- 破坏占有并等待:一次性申请所有资源。
- 破坏不可抢占:允许线程主动释放已持有资源。
- 破坏循环等待:按顺序申请资源,避免循环。
- 超时机制:设置锁获取超时,超时后释放资源并重试。