Java线程同步机制与线程池深度原理、实战与选型详解

在Java并发编程体系中,线程安全是业务系统稳定运行的核心基石。多线程并发场景下,会出现共享变量竞争、内存数据不一致、指令执行紊乱、线程资源耗尽等一系列问题。为解决上述问题,Java提供了两套核心线程同步机制:基于JVM底层实现的内置锁 synchronized、基于JDK代码层面实现的Lock锁体系,同时通过 volatile 关键字辅助解决内存可见性与指令重排问题。而线程池作为线程复用、线程资源管控的核心工具,是所有并发业务开发的必备组件。

本文将深度拆解synchronized底层锁升级机制、Lock三大核心锁底层原理与选型策略、volatile关键字底层特性,同时全方位讲解ThreadPoolExecutor核心原理、内置线程池场景、线上调优实战、自定义业务线程池,搭配海量代码案例、业务场景举例、底层源码解析,全方位覆盖Java并发核心知识点。

一、synchronized 底层实现原理与锁机制

synchronized 是Java原生内置的同步关键字,由JVM底层实现,无需手动加锁解锁,不会出现锁泄漏问题,是并发编程中最基础、最常用的线程同步工具。其核心作用是保证原子性、可见性、有序性,可以修饰实例方法、静态方法、代码块,分别对应对象锁、类锁、局部代码块锁。

在JDK1.6之前,synchronized 属于重量级锁,性能较差;JDK1.6对其进行了大量优化,引入偏向锁、轻量级锁、重量级锁的锁升级机制,同时新增锁消除、锁粗化优化策略,大幅提升了内置锁的并发性能。想要彻底理解synchronized,必须先掌握Java对象头与监视器锁(Monitor)底层结构。

1.1 Java 对象头结构

Java中所有对象的锁信息,全部存储在对象头(Object Header中,对象头是synchronized锁机制的底层载体。在HotSpot虚拟机中,对象在内存中的布局分为三部分:对象头、实例数据、对齐填充。其中对象头包含两类核心数据:Mark Word(标记字)、Klass Pointer(类型指针),数组对象会额外占用4字节存储数组长度。

1.1.1 Mark Word(核心锁存储区域)

Mark Word 占用8字节(64位),是对象头最核心的部分,用于存储对象的哈希码、GC分代年龄、锁状态标志、线程偏向ID等核心信息。为了节省内存空间,Mark Word 采用动态复用机制,不同锁状态下,存储的数据结构完全不同。

64位JVM下Mark Word 存储规则:

  • 无锁状态:25位哈希码 + 1位是否偏向锁(0) + 2位锁标志位(01)+ 4位GC年龄 + 1位是否调用偏向锁撤销 + 31位未使用
  • 偏向锁状态:54位偏向线程ID + 1位是否偏向锁(1) + 2位锁标志位(01)+ 4位GC年龄 + 3位时间戳
  • 轻量级锁状态:62位栈中锁记录指针 + 2位锁标志位(00)
  • 重量级锁状态:62位Monitor监视器指针 + 2位锁标志位(10)
  • GC标记状态:无有效数据,2位锁标志位(11)

由此可见,JVM通过Mark Word 中锁标志位+偏向锁标识位区分当前对象的锁状态,这也是锁升级机制的底层基础。

1.1.2 Klass Pointer

类型指针占用4字节(开启指针压缩)或8字节,用于指向对象对应的类元数据,JVM通过该指针确定对象属于哪个类,该区域不参与锁机制运算。

1.2 Monitor 监视器锁原理

synchronized 底层依赖 ObjectMonitor(监视器) 实现,每个Java对象都绑定一个独立的Monitor对象,Monitor是C++实现的底层对象,核心属性如下:

  • _owner:指向当前持有锁的线程,表示锁的独占持有者
  • _WaitSet:等待集合,存储所有调用wait()方法释放锁、进入阻塞等待的线程
  • _EntryList:阻塞集合,存储所有竞争锁失败、处于阻塞状态的线程
  • _count:锁计数器,实现锁的可重入特性,线程每获取一次锁,计数器+1,释放锁计数器-1

当线程执行synchronized同步代码时,首先会尝试获取对象的Monitor锁:如果Monitor的_owner为null,线程成功持有锁,赋值_owner为当前线程;如果锁已被占用,线程进入_EntryList阻塞等待;如果持有锁的线程调用wait(),则释放锁并进入_WaitSet等待,等待被notify()唤醒后重新进入锁竞争。

1.3 synchronized 锁升级完整机制(核心)

JDK1.6之后,synchronized 不会直接触发重量级锁,而是根据并发竞争激烈程度自动完成锁升级,升级顺序固定:偏向锁轻量级锁重量级锁,且锁只能升级、不能降级,目的是最大限度降低锁竞争带来的性能损耗。

1.3.1 偏向锁(无竞争场景最优解)

适用场景:全程只有单个线程竞争锁,无多线程并发竞争,是绝大多数单机低并发业务的场景。偏向锁的核心思想是:锁偏向于第一个获取它的线程,后续该线程重复获取锁时,无需任何加锁解锁操作,零开销。

当对象第一次被线程获取锁时,JVM会将Mark Word 中的偏向线程ID设置为当前线程ID,偏向锁标识位改为1。后续该线程再次获取锁,只需对比线程ID,一致则直接获取锁,无需CAS操作。

偏向锁撤销场景:当出现第二个线程竞争锁时,偏向锁失效,触发锁升级。偏向锁撤销需要等待全局安全点,暂停当前持有锁的线程,遍历线程栈判断锁使用情况,完成撤销并升级为轻量级锁。

代码案例:偏向锁生效场景

java
/**
 *
单线程循环获取锁,触发偏向锁生效
 */
public class BiasLockDemo {
    // 定义共享锁对象
    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        // 单线程循环1000次获取锁,无竞争
        for (int i = 0; i < 1000; i++) {
            synchronized (LOCK) {
                // 模拟业务执行
                System.out.println("单线程执行同步业务");
            }
        }
    }
}

 

