《JAVA八股真解》三、线程与锁
#JAVA##JAVA面经##JAVA内推#
1. 线程的状态
Java中的线程生命周期由Thread类的getState()方法返回,共有六种状态:
| 状态 | 说明 |
|---|---|
| NEW(新建) | 线程刚被创建但尚未启动,处于初始状态。 |
| RUNNABLE(运行中) | 线程已启动并正在执行或准备执行任务。此状态包括“就绪”和“运行”两个阶段,取决于是否获取到CPU时间片。 |
| BLOCKED(阻塞) | 线程因等待锁而暂停执行,例如在synchronized代码块中无法获得锁时进入该状态。 |
| WAITING(等待) | 线程主动调用wait()、join()或LockSupport.park()等方法后进入无限期等待状态,直到被其他线程唤醒。 |
| TIMED_WAITING(计时等待) | 类似于WAITING,但等待时间有限,如调用sleep(long)、wait(long)、join(long)等方法后进入该状态。 |
| TERMINATED(终止) | 线程已完成执行,退出运行状态。 |
注意:线程从一个状态转换到另一个状态的过程是自动完成的,开发人员通常无需手动干预。
2. 创建线程的方式
Java提供了多种创建线程的方法,常见的有以下几种:
(1)继承Thread类
public class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running!");
}
}
// 使用
MyThread thread = new MyThread();
thread.start();
(2)实现Runnable接口
public class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("Thread running!");
}
}
// 使用
Thread thread = new Thread(new MyRunnable());
thread.start();
(3)使用内部类
public class Main {
public static void main(String[] args) {
Thread thread = new Thread() {
@Override
public void run() {
System.out.println("Thread running!");
}
};
thread.start();
}
}
(4)使用Lambda表达式(JDK 8+)
public class Main {
public static void main(String[] args) {
Thread thread = new Thread(() -> System.out.println("Thread running!"));
thread.start();
}
}
推荐:优先使用
Runnable或Callable接口,避免继承带来的单继承限制。
3. 线程池核心参数
线程池是管理线程资源的重要工具,通过复用线程减少创建开销,提升系统性能。ThreadPoolExecutor是线程池的核心实现类。
ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 工作队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
参数详解:
- corePoolSize:核心线程数,即使空闲也不会被回收。
- maximumPoolSize:最大线程数,当工作队列满且当前线程数小于最大值时,会创建新线程。
- keepAliveTime:非核心线程的空闲超时时间,超过该时间会被回收。
- workQueue:任务队列,用于存放待处理的任务。
- threadFactory:用于创建新线程的工厂。
- handler:拒绝策略,当线程池无法接受新任务时的处理方式。
常见拒绝策略:
AbortPolicy:抛出异常。DiscardPolicy:丢弃任务。DiscardOldestPolicy:丢弃最老的任务。CallerRunsPolicy:由调用者线程执行任务。
4. 如何创建线程池?
方式一:使用Executors工具类(不推荐)
ExecutorService executor = Executors.newFixedThreadPool(10);
缺点:容易导致资源耗尽,因为默认无界队列可能导致内存溢出。
方式二:手动创建ThreadPoolExecutor(推荐)
ExecutorService executor = new ThreadPoolExecutor(
10, // corePoolSize
20, // maximumPoolSize
60L, // keepAliveTime
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100), // workQueue
new ThreadPoolExecutor.CallerRunsPolicy() // handler
);
建议:明确设置线程池参数,避免资源泄漏。
5. 线程池工作原理
线程池的工作流程如下:
- 提交任务:将任务放入工作队列。
- 检查核心线程数:如果当前线程数小于
corePoolSize,则创建新线程执行任务。 - 检查队列容量:如果线程数已达
corePoolSize,且队列未满,则将任务加入队列。 - 检查最大线程数:如果队列已满且当前线程数小于
maximumPoolSize,则创建新线程。 - 拒绝任务:如果线程数达到
maximumPoolSize且队列已满,则触发拒绝策略。
图示:
提交任务 → 检查核心线程 → 添加到队列 → 创建新线程 → 执行任务 → 完成后回收
6. 线程池大小如何设定?
合理设置线程池大小对性能至关重要。
计算公式:
- CPU密集型任务:
线程数 ≈ CPU核心数 + 1- 原因:避免过多线程争抢CPU资源,造成上下文切换开销。
- IO密集型任务:
线程数 ≈ CPU核心数 × (1 + IO等待时间 / CPU计算时间)- 原因:IO操作期间线程处于等待状态,可利用空闲时间处理其他任务。
示例:假设服务器有4核CPU,平均每个任务需要1秒CPU时间和2秒IO等待,则线程数约为:
4 × (1 + 2/1) = 12
7. 线程池的拒绝策略有哪些?
| 拒绝策略 | 行为描述 |
|---|---|
| AbortPolicy | 抛出RejectedExecutionException异常,阻止任务提交。 |
| DiscardPolicy | 直接丢弃任务,不抛异常。 |
| DiscardOldestPolicy | 丢弃队列中最老的任务,再尝试提交新任务。 |
| CallerRunsPolicy | 由调用线程直接执行任务,适用于负载较低场景。 |
选择依据:
- 若希望及时响应错误,选
AbortPolicy。 - 若允许部分任务丢失,选
DiscardPolicy。 - 若需保证关键任务执行,选
CallerRunsPolicy。
8. 线程池是一个服务还是一个组件?为什么?
线程池更像是一种服务而非简单的组件。
原因:
- 提供长期运行能力:线程池可以持续接收和处理任务,类似于Web服务器的请求处理。
- 资源管理:它负责线程的生命周期管理、任务调度、异常处理等,具备完整的生命周期。
- 可扩展性:支持动态调整线程数量、队列容量等参数,适应不同负载需求。
对比:普通组件通常是短生命周期的,而线程池作为后台服务,贯穿整个应用运行过程。
9. synchronized 和 lock 的区别
| 特性 | synchronized | Lock |
|---|---|---|
| 是否可中断 | 不可中断 | 可中断(如lockInterruptibly()) |
| 是否公平 | 非公平锁(默认) | 支持公平锁 |
| 是否可重入 | 可重入 | 可重入 |
| 是否支持条件变量 | 不支持 | 支持(Condition) |
| 是否需要显式释放 | 自动释放 | 必须手动释放(unlock()) |
| 是否支持超时 | 不支持 | 支持(tryLock(timeout)) |
推荐:在需要复杂同步逻辑时使用
Lock;简单场景下使用synchronized即可。
10. synchronized 的锁种类
synchronized关键字支持三种锁类型:
-
对象锁(实例锁):
- 锁住当前对象实例,同一时刻只能有一个线程访问。
- 示例:
synchronized(this)或synchronized(obj)。
-
类锁(静态锁):
- 锁住整个类,所有线程共享同一个锁。
- 示例:
synchronized(MyClass.class)或static synchronized method()。
-
方法锁:
- 在方法前加
synchronized修饰符,等价于在方法体内加锁。 - 示例:
public synchronized void doSomething()。
- 在方法前加
注意:
synchronized作用于对象时,锁的是对象本身;作用于类时,锁的是类的字节码。
11. 线程安全问题及解决方案
常见问题:
- 竞态条件(Race Condition):多个线程同时修改共享数据,导致结果不可预测。
- 死锁(Deadlock):两个或多个线程互相等待对方释放锁,形成循环依赖。
- 活锁(Livelock):线程不断重复相同的操作,却无法前进。
解决方案:
- 使用
synchronized或ReentrantLock保证临界区互斥。 - 使用
volatile关键字确保可见性。 - 使用原子类(如
AtomicInteger)替代基本类型。 - 使用线程安全集合(如
ConcurrentHashMap)。
12. 线程池最佳实践
- 避免使用
Executors工具类创建线程池,应手动配置参数。 - 合理设置线程池大小,根据任务类型选择合适的线程数。
- 设置合理的队列容量,防止内存溢出。
- 选择合适的拒绝策略,根据业务需求决定是否丢弃任务。
- 监控线程池状态,定期检查活跃线程数、队列长度等指标。
- 优雅关闭线程池,使用
shutdown()或shutdownNow()。
【八股真解】精炼最新高频面经 文章被收录于专栏
本专栏在精不在多,内容分为八股文、大厂真实面经,面试通过后将offer和面试题私发给我,可退还专栏的收益部分费用。欢迎大家共建专栏
查看15道真题和解析