JUC

注意:括号中为八股在每次面试中出现的概率

如何创建线程?(582/1759=33.1%)

Java 提供了多种方式来创建和管理线程,最常见的方式一共有四种,接下来我会分别进行讲述。

第一种是通过继承 Thread 类并重写其 run() 方法来创建线程。在run() 方法中定义线程需要执行的任务逻辑,然后

创建该类的实例,调用 start() 方法启动线程,start() 方法会自动调用 run() 方法中的代码逻辑。这种方式简单直观,但由于 Java 不支持多重继承,因此限制了类的扩展性。

第二种是实现 Runnable 接口并将其传递给 Thread 构造器来创建线程。Runnable 是一个函数式接口,其中的 run() 方法定义了任务逻辑。这种方式更加灵活,因为它不占用类的继承关系,同时可以更好地支持资源共享,可以让多个线程共享同一个 Runnable 实例。这种方式适用于需要解耦任务逻辑与线程管理的场景。

第三种是通过实现 Callable 接口来创建有返回值的线程。Callable 接口类似于 Runnable,但它可以返回结果并抛出异常。Callable 的 call() 方法需要通过 FutureTask 包装后传递给 Thread 构造器。通过 Future 对象可以获取线程执行的结果或捕获异常。这种方式适用于需要获取线程执行结果或处理复杂任务的场景。

第四种是通过 Executor 框架创建线程池来管理线程。Executor 框架提供了更高级的线程管理功能,例如线程复用、任务调度等。通过 submit() 或 execute() 方法提交任务,避免频繁创建和销毁线程的开销。它作为最常被使用的方式,广泛用于需要高效管理大量线程的场景。

如何记忆:

1.口诀记忆

线程创建有四招,继承 Thread 跑得早,

Runnable 接口好,资源共享不烦恼,

Callable 返回值高,FutureTask 来打包,

Executor 框架妙,线程池里效率高!

解释 :

第一句点明主题“线程创建有四招”,提到第一种方式“继承 Thread 类”,并用“跑得早”暗示它是最基础的方式。

第二句强调第二种方式“Runnable 接口”的灵活性和资源共享的特点。

第三句突出第三种方式“Callable 接口”的返回值特性,并提到 FutureTask 的作用。

最后一句总结第四种方式“Executor 框架”的高效性。

2. 联想记忆

想象一个工厂生产玩具车的过程,

Thread 类 :就像工厂自己造了一辆专属的车,但这辆车只能按照固定的设计运行,不能改装(Java 不支持多重继承)。

Runnable 接口 :工厂把车的任务外包给工人(Runnable 实例),多个工人可以共享同一套工具(资源共享)。

Callable 接口 :工厂派工人去完成任务,并要求他们带回结果(返回值)或者报告问题(异常)。

Executor 框架 :工厂建立了一个车队管理系统(线程池),统一调度车辆,避免频繁制造新车(减少开销)。

拓展:

1. 创建 Java 线程的本质

在 Java 中,线程的启动本质上是通过调用 Thread 类的 start() 方法完成的。start() 方法会通知 JVM 启动一个新的线程,并在这个新线程中执行 run() 方法中的代码逻辑。因此,无论你使用哪种方式创建线程,最终都会归结到调用 new Thread().start() 来真正启动线程。

换句话说,new Thread().start() 是启动线程的唯一入口,而其他方式(如继承 Thread、实现 Runnable 或 Callable、使用 Executor 框架)都是对 Thread 类的封装或扩展,目的是为了提供更灵活的编程模型。

2. 四种线程创建方式与new Thread().start()的关系

第一种:继承 Thread 类

实现方式:

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread running");
    }
}
MyThread t = new MyThread();
t.start(); // 启动线程

与 new Thread().start() 的关系:这里直接继承了 Thread 类,并重写了 run() 方法。调用 t.start() 时,实际上就是调用了 Thread 类的 start() 方法,启动了一个新线程。

第二种:实现 Runnable 接口

实现方式:

Runnable task = () -> System.out.println("Runnable running");
Thread t = new Thread(task);
t.start(); // 启动线程

与 new Thread().start()的关系:Runnable 是一个函数式接口,定义了线程的任务逻辑。Thread 类的构造器接受一个 Runnable 对象,并将其包装到内部的 run() 方法中。调用 t.start() 时,仍然是通过 Thread 类的 start() 方法启动线程。