上述代码中全程只有主线程竞争锁,JVM会自动开启偏向锁优化,所有锁获取操作无CAS开销,性能极高。偏向锁默认开启,启动参数可通过 -XX:-UseBiasedLocking 手动关闭。

1.3.2 轻量级锁(轻微竞争场景)

适用场景:多线程交替竞争锁,同一时刻最多只有一个线程竞争锁,竞争不激烈。轻量级锁不阻塞线程,基于自旋CAS实现锁竞争,避免了用户态与内核态切换的开销。

锁升级流程:当偏向锁遇到第二个线程竞争时,撤销偏向锁,当前线程会在自己的线程栈中创建锁记录(Lock Record,通过CAS操作将对象头的Mark Word 复制到锁记录中,并将对象头指针指向当前线程栈的锁记录,完成轻量级锁加锁。

当线程竞争锁失败时,不会直接阻塞,而是进行自适应自旋:JDK1.6之前自旋次数固定,JDK1.6之后引入自适应自旋,根据历史锁竞争成功率动态调整自旋次数。如果自旋成功则获取锁,自旋失败则升级为重量级锁。

代码案例:轻量级锁竞争场景

java
/**
 *
两个线程交替竞争锁,触发轻量级锁
 */
public class LightLockDemo {
    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        // 线程1
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (LOCK) {
                    System.out.println("线程1执行,次数:" + i);
                    try {
                        // 短暂休眠,让线程2交替获取锁,避免同时竞争
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "thread-1").start();

        // 线程2
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                synchronized (LOCK) {
                    System.out.println("线程2执行,次数:" + i);
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }, "thread-2").start();
    }
}

 

该案例中两个线程交替获取锁,无并发抢占,JVM会启用轻量级锁,通过自旋CAS完成锁竞争,无线程阻塞,性能优于重量级锁。

1.3.3 重量级锁(高并发竞争场景)

适用场景:多线程同一时刻并发抢占锁,竞争激烈,自旋多次失败。当轻量级锁自旋耗尽、仍无法获取锁时,会升级为重量级锁。

重量级锁基于操作系统内核互斥量实现,竞争失败的线程会进入内核态阻塞,释放CPU资源,避免空自旋消耗CPU。其缺点是会触发用户态与内核态切换,开销较大,性能最低,但可以保证高并发下线程安全。

代码案例:重量级锁触发场景

java
/**
 * 10
个线程同时抢占锁,高并发竞争触发重量级锁
 */
public class HeavyLockDemo {
    private static final Object LOCK = new Object();

    public static void main(String[] args) {
        // 启动10个线程并发竞争锁
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                synchronized (LOCK) {
                    // 模拟耗时业务,加剧锁竞争
                    try {
                        Thread.sleep(100);
                        System.out.println(Thread.currentThread().getName() + " 获取重量级锁执行业务");
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }, "thread-" + i).start();
        }
    }
}

 

