Java 多线程模型与核心知识点详解

深入解析 Java 多线程编程的核心概念、内存模型、同步机制、线程池及常用并发工具类,帮助开发者建立系统的并发编程知识体系。

一、基础概念

1.1 进程与线程

进程是操作系统资源分配的基本单位,拥有独立的内存空间。线程是 CPU 调度的基本单位,同一进程内的多个线程共享进程的堆内存和方法区,但每个线程拥有独立的程序计数器、虚拟机栈和本地方法栈。

1.2 并发与并行

  • 并发(Concurrency):多个任务在同一时间段内交替执行,宏观上同时进行,微观上串行。
  • 并行(Parallelism):多个任务在同一时刻真正同时执行,需要多核 CPU 支持。

1.3 上下文切换

CPU 通过时间片轮转调度线程,切换时需要保存当前线程状态并加载下一线程状态,产生上下文切换开销。减少不必要的线程创建和锁竞争是降低切换开销的关键。


二、Java 线程的创建与生命周期

2.1 创建线程的三种方式

  1. 继承 Thread 类

    class MyThread extends Thread {
        @Override
        public void run() {
            System.out.println("Thread running");
        }
    }
    
  2. 实现 Runnable 接口

    class MyRunnable implements Runnable {
        @Override
        public void run() {
            System.out.println("Runnable running");
        }
    }
    
  3. 实现 Callable 接口(带返回值,配合 FutureTask)

    Callable<String> callable = () -> "result";
    FutureTask<String> futureTask = new FutureTask<>(callable);
    new Thread(futureTask).start();
    

推荐优先使用 Runnable 或 Callable,避免单继承限制,实现代码与执行解耦。

2.2 线程生命周期

Java 线程在 java.lang.Thread.State 中定义了六种状态:

状态 说明
NEW 新建,未调用 start()
RUNNABLE 就绪或运行中,等待 CPU 调度
BLOCKED 阻塞,等待 monitor lock(如进入 synchronized 块)
WAITING 无限期等待,如调用 wait()、join() 不超时
TIMED_WAITING 限期等待,如 sleep(ms)、wait(ms)、join(ms)
TERMINATED 执行完毕或异常退出

三、Java 内存模型(JMM)

3.1 主内存与工作内存

JMM 规定所有变量存储在主内存,每个线程拥有自己的工作内存。线程对变量的读写必须在工作内存中进行,再通过特定协议与主内存同步。

3.2 三大特性

  1. 原子性:一个操作不可中断。基本类型赋值具有原子性(long/double 在 64 位 JVM 下通常也是),但复合操作如 i++ 不保证原子性。
  2. 可见性:一个线程修改共享变量后,其他线程能立即看到最新值。
  3. 有序性:禁止指令重排序导致的逻辑错误。

3.3 happens-before 规则

JMM 定义了无需同步即可保证可见性的偏序关系,包括:

  • 程序次序规则
  • 锁规则(unlock happens-before 后序 lock)
  • volatile 规则(写 happens-before 后序读)
  • 传递性

四、线程同步机制