第三种:实现 Callable 接口

实现方式:

Callable<Integer> task = () -> {
    System.out.println("Callable running");
    return 42;
};
FutureTask<Integer> futureTask = new FutureTask<>(task);
Thread t = new Thread(futureTask);
t.start(); // 启动线程

与 new Thread().start()的关系:Callable 接口允许线程返回结果或抛出异常。FutureTask 是对 Callable 的封装,实现了 Runnable 接口。最终还是通过 Thread 类的 start() 方法启动线程。

第四种:使用 Executor 框架

实现方式:

ExecutorService executor = Executors.newFixedThreadPool(2);
executor.submit(() -> System.out.println("Executor running"));
executor.shutdown();

与 new Thread().start()的关系:Executor 框架是对线程池的封装,提供了更高层次的线程管理功能。在底层,Executor 框架仍然使用 Thread 类来创建和启动线程。当你提交任务时,Executor 内部会调用 new Thread().start() 来启动线程。

通过以上分析,我们可以清楚地看到,虽然 Java 提供了多种线程创建方式,但它们都离不开 new Thread().start() 这一核心机制。每种方式只是对线程创建和管理的不同抽象,适用于不同的场景需求。

说说线程的生命周期和状态?(606/1759=34.5%)

在 Java 中,线程的生命周期可以分为六个状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、等待(Waiting)和终止(Terminated),线程在运行的整个生命周期的任意一个时刻,只可能处于其中一个状态,接下来我会详细讲述线程从创建到终止的整个过程以及每个状态的特点。

第一个是新建状态,当一个线程对象被创建时,它处于新建状态。

此时,线程还没有开始执行,仅仅是一个普通的 Java 对象,尚未与系统的线程调度器关联。在这个阶段,线程对象已经存在,但调用 start() 方法之前,它不会进入可运行状态。

第二个是就绪状态,当调用线程对象的 start() 方法后,线程进入就绪状态。

此时,线程已经被 JVM 注册到线程调度器中,但它还没有真正开始运行。线程调度器会根据操作系统的调度策略决定何时将 CPU 时间分配给该线程。需要注意的是,就绪状态的线程可能正在等待 CPU 资源,也可能已经获得了资源但尚未执行代码。

第三个是运行状态,当线程获得 CPU 时间片并开始执行其 run() 方法中的代码时,它进入运行状态。

这是线程实际执行任务的状态,也是我们编写业务逻辑的地方。运行状态的线程可能会因为某些原因(如调用 sleep() 或 wait() 方法)主动放弃 CPU,或者因为时间片耗尽而被系统强制切换回就绪状态。

第四个是阻塞状态,当线程试图获取一个锁(例如进入同步代码块或方法),但该锁正被其他线程占用时,它会进入阻塞状态。

此时,线程会暂停执行,直到获取到所需的锁为止。 阻塞状态通常发生在多线程竞争共享资源的场景中。

第五个是等待状态,当线程调用了某些特定的方法(如 Object.wait()、Thread.join() 或 LockSupport.park()),它会进入等待状态。

在这种状态下,线程会无限期地等待,直到其他线程显式地唤醒它(例如通过 notify() 或 notifyAll() 方法)。等待状态通常用于线程间的协作。

第六个是终止状态,当线程的 run() 方法执行完毕,或者由于未捕获的异常导致线程提前退出时,它进入终止状态。

此时,线程的生命周期结束,无法再被启动或复用。

如何记忆:

1.缩写记忆

将六个状态的核心词提取为:N-R-R-B-W-T

N: New(新建)

R: Runnable(就绪)

R: Running(运行)

B: Blocked(阻塞)

W: Waiting(等待)

T: Terminated(终止)

记忆技巧

可以联想成“NR-RBWT”,发音类似“嗯~绕吧我太”(轻松幽默的语气),意思是“线程的状态变化有点绕,但我能记住”。

2.联想记忆

想象一个人从出生到去世的过程,

新建状态(New) :就像一个婴儿刚刚出生,虽然存在,但还不能独立行动。

就绪状态(Runnable) :婴儿长大了一些,可以排队等待玩耍(CPU 时间片)。

运行状态(Running) :终于轮到他玩了,开始执行自己的任务(run() 方法)。

阻塞状态(Blocked) :他想玩滑梯,但滑梯被别人占着,只能等着(锁被占用)。