10个线程同时抢占唯一锁,锁竞争极其激烈,轻量级锁自旋全部失败,JVM自动升级为重量级锁,未获取锁的线程全部阻塞等待。

1.4 synchronized 额外优化机制

1.4.1 锁粗化

当JVM检测到同一个线程频繁、连续对同一个对象加锁解锁(例如循环内加锁),会将多次细小的锁操作粗化为一次全局锁操作,避免频繁加锁解锁的性能损耗。例如循环1000次同步代码块,锁粗化后只会加锁一次、解锁一次。

1.4.2 锁消除

JVM通过逃逸分析,判断同步对象仅在方法内部使用,不会逃逸到外部、不会存在线程竞争时,会直接消除锁操作,彻底规避锁开销。最典型的场景是StringBuffer的append方法,局部StringBuffer对象的同步锁会被自动消除。

二、Lock 锁体系底层原理与选型实战

synchronized 作为内置锁,存在灵活性不足、无法中断锁等待、无法实现读写分离、无超时锁等缺陷。因此JDK1.5引入 java.util.concurrent.locks 锁体系,基于AQS(抽象队列同步器)纯Java代码实现,包含三大核心锁:ReentrantLock、ReentrantReadWriteLock、StampedLock。Lock锁灵活性远超synchronized,可适配复杂的并发业务场景。

所有Lock锁的底层核心均为 AQSAbstractQueuedSynchronizer,AQS定义了锁竞争、线程排队、阻塞唤醒的通用逻辑,通过volatile修饰的state变量表示锁状态,通过双向队列存储阻塞等待的线程。

2.1 ReentrantLock 可重入独占锁

ReentrantLock(可重入锁)是最常用的显式独占锁,功能完全覆盖synchronized,且扩展性更强。支持可重入、公平锁/非公平锁、锁超时、可中断锁、条件变量唤醒等特性。

2.1.1 底层原理

ReentrantLock 内部定义静态内部类Sync继承AQS,分为FairSync(公平锁)、NonfairSync(非公平锁)两个实现类:

  • state变量:0表示锁空闲,大于0表示锁被占用,数值代表锁重入次数
  • 可重入原理:当前持有锁的线程再次加锁,state+1;释放锁时state-1,state归0代表锁完全释放
  • 公平锁:严格按照线程入队顺序获取锁,先到先得,无线程饥饿问题,性能略低
  • 非公平锁:线程获取锁时直接抢占,不排队,吞吐量更高,默认模式,可能出现线程饥饿

2.1.2 核心特性代码案例

1、锁可重入案例

java
import java.util.concurrent.locks.ReentrantLock;

/**
 * ReentrantLock
可重入特性演示
 */
public class ReentrantLockReentryDemo {
    // 默认非公平可重入锁
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        // 第一次加锁
        LOCK.lock();
        try {
            System.out.println("第一次获取锁,重入次数:" + LOCK.getHoldCount());
            // 第二次加锁,可重入,不会死锁
            LOCK.lock();
            try {
                System.out.println("第二次获取锁,重入次数:" + LOCK.getHoldCount());
            } finally {
                // 逐层释放锁
                LOCK.unlock();
            }
        } finally {
            LOCK.unlock();
        }
    }
}

 

2、锁超时特性(避免死锁核心方案)

java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;

/**
 *
锁超时机制,防止线程死锁阻塞
 */
public class ReentrantLockTimeoutDemo {
    private static final ReentrantLock LOCK = new ReentrantLock();

