1. 前言

        JUC,即Java Util Concurrent的缩写,指的是 Java 并发包 java.util.concurrent 及其子包。它提供了一套用于多线程编程的高级工具类,比如线程池(ExecutorService)、锁(Lock)、原子变量(AtomicInteger 等)、同步器(CountDownLatch、Semaphore、CyclicBarrier 等),简化了并发程序的开发。

        在传统的 Java 多线程开发中,开发者主要依赖 synchronized 关键字和 Object 类的 wait()/notify() 机制来实现线程同步。虽然这些基础工具能够满足简单的并发需求,但在面对复杂、高性能或高可靠性的并发场景时,往往显得力不从心——例如难以实现公平锁、无法中断等待中的线程、缺乏灵活的线程协作机制等。而 JUC 正是在这样的背景下应运而生,它由并发大师 Doug Lea 主导设计,基于 CAS(Compare-And-Swap)等底层无锁技术,提供了更高效、更安全、更易用的并发编程模型,极大提升了 Java 在高并发领域的开发效率与系统稳定性。

2. 如何创建线程?

2.1 创建线程的三种方式

在Java中,总共有三种创建线程的方式,分别是:

  1. 继承Thread类,重写run方法
  2. 实现Runnable接口,作为参数传入Thread中
  3. 实现Callable接口,通过FutureTask接收

接下来我们通过简单的示例进行演示

2.2 继承Thread类创建线程

2.2.1 代码示例

/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/3
 */
public class MyThread extends Thread {
    /**
     * If this thread was constructed using a separate
     * {@code Runnable} run object, then that
     * {@code Runnable} object's {@code run} method is called;
     * otherwise, this method does nothing and returns.
     * <p>
     * Subclasses of {@code Thread} should override this method.
     *
     * @see #start()
     * @see #stop()
     */
    @Override
    public void run() {
        System.out.println("这是线程" + this.getName());
    }
}
/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/3
 */
@Slf4j
public class Main {

    public static void main(String[] args) {
        new MyThread().start();
    }
}

2.2.2 源码探索

        我们通过源码,看看为什么继承Thread类,重写run方法可以实现创建线程

@Override
public void run() {
    if (target != null) {
        target.run();
    }
}

        我们可以看到,这里调用的,是target的run方法, 而target的类型则是Runnable,因此,我们重写run方法后,调用start方法本质上会走我们重写的run方法逻辑。

2.2.3 start方法和run方法的区别

        那么为什么我们需要通过start方法来启动线程,而不是run方法来启动呢?我们继续深入源码探索一下

public synchronized void start() {
    if (threadStatus != 0)
        throw new IllegalThreadStateException();
    
    group.add(this);
    boolean started = false;
    try {
        // 核心在这里
        start0();
        started = true;
    } finally {
        try {
            if (!started) {
                group.threadStartFailed(this);
            }
        } catch (Throwable ignore) {
            /* do nothing. If start0 threw a Throwable then
              it will be passed up the call stack */
        }
    }
}

// 本质上会调用这个方法来启动线程
private native void start0();

        我们可以看到,这里的start方法,调用了start0()方法,而start0()方法,是由native修饰的。而native修饰的方法表示该方法的实现是由 非 Java 代码(通常是 C/C++)编写的,运行在 Java 虚拟机(JVM)底层

        而start() 方法(通过底层的 start0())会创建一个新的操作系统线程,并在该新线程中自动调用 run() 方法。而调用 start() 的主线程会立即返回,不等待 run() 执行完毕,因此 run() 是在新线程中异步执行的。

所以,start()方法和run()方法的区别就在于,start()方法会使得run()方法异步执行在一个新线程中,而run()方法则是直接同步执行在主线程中

2.2.4 wait方法和sleep方法有什么区别?

  • 所属类不同:sleep是线程中的方法,但是wait是Object中的方法
  • 语法不同:sleep方法不依赖于同步器synchronized,但是wait需要依赖synchronized关键字
  • 参数不同:sleep必须设置睡眠时间,而wait可以不设置,不设置默认一直休眠
  • 释放锁资源不同:sleep方法不会释放锁,而wait会释放,并且会加入到等待队列中
  • 唤醒方式不同:sleep不需要被唤醒(休眠后自动退出阻塞),但是wait需要(不指定时间需要别人中断)
  • 线程进入状态不同:sleep方法会让线程进入TIMED_WAITING有时限等待状态,而调用无参数的wait方法,线程会进入WAITING无时限等待状态

2.3 实现Runnable接口创建线程

2.3.1 代码示例

/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/3
 */
public class MyRunnable implements Runnable {
    /**
     * When an object implementing interface {@code Runnable} is used
     * to create a thread, starting the thread causes the object's
     * {@code run} method to be called in that separately executing
     * thread.
     * <p>
     * The general contract of the method {@code run} is that it may
     * take any action whatsoever.
     *
     * @see Thread#run()
     */
    @Override
    public void run() {
        System.out.println("这是线程" + Thread.currentThread().getName());
    }
}

/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/3
 */
@Slf4j
public class Main {

    public static void main(String[] args) {
        // 1. 新增类实现接口Runnable
        new Thread(new MyRunnable()).start();
        // 2. 通过lambda表达式简化开发
        new Thread(() -> System.out.println("这是线程" + Thread.currentThread().getName())).start();
    }
}

2.3.2 继承Thread vs 实现Runnable接口

既然二者都可以实现创建线程,那么一般情况下,更推荐使用什么方式?

