【Java后端】Java 多线程:从原理到实战,再到高频面试题

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)ReentrantLockCondition

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/thenAcceptthenCompose/thenCombineallOf/anyOfexceptionally/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 容易误用(丢信号、虚假唤醒),相比之下 ConditionBlockingQueue 更安全可控。


十二、面试高频题(含简答)

Q1:synchronizedReentrantLock 区别?

  • 语法层面:关键字 vs API 类。
  • 功能:ReentrantLock 支持可中断、定时、多个 Conditionsynchronized 无需手动释放,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 会内存泄漏吗?

:会。在线程池中线程长期存活,ThreadLocalMapEntry 使用弱引用指向 key,但 value 是强引用,需要手动 remove();否则可能泄漏或数据串线。

Q7:死锁产生的四个必要条件?

:互斥、占有且等待、不可剥夺、循环等待。破坏任一即可避免。

Q8:CountDownLatchCyclicBarrier 区别?

:前者一次性闭锁,减少到 0 即释放;后者可复用的屏障,固定 parties 个数,每轮到齐再放行。

Q9:CompletableFutureFuture 的区别?

Future 只能阻塞 getCompletableFuture 支持链式编排、组合、异常处理与回调,更适合复杂异步流程。

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)

  1. 线程池必须有界队列 + 合理拒绝策略。
  2. 共享变量要么不共享 ,要么只读 ,要么用锁/原子类保护。
  3. 在线程池中使用 ThreadLocal 必须 try...finally remove()
  4. 锁要细化但不碎片化,统一加锁顺序,能降级就降级为读写锁。
  5. 慎用 volatile 做计数器;避免"写-读-改-写"竞态。
  6. 超时与中断是一等公民 ,API 支持就用起来(tryLock(timeout)orTimeout)。
  7. 日志打点线程名、traceId,关键路径上报拒绝次数、等待时长、任务耗时分位数。

至此,你已经具备:能写、能读、能调优、能答题的并发基础。如果你在用到具体业务(例如 I/O 密集的网关、计算密集的风控特征工程)时需要专项调参,可以在此文的代码骨架上再做针对性实验。