4.1 synchronized 关键字

  • 修饰实例方法:锁当前对象实例(this
  • 修饰静态方法:锁类的 Class 对象
  • 修饰代码块:显式指定锁对象
public synchronized void method() {}       // 实例锁
public static synchronized void method() {} // 类锁
synchronized(obj) { }                      // 对象锁

synchronized 底层依赖对象头的 Mark Word 和 monitor 机制,JDK 6 后引入偏向锁、轻量级锁、重量级锁的锁升级过程,减少无竞争场景下的性能损耗。

4.2 volatile 关键字

  • 保证变量对所有线程的可见性
  • 禁止指令重排序
  • 不保证原子性

适用场景:状态标志位、单例模式的双重检查锁定(DCL)。

4.3 Lock 接口与 ReentrantLock

java.util.concurrent.locks.Lock 提供比 synchronized 更灵活的锁操作:

Lock lock = new ReentrantLock();
lock.lock();
try {
    // 临界区
} finally {
    lock.unlock();
}

特性:

  • 可中断锁lockInterruptibly()
  • 超时获取tryLock(long time, TimeUnit unit)
  • 公平锁:构造参数 true 启用公平策略
  • 条件队列newCondition() 实现精确唤醒

4.4 读写锁 ReentrantReadWriteLock

  • 读锁:共享锁,多个读线程可同时持有
  • 写锁:独占锁,写线程独占访问

适用于读多写少的场景,提升并发性能。JDK 8 后推荐使用性能更优的 StampedLock

4.5 StampedLock

提供三种访问模式:

  • 写锁:独占
  • 悲观读锁:共享,但会阻塞写锁
  • 乐观读:无锁读取,读取后需通过 validate(stamp) 确认数据未被修改

五、线程间通信

5.1 wait / notify / notifyAll

  • 必须在同步代码块内调用
  • wait() 释放锁并进入 WAITING 状态
  • notify() 唤醒单个等待线程,notifyAll() 唤醒全部

经典模式:生产者-消费者

synchronized (queue) {
    while (queue.isEmpty()) {
        queue.wait();
    }
    queue.poll();
    queue.notifyAll();
}

5.2 Condition

ReentrantLock 的配套条件队列,支持多个等待集:

Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();

相比 wait/notifyCondition 可以精确控制唤醒对象,避免全部唤醒带来的性能损耗。


六、线程池与 Executor 框架

6.1 线程池优势

  • 降低线程创建与销毁的开销
  • 提高响应速度
  • 便于线程管理与监控
  • 控制并发线程数量

6.2 ThreadPoolExecutor 核心参数

public ThreadPoolExecutor(
    int corePoolSize,      // 核心线程数
    int maximumPoolSize,   // 最大线程数
    long keepAliveTime,    // 非核心线程空闲存活时间
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue, // 任务队列
    ThreadFactory threadFactory,       // 线程工厂
    RejectedExecutionHandler handler   // 拒绝策略
)

6.3 常用任务队列

队列 特性
SynchronousQueue 直接提交,无缓冲
LinkedBlockingQueue 无界队列,可能导致 OOM
ArrayBlockingQueue 有界数组队列,需指定容量

6.4 拒绝策略

  • AbortPolicy(默认):抛出异常
  • CallerRunsPolicy:由调用线程执行任务
  • DiscardPolicy:静默丢弃
  • DiscardOldestPolicy:丢弃队列最老任务

6.5 内置线程池(慎用)

  • Executors.newFixedThreadPool(n):固定核心数,无界队列,有 OOM 风险
  • Executors.newCachedThreadPool():允许无限创建线程,有资源耗尽风险
  • Executors.newSingleThreadExecutor():单线程,无界队列

阿里巴巴 Java 开发手册建议通过 ThreadPoolExecutor 手动创建线程池,明确参数含义。

6.6 生命周期

  • RUNNING:接受新任务并处理队列任务
  • SHUTDOWN:不接受新任务,但处理队列中剩余任务
  • STOP:不接受新任务,不处理队列任务,中断正在执行的任务
  • TIDYING:所有任务终止,执行 terminated()
  • TERMINATED:terminated() 执行完毕

七、常用并发工具类

7.1 CountDownLatch

允许一个或多个线程等待其他线程完成操作。

CountDownLatch latch = new CountDownLatch(3);
// 每个子任务执行完调用 latch.countDown()
latch.await(); // 主线程等待

7.2 CyclicBarrier

使一组线程互相等待到达公共屏障点,可循环使用。

CyclicBarrier barrier = new CyclicBarrier(3);
barrier.await();

7.3 Semaphore

控制同时访问某资源的线程数量(限流)。

Semaphore semaphore = new Semaphore(10);
semaphore.acquire();   // 获取许可
semaphore.release();   // 释放许可

7.4 CompletableFuture

JDK 8 引入的异步编程工具,支持链式组合、异常处理、多任务组合:

CompletableFuture.supplyAsync(() -> fetchData())
    .thenApply(this::process)
    .thenAccept(this::save)
    .exceptionally(ex -> { log.error(ex); return null; });

7.5 Exchanger

用于两个线程之间交换数据,适用遗传算法、管道设计等场景。


八、原子类与 CAS

java.util.concurrent.atomic 包提供无锁原子操作类:

  • AtomicIntegerAtomicLongAtomicBoolean
  • AtomicReferenceAtomicStampedReference(解决 ABA 问题)
  • LongAdder:高并发下比 AtomicLong 性能更优,采用分段累加

底层依赖 CAS(Compare-And-Swap) 操作,由 CPU 指令(如 cmpxchg)保证原子性。

CAS 三大问题:

  1. ABA 问题:值从 A 变 B 又变 A,无法感知中间变化。解决方案:AtomicStampedReference
  2. 自旋开销:高竞争下长时间自旋消耗 CPU
  3. 只能保证单个变量原子性

九、并发集合

非线程安全 线程安全替代
ArrayList CopyOnWriteArrayList
HashMap ConcurrentHashMap
HashSet ConcurrentHashMap.newKeySet() / CopyOnWriteArraySet
TreeMap ConcurrentSkipListMap

9.1 ConcurrentHashMap

  • JDK 7:分段锁(Segment),默认 16 段
  • JDK 8:CAS + synchronized 锁单个桶头节点,结构类似 HashMap(数组+链表+红黑树)

9.2 CopyOnWriteArrayList

读操作无锁,写操作复制新数组。适用于读多写极少场景,写操作开销大。


十、Fork/Join 框架

JDK 7 引入的并行计算框架,基于**工作窃取(Work-Stealing)**算法:

class MyTask extends RecursiveTask<Integer> {
    @Override
    protected Integer compute() {
        if (taskTooSmall()) {
            return computeDirectly();
        }
        MyTask left = new MyTask(...);
        MyTask right = new MyTask(...);
        left.fork();
        right.fork();
        return left.join() + right.join();
    }
}

ForkJoinPool.commonPool()CompletableFutureParallel Stream 等底层复用。


十一、ThreadLocal

为每个线程提供独立的变量副本,实现线程隔离。

ThreadLocal<Integer> local = ThreadLocal.withInitial(() -> 0);
local.set(1);
int value = local.get();

内存泄漏风险:ThreadLocal 作为 Key 被 ThreadLocalMap 持有,若线程长期存活(如线程池中的线程)且未手动 remove(),会导致 Value 无法回收。

最佳实践:

  • 使用 try { ... } finally { local.remove(); }
  • JDK 8 后可用 InheritableThreadLocal 实现子线程继承父线程值
  • 在线程池场景下尤其注意清理

十二、死锁与避免策略

12.1 死锁产生的四个必要条件

  1. 互斥条件:资源独占
  2. 请求与保持:持有资源同时请求新资源
  3. 不剥夺条件:资源只能主动释放
  4. 循环等待条件:形成等待环路

12.2 避免策略

  • 资源一次性分配:一次性申请所有所需资源
  • 有序加锁:所有线程按固定顺序获取锁
  • 超时放弃:使用 tryLock(timeout)
  • 死锁检测:通过线程 dump 分析

十三、总结

Java 多线程编程从底层的线程创建、生命周期管理,到 JMM 内存模型的理解,再到 synchronized、volatile、Lock 等同步机制,以及线程池、原子类、并发集合和 Fork/Join 框架,构成了完整的并发知识体系。

关键要点:

  • 理解 JMM 的可见性、原子性、有序性
  • 优先使用线程池管理线程生命周期
  • 选择合适的同步机制(synchronized 简单够用,Lock 更灵活)
  • 高并发下善用原子类和 LongAdder
  • 注意 ThreadLocal 的内存泄漏问题
  • 避免使用 Executors 的便捷方法创建无界线程池

掌握这些知识点,可以帮助开发者在实际项目中编写出高效、稳定、可维护的多线程程序。

Interaction

读完之后

分享海报
Interaction

评论区