Java 多线程:从原理到实战,再到高频面试题
这是一篇"可运行 + 可面试"的多线程文章:先讲原理,再给代码,最后用面试题查漏补缺。你可以把文中的代码复制到一个独立的 Maven/Gradle 项目中运行(JDK 8+)。
一、并发与并行、进程与线程
- 并发(Concurrency):同一时间段内交替处理多个任务。
- 并行(Parallelism):同一时刻同时处理多个任务(需要多核)。
- 进程:资源分配的基本单位。
- 线程:CPU 调度的基本单位,同一进程内线程共享堆和方法区,但拥有独立的栈和程序计数器。
Java 内存模型(JMM)三个关键词
- 可见性:一个线程对共享变量的写,是否能被其他线程"看见"。
- 有序性:指令是否可以重排。
- 原子性:操作是否不可分割。
二、创建线程的 4 种常见方式
1)继承 Thread
java
public class HelloThread extends Thread {
@Override
public void run() {
System.out.println("Hello from " + Thread.currentThread().getName());
}
public static void main(String[] args) {
new HelloThread().start();
}
}
优缺点:简单但受限(Java 单继承),不利于任务与线程的解耦。
2)实现 Runnable
java
public class HelloRunnable implements Runnable {
@Override
public void run() {
System.out.println("Hello from " + Thread.currentThread().getName());
}
public static void main(String[] args) {
Thread t = new Thread(new HelloRunnable());
t.start();
}
}
优点:任务与线程解耦,适合复用与线程池。
3)实现 Callable<V>
+ Future
java
import java.util.concurrent.*;
public class SumTask implements Callable<Integer> {
private final int n;
public SumTask(int n) { this.n = n; }
@Override
public Integer call() {
int sum = 0;
for (int i = 1; i <= n; i++) sum += i;
return sum;
}
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newSingleThreadExecutor();
Future<Integer> f = pool.submit(new SumTask(100));
System.out.println("result=" + f.get());
pool.shutdown();
}
}
亮点:可以有返回值、可抛出受检异常。
4)线程池 ExecutorService
java
import java.util.concurrent.*;
public class PoolDemo {
public static void main(String[] args) throws InterruptedException {
ThreadPoolExecutor pool = new ThreadPoolExecutor(
2, 4,
60, TimeUnit.SECONDS,
new LinkedBlockingQueue<>(100),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy()
);
for (int i = 0; i < 10; i++) {
int id = i;
pool.execute(() -> System.out.println("task-" + id + " -> " + Thread.currentThread().getName()));
}
pool.shutdown();
pool.awaitTermination(1, TimeUnit.MINUTES);
}
}
建议 :生产中优先手动构造 ThreadPoolExecutor
,不要直接用 Executors.newFixedThreadPool(...)
等默认策略(可能造成 OOM)。
三、关键字 synchronized
与可重入锁 ReentrantLock
1)synchronized
基本用法
java
public class Counter {
private int c = 0;
public synchronized void inc() { c++; }
public synchronized int get() { return c; }
}
- 监视器锁(对象锁/类锁),可重入。
- 可见性 与原子性 得到保障,
synchronized
的释放-获取建立 happens-before 关系。
对象锁 vs. 类锁
java
public class LockTypes {
private static int s;
private int i;
public synchronized void objLock() { i++; } // 对象锁
public static synchronized void classLock() { s++; } // 类锁
}
2)ReentrantLock
与 Condition
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.*;
public class LockDemo {
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private String data;
public void put(String v) {
lock.lock();
try {
data = v;
notEmpty.signal();
} finally { lock.unlock(); }
}
public String take() throws InterruptedException {
lock.lock();
try {
while (data == null) notEmpty.await(1, TimeUnit.SECONDS);
String v = data; data = null; return v;
} finally { lock.unlock(); }
}
}
优势:
- 可中断获取锁
lockInterruptibly()
- 可定时尝试
tryLock(timeout)
- 支持多个条件队列
Condition
选型建议 :简单临界区用 synchronized
,需要定时/可中断/多个条件队列时用 ReentrantLock
。
四、volatile
、原子类与 CAS
1)volatile
- 保证 可见性 与 禁止指令重排 ,但 不保证复合操作原子性。
java
public class VolatileDemo {
volatile boolean running = true;
void stop() { running = false; }
}
2)原子类 AtomicInteger
java
import java.util.concurrent.atomic.AtomicInteger;
public class AtomicDemo {
private final AtomicInteger ai = new AtomicInteger();
public void inc() { ai.incrementAndGet(); }
public int get() { return ai.get(); }
}
- 底层用 CAS(Compare-And-Swap) 实现无锁原子更新。
CAS 三件套 :期望值、内存地址、目标新值;失败会自旋重试。注意 ABA 问题 (可用 AtomicStampedReference
)。
五、AQS 同步器家族(简版导图)
ReentrantLock
:独占锁,AQS 独占模式。Semaphore
:信号量,限流。CountDownLatch
:计数器,等待 N 个子任务完成。CyclicBarrier
:栅栏,N 线程相互等待,聚合再继续。ReentrantReadWriteLock
:读写锁,提高读多写少场景吞吐。
代码速览
java
// CountDownLatch:等待所有子任务完成
CountDownLatch latch = new CountDownLatch(3);
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { Thread.sleep(500); } catch (InterruptedException ignored) {}
latch.countDown();
}).start();
}
latch.await();
System.out.println("all done");
// CyclicBarrier:N 个线程到齐再继续
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("go!"));
for (int i = 0; i < 3; i++) {
new Thread(() -> {
try { barrier.await(); } catch (Exception ignored) {}
System.out.println(Thread.currentThread().getName()+" passed");
}).start();
}
// Semaphore:限流
Semaphore sem = new Semaphore(2);
for (int i = 0; i < 5; i++) {
new Thread(() -> {
try { sem.acquire();
System.out.println(Thread.currentThread().getName()+" in");
Thread.sleep(300);
} catch (InterruptedException ignored) {}
finally { sem.release(); }
}).start();
}
六、阻塞队列与经典"生产者-消费者"
java
import java.util.concurrent.*;
public class ProducerConsumer {
private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
public void start() {
ExecutorService pool = Executors.newCachedThreadPool();
// Producer
pool.execute(() -> {
int i = 0;
try {
while (i < 20) {
queue.put(i);
System.out.println("P -> " + i);
i++;
}
} catch (InterruptedException ignored) {}
});
// Consumer
pool.execute(() -> {
try {
while (true) {
Integer v = queue.take();
System.out.println("C <- " + v);
if (v == 19) break;
}
} catch (InterruptedException ignored) {}
});
pool.shutdown();
}
public static void main(String[] args) { new ProducerConsumer().start(); }
}
思路 :用 BlockingQueue
自带的阻塞/唤醒语义避免手写 wait/notify
。
七、ThreadLocal
的使用与清理
java
public class TL {
private static final ThreadLocal<String> TL = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(() -> {
TL.set("trace-id-123");
try {
System.out.println(TL.get());
} finally {
TL.remove(); // 避免线程池线程复用导致的脏数据/内存泄漏
}
});
t.start();
t.join();
}
}
场景 :请求上下文、数据库连接、日期格式化器。注意 在线程池中一定 remove()
。
八、死锁、活锁与饥饿
1)死锁复现
java
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLock {
static final Lock A = new ReentrantLock();
static final Lock B = new ReentrantLock();
public static void main(String[] args) {
new Thread(() -> holdThenWait(A, B), "T1").start();
new Thread(() -> holdThenWait(B, A), "T2").start();
}
static void holdThenWait(Lock first, Lock second) {
first.lock();
try {
sleep(100);
second.lock(); // 两线程锁顺序相反 -> 死锁
try { } finally { second.unlock(); }
} finally { first.unlock(); }
}
static void sleep(long ms){ try{ Thread.sleep(ms);}catch(Exception ignored){} }
}
避免策略 :统一加锁顺序、设置超时 tryLock(timeout)
、死锁检测(jstack/可视化工具)。
2)活锁与饥饿
- 活锁:都在不断"礼让",却始终无法前进(比如不断重试但彼此让步)。
- 饥饿:高优先级线程一直占用资源,低优先级线程长期得不到执行。
九、CompletableFuture
:组合异步编程
java
import java.util.concurrent.*;
public class CF {
static String slow(String name) {
try { Thread.sleep(300); } catch (InterruptedException ignored) {}
return name + "@" + Thread.currentThread().getName();
}
public static void main(String[] args) {
ExecutorService pool = Executors.newFixedThreadPool(3);
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> slow("A"), pool);
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> slow("B"), pool);
CompletableFuture<String> both = f1.thenCombine(f2, (a, b) -> a + "+" + b);
System.out.println(both.join());
pool.shutdown();
}
}
常用方法 :thenApply/thenAccept
、thenCompose/thenCombine
、allOf/anyOf
、exceptionally/handle
。
十、线程池参数与拒绝策略(含调优建议)
- 核心线程数
corePoolSize
:长期保留的工作线程数。 - 最大线程数
maximumPoolSize
:任务堆积时允许的最大线程数。 - 存活时间
keepAliveTime
:非核心线程闲置回收时间。 - 有界队列:推荐
ArrayBlockingQueue
/LinkedBlockingQueue(capacity)
。 - 拒绝策略:
AbortPolicy
/CallerRunsPolicy
/DiscardPolicy
/DiscardOldestPolicy
。
估算思路(CPU 密集 vs I/O 密集):
- CPU 密集:
core ≈ CPU核数
或CPU核数 + 1
。 - I/O 密集:
core ≈ CPU核数 × (1 + 平均等待时间/平均计算时间)
。
监控 :采集 pool.getActiveCount()
、getQueue().size()
、任务耗时分布,配合 Runtime.getRuntime().maxMemory()
观察内存占用。
十一、Wait/Notify vs 条件队列(附最小演示)
java
public class WaitNotifyDemo {
private final Object lock = new Object();
private boolean ready = false;
public void signal() {
synchronized (lock) {
ready = true;
lock.notifyAll();
}
}
public void await() throws InterruptedException {
synchronized (lock) {
while (!ready) lock.wait();
}
}
}
对比 :wait/notify
容易误用(丢信号、虚假唤醒),相比之下 Condition
与 BlockingQueue
更安全可控。
十二、面试高频题(含简答)
Q1:synchronized
与 ReentrantLock
区别?
答:
- 语法层面:关键字 vs API 类。
- 功能:
ReentrantLock
支持可中断、定时、多个Condition
;synchronized
无需手动释放,JIT 可做锁消除/粗化/偏向等优化(JDK 不同版本行为不同)。 - 性能:差距取决于场景与版本;简单场景优先
synchronized
。
Q2:volatile
能保证原子性吗?
答 :不能,i++
仍需加锁或用原子类。volatile
保障可见性与有序性(禁止重排)。
Q3:什么是 happens-before?
答 :JMM 中保证可见性的偏序关系,如:锁的释放→之后对同一锁的获取、volatile
写→读、线程启动前对变量的写→Thread.start()
后该线程可见、线程终止 join()
前对变量的写→join()
返回后可见。
Q4:什么是 ABA 问题?如何解决?
答 :CAS 中,值从 A→B→A,CAS 仍成功但期间发生变化。可用版本号(AtomicStampedReference
)或加时序/指针不可复用策略解决。
Q5:线程池为什么不建议直接用 Executors.newFixedThreadPool()
?
答 :其队列是无界 LinkedBlockingQueue
,在任务大量堆积时可能导致 OOM;建议自行指定有界队列与拒绝策略。
Q6:ThreadLocal
会内存泄漏吗?
答 :会。在线程池中线程长期存活,ThreadLocalMap
的 Entry
使用弱引用指向 key,但 value 是强引用,需要手动 remove()
;否则可能泄漏或数据串线。
Q7:死锁产生的四个必要条件?
答:互斥、占有且等待、不可剥夺、循环等待。破坏任一即可避免。
Q8:CountDownLatch
与 CyclicBarrier
区别?
答:前者一次性闭锁,减少到 0 即释放;后者可复用的屏障,固定 parties 个数,每轮到齐再放行。
Q9:CompletableFuture
与 Future
的区别?
答 :Future
只能阻塞 get
;CompletableFuture
支持链式编排、组合、异常处理与回调,更适合复杂异步流程。
Q10:读写锁适用场景?
答:读多写少、读操作占绝对多数,且读之间可以并行的场景。注意写锁会阻塞读,避免写偏斜。
十三、实战:批量并发调用 + 超时与降级
java
import java.util.*;
import java.util.concurrent.*;
public class BulkCallDemo {
static String call(int id) {
try { Thread.sleep(200 + (id % 3) * 200); } catch (InterruptedException ignored) {}
return "OK-" + id;
}
public static void main(String[] args) {
ExecutorService pool = new ThreadPoolExecutor(
8, 16, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100)
);
List<CompletableFuture<String>> futures = new ArrayList<>();
for (int i = 0; i < 10; i++) {
int id = i;
CompletableFuture<String> f = CompletableFuture
.supplyAsync(() -> call(id), pool)
.orTimeout(400, TimeUnit.MILLISECONDS)
.exceptionally(ex -> "FALLBACK-" + id);
futures.add(f);
}
List<String> results = futures.stream().map(CompletableFuture::join).toList();
System.out.println(results);
pool.shutdown();
}
}
要点:
- 统一线程池,控制并发度
- 每个任务设置单独超时
- 异常统一降级,保持整体可用性
十四、排错与调试
jstack
:线程栈与死锁检测。jmap
:堆转储,排查内存泄漏。jconsole
/visualvm
/Java Flight Recorder
:观察线程状态、CPU、锁竞争。
十五、最佳实践清单(可做 CR Checklist)
- 线程池必须有界队列 + 合理拒绝策略。
- 共享变量要么不共享 ,要么只读 ,要么用锁/原子类保护。
- 在线程池中使用
ThreadLocal
必须try...finally remove()
。 - 锁要细化但不碎片化,统一加锁顺序,能降级就降级为读写锁。
- 慎用
volatile
做计数器;避免"写-读-改-写"竞态。 - 超时与中断是一等公民 ,API 支持就用起来(
tryLock(timeout)
、orTimeout
)。 - 日志打点线程名、traceId,关键路径上报拒绝次数、等待时长、任务耗时分位数。
至此,你已经具备:能写、能读、能调优、能答题的并发基础。如果你在用到具体业务(例如 I/O 密集的网关、计算密集的风控特征工程)时需要专项调参,可以在此文的代码骨架上再做针对性实验。