更推荐实现Runnable接口,原因:
1. 避免Java单继承限制(Thread已继承自Object
2. 任务与线程解耦,便于复用(如线程池复用任务)
3. 与JUC线程池(ExecutorService)天然兼容

2.4 实现Callable接口创建线程

2.4.1 代码示例

/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/7
 */
public class MyCallable implements Callable<Integer> {
    
    private static final Random RANDOM = new Random();
    
    /**
     * Computes a result, or throws an exception if unable to do so.
     *
     * @return computed result
     * @throws Exception if unable to compute a result
     */
    @Override
    public Integer call() throws Exception {
        int nextInt = RANDOM.nextInt();
        System.out.println("这是线程" + Thread.currentThread().getName() + "的计算结果: " + nextInt);
        return nextInt;
    }
}
/**
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/3
 */
@Slf4j
public class Main {

    public static void main(String[] args) throws ExecutionException, InterruptedException {
        // 1. 通过实现Callable接口创建线程
        FutureTask<Integer> futureTask = new FutureTask<>(new MyCallable());
        // 2. 通过匿名内部类形式创建线程
        FutureTask<Integer> futureTask1 = new FutureTask<>(() -> {
            int nextInt = new Random().nextInt();
            System.out.println("这是FutureTask的任务:" + nextInt);
            return nextInt;
        });

        new Thread(futureTask).start();
        new Thread(futureTask1).start();

        log.info("这是futureTask的返回结果: {}", futureTask.get());
        log.info("这是futureTask1的返回结果: {}", futureTask1.get());
    }
}

2.2.2 为什么要这样创建线程?

我们可以继续查看相应的源码,查看为什么要以FutureTask接收Callable接口的实现类,再通过Thread类接受FutureTask来实现创建线程

// 这里FutureTask实现的是RunnableFuture的接口
public class FutureTask<V> implements RunnableFuture<V> {}

// 这里RunnableFuture是Runnable的子接口
public interface RunnableFuture<V> extends Runnable, Future<V> {
    /**
     * Sets this Future to the result of its computation
     * unless it has been cancelled.
     */
    void run();
}

我们可以看到,FutureTask本质上也是Runnable的实现类

public void run() {
    if (state != NEW ||
        !RUNNER.compareAndSet(this, null, Thread.currentThread()))
        return;
    try {
        Callable<V> c = callable;
        if (c != null && state == NEW) {
            V result;
            boolean ran;
            try {
                // 核心,通过这里的逻辑调用Callable的call方法
                result = c.call();
                ran = true;
            } catch (Throwable ex) {
                result = null;
                ran = false;
                setException(ex);
            }
            if (ran)
                // 在这里设置返回值,并通过get方法获取返回值
                set(result);
        }
    } finally {
        // runner must be non-null until state is settled to
        // prevent concurrent calls to run()
        runner = null;
        // state must be re-read after nulling runner to prevent
        // leaked interrupts
        int s = state;
        if (s >= INTERRUPTING)
            handlePossibleCancellationInterrupt(s);
    }
}

2.5 总结

1. 继承 Thread 类:简单直接,但 Java 不支持多继承,灵活性差,一般不推荐。
2. 实现 Runnable 接口:解耦任务与线程,支持资源共享,是创建线程的首选方式。
3. 实现 Callable 接口 + FutureTask:适用于需要返回结果或处理异常的异步任务,常用于配合线程池使用。        

三者本质都是通过 Thread 启动新线程,最终执行的都是 run() 方法;而 Callable 能返回结果,是因为 FutureTask 在 run() 内部调用了 call() 并保存了结果。

3. 线程池

3.1 线程池简介

1. 线程池是Java并发编程(java.util.concurrent包)中非常核心且实用的组件。简单来说,它就像一个“线程的仓库”
2. 如果不使用线程池,那么就需要手动管理任务和线程创建、销毁之间的关系,每遇到一个新任务就需要创建一个新线程,而任务完成后线程就会被销毁,效率极低且浪费资源。

3. 使用线程池有什么优势?

  • 降低资源消耗: 通过复用线程,减少了创建和销毁线程的CPU和内存开销。
  • 提高响应速度: 任务到达时,无需等待线程创建,直接由空闲线程执行。
  • 提高线程的可管理性: 你可以统一控制线程的数量,防止无限制创建线程导致系统崩溃(如内存溢出 OOM)。
  • 提供强大的功能: 支持定时执行、周期性执行等高级功能。

3.2 自定义线程池快速使用

3.2.1 使用示例

public class MyThreadPool {

    // 线程池默认线程数量
    private static final int CORE_POOL_SIZE = 10;
    // 线程池最大线程数量
    private static final int MAXIMUM_POOS_SIZE = 20;
    // 线程池线程空闲时间
    private static final long KEEP_ALIVE_TIME = 10L;
    // 线程池线程空闲时间单位
    private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
    // 线程池任务队列
    private static final BlockingQueue<Runnable> WORK_QUEUE = new LinkedBlockingQueue<>(5);
    // 线程工厂
    private static final ThreadFactory DEFAULT_THREAD_FACTORY = Executors.defaultThreadFactory();
    // 拒绝策略
    private static final RejectedExecutionHandler DEFAULT_POLICY = new ThreadPoolExecutor.DiscardOldestPolicy();
    
    /**
     * 创建默认参数线程池
     * @return
     */
    public static ExecutorService createDefaulExecutors() {
        return new ThreadPoolExecutor(
            CORE_POOL_SIZE,
            MAXIMUM_POOS_SIZE,
            KEEP_ALIVE_TIME,
            KEEP_ALIVE_TIME_UNIT,
            WORK_QUEUE,
            DEFAULT_THREAD_FACTORY,
            DEFAULT_POLICY);
    }
}

public class Main {

    public static void main(String[] args) {
        ExecutorService defaulExecutors = MyThreadPool.createDefaulExecutors();

        for (int i = 0; i < 10000; i++) {
            defaulExecutors.execute(() -> {
                System.out.println(Thread.currentThread().getName());
            });
        }
    }
}

3.2.2 参数说明

线程池的自定义参数类型以及解释如下:

  • 核心线程数(corePoolSize): 线程池处于空闲状态下的最小线程数。任务提交时,如果当前线程数小于核心线程数,线程池也会创建新线程处理任务
  • 最大线程数(maximumPoolSize): 线程池允许创建的最大线程数,当任务队列已满,并且当前线程数小于最大线程数时,线程池会创建新线程处理任务
  • 空闲线程存活时间(keepAliveTime):当线程数超过核心线程数时候,在线程回收前等待新任务的最长时间。
  • 时间单位(keepAliveTimeUnit):用于指定空闲线程存活时间的单位。例如秒(TimeUnit.SECONDS)、毫秒(TimeUnit.MILLISECONDS)等。
  • 任务队列(workQueue):用于保存等待任务执行的阻塞队列(接口类型,为BlockingQueue<Runnable>),常见实现包括:
    • LinkedBlockingQueue:基于链表的无界队列(默认容量为Integer.MAX_VALUE),可能导致内存溢出。
    • ArrayBlockingQueue:基于数组的有界队列,需指定固定容量。
    • SynchronousQueue:不存储元素的队列,每个插入操作必须等待另一个线程的移除操作。
  • 线程工厂(threadFactory): 用于创还能新线程的工厂类,可以自定义线程名称、优先级、守护线程状态等属性。默认使用 Executors.defaultThreadFactory()。
  • 拒绝策略(RejectedExecutionHandler):当任务队列已满且线程数达到最大值的时候,针对新提交的任务的拒绝策略,常见策略包括:
    • AbortPolicy(默认):抛出RejectedExecutionException异常。
    • CallerRunsPolicy:由提交任务的线程直接执行该任务。
    • DiscardPolicy:静默丢弃任务,不抛异常。
    • DiscardOldestPolicy:丢弃队列中最旧的任务,然后尝试重新提交当前任务。

3.3.3 任务入池策略

由上述的参数说明,我们可以得出当任务不断进入线程池的核心调度逻辑,当调用execute(Runnable command)提交一个新任务时,ThreadPoolExecutor 会按照以下顺序判断,并执行不同的策略

  • 当前线程数 < 核心线程数:会立即创建一个新线程(即使仍然有空闲线程)来执行该任务(这是预热阶段,优先保证核心线程被创建)
  • 当前线程数 >= 核心线程数 且 阻塞队列未满:任务入队等待
  • 当前线程数 < 最大线程数 且 阻塞队列已满:创建新线程(非核心)执行任务
  • 线程数 >= 最大线程数 且 阻塞队列已满:执行拒绝策略

注意事项:

  • 上述策略适用于有界队列,而无界队列(例如LinkedBlockingQueue无参构造,默认容量Integer.MAX_VALUE)新任务会一直入队,可能会导致OOM
  • 合理设置core / max / queue size的三者关系
  • 明确拒绝策略的业务含义(如记录日志、降级处理等)

3.3 Executors工具类

3.3.1 Executors简介

1. 什么是Executors

Executors是Java并发编程中用于管理线程池的核心工具类,属于java.util.concurrent包。它提供了一种简化多线程任务执行的机制,避免开发者直接操作线程的创建和销毁,提升资源利用率与性能。

2. 核心功能

  • 线程池管理:通过预创建或动态调整线程数量,减少频繁创建/销毁线程的开销。
  • 任务调度:支持立即执行、延迟执行或定期执行任务。
  • 资源控制:限制并发线程数,防止系统过载。

3.3.2 常见线程池类型

Executors提供多种创建简化线程池的方法,其常见线程池有以下几种类型

  • FixedThreadPool:固定大小的线程池,适用于负载稳定的场景。
  • CachedThreadPool:动态调整线程数量,适合短时异步任务。
  • SingleThreadExecutor:单线程池,保证任务顺序执行。
  • ScheduledThreadPool:支持定时或周期性任务调度。

1. FixedThreadPool

  • 线程数固定为n:其固定大小的特性,意味着corePoolSize == maximumPoolSize = n
  • 使用无界队列(LinkedBlockingQueue,默认容量Integer.MAX_VALUE)来接受任务,可能会有OOM的风险
  • 适用场景:负载稳定,且任务执行时间可控,吞吐量可预测的场景

使用示例

import java.util.concurrent.*;

public class FixedThreadPoolExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个固定大小的线程池,参数2表示线程池中有2个工作线程
        // 这些线程会被重复利用来执行多个任务
        ExecutorService executor = Executors.newFixedThreadPool(2); // 固定2个线程

        // 循环创建5个任务并提交给线程池执行
        for (int i = 1; i <= 5; i++) {
            // 将循环变量i赋值给taskId,因为lambda表达式中只能使用有效的final变量
            final int taskId = i;
            // 提交任务到线程池,任务是一个lambda表达式实现的Runnable
            executor.submit(() -> {
                // 打印当前任务编号和正在执行它的线程名称
                System.out.println("FixedThreadPool - Task " + taskId + 
                    " running on thread: " + Thread.currentThread().getName());
                try {
                    // 模拟任务执行,休眠1000毫秒,代表实际的工作负载
                    Thread.sleep(1000); // 模拟耗时操作
                } catch (InterruptedException e) {
                    // 如果线程被中断,则恢复中断状态
                    Thread.currentThread().interrupt();
                }
                // 任务完成时打印完成信息
                System.out.println("FixedThreadPool - Task " + taskId + " finished.");
            });
        }

        // 关闭线程池,不再接受新任务,但会继续执行已提交的任务
        executor.shutdown();
        // 等待最多10秒,直到所有任务完成执行
        executor.awaitTermination(10, TimeUnit.SECONDS);
        // 所有任务完成后打印示例结束信息
        System.out.println("FixedThreadPool example done.");
    }
}

2. CachedThreadPool

  • 动态伸缩:意味着线程数会根据任务数动态调整,意味着corePooSize = 0,maximumPoolSize == Integer.MAX_VALUE
  • 使用队列:直接使用SynchronousQueue(不存储元素的阻塞队列)
  • 注意事项
    • 在这种线程池中,没有核心线程的概念,因为corePoolSize == 0,
    • 所有线程都是临时线程
    • 只要有空闲线程,新任务就会复用它
    • 如果没有空闲线程,就会新建一个
    • 空闲超过60s的线程会自动销毁
  • 适用场景:大量短生命周期的异步任务(比如web处理请求)。
  • 风险:如果任务运行时间长或者任务提交过快,可能会创建海量线程,导致系统资源耗尽。

使用示例

import java.util.concurrent.*;

public class CachedThreadPoolExample {
    public static void main(String[] args) throws InterruptedException {
        // 创建一个缓存线程池,它会根据需要创建新线程来处理提交的任务,
        // 并在适当的时候重用以前构造的线程。
        ExecutorService executor = Executors.newCachedThreadPool();

        // 循环创建5个任务并提交给线程池执行
        for (int i = 1; i <= 5; i++) {
            // 将循环变量i赋值给taskId,因为lambda表达式中只能使用有效的final变量
            final int taskId = i;
            // 提交任务到线程池,任务是一个lambda表达式实现的Runnable
            executor.submit(() -> {
                // 打印当前任务编号和正在执行它的线程名称
                System.out.println("CachedThreadPool - Task " + taskId + 
                    " running on thread: " + Thread.currentThread().getName());
                try {
                    // 模拟任务执行,休眠200毫秒
                    Thread.sleep(200); // 短任务
                } catch (InterruptedException e) {
                    // 如果线程被中断,则恢复中断状态
                    Thread.currentThread().interrupt();
                }
                // 任务完成时打印完成信息
                System.out.println("CachedThreadPool - Task " + taskId + " finished.");
            });
        }

        // 关闭线程池,不再接受新任务,但会继续执行已提交的任务
        executor.shutdown();
        // 等待最多5秒,直到所有任务完成执行
        executor.awaitTermination(5, TimeUnit.SECONDS);
        // 所有任务完成后打印示例结束信息
        System.out.println("CachedThreadPool example done.");
    }
}

