【Java后端】Java 多线程:从原理到实战,再到高频面试题
Java 多线程:从原理到实战,再到高频面试题
这是一篇“可运行 + 可面试”的多线程文章:先讲原理,再给代码,最后用面试题查漏补缺。你可以把文中的代码复制到一个独立的 Maven/Gradle 项目中运行(JDK 8+)。
一、并发与并行、进程与线程
- 并发(Concurrency):同一时间段内交替处理多个任务。
- 并行(Parallelism):同一时刻同时处理多个任务(需要多核)。
- 进程:资源分配的基本单位。
- 线程:CPU 调度的基本单位,同一进程内线程共享堆和方法区,但拥有独立的栈和程序计数器。
Java 内存模型(JMM)三个关键词
- 可见性:一个线程对共享变量的写,是否能被其他线程“看见”。
- 有序性:指令是否可以重排。
- 原子性:操作是否不可分割。
二、创建线程的 4 种常见方式
1)继承 Thread
public class HelloThread extends Thread {@Overridepublic void run() {System.out.println("Hello from " + Thread.currentThread().getName());}public static void main(String[] args) {new HelloThread().start();}
}
优缺点:简单但受限(Java 单继承),不利于任务与线程的解耦。
2)实现 Runnable
public class HelloRunnable implements Runnable {@Overridepublic 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
import java.util.concurrent.*;public class SumTask implements Callable<Integer> {private final int n;public SumTask(int n) { this.n = n; }@Overridepublic 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
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
基本用法
public class Counter {private int c = 0;public synchronized void inc() { c++; }public synchronized int get() { return c; }
}
- 监视器锁(对象锁/类锁),可重入。
- 可见性与原子性得到保障,
synchronized
的释放-获取建立 happens-before 关系。
对象锁 vs. 类锁
public class LockTypes {private static int s;private int i;public synchronized void objLock() { i++; } // 对象锁public static synchronized void classLock() { s++; } // 类锁
}
2)ReentrantLock
与 Condition
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
- 保证 可见性 与 禁止指令重排,但 不保证复合操作原子性。
public class VolatileDemo {volatile boolean running = true;void stop() { running = false; }
}
2)原子类 AtomicInteger
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
:读写锁,提高读多写少场景吞吐。
代码速览
// 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();
}
六、阻塞队列与经典“生产者-消费者”
import java.util.concurrent.*;public class ProducerConsumer {private final BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);public void start() {ExecutorService pool = Executors.newCachedThreadPool();// Producerpool.execute(() -> {int i = 0;try {while (i < 20) {queue.put(i);System.out.println("P -> " + i);i++;}} catch (InterruptedException ignored) {}});// Consumerpool.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
的使用与清理
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)死锁复现
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
:组合异步编程
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 条件队列(附最小演示)
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:读写锁适用场景?
答:读多写少、读操作占绝对多数,且读之间可以并行的场景。注意写锁会阻塞读,避免写偏斜。
十三、实战:批量并发调用 + 超时与降级
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 密集的网关、计算密集的风控特征工程)时需要专项调参,可以在此文的代码骨架上再做针对性实验。