等待状态(Waiting) :他玩累了,躺在椅子上休息,直到有人叫醒他(显式唤醒)。

终止状态(Terminated) :人老了,生命结束了,无法再复活。

拓展:

1.描述一下线程的生命周期图

线程在创建后首先进入 NEW(新建)状态,调用 start() 方法后进入 READY(可运行)状态,等待 CPU 时间片分配;当线程获得时间片后进入 RUNNING(运行)状态并执行任务。若线程调用 wait() 方法,则进入 WAITING(等待)状态,需依赖其他线程的通知才能恢复运行;而通过 sleep(long millis) 或 wait(long millis) 方法,线程会进入 TIMED_WAITING(超时等待)状态,并在超时结束后返回可运行状态。如果线程试图获取 synchronized 锁但被占用,则进入 BLOCKED(阻塞)状态,直到锁可用。最后,当线程执行完 run() 方法或因异常退出时,进入 TERMINATED(终止)状态,生命周期结束。

2.线程的等待状态

线程的等待状态(Waiting State)是线程生命周期中的一个重要且复杂的状态,表示线程暂时放弃 CPU 的使用权,进入一种“暂停”的状态。等待状态的核心原因是线程需要等待某些条件满足后才能继续运行,比如等待资源、锁、通知或超时等。为了更好地理解等待状态,我们可以将其细分为几种具体的阻塞情况,包括等待阻塞、同步阻塞和其他阻塞。

(1)等待阻塞

定义:当一个线程调用 wait() 方法时,JVM 会将该线程放入等待队列,并使其进入等待阻塞状态。

触发条件:调用 Object.wait() 方法(必须在同步代码块或同步方法中调用)。调用后,当前线程会释放持有的锁,并进入等待队列,直到其他线程调用 notify() 或 notifyAll() 方法唤醒它。

特点:线程不会自动恢复运行,必须依赖外部的通知机制。常用于线程间的协作,例如生产者-消费者模型。

示例:

synchronized (lock) {
    while (!condition) {
        lock.wait(); // 当前线程进入等待阻塞状态
    }
    // 条件满足后继续执行
}

(2)同步阻塞

定义:当一个线程试图获取某个对象的同步锁,但该锁已经被其他线程占用时,JVM 会将该线程放入锁池,并使其进入同步阻塞状态。

触发条件:线程尝试进入 synchronized 方法或代码块,但所需的锁被其他线程持有。

特点:线程会持续尝试获取锁,直到锁可用为止。锁池中的线程不会主动放弃竞争,而是等待锁释放后重新参与竞争。

示例:

synchronized (lock) {
    // 只有获得锁的线程才能进入此代码块
    System.out.println("Thread is running in synchronized block");
}

如果多个线程同时尝试访问这段代码,未获得锁的线程会被放入锁池,进入同步阻塞状态。

(3)其他阻塞

定义:除了上述两种阻塞外,还有一些其他场景会导致线程进入阻塞状态,主要包括以下几种:

Thread.sleep() 方法:调用 Thread.sleep(long millis) 方法会使线程暂停指定的时间。在这段时间内,线程不占用 CPU 时间片,也不会参与调度。当睡眠时间结束后,线程会自动恢复到可运行状态。

Thread.join() 方法:调用 join() 方法会使当前线程等待目标线程执行完毕后再继续运行。这种阻塞状态会在目标线程终止后自动解除。

I/O 阻塞:当线程发起 I/O 操作(如文件读写、网络通信)时,如果操作尚未完成,线程会进入阻塞状态。当 I/O 操作完成后,线程会恢复运行。

特点:这些阻塞状态通常是临时性的,线程会在特定条件满足后自动恢复运行。不需要显式的唤醒机制(如 notify() 或 notifyAll())。

示例:

// Thread.sleep 示例
try {
    Thread.sleep(1000); // 当前线程休眠 1 秒
} catch (InterruptedException e) {
    e.printStackTrace();
}

// Thread.join 示例
Thread t = new Thread(() -> System.out.println("Child thread running"));
t.start();
try {
    t.join(); // 主线程等待子线程执行完毕
} catch (InterruptedException e) {
    e.printStackTrace();
}

什么是线程上下文切换?(388/1759=22.1%)

线程上下文切换是多线程编程中的一个概念,它直接影响程序的性能和效率。接下来我会详细讲述线程上下文切换的定义、发生时机、过程和影响。