    public static void main(String[] args) {
        // 线程1永久持有锁
        new Thread(() -> {
            LOCK.lock();
            try {
                System.out.println("线程1永久持有锁");
                TimeUnit.SECONDS.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                LOCK.unlock();
            }
        }).start();

        // 线程2尝试获取锁,超时3秒自动放弃
        new Thread(() -> {
            boolean tryLock = false;
            try {
                // 3秒获取不到锁直接返回false
                tryLock = LOCK.tryLock(3, TimeUnit.SECONDS);
                if (tryLock) {
                    System.out.println("线程2获取锁成功");
                } else {
                    System.out.println("线程2获取锁超时,放弃竞争");
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                if (tryLock) {
                    LOCK.unlock();
                }
            }
        }).start();
    }
}

2.1.3 ReentrantLock synchronized 选型对比

对比维度

synchronized

ReentrantLock

实现方式

JVM底层原生实现

JDK代码AQS实现

锁类型

默认非公平,不可手动修改

支持公平/非公平锁手动配置

功能拓展

功能单一,无超时、中断机制

支持锁超时、可中断、条件变量

使用复杂度

简单,自动加锁解锁,无泄漏

复杂,需手动unlock,易锁泄漏

适用场景

简单同步场景、低并发场景

复杂并发、需要超时、公平锁场景

2.2 ReentrantReadWriteLock 读写锁

ReentrantLock 是独占锁,同一时刻仅允许一个线程执行,但是绝大多数业务场景都是读多写少(例如商品查询、配置读取),独占锁会严重降低并发吞吐量。因此JDK提供 ReentrantReadWriteLock(可重入读写锁),实现读写分离

核心锁规则:读读共享、读写互斥、写写互斥。即多个读线程可同时获取锁,提升查询吞吐量;写线程与所有读写线程互斥,保证数据一致性。

2.2.1 底层原理

基于AQS实现,将state变量拆分为高低16位:高16位表示读锁计数,低16位表示写锁计数。完美实现读写锁状态分离统计,同时支持读写锁可重入。

额外支持锁降级特性:写锁可以降级为读锁(持有写锁 -> 获取读锁 -> 释放写锁),保证数据可见性;不支持读锁升级为写锁,避免并发死锁。

2.2.2 业务实战案例(商品缓存读写)

java
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.locks.ReentrantReadWriteLock;

/**
 *
基于读写锁实现高性能本地缓存(读多写少场景)
 */
public class ReadWriteLockCacheDemo {
    // 本地缓存容器
    private static final Map<String, Object> CACHE_MAP = new HashMap<>();
    // 读写锁
    private static final ReentrantReadWriteLock RW_LOCK = new ReentrantReadWriteLock();
    private static final ReentrantReadWriteLock.ReadLock READ_LOCK = RW_LOCK.readLock();
    private static final ReentrantReadWriteLock.WriteLock WRITE_LOCK = RW_LOCK.writeLock();

    // 读缓存
    public static Object get(String key) {
        READ_LOCK.lock();
        try {
            // 多线程可同时读,高并发无阻塞
            return CACHE_MAP.get(key);
        } finally {
            READ_LOCK.unlock();
        }
    }

    // 写缓存
    public static void put(String key, Object value) {
        WRITE_LOCK.lock();
        try {
            // 写操作独占锁,保证数据安全
            CACHE_MAP.put(key, value);
        } finally {
            WRITE_LOCK.unlock();
        }
    }

    public static void main(String[] args) {
        // 100个读线程,并发查询缓存
        for (int i = 0; i < 100; i++) {
            new Thread(() -> get("goodsInfo")).start();
        }
        // 1个写线程,更新缓存
        new Thread(() -> put("goodsInfo", "商品详情数据")).start();
    }
}

 

2.3 StampedLock 乐观读写锁

ReentrantReadWriteLock 存在读锁饥饿问题:大量读线程持续占用读锁,写线程一直无法获取锁,导致写操作永久阻塞。JDK1.8 新增 StampedLock,解决读写锁饥饿问题,性能优于传统读写锁。

StampedLock 摒弃了传统读写互斥思维,引入乐观读模式,三种工作模式:

  1. 乐观读:无锁读取,不阻塞写线程,通过版本戳(stamp)校验数据是否被修改
  1. 悲观读:与传统读锁一致,阻塞写线程
  1. 写锁:独占锁,阻塞所有读写线程

2.3.1 核心实战案例

java
import java.util.concurrent.locks.StampedLock;

/**
 * StampedLock
乐观读实战,解决读锁饥饿
 */
public class StampedLockDemo {
    private static final StampedLock STAMPED_LOCK = new StampedLock();
    private static int data = 0;

    // 乐观读模式
    public static int optimisticRead() {
        // 获取乐观读版本戳
        long stamp = STAMPED_LOCK.tryOptimisticRead();
        int currentData = data;
        // 校验数据是否被写线程修改
        if (!STAMPED_LOCK.validate(stamp)) {
            // 数据被修改,升级为悲观读锁
            stamp = STAMPED_LOCK.readLock();
            try {
                currentData = data;
            } finally {
                STAMPED_LOCK.unlockRead(stamp);
            }
        }
        return currentData;
    }