3. SingleThreadExecutor

  • 固定单线程:内部是一个corePoolSize == maximumPoolSize = 1的ThreadPoolExecutor
  • 使用队列:无界队列(LinkedBlockingQueue),保证任务按照顺序执行
  • 异常处理:即使该线程异常终止,线程池会自动创建一个新线程替代它,保证“单线程”语义
  • 适用场景:需要串行化执行、保证任务顺序、避免并发的场景(如日志写入、状态机更新)。

使用示例

import java.util.concurrent.*;

public class SingleThreadExecutorExample {

    public static void main(String[] args) throws InterruptedException {
        // 创建单线程执行器,它返回一个只有一个工作线程的ExecutorService
        // 这个线程池确保所有任务按照提交顺序依次执行
        // 线程名称通常为 "pool-X-thread-1" 格式
        ExecutorService executor = Executors.newSingleThreadExecutor();

        // 循环创建5个任务并提交给单线程执行器
        for (int i = 1; i <= 5; i++) {
            // 将循环变量i赋值给taskId,因为lambda表达式中只能使用有效的final变量
            // 或者在lambda中引用的局部变量必须是final或有效的final
            final int taskId = i;
            
            // 提交任务到单线程执行器,任务是一个lambda表达式实现的Runnable
            // 所有任务将在同一个线程中按顺序执行
            executor.submit(() -> {
                // 打印当前任务编号和正在执行它的线程名称
                // 由于是单线程执行器,所有任务都会显示相同的线程名称
                System.out.println("SingleThread - Task " + taskId + 
                    " running on thread: " + Thread.currentThread().getName());
                
                try {
                    // 模拟任务执行,休眠500毫秒,代表实际的工作负载
                    // 这期间该线程被占用,无法执行其他任务
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    // 如果线程被中断,则恢复中断状态
                    // 这是处理InterruptedException的标准做法
                    Thread.currentThread().interrupt();
                }
                
                // 任务完成时打印完成信息
                // 由于串行执行,我们可以预期这些消息按顺序出现
                System.out.println("SingleThread - Task " + taskId + " finished.");
            });
        }

        // 关闭线程池,不再接受新任务,但会继续执行已提交的任务
        // 对于单线程执行器,这是安全的操作
        // shutdown()方法是非阻塞的,它只会将线程池状态改为SHUTDOWN
        executor.shutdown();
        
        // 等待最多10秒,直到所有任务完成执行
        // 这个方法会阻塞当前线程,直到所有任务完成或超时
        // 返回true表示所有任务都已完成,false表示超时仍有任务未完成
        executor.awaitTermination(10, TimeUnit.SECONDS);
        
        // 所有任务完成后打印示例结束信息
        // 在实际应用中,这里可以进行一些清理工作
        System.out.println("SingleThreadExecutor example done.");
    }
}

4. ScheduledThreadPool

  • 基于DelayedWorkQueue(内部优先级队列)
  • 支持:
    • schedule(Runnable, delay, unit):延迟执行一次
    • scheduleAtFixedRate(...):固定频率执行(不管上次是否完成)
    • scheduleWithFixedDelay(...):固定延迟执行(等上次完成后再等 delay)
  • 多线程支持(n > 1 时可并行执行多个定时任务)
  • 异常不会导致整个调度器崩溃(而 Timer 会)
  • 注意:即使 n=1,也不是单线程执行所有任务——它只是限制同时执行的任务数 ≤ 1,但调度逻辑本身是线程安全的。

使用示例

import java.util.concurrent.*;

public class ScheduledThreadPoolExample {
    public static void main(String[] args) throws InterruptedException {
        ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(2);

        // 延迟2秒后执行一次
        scheduler.schedule(() -> {
            System.out.println("Scheduled once - runs after 2s on " + Thread.currentThread().getName());
        }, 2, TimeUnit.SECONDS);

        // 每隔1秒执行一次(固定频率,不管上次是否完成)
        ScheduledFuture<?> periodic = scheduler.scheduleAtFixedRate(() -> {
            System.out.println("Periodic task - " + System.currentTimeMillis() / 1000 % 60 + "s");
            try {
                Thread.sleep(300); // 模拟工作
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }, 0, 1, TimeUnit.SECONDS);

        // 运行5秒后取消周期任务
        scheduler.schedule(() -> {
            periodic.cancel(false);
            System.out.println("Periodic task cancelled.");
        }, 5, TimeUnit.SECONDS);

        // 主线程等待
        Thread.sleep(6000);
        scheduler.shutdownNow();
        System.out.println("ScheduledThreadPool example done.");
    }
}

线程池特性对比

线程池类型核心线程数最大线程数队列类型是否扩容是否回收线程适用场景
FixedThreadPoolnn无界队列❌(默认)稳定负载
CachedThreadPool0SynchronousQueue✅(60s空闲)短时异步任务
SingleThreadExecutor11无界队列串行任务
ScheduledThreadPooln延迟队列✅(按需)✅(空闲超时)定时/周期任务

3.3.3 优势与注意事项

