一、线程基础
1. 创建线程的几种方式?
答案:
-
自定义类来继承 Thread 类,重写
run()方法。 -
实现 Runnable 接口 :重写
run(),将实例传给 Thread 对象。 -
实现 Callable 接口 :有返回值,配合
FutureTaskjavaclass MyCallable implements Callable<String> { public String call() throws Exception { return "result"; } } FutureTask<String> ft = new FutureTask<>(new MyCallable()); new Thread(ft).start(); String result = ft.get(); -
使用线程池 (推荐):通过
Executors工厂类或ThreadPoolExecutor创建。
2. 线程生命周期
在 Java 中,线程的生命周期由 Thread.State 枚举定义了 6 种状态,分别是:
- NEW(新建)
线程对象被创建后(比如new Thread()),但还没有调用start()方法。此时线程还未获得执行资源。 - RUNNABLE(可运行)
调用了start()后,线程处于RUNNABLE状态。在 JVM 层面,它可能是正在运行,也可能在等待操作系统分配时间片(也就是传统意义上的 READY 和 RUNNING 的合并状态)。 - BLOCKED(阻塞)
线程等待进入synchronized代码块或方法时,因为锁被其他线程持有而被阻塞。当锁释放后,它会重新回到RUNNABLE状态参与竞争。 - WAITING(无限等待)
线程调用了Object.wait()(不设超时)、Thread.join()(不设超时)或LockSupport.park()后,会进入WAITING状态。它需要被其他线程显式唤醒(如notify/notifyAll,或等待的线程结束)。 - TIMED_WAITING(计时等待)
类似WAITING,但带有超时时间。例如Thread.sleep(time)、wait(timeout)、join(timeout)、LockSupport.parkNanos()等。时间到期或被唤醒后,会返回RUNNABLE。 - TERMINATED(终止)
线程的run()方法正常执行完毕,或者因异常而结束。一旦终止,就不能再通过start()重新启动。
关键状态转换
- NEW → RUNNABLE :调用
start() - RUNNABLE → WAITING :
wait()(需先获得锁)、join()、LockSupport.park() - RUNNABLE → TIMED_WAITING :
sleep(time)、wait(time)、join(time)、parkNanos() - RUNNABLE → BLOCKED :尝试进入
synchronized块但锁被占用 - WAITING/TIMED_WAITING → RUNNABLE :被
notify/notifyAll唤醒、超时到期、等待的线程结束 - BLOCKED → RUNNABLE:锁被释放,线程获得锁
- RUNNABLE → TERMINATED :
run()结束或抛出未捕获异常
注意:
sleep()和yield()不会释放锁,而wait()会释放锁并等待队列。
3. yield、join、wait、sleep 方法
Thread.yield() 是一个静态方法,提示线程调度器当前线程愿意主动让出 CPU 时间片,让同级或更高级的线程先执行。它不会释放锁,也不会改变线程状态(仍为 RUNNABLE)。实际中由于平台依赖性强,效果不确定,很少用于生产代码,一般用 sleep(0) 或 LockSupport 替代。
join() 是 Thread 类的一个实例方法,作用是让当前线程阻塞等待目标线程执行完毕。它的底层实现依赖 wait/notify:调用 join() 时,如果目标线程还活着,当前线程会在目标线程对象上调用 wait() 进入阻塞,直到目标线程结束后 JVM 自动调用 notifyAll() 将其唤醒。join() 可以带超时参数,响应中断,并且在等待期间只会释放目标线程对象的锁,不会释放其他锁。
与 yield() 不同,join() 会导致线程进入 WAITING/TIMED_WAITING 状态,而 yield() 保持在 RUNNABLE 状态
wait() 是 Object 类的实例方法,必须在 synchronized 代码块内调用,否则会抛异常。调用 wait() 会使当前线程进入 WAITING 状态,并释放该对象的锁 ,让其他线程有机会进入同步块。当其他线程调用同一对象的 notify() 或 notifyAll() 时,被唤醒的线程会重新竞争锁,拿到锁后从 wait() 返回继续执行。为了避免虚假唤醒,必须使用 while 循环而不是 if 来检查条件。与 sleep() 不同,wait() 会释放锁,而 sleep() 不会。wait() 是实现生产者-消费者模式的基础。
sleep() 是 Thread 的一个静态方法,让当前线程暂停执行指定的时间,进入 TIMED_WAITING 状态。它不会释放任何锁 ,因此如果线程持有锁,其他线程即使进入同步块也得继续等待。sleep() 可以响应中断,抛出 InterruptedException。与 wait() 不同,wait() 需要配合 synchronized 使用且会释放锁;与 yield() 不同,yield() 只是让出CPU但状态仍是 RUNNABLE,而 sleep() 确确实实让线程休眠一段至少给定的时间。实际开发中,常用 sleep() 做简单延时或降低轮询频率,但精确任务调度应使用定时器或线程池。
| 特性 | wait() |
sleep() |
join() |
yield() |
|---|---|---|---|---|
| 所属类 | Object |
Thread |
Thread |
Thread |
| 调用前提 | 必须在同步块中 | 任意位置 | 任意位置 | 任意位置 |
| 释放锁 | 释放对象锁 | 不释放 | 不释放(只释放目标线程的锁,但目标线程已经结束) | 不释放 |
| 线程状态 | WAITING / TIMED_WAITING |
TIMED_WAITING |
WAITING / TIMED_WAITING |
RUNNABLE |
| 唤醒方式 | notify/notifyAll 或超时 |
超时或中断 | 目标线程结束或超时 | 不可主动唤醒(仅CPU重新竞争) |
| 典型用途 | 线程间协作(等待条件) | 暂停执行一段时间 | 等待另一个线程结束 | 提示调度器让出CPU |
4. wait() 后线程什么时候被唤醒?notify 和 notifyAll 的区别?唤醒后需要重新获取 monitor 吗?
调用 wait() 后,线程进入等待状态,可以被 notify、notifyAll、超时或中断唤醒。
notify 只唤醒一个等待线程,notifyAll 唤醒所有等待线程,推荐使用 notifyAll 以避免信号丢失。
无论哪种唤醒,线程都必须重新获取 monitor(锁) 才能从 wait() 返回继续执行。在获取锁之前,线程处于 BLOCKED 状态
5. 用两个线程交替 1-100
java
public class AlternatePrint {
private static final Object lock = new Object();
private static int num = 1;
private static final int MAX = 100;
public static void main(String[] args) {
Thread oddThread = new Thread(() -> {
while (num <= MAX) {
synchronized (lock) {
if (num % 2 == 1 && num <= MAX) { // 奇数
System.out.println(Thread.currentThread().getName() + " -> " + num);
num++;
lock.notify(); // 唤醒另一个线程
} else {
try {
lock.wait(); // 不是自己的轮次,等待
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}, "奇数线程");
Thread evenThread = new Thread(() -> {
while (num <= MAX) {
synchronized (lock) {
if (num % 2 == 0 && num <= MAX) { // 偶数
System.out.println(Thread.currentThread().getName() + " -> " + num);
num++;
lock.notify();
} else {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
}
}, "偶数线程");
oddThread.start();
evenThread.start();
}
}
二、锁与同步
1. volatile 和 synchronized 的区别?
答案:
- volatile:轻量级,只能修饰变量,保证可见性和有序性(禁止指令重排),不保证原子性。
- synchronized:重量级,可修饰方法/代码块,保证原子性、可见性、有序性。JDK6后已优化(锁升级)。
- 适用场景:volatile适合单写多读的状态标志;复杂操作(如i++)必须用synchronized或Atomic类
2. synchronized 的锁升级过程?
答案(JDK 1.6 优化后):
- 无锁:对象刚创建,没有线程竞争。
- 偏向锁:第一个线程获取锁时,会在对象头中记录线程 ID,无需 CAS。适用于同一线程反复获取同一锁的场景。
- 轻量级锁:当另一个线程来竞争时,偏向锁升级为轻量级锁。线程通过 CAS 尝试获取锁,失败则自旋。
- 重量级锁:自旋超过一定次数(JDK 1.6 自适应自旋)或并发激烈时,升级为重量级锁,最终依赖操作系统的互斥锁来保证正确性。
3. synchronized 和 Lock 的区别?
| 核心维度 | synchronized |
Lock(ReentrantLock) |
|---|---|---|
| 类型 | Java 内置关键字 | 接口 |
| 使用方式 | 自动获取/释放锁 | 手动 lock() / unlock()(必须 finally 释放) |
| 可中断性 | 不可中断,一直阻塞 | 可中断(lockInterruptibly()) |
| 超时尝试 | 不支持 | 支持 tryLock(timeout) |
| 公平锁 | 只能是非公平 | 可构造公平锁(new ReentrantLock(true)),默认非公平 |
| 条件变量 | 通过 wait()/notify()/notifyAll() 实现,每个对象只有一个等待队列,无法指定唤醒哪类线程 |
可通过 newCondition() 创建多个 Condition,实现分组等待与精确唤醒 |
简单场景用 synchronized,需要高级特性时用 Lock
4. volatile 原理
可见性原理 :volatile 修饰的变量,写操作会立即刷新到主内存 ,读操作会从主内存重新读取 ,不使用工作内存副本。因此一个线程对 volatile 变量的修改,对其他线程立即可见。
禁止指令重排序原理 :通过内存屏障 实现,写操作前后插入屏障(StoreStore 前,StoreLoad 后),读操作后插入屏障(LoadLoad 和 LoadStore),确保 volatile 变量前后的代码不会被重排序越过该变量,从而保证有序性。
5. 什么是 CAS?有什么问题?
CAS 是乐观锁 思想的一种典型实现。它假设并发冲突很少发生,因此在操作时不加锁,而是在提交更新时检查是否有其他线程修改过数据。如果更新失败,线程通常会通过自旋(循环重试)的方式再次尝试,直到成功为止。
- CAS是一种原子操作,包含三个操作数:内存位置 V、期望值 A、新值 B。只有当 V 的值等于 A 时,才将 V 更新为 B,否则什么都不做。整个过程是原子的。
- 问题 :
- ABA 问题 :值从 A 变成 B 又变回 A,CAS 会认为没有变化。解决方案 :为变量添加一个版本号 或时间戳。
- 循环时间长开销大:自旋 CAS 长时间不成功会消耗 CPU。
- 只能保证单个变量的原子操作:多个变量需要锁。
示例 :AtomicInteger 的 incrementAndGet() 内部就是 CAS 自旋。
6. 什么是 AQS(AbstractQueuedSynchronizer)?
答案:
AQS 是 java.util.concurrent 包中构建锁和同步器的框架 ,它采用了模板方法模式 。内部维护了一个 volatile int state(表示同步状态)和一个 FIFO 双向等待队列 (CLH 锁变体)。
工作方式:
- 子类通过实现
tryAcquire()/tryRelease()(独占模式)或tryAcquireShared()/tryReleaseShared()(共享模式)来定义如何修改state。 - 获取同步状态失败时,AQS 将当前线程封装成
Node节点加入队列,并通过LockSupport.park()阻塞;成功时则尝试唤醒队列中的后继节点。
常见子类 :ReentrantLock(独占可重入)、Semaphoreˈseməfɔː®(共享信号量)、CountDownLatch(共享倒计数)、ReentrantReadWriteLock(内部用两个 AQS 分别管理读锁和写锁)。
一句话总结 :AQS把复杂的同步逻辑抽象为"状态 + 队列 ",子类只需实现tryAcquire、tryRelease等方法。
7. a++ 是线程安全的吗?
a++ 不是线程安全的。因为它不是一个原子操作,包含了读取、加一、写入三个步骤。多线程并发执行时,可能出现线程A 读取后还未写入,线程B 也读取了相同的旧值,导致最终结果比预期少一次自增。要保证安全,可以使用 synchronized、ReentrantLock,或者更推荐使用 AtomicInteger(呃涛米克) 的 incrementAndGet() 方法,它基于 CAS 实现,无锁且高效。
三、线程池
1. 线程池的核心参数及工作流程?
核心参数 (ThreadPoolExecutor 构造方法):
corePoolSize:核心线程数,即使空闲也会保留。maximumPoolSize:最大线程数。keepAliveTime:非核心线程空闲存活时间。unit:时间单位。workQueue:阻塞队列(如ArrayBlockingQueue、LinkedBlockingQueue、SynchronousQueue)。ArrayBlockingQueue:有界队列,推荐使用,可防止任务无限堆积导致内存溢出。LinkedBlockingQueue:默认是无界队列,任务可能无限堆积,有内存溢出风险。SynchronousQueue:不存储元素的同步队列,任务必须直接交付给线程,否则就创建新线程。
threadFactory:线程工厂,用于创建新线程。handler:拒绝策略(当队列满且线程数达到最大时触发)。
🔄 核心工作流程
工作流程:
- 提交任务时,如果运行的线程数 <
corePoolSize,直接创建新线程执行。 - 如果 ≥
corePoolSize,尝试将任务放入workQueue。 - 如果队列已满,且线程数 <
maximumPoolSize,创建非核心线程执行。 - 如果线程数 ≥
maximumPoolSize,执行拒绝策略。
拒绝策略:
AbortPolicy:抛异常(默认)。DiscardPolicy:静默丢弃。CallerRunsPolicy:由调用者线程执行。DiscardOldestPolicy:丢弃队列头任务,重试提交。
2. new ThreadPoolExecutor 的核心线程数怎么计算得到的?
"关于 ThreadPoolExecutor 核心线程数的设置,在实际工作中,我通常会先分析业务的任务类型,主要分为 CPU 密集型 和 I/O 密集型 两种场景来评估:
1. 如果是 CPU 密集型任务 (比如复杂的数学运算、加密解密、图像处理等):
这类任务会一直消耗 CPU 资源。如果线程数过多,频繁的线程上下文切换反而会浪费 CPU 性能。
- 计算公式: 核心线程数 = CPU 核心数 + 1。
- 补充说明: 多加的 1 个线程,是为了在某个线程因为偶尔的页缺失或异常阻塞时,CPU 依然能保持忙碌状态,不浪费计算资源。
2. 如果是 I/O 密集型任务 (比如数据库查询、远程 RPC/HTTP 调用、文件读写等):
这类任务大部分时间线程都在等待 I/O 响应,处于阻塞状态,不占用 CPU。因此我们可以配置更多的线程,让 CPU 在等待期间去处理其他任务。
-
经验公式: 在快速评估时,通常会设置为 CPU 核心数 * 2。
-
科学公式:如果想要更精准的配置,我会使用《Java并发编程实战》中的经典公式:
最佳线程数 = N_cpu * U_cpu * (1 + W / C)N_cpu:CPU 核心数(通过Runtime.getRuntime().availableProcessors()获取)。U_cpu:期望的 CPU 利用率(通常设为 0.7~0.9,预留资源给系统)。W / C:线程等待时间(Wait)与计算时间(Compute)的比率。- 举个例子: 假设 8 核 CPU,目标利用率 80%,任务的等待时间是计算时间的 4 倍,那么核心线程数就是
8 * 0.8 * (1 + 4) = 32个。
3. 实战中的最终落地:
当然,无论是经验公式还是科学公式,计算出来的都只是理论基准值 。在实际生产环境中,我一定会结合线上压测 (比如用 JMeter)和实时监控(观察 CPU 利用率、任务排队积压情况、接口响应时间等指标)来进行动态微调,最终找到最契合当前业务场景的'黄金参数'。"
3. 为什么不建议使用 Executors 工具类去创建线程池?
这是因为 Executors 提供的快捷方法隐藏了线程池的关键配置细节,在真实的高并发生产环境中,极易引发内存溢出(OOM)或系统资源耗尽等严重问题。具体可以分为以下两种场景来理解:
1. 固定线程池的隐患(newFixedThreadPool 和 newSingleThreadExecutor)
这两个方法底层默认使用的是无界队列 LinkedBlockingQueue(容量默认为 Integer.MAX_VALUE)。
- 风险: 当任务提交的速度远大于线程处理的速度时(例如大促期间订单暴增,但下游短信接口变慢),任务会源源不断地堆积在队列中。由于队列几乎无界,最终会耗尽堆内存,导致系统抛出
OutOfMemoryError(OOM),甚至引发系统雪崩。
2. 缓存线程池的隐患(newCachedThreadPool)
这个方法的最大线程数被设置为了 Integer.MAX_VALUE。
- 风险: 当线上出现突发的高并发流量时,线程池会无限制地创建新线程来应对任务。这不仅会瞬间耗尽系统的 CPU 资源(导致频繁的上下文切换),还会因为每个线程默认占用 1MB 栈内存而迅速耗尽服务器内存,同样会引发 OOM 或系统假死。
一句话总结 :
Executors 工具类默认允许无界队列或无限创建线程,在高并发场景下极易导致内存溢出(OOM)或系统资源耗尽,因此生产环境必须使用 ThreadPoolExecutor 手动创建线程池以实现可控的并发管理。
4. 线程池中 submit() 和 execute() 方法有什么区别?
答案:
| 特性 | execute() | submit() |
|---|---|---|
| 所属接口 | Executor |
ExecutorService |
| 参数类型 | 只支持Runnable |
支持 Runnable 和 Callable |
| 返回值 | 无返回值 (void) |
返回 Future 对象 |
| 异常处理 | 异常直接抛出,主线程无法捕获 | 异常被封装在 Future 中,调用 get() 时抛出 |
四、并发工具与通信
1. CountDownLatch 和 CyclicBarrier 的区别是什么?
CountDownLatch 是一个一次性的倒计时门闩(shuān),核心是一对多的单向等待。适用场景是一个主线程等待多个子线程完成任务。
CyclicBarrier(ˈsaɪklɪk ˈbæriər)是一个可循环使用的栅栏,核心是多对多的相互等待,所有线程都到达同一个公共屏障点(调用 await())后才能一起继续执行。适合多阶段的任务。
java
// CountDownLatch: 主线程等待 3 个子线程完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// do work
latch.countDown();
}).start();
}
latch.await(); // 主线程等待
// CyclicBarrier: 3 个线程互相等待,同时开始下一阶段
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("都到了,出发!"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
barrier.await(); // 等待其他线程
// 同时执行后续操作
} catch (Exception e) {}
}).start();
}
2. Semaphore 的原理和使用场景是什么?
Semaphore ˈseməfɔː®基于 AQS 维护许可计数器,用于控制同时访问某资源的线程数量(如限流、连接池管理)。
java
// 最多允许 10 个线程同时执行
Semaphore semaphore = new Semaphore(10);
semaphore.acquire();
try {
// 访问资源
} finally {
semaphore.release();
}
3. ReadWriteLock 读写锁的原理?读写锁有哪些适用场景?
ReadWriteLock 的核心原理是读写分离 ,通过维护一对锁来实现读读不互斥,读写、写写互斥,从而大幅提升'读多写少'场景下的并发性能。
以 Java 的 ReentrantReadWriteLock 为例,它底层基于 AQS 实现。为了在 int 类型的 state 变量中同时记录读锁和写锁的状态,它采用了**'按位切割'**的设计:高 16 位用于记录读锁的获取次数(共享计数),低 16 位用于记录写锁的重入次数(独占计数)。
此外,它还有一个重要特性是支持锁降级 (写锁可以降级为读锁),但不支持锁升级(读锁不能升级为写锁,否则会死锁)。在实际应用中,它默认是非公平锁以保证高吞吐,但也支持配置为公平锁来避免写线程饥饿。
java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;
public class LocalCache<K, V> {
// 共享的缓存数据存储
private final Map<K, V> cache = new HashMap<>();
// 创建读写锁实例
private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
// 获取读锁和写锁
private final ReentrantReadWriteLock.ReadLock readLock = rwLock.readLock();
private final ReentrantReadWriteLock.WriteLock writeLock = rwLock.writeLock();
/**
* 获取缓存数据(读操作)
* 多个线程可以同时调用此方法,互不阻塞
*/
public V get(K key) {
readLock.lock(); // 1. 加读锁
try {
System.out.println(Thread.currentThread().getName() + " 正在读取 key: " + key);
// 模拟读取耗时
Thread.sleep(100);
return cache.get(key);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
readLock.unlock(); // 2. 必须在 finally 中释放读锁
}
}
/**
* 写入/更新缓存数据(写操作)
* 同一时刻只能有一个线程执行此方法,且会阻塞所有读操作
*/
public void put(K key, V value) {
writeLock.lock(); // 1. 加写锁(独占)
try {
System.out.println(Thread.currentThread().getName() + " 正在写入 key: " + key);
// 模拟写入耗时
Thread.sleep(500);
cache.put(key, value);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
writeLock.unlock(); // 2. 必须在 finally 中释放写锁
}
}
}
4. 什么是 Future?CompletableFuture 相比 Future 有什么改进?
Future 代表异步结果,但只能阻塞获取且难以组合;CompletableFuture 提供了非阻塞的回调链、异常处理、多任务组合等函数式能力,是 Java 异步编程的进化版本
java
// 非阻塞的回调链
CompletableFuture.supplyAsync(() -> "hello")
.thenApply(s -> s + " world")
.thenAccept(System.out::println); // 异步打印 "hello world"
// 异常处理
CompletableFuture.supplyAsync(() -> {
if (true) throw new RuntimeException("出错了");
return "ok";
}).exceptionally(ex -> "默认值: " + ex.getMessage())
.thenAccept(System.out::println);
// 任务组合
CompletableFuture<Integer> f1 = CompletableFuture.supplyAsync(() -> 1);
CompletableFuture<Integer> f2 = CompletableFuture.supplyAsync(() -> 2);
f1.thenCombine(f2, (a, b) -> a + b)
.thenAccept(System.out::println); // 输出 3
五、高级特性
1. ThreadLocal 的原理及内存泄漏问题?
答案:
- 原理 :每个
Thread内部有一个ThreadLocalMap(类似 HashMap),key 是ThreadLocal对象的弱引用,value 是存储的值。- 当调用
set()时,根据当前线程找到其 map,将 value 存入。 get()同理。
- 当调用
- 内存泄漏 :由于 key 是弱引用,当
ThreadLocal对象没有强引用时,下一次 GC 就会回收 key,导致 map 中出现了 key 为 null 的 entry,但 value 仍然存在(强引用链:Thread → ThreadLocalMap → Entry → value)。如果线程长期运行(如线程池),这些 value 永远无法被访问,导致内存泄漏。 - 解决方法 :使用完
ThreadLocal后,显式调用remove()方法删除 entry。 - 正确使用 :通常将
ThreadLocal声明为private static,使得多个实例共享同一个ThreadLocal对象(避免重复创建),并且记得在 finally 块中remove()。
2. 什么是 happens-before 原则?列举几个常见的 happens-before 规则。
happens-before 是 Java 内存模型(JMM)中判断数据是否存在竞争、线程是否安全的核心依据。如果操作A happens-before 操作B,则A的结果对B可见。常见规则包括:
- 程序次序规则:单线程内代码顺序执行。
- volatile规则 :线程 A 写了一个
volatile变量,那么在线程 A 写操作之后任何线程 B 再去读这个变量,一定能读到线程 A 写入的最新值。 - 锁规则:一个线程在释放锁之前对共享变量的所有修改,对于后续获取到同一把锁的线程来说都是可见的。
3. 并发编程的三大特性是什么?分别如何保证?
1、可见性:一个线程修改了共享变量,其他线程能立即看到。
- volatile :被
volatile修饰的变量,在修改后会强制刷新到主内存,其他线程需要从主内存读取。 - synchronized / Lock:遵循 Happens-Before 原则,线程在释放锁之前,必须把共享变量的最新值刷新到主内存;获取锁时,会清空工作内存,从主内存重新加载最新值。
- final :被
final修饰的字段,在构造器初始化完成后,对其他线程立即可见。
2、原子性:操作不可被中断,要么全做要么全不做。
- synchronized / Lock
- JUC 原子类(如
AtomicInteger):它基于 CAS 无锁机制来保证单个变量操作的原子性 - 注意:
volatile不能保证原子性
3、有序性:禁止指令重排序,保证程序执行顺序。
- volatile:通过插入内存屏障(Memory Barrier),禁止特定类型的指令重排序(例如禁止将 volatile 写操作之前的普通写操作重排到其后)。
- synchronized / Lock:同样通过内存屏障,保证同一时刻只有一个线程执行,从而间接保证了代码块内操作的有序性。
- Happens-Before 原则:JMM 定义的一系列先行发生规则(如程序次序规则、锁规则、volatile 规则等),从规范层面保证了操作的有序性。
| 特性 | 核心问题 | 常用保证机制 |
|---|---|---|
| 可见性 | 线程读取到过期的旧值 | volatile、synchronized、Lock、final |
| 原子性 | 操作被线程切换打断 | synchronized、Lock、JUC原子类 |
| 有序性 | 编译器/CPU 指令重排 | volatile、synchronized、Lock |
4. 什么是指令重排?为什么要进行指令重排?如何禁止指令重排?
指令重排 是指编译器和处理器为了优化性能,在不改变单线程程序语义 的前提下,对指令的执行顺序进行调整的行为。目的是提升性能。
禁止方法 :使用 volatile、synchronized、Lock、final 或显式内存屏障
5. 线程中断机制:interrupt()、isInterrupted() 的区别?
1. interrupt() -- 设置中断标志
- 作用 :将目标线程的中断标志设置为
true。 - 特殊行为 :如果目标线程正处于
wait()、sleep()、join()等可中断阻塞 状态,则会抛出InterruptedException(同时清除中断标志,因为异常被抛出后标志已无意义)。 - 示例 :
thread.interrupt();
2. isInterrupted() -- 仅测试中断标志(不清除)
- 作用 :检查目标线程的中断标志是否为
true,不修改标志。 - 调用方式 :实例方法,通过线程对象调用,如
thread.isInterrupted()。 - 返回值 :若中断标志为
true则返回true,否则false。
interrupt() 设置中断标志;isInterrupted() 检查标志(不清除);
优雅停掉代码的实现方式
java
// 1. 在循环中,推荐使用 isInterrupted() 持续检查当前线程是否被中断
while (!Thread.currentThread().isInterrupted()) {
try {
// 模拟耗时或阻塞任务
Thread.sleep(1000);
} catch (InterruptedException e) {
// 2. 捕获到异常后,JVM 已经把标志位清除了。
// 如果你想让外层的 while 循环也能感知到中断并退出,
// 必须在这里手动再次调用 interrupt() 把标志位"补"回去!
Thread.currentThread().interrupt();
break; // 退出循环
}
}
6. 什么是守护线程?设置守护线程需要注意什么?
守护线程(Daemon Thread)是一种后台线程,它的存在依赖于用户线程(非守护线程)。当 JVM 中所有用户线程都结束后,JVM 会立即退出,不会等待守护线程执行完毕。典型例子:JVM 的垃圾回收线程
注意事项:
必须在 start() 前调用 setDaemon(true)ˈdiːmən,且不能持有必须要释放的临界资源。
六、问题排查
1. 什么是死锁?如何避免?
答案:
- 死锁:两个或多个线程互相持有对方需要的锁,导致所有线程都无法继续执行。
- 产生条件 (缺一不可):
- 互斥:资源不能被共享。
- 持有并等待:线程持有至少一个资源,并等待其他资源。
- 不可剥夺:资源不能被强制抢占。
- 循环等待:线程间形成等待环。
- 避免方法 :
- 破坏"持有并等待":一次性申请所有资源。
- 破坏"不可剥夺":获取不到所需资源时,释放已占有的资源。
- 破坏"循环等待":按固定顺序申请资源。
- 使用
tryLock(ReentrantLock)超时放弃。
2. 如何排查死锁?有哪些工具或命令?
排查死锁(如果 CPU 不高但系统挂起)
步骤一:首先用 ps -ef 或jps找到PID
步骤二:执行jstack命令导出线程堆栈
bash
jstack -l PID > thread_dump.txt
步骤三:分析线程堆栈,例如
Found one Java-level deadlock:
=============================
"Thread-1":
waiting to lock monitor 0x00007f8a1c0a6a00 (object 0x00000007d5a8a5a0, a java.lang.Object)
which is held by "Thread-0"
"Thread-0":
waiting to lock monitor 0x00007f8a1c0a6a80 (object 0x00000007d5a8a5b0, a java.lang.Object)
which is held by "Thread-1"