    // 写锁模式
    public static void writeData(int newData) {
        long stamp = STAMPED_LOCK.writeLock();
        try {
            data = newData;
        } finally {
            STAMPED_LOCK.unlockWrite(stamp);
        }
    }
}

 

2.4 三大Lock锁选型标准(生产必备)

  1. 独占同步场景:无读写区分,单纯保证线程安全,优先 ReentrantLock,支持超时、中断,灵活性最高
  1. 标准读多写少场景:无频繁写操作,允许轻微写饥饿,优先 ReentrantReadWriteLock,API简单稳定
  1. 超高并发读、频繁写场景:需要避免写线程饥饿,优先 StampedLock,吞吐量最高
  1. 简单低并发场景:直接使用 synchronized,减少代码复杂度,避免锁泄漏

三、volatile 关键字深度原理

volatile 是Java轻量级同步关键字,不具备互斥性、不保证原子性,核心作用是保证内存可见性、禁止指令重排序,是并发编程的辅助同步工具,常用于状态标记、双重检查锁单例等场景,开销极低。

3.1 JMM内存模型基础

Java内存模型(JMM)规定:所有变量存储在主内存,每个线程拥有独立的工作内存(CPU缓存)。线程读取变量时,会从主内存拷贝副本到工作内存,读写操作均基于工作内存,多线程下会出现数据不一致问题,volatile 就是为解决JMM内存交互缺陷设计。

3.2 内存可见性原理

可见性问题:普通变量修改后,仅更新当前线程工作内存,不会立刻同步到主内存,其他线程无法感知数据修改,导致脏数据。

volatile 修饰变量时,会强制实现两点规则:

  1. 线程修改volatile变量后,立刻刷新到主内存
  1. 线程读取volatile变量前,清空工作内存缓存,强制从主内存读取最新数据

可见性代码案例

java
/**
 * volatile
保证内存可见性演示
 */
public class VolatileVisibleDemo {
    // 不加volatile,主线程无法感知变量修改
    private static volatile boolean flag = false;

    public static void main(String[] args) throws InterruptedException {
        // 子线程修改flag
        new Thread(() -> {
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            flag = true;
            System.out.println("子线程修改flag为true");
        }).start();

        // 主线程循环检测flag
        while (!flag) {
            // 循环等待
        }
        System.out.println("主线程感知到flag变更,退出循环");
    }
}

 

去除volatile关键字后,主线程会永久死循环;添加volatile后,主线程可实时感知变量修改。

3.3 禁止指令重排序原理

为提升执行效率,编译器和CPU会对无依赖的指令进行指令重排序,单线程下重排序不会影响结果,多线程下会出现业务逻辑错乱。volatile 通过内存屏障禁止指令重排。

内存屏障规则:

  • 写屏障:volatile写操作之前的指令,禁止重排到写操作之后
  • 读屏障:volatile读操作之后的指令,禁止重排到读操作之前

经典场景:双重检查锁单例(必须使用volatile

java
/**
 * volatile
禁止指令重排,解决单例空指针问题
 */
public class SingletonDemo {
    // 必须加volatile,禁止对象初始化指令重排
    private static volatile SingletonDemo instance;

    // 私有构造
    private SingletonDemo() {}

    public static SingletonDemo getInstance() {
        if (instance == null) {
            synchronized (SingletonDemo.class) {
                if (instance == null) {
                    // 对象初始化分为三步:分配内存、初始化对象、引用赋值
                    // 无volatile会重排为:分配内存、引用赋值、初始化对象,导致返回未初始化对象
                    instance = new SingletonDemo();
                }
            }
        }
        return instance;
    }
}

 

3.4 volatile 不保证原子性的核心原因

原子性指一组操作要么全部执行成功,要么全部失败。volatile 仅保证单次读写操作原子性,不保证复合操作原子性(例如i++、i+=1)。

核心原理:i++ 并非单条指令,分为三步:读取变量、变量自增、写入变量。volatile 只能保证每一步的可见性,无法保证三步操作整体不可中断,多线程并发下会出现数据覆盖,导致数据丢失。

原子性失效案例

java
/**
 * volatile
不保证复合操作原子性
 */
public class VolatileAtomicDemo {
    private static volatile int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 10个线程,每个线程累加1000次
        for (int i = 0; i < 10; i++) {
            new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    count++;
                }
            }).start();
        }
        Thread.sleep(3000);
        // 最终结果永远小于10000
        System.out.println("最终累加值:" + count);
    }
}

 