首先讲一下什么是线程上下文切换,它是指当 CPU 从一个线程切换到另一个线程时,操作系统需要保存当前线程的执行状态,并加载下一个线程的执行状态,以便它们能够正确地继续运行。执行状态主要包括:寄存器状态、程序计数器(PC)、栈信息、线程的优先级等。

接下来讲一下发生时机,通常有四种情况会发生线程上下文切换。

第一种是时间片耗尽,操作系统为每个线程分配了一个时间片,当线程的时间片用完后,操作系统会强制切换到其他线程,这是为了保证多个线程能够公平地共享 CPU 资源。

第二种是线程主动让出 CPU,当线程调用了某些方法,如 Thread.sleep()、Object.wait() 或 LockSupport.park()等,会使线程主动让出 CPU,导致上下文切换。

第三种是调用了阻塞类型的系统中断,比如:线程执行 I/O 操作时,由于 I/O 操作通常需要等待外部资源,线程会被挂起,会触发上下文切换。

第四种是被终止或结束运行

然后再讲一下线程上下文切换的过程,分为四步。

第一步是保存当前线程的上下文,将当前线程的寄存器状态、程序计数器、栈信息等保存到内存中。

第二步是根据线程调度算法,如:时间片轮转、优先级调度等,选择下一个要运行的线程。

第三步是加载下一个线程的上下文,从内存中恢复所选线程的寄存器状态、程序计数器和栈信息。

第四步是 CPU 开始执行被加载的线程的代码

最后讲一下线程上下文切换所带来的影响。线程上下文切换虽然能够实现多任务并发执行,但它也会带来 CPU 时间消耗、缓存失效以及资源竞争等问题。为了减少线程上下文切换带来的性能损失,可以采取减少线程数量、使用无锁数据结构等方式进行优化。

如何记忆:

1.联想记忆

想象一场接力赛跑:

定义 :接力棒代表线程的上下文(寄存器状态、程序计数器等)。当一个选手(线程)跑完自己的路程时,需要把接力棒交给下一个选手(线程)。

发生时机

  1. 时间片耗尽 :裁判规定每个人只能跑一段路(时间片),跑完就必须换人。
  2. 主动让出 CPU :某个选手累了,主动停下来休息(Thread.sleep())。
  3. 阻塞 I/O :某个选手跑到半路发现鞋子坏了(I/O 操作),需要等待修鞋师傅处理。
  4. 终止或结束 :某个选手跑完全程(线程结束)。

切换过程

  1. 第一步是保存当前选手的状态(保存上下文)。
  2. 第二步是选择下一个选手(线程调度)。
  3. 第三步是把接力棒交给下一位选手(加载上下文)。
  4. 第四步是下一位选手开始跑(CPU 执行代码)。

影响 :频繁交接接力棒会浪费时间(性能损失),所以要尽量减少交接次数(优化线程数量)。

拓展:

1.线程上下文切换的影响

然后,线程上下文切换虽然能够实现多任务并发执行,但它也会带来一定的开销:

CPU 时间消耗 :保存和恢复上下文需要额外的 CPU 周期,这会降低系统的整体性能。

缓存失效 :每次切换线程时,CPU 缓存中的数据可能会失效,导致下一线程需要重新加载数据,进一步增加延迟。

资源竞争 :如果线程数量过多,频繁的上下文切换会导致系统资源的竞争加剧,从而降低吞吐量。

2.如何减少线程上下文切换的影响?

最后,为了减少线程上下文切换带来的性能损失,可以采取以下优化措施:

减少线程数量 :合理控制线程池的大小,避免创建过多的线程。

使用无锁数据结构 :通过 CAS(Compare-And-Swap)等无锁操作减少线程间的锁竞争。

协程或异步编程 :使用轻量级的协程或异步模型(如 Java 的 CompletableFuture 或 Kotlin 的协程)替代传统的线程模型,减少上下文切换的频率。

优化线程调度策略 :根据任务的特点选择合适的线程调度算法,例如优先处理高优先级任务。

并发和并行的区别?(649/1759=36.9%)

并发和并行是多线程编程中的两个核心概念,它们描述了任务执行的不同方式。虽然这两个术语经常被混用,但它们有着本质的区别。接下来我会详细讲述并发和并行的定义以及它们之间的区别。

