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 密集的网关、计算密集的风控特征工程)时需要专项调参,可以在此文的代码骨架上再做针对性实验。