生产中如需保证复合操作原子性,需配合 synchronized、ReentrantLock 或 Atomic 原子类使用。

四、线程池深度原理、选型与调优实战

Java线程是操作系统稀缺资源,线程的创建、销毁、上下文切换开销极大。如果业务中频繁创建销毁线程,会严重浪费CPU资源、降低系统吞吐量,甚至导致OOM。线程池通过线程复用机制,统一管理线程生命周期、管控任务队列、限制并发数量,是Java并发业务的核心组件。

4.1 ThreadPoolExecutor 七大核心参数

所有线程池底层均基于 ThreadPoolExecutor 实现,其构造方法包含七大核心参数,决定线程池的运行规则、并发能力、容错策略。

java
public ThreadPoolExecutor(int corePoolSize,
                          int maximumPoolSize,
                          long keepAliveTime,
                          TimeUnit unit,
                          BlockingQueue<Runnable> workQueue,
                          ThreadFactory threadFactory,
                          RejectedExecutionHandler handler)

 

  1. corePoolSize(核心线程数):线程池常驻线程数量,线程池初始化后,除非手动关闭,否则核心线程永久存活,不会被回收
  1. maximumPoolSize(最大线程数):线程池允许创建的最大线程数量,核心线程满、队列满后,创建非核心线程处理任务
  1. keepAliveTime(空闲超时时间):非核心线程空闲等待任务的最大时长,超时后自动被回收,释放资源
  1. unit(时间单位):空闲超时时间单位
  1. workQueue(任务阻塞队列):存储等待执行的任务,核心线程全部繁忙时,新任务进入队列排队
  1. threadFactory(线程工厂):用于创建线程,可自定义线程名称、优先级、守护线程属性,方便线上问题排查
  1. handler(拒绝策略):当核心线程满、队列满、最大线程数满,线程池无法处理新任务时的容错策略

4.2 线程池完整工作原理

当新任务提交到线程池时,严格按照以下顺序执行,优先级:核心线程 > 阻塞队列 > 非核心线程 > 拒绝策略

  1. 线程池接收新任务,判断当前运行线程数是否小于核心线程数,是则新建核心线程执行任务
  1. 核心线程已满,判断阻塞队列是否已满,队列未满则将任务存入队列排队
  1. 队列已满,判断当前线程数是否小于最大线程数,是则新建非核心线程执行任务
  1. 线程数已达到最大值,无法处理新任务,触发拒绝策略

4.3 四大内置线程池原理与适用场景

JDK封装了四种常用内置线程池,底层均为ThreadPoolExecutor,适配不同基础业务场景,但生产环境禁止直接使用内置线程池,存在资源溢出风险。

4.3.1 FixedThreadPool(固定线程池)

核心线程数=最大线程数,无非核心线程,空闲线程不会回收,队列是无界LinkedBlockingQueue。

特点:线程数量固定,无线程频繁创建销毁,性能稳定;无界队列可能堆积大量任务,触发OOM

适用场景CPU密集型、任务耗时稳定、并发量固定的业务,如数据计算、格式解析

4.3.2 CachedThreadPool(缓存线程池)

核心线程数为0,最大线程数无限,空闲线程超时60秒回收,队列是同步队列SynchronousQueue(不存储任务)。

特点:任务来了必创建线程,线程可无限扩容,空闲线程自动回收;高并发下会创建海量线程,耗尽服务器资源

适用场景短时、轻量、IO密集型、并发波动大的业务,如接口调用、文件读写

4.3.3 ScheduledThreadPool(定时线程池)

支持延迟执行、周期执行任务,基于DelayedWorkQueue延迟队列实现。

适用场景:定时任务、延时任务,如定时日志清理、定时数据同步、心跳检测

4.3.4 SingleThreadExecutor(单线程线程池)

全程只有一个线程执行任务,无界队列存储任务,保证任务串行执行。

适用场景需要任务有序执行、串行消费的业务,如消息队列消费、日志顺序写入

4.4 线程池拒绝策略详解与场景选型

JDK内置4种拒绝策略,均实现RejectedExecutionHandler接口:

  1. AbortPolicy(默认):直接抛出RejectedExecutionException异常,中断业务,适用于核心业务,快速暴露问题
  1. CallerRunsPolicy:由调用者线程执行任务,不抛异常、不丢失任务,适用于非核心、允许延迟的业务
  1. DiscardPolicy:直接丢弃任务,无异常,适用于非核心、可丢失的日志、统计类任务
  1. DiscardOldestPolicy:丢弃队列最旧的未执行任务,存入新任务,适用于时效性优先的业务