首先说一下什么是并发并发指的是多个任务在同一时间段内交替执行的能力。换句话说,并发并不一定要求任务同时进行,而是通过快速切换任务来实现“看起来同时运行”的效果。

然后再说一下什么是并行,并行指的是多个任务在同一时刻真正同时执行的能力。并行通常需要多核 CPU 的支持,每个核心独立处理一个任务,从而实现真正的并行计算。

最后说一下它们之间的四点区别

第一是执行方式的不同并发是任务交替执行,强调的是任务调度的时间分片;而并行是任务同时执行,强调的是多核资源的利用。

第二是对硬件的要求不同并发可以在单核 CPU 上实现,通过时间片轮转完成任务切换;而并行需要多核 CPU 的支持,每个核心独立处理一个任务。

第三是适用场景的不同,并发适合 I/O 密集型任务,因为这类任务通常需要等待外部资源,如磁盘、网络等,CPU 可以在这段时间切换到其他任务;而并行适合计算密集型任务,因为这类任务需要大量 CPU 计算,多核并行可以显著加速处理速度。

第四是核心目标的不同,并发的核心目标是提高系统的响应能力和资源利用率;而并行的核心目标是提高系统的吞吐量和计算效率。

如何记忆:

1.联想记忆

想象一个餐厅厨房的运作:

并发

厨房只有一个厨师(单核 CPU),他需要在多个任务之间快速切换,比如切菜、煮汤、炒菜。虽然他不能同时做所有事情,但通过快速切换任务,看起来像是同时进行(交替执行)。

这种方式适合需要等待的任务,比如等水烧开时去切菜(I/O 密集型任务)。

并行

厨房有多个厨师(多核 CPU),每个人独立负责一个任务,比如一个切菜、一个煮汤、一个炒菜。他们真正同时工作,效率更高(同时执行)。

这种方式适合需要大量计算的任务,比如同时处理多个复杂的菜品(计算密集型任务)。

区别

执行方式 :单个厨师切换任务 vs 多个厨师同时工作。

硬件要求 :单个厨师 vs 多个厨师。

适用场景 :等待外部资源(如等水烧开) vs 需要大量计算(如复杂烹饪)。

核心目标 :单个厨师提高效率(响应能力) vs 多个厨师加速完成(吞吐量)。

拓展:

1.并发和并行的实际应用

最后,在实际开发中,并发和并行往往是结合使用的。例如:

(1)在 Java 中,ExecutorService 提供了线程池机制,可以通过合理配置线程池大小来实现并发任务调度。

(2)对于计算密集型任务,可以使用 ForkJoinPool 或并行流(parallelStream)来充分利用多核 CPU 的并行能力。

(3)在分布式系统中,通过将任务分解为多个子任务并发执行,并在多个节点上并行处理,可以进一步提升系统的整体性能。

同步和异步的区别?(534/1759=30.4%)

同步和异步是编程中两种常见的任务执行模式,接下来我会详细讲述同步和异步的定义以及它们之间的区别。

首先说一下什么是同步同步指的是任务按照顺序依次执行的方式。在这种模式下,调用者会阻塞等待任务完成并返回结果后,才会继续执行后续的操作。

然后再说一下什么是异步,异步指的是任务无需等待立即返回,调用方可以继续执行其他操作,而任务的结果会在稍后通过如回调函数、事件通知或 Future 对象等机制传递给调用方。

最后说一下同步和异步的区别

第一是执行方式不同同步是阻塞式的,调用方需要等待任务完成才能继续;而异步是非阻塞式的,调用方无需等待任务完成即可继续执行。

第二是响应机制不同同步直接返回任务的结果,调用方可以直接使用;而异步通常通过回调函数、事件通知或 Future 对象等方式传递结果。

第三是适用场景不同同步适合简单、短时间的任务,或者需要立即获取结果的场景;而异步适合需要提高系统吞吐量的场景,或者用于耗时较长的任务,如:网络请求、文件读写等。

如何记忆:

1.口诀记忆

示例 :

同步阻塞等结果,异步非阻跑得快。同步直接拿数据,异步回调再安排。简单任务用同步,耗时操作靠异步!

解释 :

第一句“同步阻塞等结果”:描述同步模式的特点,调用方需要等待任务完成。

第二句“异步非阻跑得快”:强调异步模式的非阻塞性,调用方可以继续执行其他任务。

第三句“同步直接拿数据”:说明同步模式的结果可以直接使用。

