一、什么是多线程?
多线程就是在同一进程内同时存在多个执行流(线程),每个线程都有自己的执行路径、程序计数器和栈,但共享进程的堆内存与全局资源。
在 Java 中,线程可以通过继承 Thread 或实现 Runnable/Callable(并配合 ExecutorService)来创建和管理。
关键点:
-
并发(concurrency) ≠ 并行(parallelism):并发是多个任务在重叠的时间段内进展;并行是在同一时刻多个任务同时运行(需要多核才是真正的并行)。
-
线程可以用来把复杂任务拆成多个可以同时"推进"的子任务(尤其是 I/O 密集型或可并行的计算)。
二、多线程的优势
-
提高响应性:GUI/服务端能同时处理请求或保持界面不阻塞(例如:一个线程处理 I/O,另一个更新界面)。
-
更好利用等待时间:I/O 等待(磁盘、网络)期间,CPU 可以切换去执行其它线程,从而提高吞吐。
-
并行计算(在多核 CPU):把可分解的计算任务分配到多个核心上,能显著缩短完成时间。
-
资源隔离、结构清晰:把不同职责放到不同线程(worker、监听、定时器)更易于设计清晰的系统。
三、单核 CPU 支持多线程吗?
单核 CPU 可以支持多线程 :通过 时间片轮转(time-slicing),OS 在多个线程之间快速切换,使得从用户角度看多个线程"同时"运行。
并行: 多核 CPU 下,多个线程同时在不同核心上执行(真正的 "同时");
**并发:**单核 CPU 下,操作系统通过 "时间片轮转" 机制,快速切换不同线程的执行(比如每个线程执行 10ms,切换速度极快,肉眼感觉像 "同时")。
例如:
单核 CPU 下,你一边听歌(音频线程),一边写代码(编辑器线程),操作系统会快速在这两个线程间切换,你感觉是同时进行的,但实际上 CPU 同一时刻只执行一个线程。
四、单核 CPU 使用多线程效率一定就高吗?
不一定,甚至可能更低:
-
效率高的场景:线程中有大量 "IO 操作"(比如网络请求、文件读写),此时 CPU 会闲置,多线程可以利用闲置时间执行其他线程。比如:
- 单线程:请求 A(等待网络响应 2 秒)→ 处理 A(0.1 秒)→ 请求 B(等待 2 秒)→ 处理 B(0.1 秒),总耗时 4.2 秒;
- 多线程:线程 1 请求 A(等待 2 秒),同时线程 2 请求 B(等待 2 秒),之后同时处理,总耗时 2.1 秒。
-
效率低的场景:线程都是 "CPU 密集型"(比如大量计算,无 IO 等待),此时多线程会增加 "线程切换开销"(保存线程上下文、恢复上下文),反而比单线程慢。比如:
- 单线程:计算 100 万次,耗时 1 秒;
- 多线程:两个线程各计算 50 万次,线程切换耗时 0.2 秒,总耗时 1.2 秒。
五、什么是死锁?
死锁是多线程中最典型的问题,指多个线程互相持有对方需要的资源,且都不释放自己持有的资源,导致所有线程都无法继续执行。
经典产生条件(四个必要条件):
-
互斥(Mutual exclusion):资源不能被多个线程同时使用(例如互斥锁)。
-
占有并等待(Hold and wait):线程已持有至少一个资源并等待获取其他被占用的资源。
-
不可剥夺(No preemption):资源一旦分配,不能被强制剥夺,只有线程自己释放。
-
循环等待(Circular wait):存在线程环,T1 等待 T2,T2 等待 T3,... 最后等待回 T1。
java
public class DeadlockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("t1 持有 lockA");
sleep(100); // 模拟工作,确保顺序
synchronized (lockB) {
System.out.println("t1 持有 lockB");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("t2 持有 lockB");
sleep(100);
synchronized (lockA) {
System.out.println("t2 持有 lockA");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long ms) {
try { Thread.sleep(ms); } catch (InterruptedException ignored) {}
}
}
t1 先锁 lockA 再想要 lockB;t2 先锁 lockB 再想要 lockA ------ 如果两个都在各自的第一把锁后等待第二把锁,就会死锁。
六、如何检测死锁?
-
固定锁获取顺序(最简单且常用):对多个资源设定全局顺序,所有线程都按相同顺序获取锁(例如先锁 A 再锁 B),这样就消除了循环等待条件。
-
减少锁的持有时间:尽量把同步块变窄,只保护必要的临界区。
-
使用
tryLock(带超时)而不是阻塞式获取 (适用于java.util.concurrent.locks.Lock):-
如果一段时间内获取不到锁,就释放已持有的锁、退避并重试,从而避免无限等待。
-
例子:
javaimport java.util.concurrent.locks.Lock; import java.util.concurrent.locks.ReentrantLock; import java.util.concurrent.TimeUnit; Lock la = new ReentrantLock(); Lock lb = new ReentrantLock(); boolean safeAcquire() throws InterruptedException { while (true) { boolean aLocked = la.tryLock(50, TimeUnit.MILLISECONDS); if (!aLocked) continue; try { boolean bLocked = lb.tryLock(50, TimeUnit.MILLISECONDS); if (!bLocked) { // 无法获得第二把锁,释放第一把,稍后重试 la.unlock(); Thread.sleep(10); continue; } // 两把锁都拿到了 return true; } finally { // 在使用完成后解锁(调用者负责) } } }
-
-
使用更高级的并发数据结构/算法 :
ConcurrentHashMap、BlockingQueue等并发容器通常内部已避免复杂锁竞争。 -
尽可能使用不可变对象或无锁算法:减少需要加锁的场景。
-
避免嵌套锁或减少锁的层级:尽量不在持有多个锁时调用外部可阻塞的方法。
-
用单线程执行关键段(actor 模型 / 消息队列):把需要串行化的操作发送到单独线程处理(例如事件队列),从而避免锁。
小结
-
多线程是把多个执行流放入同一进程,共享内存但独立栈/PC。并发 ≠ 并行。
-
单核 CPU 支持多线程(通过时间片切换),但多线程不总是更快;CPU 密集型任务在单核上多线程可能更慢。
-
死锁由互斥、占有并等待、不可剥夺、循环等待四条件共同导致。
-
检测死锁:
jstack、JVisualVM、ThreadMXBean.findDeadlockedThreads()等工具。 -
避免死锁:统一锁顺序、tryLock(超时)、缩小锁粒度、使用并发容器或单线程处理关键逻辑。
-
设计和代码审查阶段预防胜过事后抢救。