4.5 线程池生产调优实战(核心重点)

线程池调优的核心是:根据任务类型匹配线程数,最大化CPU利用率,避免线程阻塞、资源溢出。分为CPU密集型、IO密集型两类场景。

4.5.1 CPU密集型任务调优

任务大量占用CPU,无阻塞、无IO等待,线程过多会触发频繁上下文切换。

公式:核心线程数 = CPU核心数 + 1

业务场景:数据加密、算法计算、JSON解析、数据校验

4.5.2 IO密集型任务调优

任务大量阻塞在磁盘IO、网络IO、数据库查询,CPU空闲时间多,可扩容线程数提升吞吐量。

公式:核心线程数 = CPU核心数 * 2 或 CPU核心数 / (1 - 阻塞系数)

业务场景:数据库查询、HTTP接口调用、文件读写、Redis操作

4.5.3 通用生产调优规范

  • 队列必须使用有界队列,禁止无界队列,防止任务堆积OOM
  • 必须自定义线程工厂,命名线程名称,方便线上栈日志排查问题
  • 核心业务使用AbortPolicy快速报错,非核心业务使用CallerRunsPolicy保证任务不丢失
  • 必须手动设置线程池参数,禁止使用Executors内置线程池

4.6 自定义业务线程池(生产落地完整案例)

结合电商业务场景,自定义订单处理线程池,适配订单创建、订单支付、订单超时关闭等混合任务,包含自定义线程工厂、有界队列、自定义拒绝策略、线程池参数调优。

java
import java.util.concurrent.*;

/**
 *
电商订单自定义线程池(生产可用)
 * 适配IO密集型订单业务:数据库读写、RPC调用、消息发送
 */
public class OrderBusinessThreadPool {
    // 自定义线程工厂,统一线程命名
    private static class OrderThreadFactory implements ThreadFactory {
        private int count = 1;
        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r, "order-business-thread-" + count);
            count++;
            // 非守护线程,保证订单任务执行完成
            thread.setDaemon(false);
            return thread;
        }
    }

    // 自定义拒绝策略:订单任务优先不丢失,调用者执行+日志告警
    private static class OrderRejectPolicy implements RejectedExecutionHandler {
        @Override
        public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
            System.err.println("订单线程池已满,任务队列溢出,调用主线程执行任务");
            try {
                // 调用者线程执行,保证订单任务不丢失
                r.run();
            } catch (Exception e) {
                System.err.println("订单任务执行失败,触发降级告警");
            }
        }
    }

    // 初始化订单线程池
    public static ThreadPoolExecutor getOrderThreadPool() {
        // 8核CPU,IO密集型,核心线程16,最大线程32,队列容量200
        return new ThreadPoolExecutor(
                16,
                32,
                30L,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(200),
                new OrderThreadFactory(),
                new OrderRejectPolicy()
        );
    }

    // 测试订单任务处理
    public static void main(String[] args) {
        ThreadPoolExecutor orderThreadPool = getOrderThreadPool();
        // 模拟1000个订单任务
        for (int i = 0; i < 1000; i++) {
            int orderId = i;
            orderThreadPool.execute(() -> {
                System.out.println("处理订单:" + orderId + ",线程:" + Thread.currentThread().getName());
                try {
                    // 模拟订单数据库操作、RPC调用
                    Thread.sleep(50);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
        orderThreadPool.shutdown();
    }
}

 

该自定义线程池完全贴合电商订单业务,解决了内置线程池OOM、线程命名混乱、任务丢失等问题,可直接落地生产环境。

五、全文总结

本文全方位讲解了Java并发核心体系:synchronized基于对象头与Monitor实现锁升级,适配不同并发场景;Lock锁体系基于AQS实现,细分独占锁、读写锁、乐观锁,适配复杂业务;volatile作为轻量级同步关键字,解决可见性与重排序问题;线程池通过参数调优、自定义实现,解决线程资源滥用问题。

在生产开发中,简单同步场景优先使用synchronized;复杂同步、需要超时/公平锁使用ReentrantLock;读多写少使用读写锁;超高并发读写场景使用StampedLock;状态标记使用volatile;所有业务并发任务必须使用自定义有界线程池,严格规避并发安全与资源溢出问题。

更多推荐