  • 优势:降低线程生命周期开销,提供任务队列和拒绝策略等管理功能。提供简化创建线程池的方式
  • 注意事项
    • 需显式关闭线程池(shutdown(),避免资源泄漏;根据场景选择合适线程池类型。推荐放在 final{} 代码块中执行,当执行shutdown方法后,并不会立刻关闭线程池,而是先等待执行完任务后再关闭,可以通过isTerminated()方法判断是否真正关闭
    • 生产环境慎用 Executors.xxx 工厂方法。因为它们隐藏了关键参数(如无界队列、无限线程),容易引发 OOM 或资源耗尽。
    • 推荐手动创建 ThreadPoolExecutor,显式控制。
  • shutdown()与shutdownNow()方法的区别:shutdown()方法会等待所有已提交的任务执行完后才关闭线程池。而shutdownNow()会等待现在正在执行的任务执行完后直接关闭。

3.4 execute()和sumbit()

3.4.1 execute和sumbit的区别

方法所属接口签名
execute(Runnable command)Executor(父接口)void execute(Runnable)
submit(...)ExecutorService(子接口)多个重载:
• Future<?> submit(Runnable task)
• <T> Future<T> submit(Callable<T> task)
• <T> Future<T> submit(Runnable task, T result)
特性execute()submit()
返回值❌ 无返回值(void✅ 返回 Future<?> 对象
任务类型只接受 Runnable接受 Runnable 或 Callable
获取结果❌ 无法获取任务执行结果✅ 可通过 Future.get() 获取结果或异常
异常处理异常被“吞掉”(除非自定义 ThreadFactory 捕获)异常会封装在 ExecutionException 中,调用 Future.get() 时抛出
适用场景只关心“执行”,不关心结果或异常需要结果、状态、取消任务或处理异常

3.4.2 代码示例对比

1. 使用 execute() —— 无返回、异常静默

ExecutorService executor = Executors.newFixedThreadPool(1);

executor.execute(() -> {
    System.out.println("Task started");
    throw new RuntimeException("Oops in execute!");
});

executor.shutdown();
// 输出:Task started
// 异常被记录到 UncaughtExceptionHandler(默认可能只打印到 stderr),但主线程无法感知

⚠️ 你无法知道任务是否失败,除非设置全局异常处理器。

2. 使用submit() + Runnable—— 可捕获异常
ExecutorService executor = Executors.newFixedThreadPool(1);

Future<?> future = executor.submit(() -> {
    System.out.println("Task started");
    throw new RuntimeException("Oops in submit!");
});

try {
    future.get(); // 会抛出 ExecutionException,包裹原始异常
} catch (ExecutionException e) {
    System.out.println("Caught exception: " + e.getCause().getMessage());
} catch (InterruptedException e) {
    Thread.currentThread().interrupt();
}

executor.shutdown();
// 输出:
// Task started
// Caught exception: Oops in submit!

✅ 主线程能明确知道任务失败,并处理异常。

3. 使用 submit() + Callable —— 获取返回值

ExecutorService executor = Executors.newFixedThreadPool(1);

Future<Integer> future = executor.submit(() -> {
    System.out.println("Computing...");
    return 42;
});

try {
    Integer result = future.get();
    System.out.println("Result: " + result); // Result: 42
} catch (Exception e) {
    e.printStackTrace();
}

executor.shutdown();

🔸 Callable 支持返回值和抛出受检异常,Runnable 不行。

4. 异常处理机制差异

方法异常去哪了?
execute()交给线程的 UncaughtExceptionHandler
(默认行为:打印到 System.err,但调用方无法捕获
submit()异常被封装进 Future
调用 future.get() 时以 ExecutionException 形式抛出

5. 如何选择?

场景推荐方法
只需“触发”一个操作,不关心结果(如发通知、写日志)execute()
需要获取结果、判断是否完成、取消任务、处理异常submit()
任务可能失败,且失败需要被主流程感知必须用 submit()
使用 Callable只能用 submit()

6. 补充:submit(Runnable, T result) 的妙用

Future<String> future = executor.submit(() -> {
    // do something
}, "success"); // 无论 Runnable 做什么,get() 都返回 "success"

String result = future.get(); // "success"

可用于:标记任务成功/失败状态,或传递上下文。

在绝大多数需要可靠性的场景中,优先使用 submit(),哪怕你暂时不需要返回值——因为未来你可能需要处理异常或取消任务。

3.5 深入源码

3.5.1 JDK和Tomcat线程池区别

  • 区别:Tomcat本质上是JDK源码的拷贝+微调版本,二者几乎无异。
  • 构造方法:JDK不会在构造方法中创建线程,而Tomcat会在构造方法的最后额外调用prestartAllCoreThreads() 方法,提前启动所有核心线程。(这个方法就在JDK源码中有提供,Tomcat线程池则是额外调用)

3.5.2 线程池如何保活线程

正常来说,一个线程通过.start()方法来开启,但是并没有显式的关闭方法,当代码运行结束后,该线程会自行消失。那么在线程池中,是如何保证线程执行完,不立刻消失的?

核心在于阻塞队列,通过调用BlockingQueue.poll()方法,当前线程就会阻塞,直到当前线程拿到任务后继续执行。从而实现了线程的重复利用。伪代码如下

// 创建新县城
new Thread(new Runnable() {
    @Override
    public void run() {
        // 从传入的参数中获取到要首先执行的任务
        Runnable task = this.firstTask;
        // 优先执行第一个任务
        task.run();
        // 从阻塞队列中获取任务,通过poll()方法会阻塞等待。直到拿到新任务
        while ((task = blockingQueue.poll()) != null) {
            task.run();
        }
    }
});

因此,当线程池中的线程处于空闲状态时候,其本质上是在阻塞等待获取新任务,而当线程数小于核心线程数时,会优先创建新线程执行当前任务。反之则会尝试入队,并被空闲中的线程获取到。

3.5.3 线程池的任务执行顺序是否公平?

线程池的任务执行,会分别经过:核心线程创建 ——> 任务入队 ——> 临时线程创建 ——> 任务拒绝的流程,而临时线程创建并运行任务,其运行的顺序会优先于在阻塞队列中的任务。因此线程池的任务执行顺序本质是不公平的

3.5.4 线程淘汰策略

在线程池中,当线程数超过核心线程数时候,需要额外淘汰多余线程。但是每个线程创建并执行完第一个任务后,都会尝试去阻塞队列中获取任务进行保活,如何精准控制淘汰线程?

关键在于如何控制多余的线程及时执行完代码,每个线程执行完第一个任务后,会从 getTask()方法中获取任务,在这个方法中,我们可以精准控制是返回 null 还是阻塞等待返回任务,从而让多余的线程结束while循环,从而让其消失。

而getTask的大致流程是:

1. 首次进入,会先阻塞等待keepAliveTime
2. 如果获取不到任务,则会进入第二轮循环
3. 在这一轮循环中,第一轮超时时间已过,且线程数大于核心线程数,此时会尝试进行竞争(假设多个线程同时执行),成功的线程会直接返回空,从而直接结束当前的while循环
4. 而其他竞争失败的线程则会开始新一轮循环。直到线程数等于核心线程数。
5. 随后会进行无限阻塞等待获取任务。

所以在淘汰线程中,并没有核心线程与临时线程的区分,竞争淘汰,只有留下来的才是核心线程。

3.5.5 线程池异常处理策略

线程池中的线程执行任务时,当遇到异常后,其会导致while循环的异常退出(因为后续代码直接中断,但是依旧会执行finally代码块,借助这个差异来决定后续逻辑)

当异常退出后,会新增一个线程,同时当前的线程销毁掉。

提问:为什么不直接重新进行新一轮循环,反而直接抛出异常,销毁当前线程并新建线程?

因为如果直接继续,就会导致setUncaughtExceptionHandler机制失效,这个机制的使用示例如下

ThreadFactory threadFactory = new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                Thread t = new Thread(r);
                t.setUncaughtExceptionHandler(new Thread.UncaughtExceptionHandler() {
                    @Override
                    public void uncaughtException(Thread arg0, Throwable arg1) {
                        System.out.println("uncaughtException: " + arg1.getMessage());
                    }
                    
                });
                return t;
            }
        };
        

将该ThreadFactory作为参数传入自定义线程池,即可实现线程的异常捕获处理。所以异常必须抛出,才能触发这一个机制。因此JDK选择保留这个机制,让线程异常退出后重新创建一个新的线程。

3.5.6 线程池如何shutdown?

1. 当调用shutdown()以及shutdownNow()的方法后,线程池的状态会被修改为STOP
2. 而线程通过getWork()获取任务的时候,会检查线程池状态,如果是shutdown()则会同时检查workQueue是否为空,如果是shutdownNow()则不会检查
3. 如果都满足,那么会直接返回null,从而跳出while循环,线程结束并销毁。

提问:这个机制针对的是尝试获取任务,并且还没有到阻塞等待新任务的状态的线程。那么针对阻塞等待的线程,如何让其退出销毁?

通过interrupted()中断方法,当调用到中断方法后,阻塞(睡眠)状态中的线程会抛出异常,通过抛出的异常,可以让这些线程进入新一轮循环。从而获取到空任务。结束并销毁。

4. 线程安全

4.1 线程安全简介

1. 什么是线程安全?

线程安全就是当多线程同时针对一个资源进行写操作的时候,会出现数据混乱的线程不安全现象。

2. 出现线程安全的三要素?

  • 多个线程
  • 操作同一资源
  • 写操作

4.2 原子性

1. 要解决线程安全问题,核心之一就是实现核心写操作的“原子性”,即保证对应的操作是单一不可分割的一部分。同一时间只能有单个线程进行这个流程。

2. 在日常使用中,有很多看似是原子性的操作,实际上并不具备原子性,示例如下

i = 0; // 是原子性

i = j; // 不是原子性,需要先获取到j的值,再赋值给i

i++; // 不是原子性,需要先拿到i的值,再i + 1,再赋值给i

i = j + 1; // 不是原子性,需要先拿到j的值,再j + 1,再赋值给i

3. 而在宏观上,如何保证一段复杂的操作也实现线程安全?通过加锁,让同一时间内,只有单个线程执行这个操作,从而达到线程安全的目的。

示例代码:

/**
 * 线程安全的售票程序演示类
 * 使用同步方法实现多线程环境下的票务管理
 */
public class LockMain {

    /**
     * 静态内部类,实现Runnable接口用于多线程售票
     * 使用静态内部类是因为需要在静态方法(main)中直接实例化
     */
    static class MyRunnable implements Runnable {
        // 共享资源:票数,使用static保证所有线程共享同一份数据
        static int stock = 100;

        /**
         * 重写run方法,定义线程执行的具体逻辑
         * 使用synchronized关键字保证方法的原子性,防止多线程并发问题
         */
        @Override
        public synchronized void run() {
            // 判断是否有余票
            if (stock > 0) {
                // 打印当前线程名称及正在卖出的票号
                System.out.println(Thread.currentThread().getName() + "正在卖第" + stock + "张票");
                // 卖出一张票,票数减一
                stock--;
            }
        }
    }

    /**
     * 主方法,程序入口
     * 创建多个线程模拟售票过程
     */
    public static void main(String[] args) throws Exception {
        // 创建Runnable任务实例
        MyRunnable myRunnable = new MyRunnable();

        // 创建线程池,用于执行任务
        ExecutorService defaulExecutors = MyThreadPool.createDefaulExecutors();

        // 循环提交任务到线程池执行,模拟多线程售票场景
        for (int i = 0; i < 100; i++) {
            // 每次提交任务前暂停20毫秒,模拟任务到达的时间间隔
            Thread.sleep(20);
            // 将任务提交给线程池执行
            defaulExecutors.execute(myRunnable);
        }
    }
}

4.3 可见性

在并发编程中,线程安全不仅依赖于原子性(Atomicity),还依赖于可见性(Visibility)

1. 什么是可见性
可见性
指的是:当一个线程修改了共享变量的值,其他变量可以立刻看到修改后的最新值

2. 没有可见性,会有什么问题?
在多线程环境中,由于每个线程可能有自己的工作内存(如 CPU 缓存),它们并不总是直接从主内存读写数据。因此,一个线程对共享变量的修改,可能暂时只存在于它自己的缓存中,而没有及时刷新到主内存,导致其他线程读取到的是旧值,从而引发错误。

3. 可见性示例
举一个例子:

boolean flag = false;

// 线程A
new Thread(() -> {
    while (!flag) {
        // 等待 flag 变为 true
    }
    System.out.println("退出循环");
}).start();

// 主线程稍后设置 flag = true;
Thread.sleep(1000);
flag = true;

如果 flag 没有被声明为 volatile,那么线程A可能永远看不到主线程对 flag 的修改,因为它一直在读自己缓存中的 false 值,导致死循环。

解决方案:

  • volatile 关键字(Java):确保变量的每次读写都直接访问主内存,且写操作对其他线程立即可见。
  • synchronized 块/方法:进入和退出同步块时,会强制刷新和同步工作内存与主内存的数据。
  • Lock 接口(如 ReentrantLock):加锁和释放锁时也会建立 happens-before 关系,保证可见性。

4.4 有序性

有序性(Ordering) 是并发编程中的第三个核心概念,与原子性可见性并列,共同构成线程安全的基础。

1. 什么是有序性?
有序性
指的是:程序代码在执行时,其指令的执行顺序是否与程序员编写的源代码顺序一致

2. 没有有序性,会出现什么问题?
在单线程环境下,我们通常认为代码是“按顺序”执行的。但在多线程或现代处理器/编译器优化下,实际执行顺序可能被重排(reordering),只要这种重排不影响当前线程的执行结果(即保持“as-if-serial”语义)。然而,在多线程环境中,这种重排可能导致其他线程观察到违反直觉的执行顺序,从而引发错误。

3. 举个例子

// 线程1
a = 1;           // (1)
flag = true;     // (2)

// 线程2
if (flag) {      // (3)
    System.out.println(a); // (4)
}

正常情况下,我们期望如果flag == true,那么输出的a就一定是1,但是通过指令重排,flag = true可能会先发生在a = 1之前,而此时再输出a,就看是0(初始值)。输出结果错误。

解决方案:

  • volatile 关键字(Java):volatile 写之前的所有操作,不能重排到它之后;它之后的操作也不能重排到它之前。(类似一道屏障,分割前后两部分)。
// 线程1
a = 1;           // (1)
volatile flag = true;     // (2)

// 线程2
if (flag) {      // (3)
    System.out.println(a); // (4)
}
  • 使用 synchronized 同步块:通过加锁,两个线程强制变为串行执行,在单线程情况下,遵循as-if-serial原则即可保证有序性。

4.5 其他保证线程安全的方式

  • 共享变量只读:如果允许,则通过final修饰变量
  • 局部变量:如果允许,将共享变量修改为局部变量,每个线程的局部变量存储在栈帧中。各线程创建多份,可以避免线程安全问题
  • ThreadLocal:线程本地变量,如果创建了一份ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一份本地拷贝。多线程操作这个变量实际上是操作自己本地内存中的拷贝变量。从而实现线程隔离的作用。避免了线程安全问题。

4.6 JMM内存模型

4.6.1 JMM简介

1. 什么是JMM?
JMM(Java Memory Model,Java内存模型)
 是 Java 语言规范中定义的一套规则,用于描述多线程程序中各个线程如何访问共享变量,以及在什么情况下一个线程对变量的修改对其他线程可见

2. JMM的核心目标让 Java 程序在不同硬件和操作系统上,多线程行为保持一致

3. JMM是如何运作的?
JMM将内存分为两类

  • 主内存(Main Memory):所有线程共享,存储所有的共享变量(如实例字段、静态变量)。
  • 工作内存(Working Memory):每个线程私有(可理解为 CPU 缓存或寄存器),保存了它用到的主内存变量的副本

 线程不能直接读写主内存,所有操作都必须在自己的工作内存中进行。

变量读写流程(8 大原子操作):当线程要读/写一个共享变量时,JMM 规定了以下步骤(以写为例):

线程A:
  assign → store → write   // 修改本地副本 → 传给主内存 → 主内存更新

线程B:
  read → load → use        // 从主内存读 → 加载到本地副本 → 使用

这 8 个底层操作(read, load, use, assign, store, write, lock, unlock)是原子的,构成了线程与主内存交互的基础。

 4. JMM架构图:

4.6.2 JMM与可见性的关系

JMM 的核心目标之一,就是定义并解决多线程环境下的可见性问题。

1. 为什么会出现可见性问题?

在多线程程序中,一个线程修改了共享变量的值,其他线程可能看不到这个修改,因为:

  • 每个线程有自己的工作内存(如 CPU 缓存);
  • 修改可能只停留在本地缓存,未及时同步到主内存
  • 其他线程读取时,仍从自己的缓存中拿到旧值。

这就会导致数据不一致

2. JMM如何解决可见性问题?
JMM 通过以下机制明确定义在什么条件下,一个线程对变量的修改必须对其他线程可见

  • 所有共享变量存在主内存
  • 线程操作变量时,先从主内存load到工作内存,修改后再store/write回主内存;
  • JMM 规定了这些操作的同步时机

并且只要满足以下规则,就保证可见性

  • volatile 变量规则:对 volatile 变量的写 happens-before 后续对该变量的读 → 读线程一定看到最新值。
  • 锁规则:释放锁(unlock)happens-before 后续获取同一把锁(lock)→ 锁保护的变量修改对下一个持有者可见。
  • 线程启动/终止规则:主线程启动子线程前的操作,对子线程可见。

3. JMM如何保证缓存一致性?
JMM通过缓存一致性协议(MESI)来保证缓存一致性

  • 线程会通过 read (从主内存读取数据) 、load(加载到本地副本),将数据加载到线程内存中,并通过use(cpu读取变量使用)使用该变量
  • 当有一个线程修改该变量(assign)后,会立刻store(将本地副本加载到主内存中)并且wriite(修改主内存中该变量的值)
  • 随后会通过总线机制,推送到所有持有该变量副本的线程中。其他的线程会直接使该变量失效,并从主内存中重新加载数据,从而实现了缓存一致性。

4.7 原子类

4.7.1 原子类简介

1. 什么是原子类?
原子类是 Java 中位于 java.util.concurrent.atomic 包下的一组类(如 AtomicIntegerAtomicLongAtomicReference 等),它们利用 CAS(Compare-And-Swap) 机制,在多线程环境下能无锁地实现对变量的原子操作(如自增、赋值、比较并设置等),从而避免使用 synchronized,提高并发性能。

2.相比较于synchronized,原子类有什么优势?

  • 粒度更细:原子变量可以把竞争范围缩小到变量级别,这是我们可以获得的最细的粒度情况,通常锁的粒度要大于原子变量的粒度。
  • 效率更高:通常,使用原子类的效率会比使用锁的效率更高。除了高度竞争的情况。

3. 常用原子类总结

类别原子类用途说明
基本类型AtomicBoolean原子操作的布尔值
AtomicInteger原子操作的 int 类型(如自增、CAS)
AtomicLong原子操作的 long 类型
引用类型AtomicReference<V>原子操作的对象引用
AtomicMarkableReference<V>带一个 boolean 标记位的引用(用于解决 ABA 问题)
AtomicStampedReference<V>带一个 int “戳”(stamp)的引用(更可靠地解决 ABA 问题)
数组类型AtomicIntegerArray数组中每个 int 元素支持原子操作
AtomicLongArray数组中每个 long 元素支持原子操作
AtomicReferenceArray<E>数组中每个对象引用支持原子操作
字段更新器
(基于反射)
AtomicIntegerFieldUpdater<T>对任意对象的 volatile int 字段进行原子更新
AtomicLongFieldUpdater<T>对任意对象的 volatile long 字段进行原子更新
AtomicReferenceFieldUpdater<T, V>对任意对象的 volatile V 引用字段进行原子更新
高性能累加器
(JDK 8+)
LongAdder / DoubleAdder高并发下的高效累加(如计数器),比 AtomicLong 吞吐更高
LongAccumulator / DoubleAccumulator支持自定义累积函数(如 max、min、sum 等)

4. 简单使用示例

public class AtomicMain {

    /**
     * 实现Runnable接口的内部类,用于模拟抢购场景
     * 使用原子类AtomicInteger来处理并发环境下的库存扣减
     */
    static class MyRunnable implements Runnable {

        // 定义初始库存为100的原子整数变量,确保多线程环境下的原子性操作
        public static AtomicInteger stock = new AtomicInteger(100);

        @Override
        public void run() {
            // 判断当前库存是否大于0,决定是否可以继续抢购
            if (stock.get() > 0) {
                // 执行库存扣减操作,使用decrementAndGet()确保扣减操作的原子性
                int i = stock.decrementAndGet();
                // 输出当前线程名称及抢购结果,以及剩余库存数量
                System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余:" + i);
            } else {
                // 库存不足时输出抢购失败信息
                System.out.println(Thread.currentThread().getName() + " 抢购失败");
            }
        }
    }


    /**
     * 主方法,启动多线程环境进行抢购测试
     * 创建固定大小的线程池,并提交多个抢购任务到线程池中执行
     */
    public static void main(String[] args) {
        // 创建一个拥有40个线程的固定大小线程池
        ExecutorService newFixedThreadPool = Executors.newFixedThreadPool(40);

        // 提交200个抢购任务到线程池中,模拟高并发抢购场景
        for (int i = 0; i < 200; i++) {
            newFixedThreadPool.execute(new MyRunnable());
        }
    }

}

4.7.2 原子类常用方法速查

原子类常用方法功能说明
AtomicInteger
(同理适用于 AtomicLong
get()获取当前值
set(int newValue)直接设置新值(无原子保障以外的同步)
getAndSet(int newValue)先返回旧值,再设为新值(原子)
compareAndSet(int expect, int update)
(CAS)
若当前值 == expect,则设为 update;返回是否成功
incrementAndGet()自增 1 并返回新值(++i)
getAndIncrement()返回当前值,再自增 1(i++)
addAndGet(int delta)加 delta 并返回新值
getAndAdd(int delta)返回当前值,再加 delta
AtomicBooleanget() / set(boolean)获取/设置布尔值
getAndSet(boolean)原子地设新值并返回旧值
compareAndSet(boolean expect, boolean update)CAS 操作
AtomicReference<V>get() / set(V)获取/设置引用
getAndSet(V)原子设新引用,返回旧引用
compareAndSet(V expect, V update)引用级别的 CAS(比较引用是否相等)
AtomicIntegerFieldUpdater<T>
(其他 FieldUpdater 类似)
get(T obj)获取 obj 的目标字段值
set(T obj, int newValue)设置 obj 的目标字段值
compareAndSet(T obj, int expect, int update)对 obj 的字段执行 CAS
> ⚠️ 要求字段是 volatile int,且非 static
LongAdder
(高并发计数推荐)
add(long x)累加 x
increment()自增 1(等价于 add(1)
sum()获取当前总和(注意:非原子快照,但最终一致)
reset()重置为 0
> 💡 适用于写多读少的高并发计数场景
LongAccumulatoraccumulate(long x)按构造时指定的函数(如 Long::max)累积 x
get()获取当前累积结果
> 示例:
new LongAccumulator(Long::max, 0L)
实现原子取最大值

5. 锁

5.1 乐观锁与悲观锁

简介

乐观锁和悲观锁是并发编程中两种对数据并发访问的控制策略,核心区别在于对“冲突是否经常发生”的假设不同

1. 悲观锁

  • 思想
    总是假设最坏情况——只要不去加锁,就一定会发生冲突。”
    所以在访问数据前,先加锁,确保独占资源。
  • 实现方式
    • Java 中:synchronizedReentrantLock 等。
    • 数据库中:SELECT ... FOR UPDATE
  • 特点
    • ✅ 保证绝对安全,不会出现并发问题。
    • ❌ 性能开销大(线程阻塞、上下文切换)。
    • 适合写操作多、竞争激烈的场景。

📌 “先上锁,再操作” —— 宁可错杀,不可放过。

2. 乐观锁

  • 思想
    假设一般不会冲突,所以不加锁;只在提交更新时检查是否被别人改过。”
    如果发现冲突,则重试或失败
  • 实现方式
    • Java 中:AtomicInteger 等原子类(基于 CAS:Compare-And-Swap)。
    • 数据库中:通过版本号(version) 或 时间戳 字段实现。
      例如:UPDATE table SET value=..., version=version+1 WHERE id=1 AND version=old_version;
  • 特点
    • ✅ 无锁、高并发性能好(无阻塞)。
    • ❌ 冲突频繁时,重试成本高(可能多次失败)。
    • 适合读多写少、冲突较少的场景。

📌 “先操作,提交时校验” —— 相信世界是美好的,但留一手验证。

总结来说:悲观锁要求必须先获取到资源,才能进行对资源的操作。而乐观锁则允许共同尝试操作资源,但是会通过校验来保证只有少部分操作通过并使资源达到目标状态。

CAS

1. 什么是CAS?
CAS 通常指的是 Compare-And-Swap(比较并交换),它是一种用于实现多线程同步的原子操作。

2. CAS如何实现?
简单来说,CAS包括三个参数,分别是内存(也可以是数据库等其他地方)中的实际值,线程读取到的期望值,以及需要替换的新值。当线程读取到的值与内存中的实际值相同时,就执行替换操作,反之则返回失败(或者不执行任何操作)。这个过程被封装为原子操作。要么同时成功,要么同时失败。

3. CAS的优缺点?
优点
:避免了传统锁带来的线程阻塞和上下文切换开销。
缺点:可能遇到 ABA 问题(值从 A 变成 B 又变回 A,导致 CAS 误判)、循环时间长开销大等。

4. CAS使用示例

public class CasExample {

    /**
     * 库存扣减任务类
     * 实现Runnable接口,用于在多线程环境中执行库存扣减操作
     */
    static class CasRunnable implements Runnable {

        // 使用AtomicInteger保证库存操作的原子性,初始库存为20
        static AtomicInteger stock = new AtomicInteger(20);

        @Override
        public void run() {
            int oldValue; // 保存当前库存值
            int newValue; // 计算扣减后的库存值

            // 使用CAS算法实现无锁化的原子更新
            do {
                oldValue = stock.get(); // 获取当前库存值
                // 如果库存大于0则减1,否则保持为0(不小于0)
                newValue = oldValue > 0 ? oldValue - 1 : 0;
                
                // 比较并交换:仅当当前值仍等于oldValue时才更新为newValue
                // 同时确保只有在newValue > 0时才继续尝试(即至少还有1个库存)
            } while (!stock.compareAndSet(oldValue, newValue) && newValue > 0);
            
            // 输出操作结果
            if (newValue == 0) {
                System.out.println(Thread.currentThread().getName() + " 抢购失败");
            } else {
                System.out.println(Thread.currentThread().getName() + " 抢购成功,剩余:" + newValue);
            }
        }
        
    }

    /**
     * 主方法
     * 创建多个线程并发执行库存扣减操作,模拟高并发场景下的抢购功能
     */
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService defaulExecutors = MyThreadPool.createDefaulExecutors();
        // 提交50个抢购任务到线程池
        for (int i = 0; i < 50; i++) {
            defaulExecutors.execute(new CasRunnable());
        }
        // 注意:这里没有关闭线程池,程序可能不会自动结束
    }
}

5.2 自旋锁与互斥锁

1. 什么是自旋锁?
当一个线程尝试获取已被占用的锁时,不会立即阻塞,而是循环(“自旋”)不断重试获取锁,直到成功。

2. 什么是互斥锁?
当一个线程尝试获取已被其他线程占用的锁时,该线程会被挂起(阻塞),进入等待状态,直到锁被释放。

3. 自旋锁特点

  • 线程一直占用 CPU,持续检查锁是否可用;
  • 无上下文切换开销
  • 适用于锁持有时间非常短的场景(比如几纳秒到微秒级);
  • 如果锁竞争激烈或持有时间长,会浪费大量 CPU 资源
  • 其底层经常采用CAS提供的原子操作来实现

4. 互斥锁特点

  • 操作系统会将该线程从运行队列中移出,不占用 CPU 资源
  • 锁释放后,操作系统负责唤醒等待的线程;
  • 适用于锁持有时间较长的场景;
  • 上下文切换开销(因为涉及线程阻塞/唤醒)。

5. 补充:现代很多锁实现(如 Java 的 synchronized 在 JDK 6+)采用了自适应自旋策略:

  • 先尝试自旋几次;
  • 如果失败,再转为阻塞;
  • 这样结合了两者优点,提升性能。

6. 总结来说互斥锁让线程“睡着等”,自旋锁让线程“站着等”——前者省 CPU,后者省切换。

5.2 synchronized锁升级

1. synchronized锁升级是什么?
synchronized 锁升级是 Java 虚拟机(JVM)为了提升 synchronized 关键字的性能,在运行时根据竞争情况动态调整锁状态的一种优化机制。它主要出现在 JDK 6 及之后的版本中,属于 HotSpot 虚拟机对内置锁(也叫监视器锁)的优化策略。

2. 锁升级的四种状态

  1. 无锁状态:对象刚创建时没有被任何线程锁定。
  2. 偏向锁(Biased Locking)
    • 适用于只有一个线程反复进入同步块的场景。
    • JVM 会将对象头中的标记字段(Mark Word)记录该线程 ID,后续该线程再次进入时无需进行 CAS 操作,直接获得锁。
    • 目的是减少无竞争情况下的同步开销。
  3. 轻量级锁(Lightweight Locking)
    • 当有第二个线程尝试竞争锁时,偏向锁会撤销,升级为轻量级锁。
    • 轻量级锁通过自旋 + CAS 尝试获取锁,避免线程阻塞和操作系统上下文切换。
    • 适用于短时间、低竞争的同步场景。
  4. 重量级锁(Heavyweight Locking)
    • 如果自旋次数过多或竞争激烈,轻量级锁会膨胀为重量级锁。
    • 此时线程会真正挂起,由操作系统调度,涉及用户态与内核态切换,开销较大。

注意:锁可以升级但不能降级(在常规执行路径下)。不过从 JDK 15 开始,偏向锁已被默认禁用,并计划在未来版本中移除,因为现代应用中多线程竞争更常见,偏向锁反而可能带来额外开销。

5.3 可重入锁

1. 什么是可重入锁?
可重入锁(Reentrant Lock)
 是指:同一个线程可以多次获取同一把锁而不会发生死锁的锁机制。
在 Java 中,synchronized 和 java.util.concurrent.locks.ReentrantLock 都是可重入锁

2. 可重入锁示例

public class Example {
    public synchronized void methodA() {
        System.out.println("进入 methodA");
        methodB(); // 调用另一个同步方法
    }

    public synchronized void methodB() {
        System.out.println("进入 methodB");
    }
}
// 额外说明:被synchronized修饰的方法,本质上是调用了synchronized (this),锁住的是当前对象实例

当一个线程进入methodA()后,其会获取到该对象的锁,而当进入methodB()后,他并不会因为当前对象实例的锁已被持有(持有者就是该线程本身)而被阻塞

为什么不会死锁?
因为JVM 为每个对象的锁维护了一个持有线程 + 重入计数器

  • 第一次进入 methodA():线程 T 获取 this 的锁,计数器 = 1。
  • 在 methodA() 中调用 methodB():线程 T 再次请求 this 的锁。
    • JVM 发现:当前锁的持有者就是 T → 允许进入,计数器 = 2。
  • 退出 methodB():计数器减为 1。
  • 退出 methodA():计数器减为 0,锁被真正释放。

 这就是可重入性的核心:同一个线程可以重复获取同一个锁,而不会阻塞自己。

5.4 ReentrantLock 

5.4.1 ReentrantLock 简介

1. 什么是ReentrantLock?
ReentrantLock 是 Java 并发包(java.util.concurrent.locks)中提供的一种可重入的互斥锁,用于替代传统的 synchronized 关键字,实现线程同步。

2. 核心特点

  1. 可重入性(Reentrant)
    同一个线程可以多次获取同一个锁而不会死锁。每次加锁,内部计数器加一;每次释放锁,计数器减一;只有当计数器归零时,锁才真正被释放。
  2. 显式加锁/解锁
    与 synchronized 自动加锁和释放不同,ReentrantLock 需要程序员手动调用 lock() 获取锁、unlock() 释放锁。通常建议在 try-finally 块中使用,确保锁一定会被释放:
  3. 支持公平/非公平策略
    • 默认是非公平锁(性能更好):线程争抢锁时不严格按请求顺序。
    • 可通过构造函数指定为公平锁(new ReentrantLock(true)),保证等待时间最长的线程优先获取锁。
  4. 功能更丰富
    • 支持可中断的锁获取(lockInterruptibly()
    • 支持带超时的锁获取(tryLock(timeout, unit)
    • 可查询当前是否被锁定、是否有线程在等待等

3. 使用示例

/**
 * 演示使用 ReentrantLock 实现线程安全的售票系统。
 * 本例中,多个线程并发尝试购买有限数量的票(初始为100张),
 * 通过显式锁 ReentrantLock 确保对共享资源(STOCK)的安全访问。
 *
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/22
 */
public class ReentrantLockExample implements Runnable {

    /**
     * 可重入互斥锁,用于保护对共享变量 STOCK 的并发访问。
     * 使用静态 final 保证所有线程共享同一把锁,从而实现同步控制。
     */
    private static final ReentrantLock LOCK = new ReentrantLock();

    /**
     * 共享的票库存数量,初始值为100。
     * 该变量被多个线程并发读写,必须通过锁保护以避免竞态条件。
     */
    private static int STOCK = 100;

    /**
     * 线程执行体:尝试获取锁并购买一张票。
     * 若库存大于0,则成功售出并打印信息;否则提示购票失败。
     * 
     * 注意:必须在 finally 块中释放锁,确保即使发生异常也能正确解锁,
     * 避免死锁。同时通过 isHeldByCurrentThread() 判断当前线程是否持有锁,
     * 提高健壮性(尽管在此逻辑下通常总是持有)。
     */
    @Override
    public void run() {
        try {
            // 获取锁(阻塞直到获得)
            LOCK.lock();

            // 检查库存是否还有余票
            if (STOCK > 0) {
                // 售出一张票
                STOCK--;
                // 打印售出信息(注意:(STOCK + 1) 是刚售出的票号)
                System.out.println("当前售卖第" + (STOCK + 1) + "张票,剩余: " + STOCK);
            } else {
                // 库存已空,购票失败
                System.out.println(Thread.currentThread().getName() + "购票失败");
            }
        } finally {
            // 安全释放锁:仅当当前线程持有该锁时才解锁
            // (防止 IllegalMonitorStateException,增强代码鲁棒性)
            if (LOCK.isHeldByCurrentThread()) {
                LOCK.unlock();
            }
        }
    }

    /**
     * 主方法:启动200个线程模拟高并发购票场景。
     * 由于票数只有100张,预期前100个成功获取锁且库存未耗尽的线程能购票成功,
     * 其余线程将因库存为0而购票失败。
     *
     * @param args 命令行参数(本例未使用)
     */
    public static void main(String[] args) {
        // 创建200个线程
        Thread[] threads = new Thread[200];

        for (int i = 0; i < 200; i++) {
            // 每个线程执行同一个 Runnable 实例(无状态,安全)
            threads[i] = new Thread(new ReentrantLockExample());
            threads[i].start();
        }

        // 注意:主线程未等待子线程结束,实际应用中可能需要 join()
        // 但本例仅用于演示并发行为,输出顺序可能因调度而异。
    }
}

4. 与 synchronized 的对比

特性ReentrantLocksynchronized
可重入
自动释放❌(需手动 unlock)
公平性控制✅(可选)❌(始终非公平)
可中断/超时
条件变量(Condition)✅(支持多个)

❌(仅 wait/notify)

5. 适用场景:

  • 需要更灵活的锁控制(如超时、中断)
  • 高竞争环境下希望使用公平锁
  • 需要多个条件变量进行线程通信

总之,ReentrantLock 提供了比 synchronized 更强大、灵活的同步机制,但使用时需格外小心,避免忘记释放锁导致死锁。

5.4.2 tryLock 方法

1. 简单介绍
ReentrantLock 的 tryLock() 方法是其区别于 synchronized 的一个重要特性,它提供了一种非阻塞带超时的尝试获取锁的方式。

2. 无参 tryLock()

boolean tryLock()
  • 作用:立即尝试获取锁。
  • 返回值
    • 如果锁可用(即没有被其他线程持有),则立即获取锁并返回 true
    • 如果锁已被占用,则不等待、直接返回 false
  • 特点非阻塞,不会让当前线程挂起。

✅ 适用场景:希望快速尝试加锁,失败就做别的事(比如重试、降级处理等)。

使用示例:

ReentrantLock lock = new ReentrantLock();
if (lock.tryLock()) {
    try {
        // 执行临界区代码
    } finally {
        lock.unlock();
    }
} else {
    // 锁被占用,执行其他逻辑(如记录日志、跳过、排队等)
}

⚠️ 注意:即使成功获取了锁,也必须在 finally 块中释放,否则可能导致死锁。

3. 带超时的 tryLock(long timeout, TimeUnit unit)

boolean tryLock(long timeout, TimeUnit unit) throws InterruptedException
  • 作用:在指定时间内尝试获取锁
  • 行为
    • 如果锁立即可用,获取并返回 true
    • 如果锁不可用,则最多等待 timeout 时间
    • 若在超时前获得锁,返回 true
    • 若超时仍未获得锁,返回 false
    • 等待期间如果线程被中断,会抛出 InterruptedException
  • 特点可中断 + 带超时,避免无限期等待。

✅ 适用场景:需要限制等待时间,防止线程长时间阻塞(如响应式系统、高可用服务)。

使用示例:

ReentrantLock lock = new ReentrantLock();
try {
    if (lock.tryLock(2, TimeUnit.SECONDS)) {
        try {
            // 执行临界区代码
        } finally {
            lock.unlock();
        }
    } else {
        // 超时未获取到锁,处理超时逻辑
    }
} catch (InterruptedException e) {
    Thread.currentThread().interrupt(); // 恢复中断状态
    // 处理中断
}

4. 与lock对比

方法是否阻塞是否可中断是否超时返回值
lock()是(永久阻塞直到获取)void
tryLock()否(立即返回)boolean
tryLock(timeout, unit)是(最多阻塞 timeout)boolean

5. 总结

  • tryLock() 让你主动控制锁竞争的行为,避免死锁或长时间阻塞;
  • 特别适合实现高响应性容错性强的并发程序;
  • 使用时务必注意资源释放中断处理

5.4.3 中断获取锁的过程

1. 简单介绍
ReentrantLock 获取锁的过程在某些方法下是可以被中断的,比如支持中断的加锁方法:lockInterruptibly()

void lockInterruptibly() throws InterruptedException
  • 行为
    • 如果锁可用,立即获取并返回;
    • 如果锁不可用,当前线程会阻塞等待
    • 但在等待过程中,如果线程被其他线程调用 interrupt() 中断,则会抛出 InterruptedException 并退出等待不会获得锁
  • 特点可响应中断,适合需要取消长时间等待的场景。

2. 使用示例:

ReentrantLock lock = new ReentrantLock();

Thread t = new Thread(() -> {
    try {
        lock.lockInterruptibly(); // 可中断地获取锁
        try {
            // 临界区代码
            System.out.println("执行任务...");
        } finally {
            lock.unlock();
        }
    } catch (InterruptedException e) {
        System.out.println("等锁时被中断,放弃获取锁!");
        // 线程可以优雅退出或做清理
    }
});

t.start();

// 主线程稍后中断它
Thread.sleep(100);
t.interrupt(); // 触发中断

⚠️ 注意:必须捕获 InterruptedException,并在中断后不要继续操作临界资源(因为没拿到锁)。

5.5 公平锁与非公平锁

5.5.1 公平锁介绍

  • 定义:公平锁会按照线程请求锁的顺序来分配锁,即“先来先得”。
  • 实现方式:当一个线程尝试获取锁时,如果锁已被占用,该线程会被放入一个 FIFO(先进先出)的等待队列中;只有队列中最前面的线程才有资格在锁释放后获取它。
  • 优点:避免线程“饥饿”,保证所有线程最终都能获得锁。
  • 缺点:吞吐量通常较低,因为频繁的上下文切换和严格的排队机制会带来额外开销。
  • 创建方式
    ReentrantLock fairLock = new ReentrantLock(true); // true 表示启用公平模式

5.5.2 非公平锁介绍

  • 定义:非公平锁允许插队。即使有线程在等待队列中,新来的线程仍可能在锁可用时直接获取它。
  • 实现方式:当锁被释放时,任何尝试获取锁的线程(包括刚到达的)都有机会立即抢到锁,而不一定按排队顺序。
  • 优点:整体吞吐量更高,响应更快,因为减少了线程挂起/唤醒的开销。
  • 缺点:可能导致某些线程长时间得不到锁(即“线程饥饿”)。
  • 创建方式
    ReentrantLock nonFairLock = new ReentrantLock();        // 默认是非公平
    ReentrantLock nonFairLock2 = new ReentrantLock(false); // 显式指定非公平

5.5.3 补充说明

  • Java 内置的 synchronized 关键字实现的是非公平锁
  • 选择公平还是非公平,需根据具体应用场景权衡:对响应时间敏感的系统通常用非公平锁;对公平性要求高的场景(如任务调度)可考虑公平锁。

5.5.4 代码示例

/**
 * 公平锁与非公平锁行为演示示例
 *
 * @author 王玉涛
 * @version 1.0
 * @since 2026/1/22
 *
 * 说明:
 * - 本例通过 ReentrantLock 的构造参数控制锁的公平性。
 * - 当前代码使用的是 **非公平锁**(new ReentrantLock(false))。
 * - 若想观察公平锁行为,请将 false 改为 true。
 */
public class FairLockExample implements Runnable {
    private String name;

    // ⚠️ 当前为【非公平锁】。若需测试公平锁,请将参数改为 true
    public static final ReentrantLock LOCK = new ReentrantLock(false);

    /**
     * 构造方法:初始化线程名称
     *
     * @param name 线程标识名,如 "线程1"
     */
    public FairLockExample(String name) {
        this.name = name;
    }

    /**
     * 线程执行逻辑:尝试获取锁并打印信息,共执行5次
     */
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            try {
                // 尝试获取锁(公平锁:排队等待;非公平锁:可能直接抢到)
                LOCK.lock();
                System.out.println(this.name + "获取到锁");
                
                // 模拟临界区操作(可选)
                // Thread.sleep(1); // 若取消注释,公平性表现会更明显
                
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                // 安全释放锁:仅当当前线程持有锁时才解锁
                if (LOCK.isHeldByCurrentThread()) {
                    LOCK.unlock();
                }
            }
        }
    }

    /**
     * 主方法:启动3个线程并发竞争锁
     *
     * @param args 命令行参数(未使用)
     * @throws InterruptedException 主线程等待子线程时可能抛出
     */
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[3];
        
        // 创建并启动3个线程
        for (int i = 0; i < 3; i++) {
            threads[i] = new Thread(new FairLockExample("线程" + (i + 1)));
            threads[i].start();
        }

        // 等待所有线程执行完毕
        for (int i = 0; i < 3; i++) {
            threads[i].join();
        }
        
        System.out.println("所有线程执行完毕。");
    }
}
公平锁:
线程1获取到锁
线程2获取到锁
线程3获取到锁
线程1获取到锁
线程2获取到锁
线程3获取到锁
线程1获取到锁
线程2获取到锁
线程3获取到锁
线程1获取到锁
线程2获取到锁
线程3获取到锁
线程1获取到锁
线程2获取到锁
线程3获取到锁

非公平锁:
线程1获取到锁
线程1获取到锁
线程1获取到锁
线程1获取到锁
线程1获取到锁
线程2获取到锁
线程2获取到锁
线程2获取到锁
线程2获取到锁
线程2获取到锁
线程3获取到锁
线程3获取到锁
线程3获取到锁
线程3获取到锁
线程3获取到锁

    非公平锁的输出示例,本质上是因为当前正在执行的任务的线程,其释放资源后又可以立即参与到锁的竞争中。而其他的线程可能因为阻塞唤起等原因,落后于当前线程,因此才呈现线程1 -> 线程2 -> 线程3的输出结果。相比较于公平锁,非公平锁使得CPU的线程上下文切换开销变得更小。

5.6 共享锁与排它锁

5.6.1 简介

在 Java 中,共享锁(Shared Lock)和排它锁(Exclusive Lock,也称独占锁)是并发控制中两种基本的锁模式,主要用于协调多个线程对共享资源的访问。它们的概念源自操作系统和数据库,并在 Java 的并发工具类(如 ReentrantReadWriteLock、AQS 框架等)中得到实现。

5.6.2 共享锁

  • 定义:允许多个线程同时持有该锁,但不能与排它锁共存
  • 用途:用于只读操作,提高并发读性能。
  • 特点
    • 多个读线程可以并发执行(提升吞吐量)。
    • 如果有线程持有共享锁,其他线程不能获取排它锁(写必须等待所有读完成)。
    • 如果有线程持有排它锁,其他线程既不能读也不能写
    • Java 中典型实现:
      • ReentrantReadWriteLock.ReadLock
      • AQS 中的 tryAcquireShared / tryReleaseShared 方法

✅ 示例
多个线程同时读取缓存中的配置信息,使用共享锁可避免不必要的阻塞。

5.6.3 排它锁

  • 定义:同一时刻只允许一个线程持有该锁。
  • 用途:用于写操作或需要修改共享资源的场景,确保数据一致性。
  • 特点
    • 具有互斥性:其他线程(无论是读还是写)都必须等待锁释放。
    • Java 中典型的排它锁包括:
      • synchronized 关键字
      • ReentrantLock
      • ReentrantReadWriteLock.WriteLock

✅ 示例
多个线程要修改同一个计数器,必须用排它锁防止并发修改导致数据错误。

5.6.4 对比

特性共享锁(读锁)排它锁(写锁)
是否允许多个线程同时持有✅ 是❌ 否(只能一个)
是否允许与对方共存❌ 不能与排它锁共存❌ 不能与任何锁共存
适用场景只读操作写操作或读写混合
典型实现ReentrantReadWriteLock.ReadLockReentrantLock, WriteLock

5.6.5 使用示例

ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();

// 读操作(共享)
rwLock.readLock().lock();
try {
    // 多个线程可同时执行此处
} finally {
    rwLock.readLock().unlock();
}

// 写操作(排它)
rwLock.writeLock().lock();
try {
    // 只有一个线程能执行此处
} finally {
    rwLock.writeLock().unlock();
}

总结:多个线程可以同时申请共享锁。如果有线程正在持有共享锁(读锁),任何尝试申请排它锁(写锁)的线程都必须等待共享锁完全释放。而如果有线程正在持有排它锁(写锁),其他线程无论是尝试申请共享锁还是排它锁都必须等待它完全释放。

  1. 读-读:允许并发 ✅
  2. 读-写:互斥,写需等所有读完成 ⏳
  3. 写-写:互斥,只能一个写 ❌
  4. 写-读:互斥,读需等写完成 ⏳

这就是经典的 “读写锁语义” —— 允许多读,禁止读写/写写并发

5.7 synchronized 和 Lock的区别、

  1. 语法层面
    • synchronized 是 Java 语言关键字,由 JVM 原生支持;
    • Lock 是 java.util.concurrent.locks 包中的接口,属于 JDK API。
  2. 加锁与释放方式
    • synchronized 自动加锁和释放锁(进入同步块时加锁,退出时自动释放,包括异常情况);
    • Lock 必须手动调用 lock() 加锁,并在 finally 块中显式调用 unlock() 释放锁,否则可能导致死锁。
  3. 响应中断能力
    • synchronized 不可中断:线程在等待锁时无法响应中断;
    • Lock 支持中断:可通过 lockInterruptibly() 方法使等待线程响应 Thread.interrupt()
  4. 超时获取锁
    • synchronized 不支持超时,只能一直阻塞等待;
    • Lock 支持带超时的尝试获取锁,如 tryLock(long timeout, TimeUnit unit)
  5. 非阻塞尝试获取锁
    • synchronized 无法“尝试”获取锁,只能阻塞;
    • Lock 提供 tryLock() 方法,可立即返回是否成功获取锁,不阻塞。
  6. 公平性控制
    • synchronized 仅支持非公平锁;
    • Lock(如 ReentrantLock)可通过构造函数指定是否为公平锁(new ReentrantLock(true))。
  7. 条件变量(Condition)
    • synchronized 只能使用对象内置的一个隐式条件(通过 wait() / notify() / notifyAll());
    • Lock 可创建多个 Condition 对象,实现更精细的线程通信(如生产者-消费者多队列场景)。
  8. 锁状态查询
    • synchronized 无法查询锁的状态(如是否被持有、持有次数等);
    • Lock 提供方法如 isLocked()isHeldByCurrentThread()getHoldCount() 等用于监控和调试。
  9. 底层实现机制
    • synchronized 基于 JVM 的 Monitor(监视器),依赖对象头中的 Mark Word;
    • Lock 基于 AQS(AbstractQueuedSynchronizer)框架,使用 CAS 和 CLH 队列实现。
  10. 性能表现(现代 JVM)
    • 在低竞争或简单同步场景下,synchronized 经过 JVM 优化(偏向锁、轻量级锁等)性能优异且更轻量;
    • 在高竞争、复杂同步逻辑或需要高级功能时,Lock 更灵活可控,但代码复杂度更高。

✅ 最佳实践建议:优先使用 synchronized 保证简洁与安全;仅当需要中断、超时、公平锁或多条件等高级特性时,才选用 Lock

6. 总结

Java 并发编程是构建高性能、高可靠系统的核心能力之一。JUC(java.util.concurrent)包作为 JDK 提供的高级并发工具集,极大地简化了多线程开发的复杂性。本文系统梳理了从线程创建、线程池管理,到线程安全三大特性(原子性、可见性、有序性)、JMM 内存模型,再到各类锁机制(悲观/乐观、公平/非公平、共享/排它)的核心知识点。

通过对比传统 synchronized 与现代 ReentrantLockAtomic 类等无锁编程方案,我们看到:并发设计的本质是在正确性、性能与复杂度之间寻找平衡。合理选择线程模型、谨慎配置线程池参数、善用原子类与显式锁,是写出健壮并发代码的关键。

需谨记:没有银弹Executors 工厂方法虽便捷,但隐藏的风险不容忽视;synchronized 虽简单,但在高竞争场景下可能成为瓶颈。真正的高手,既懂“开箱即用”,更知“底层何为”。

掌握 JUC,不仅是掌握 API,更是理解并发思想。愿你在高并发的世界里,写得出高效代码,也守得住数据一致。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