第四句“异步回调再安排”:指出异步模式通常通过回调等方式传递结果。

最后两句总结了两者的适用场景。

2.联想记忆

想象你在餐厅点餐的过程:

同步模式

你点了一份菜,服务员告诉你要等厨师做好才能上桌。

在等待的过程中,你只能坐在那里干等(阻塞),直到菜端上来才能继续吃饭。

异步模式

你点了一份菜,服务员告诉你不用等,你可以先去逛商场或者做别的事情(非阻塞)。

当菜做好后,服务员会通过广播通知你(回调函数或事件通知),你再回来享用。

拓展:

1.同步和异步的实际应用

最后,在实际开发中,同步和异步的选择取决于具体的业务需求。例如:

(1)在 Java 的传统 I/O 操作中,文件读取或网络请求通常是同步的,调用线程会阻塞直到操作完成。

(2)在 Java NIO 或第三方框架(如 Netty)中,I/O 操作是异步的,调用线程不会被阻塞,数据到达后通过回调或事件通知的方式处理。

(3)在现代 Web 开发中,异步编程模型(如 Spring 的 @Async 注解或 Reactor 框架)被广泛应用于提升系统的并发能力和响应速度。

线程池的七大参数?(679/1759=38.6%)

线程池是 Java 并发编程中的重要工具,它通过复用线程来减少线程创建和销毁的开销,从而提高系统的性能和稳定性。在 Java 中,线程池的核心实现类是 ThreadPoolExecutor,它提供了七个重要的参数来配置线程池的行为。接下来我会详细讲述这七大参数的定义、作用以及它们如何影响线程池的工作机制。

第一个是核心线程数(corePoolSize),它是指线程池中始终保持存活的线程数量,即使这些线程处于空闲状态。

当提交一个新任务时,如果当前线程数小于核心线程数,线程池会优先创建新线程来处理任务,而不是将任务放入队列。例如,设置 corePoolSize=5 表示线程池会始终维护至少 5 个线程。

第二个是最大线程数(maximumPoolSize),它是指线程池中允许的最大线程数量。当任务队列已满且当前线程数小于最大线程数时,线程池会继续创建新线程来处理任务。如果线程数已经达到最大值,则任务会被拒绝。例如,设置 maximumPoolSize=10 表示线程池最多可以创建 10 个线程。

第三个是线程空闲时间(keepAliveTime),它是指非核心线程在空闲状态下保持存活的时间。当线程池中的线程数超过核心线程数时,多余的空闲线程会在指定的空闲时间后被回收。例如,设置 keepAliveTime=60 表示非核心线程在空闲 60 秒后会被销毁。

第四个是时间单位(unit),它用于指定线程空闲时间的计量单位。常见的单位包括 TimeUnit.SECONDS(秒)、TimeUnit.MILLISECONDS(毫秒)等。例如,unit=TimeUnit.SECONDS 表示空闲时间以秒为单位。

第五个是任务队列(workQueue),它是一个阻塞队列,用于存放等待执行的任务。当线程池中的线程数达到核心线程数时,新提交的任务会被放入任务队列中等待执行。常见的队列类型包括:

ArrayBlockingQueue:有界队列,适用于控制资源使用。

LinkedBlockingQueue:无界队列,适用于任务量较大的场景。

SynchronousQueue:不存储任务的队列,适用于直接传递任务给线程的场景。

第六个是线程工厂(threadFactory),它用于创建线程池中的线程。通过自定义线程工厂,可以为线程设置名称、优先级或其他属性,便于调试和管理。例如,使用 Executors.defaultThreadFactory() 创建默认线程工厂。

第七个是拒绝策略(handler),它用于处理当线程池无法接受新任务时的情况(例如线程数达到最大值且任务队列已满)。常见的拒绝策略包括:

AbortPolicy:抛出异常,拒绝任务。

CallerRunsPolicy:由调用线程执行任务。

DiscardPolicy:直接丢弃任务。

DiscardOldestPolicy:丢弃队列中最旧的任务,并尝试重新提交新任务。

如何记忆:

1.联想记忆

想象一个餐厅厨房的运作:

核心线程数(CorePoolSize) :厨房里有固定的厨师(核心线程),他们始终在岗,即使暂时没有客人点餐(空闲状态)。

最大线程数(MaximumPoolSize) :如果客人太多,厨房可以临时增加一些帮厨(非核心线程),但最多只能增加到一定数量(最大线程数)。

空闲时间(KeepAliveTime) :当客人减少时,临时帮厨会在空闲一段时间后离开(非核心线程被回收)。

时间单位(Unit) :规定帮厨的空闲时间是以分钟还是小时计算(时间单位)。

任务队列(workQueue) :如果厨师忙不过来,新的订单会被放入待处理队列中(任务队列)。

线程工厂(ThreadFactory) :餐厅经理负责招聘和培训厨师(线程工厂创建线程)。

拒绝策略(Handler) :如果厨房爆满且订单队列已满,餐厅可以选择拒绝接单(拒绝策略)。

拓展:

1.线程池创建的两种方式

(1)通过 ThreadPoolExecutor 手动创建线程池

ThreadPoolExecutor 是线程池的核心实现类,它允许开发者通过构造函数灵活地配置线程池的参数。以下是 ThreadPoolExecutor 的构造函数:

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

优点: 配置灵活,可以根据业务需求精确控制线程池的行为。 易于调试和排查问题,因为所有参数都是显式设置的。

示例代码:

// 创建一个自定义线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    2,                      // 核心线程数
    4,                      // 最大线程数
    60,                     // 空闲线程存活时间
    TimeUnit.SECONDS,        // 时间单位
    new LinkedBlockingQueue<>(10), // 任务队列
    Executors.defaultThreadFactory(), // 线程工厂
    new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
executor.submit(() -> System.out.println("任务执行"));

(2)通过 Executors 工具类创建线程池(不建议使用)

Executors 是一个工具类,提供了几种常用的线程池创建方法,例如:

newFixedThreadPool(int nThreads):创建固定大小的线程池。

newCachedThreadPool():创建一个根据需要创建新线程的线程池。

newSingleThreadExecutor():创建只有一个线程的线程池。

newScheduledThreadPool(int corePoolSize):创建支持定时及周期性任务执行的线程池。

优点:使用简单,适合快速开发和测试场景。

示例代码:

// 使用 Executors 创建固定大小的线程池
ExecutorService executor = Executors.newFixedThreadPool(4);
executor.submit(() -> System.out.println("任务执行"));

2. 为什么《阿里巴巴 Java 开发手册》禁止使用 Executors 创建线程池?

尽管 Executors 提供了便捷的线程池创建方式,但《阿里巴巴 Java 开发手册》明确指出:线程池不允许使用 Executors 去创建,而是推荐通过 ThreadPoolExecutor 手动创建线程池。原因如下:

(1)资源耗尽风险

Executors 创建的线程池在某些情况下可能导致资源耗尽,进而引发系统崩溃。例如:

newFixedThreadPool 和 newSingleThreadExecutor:它们使用的是无界队列(LinkedBlockingQueue),如果任务提交速度远高于任务处理速度,队列会不断增长,最终导致内存溢出(OutOfMemoryError)。

ExecutorService executor = Executors.newFixedThreadPool(4);
for (int i = 0; i < Integer.MAX_VALUE; i++) {
    executor.submit(() -> {
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {}
    });
}

在这个例子中,任务队列会无限增长,最终耗尽内存。

newCachedThreadPool:它允许创建大量线程(理论上没有上限),如果任务提交过多,可能会导致线程数量激增,消耗大量系统资源,甚至导致系统崩溃。

ExecutorService executor = Executors.newCachedThreadPool();
for (int i = 0; i < 10000; i++) {
    executor.su

剩余60%内容,订阅专栏后可继续查看/也可单篇购买

【神品八股】1759篇面经精华 文章被收录于专栏

神哥引路,稳稳起步!!早鸟特惠,仅剩177名额!晚了就涨到29.9了! 核心亮点: 1.数据驱动,精准高频:基于1759篇面经、24139道八股题,精准提炼真实高频八股。 2.科学记忆,高效掌握:融合科学记忆法和面试表达技巧,记得住,说得出。 3.提升思维,掌握财商:不仅可学习八股,更可教你变现,3个月赚不回购买价,全额退。 适宜人群: 在校生、社招求职者及自学者。

全部评论
1 回复 分享
发布于 02-17 19:27 四川
求 JMM 神哥
1 回复 分享
发布于 02-20 17:34 上海

相关推荐

点赞 评论 收藏
分享
评论
4
9
分享

创作者周榜

更多
牛客网
牛客企业服务