面试题整理:Java多线程(二)多线程、死锁、乐观锁悲观锁、线程池

文章目录

      • 线程
        • [1. ⭐什么是线程和进程?区别和联系?](#1. ⭐什么是线程和进程?区别和联系?)
        • [2. 堆和方法区是什么?](#2. 堆和方法区是什么?)
        • [3. 如何创建线程?](#3. 如何创建线程?)
        • [4. ⭐线程的生命周期和状态有什么?](#4. ⭐线程的生命周期和状态有什么?)
        • [5. 什么是线程上下文切换?](#5. 什么是线程上下文切换?)
        • [6. Thread.sleep()和Object.wait()的异同点?](#6. Thread.sleep()和Object.wait()的异同点?)
        • [7. 直接调用Thread类的run方法会怎么样?](#7. 直接调用Thread类的run方法会怎么样?)
      • 多线程
        • [1. 并发与并行、同步和异步?](#1. 并发与并行、同步和异步?)
        • [2. ⭐为什么需要使用多线程?](#2. ⭐为什么需要使用多线程?)
        • [3. ⭐单核CPU支持Java多线程吗?](#3. ⭐单核CPU支持Java多线程吗?)
        • [4. ⭐单核CPU上运行多个线程效率一定高吗?](#4. ⭐单核CPU上运行多个线程效率一定高吗?)
        • [5. 并发编程可能带来的问题?什么是线程安全和不安全?](#5. 并发编程可能带来的问题?什么是线程安全和不安全?)
      • 👉死锁
        • [1. 什么是线程死锁?Java如何检测死锁?](#1. 什么是线程死锁?Java如何检测死锁?)
        • [2. 如何预防和避免死锁?](#2. 如何预防和避免死锁?)
      • 👉乐观锁、悲观锁
        • [1. 什么是乐观锁?问题?使用场景?](#1. 什么是乐观锁?问题?使用场景?)
        • [2. 什么是悲观锁?问题?使用场景?](#2. 什么是悲观锁?问题?使用场景?)
        • [3. 乐观锁如何实现的?什么是CAS算法?](#3. 乐观锁如何实现的?什么是CAS算法?)
        • [4. java中是如何实现CAS算法的?有什么问题?](#4. java中是如何实现CAS算法的?有什么问题?)
        • [5. 什么是自旋锁?](#5. 什么是自旋锁?)
      • 👉线程池
        • [1. ⭐ 什么是线程池,作用和优势?](#1. ⭐ 什么是线程池,作用和优势?)
        • [2. Java 中常见的线程池有哪些?如何创建?](#2. Java 中常见的线程池有哪些?如何创建?)
        • [3. ⭐为什么不推荐使用内置线程池?](#3. ⭐为什么不推荐使用内置线程池?)
        • [4. 线程池常见参数有哪些?如何解释?⭐线程池处理任务的流程了解吗?](#4. 线程池常见参数有哪些?如何解释?⭐线程池处理任务的流程了解吗?)
        • [5. ⭐线程池的拒绝策略有哪些?](#5. ⭐线程池的拒绝策略有哪些?)
        • [6. 线程池常见的阻塞队列有哪些?](#6. 线程池常见的阻塞队列有哪些?)
        • [7. ⭐线程池中线程异常后,销毁还是复用?](#7. ⭐线程池中线程异常后,销毁还是复用?)
        • [8. ⭐如何给线程池命名?](#8. ⭐如何给线程池命名?)
        • [9. 如何配置线程池的大小?](#9. 如何配置线程池的大小?)
        • [10. ⭐如何动态修改线程池参数?](#10. ⭐如何动态修改线程池参数?)
        • [11. ⭐如何设计一个能根据任务优先级来执行的线程池?](#11. ⭐如何设计一个能根据任务优先级来执行的线程池?)

线程

1. ⭐什么是线程和进程?区别和联系?

进程:程序的一次执行过程。一个 Java 程序的运行一般是 main 线程和多个其他线程同时运行。

线程:比进程更小的执行单位。同类的多个线程共享进程的堆和方法区,但每个线程有自己的程序计数器、虚拟机栈、本地方法栈。

程序计数器私有主要是为了++线程切换后能恢复到正确的执行位置++。 为了保证线程中的局部变量不被别的线程访问到,虚拟机栈和本地方法栈是线程私有的。

  • 程序计数器:记录java代码下一条指令的地址,字节码解释器通过改变程序计数器来依次读取指令,从而实现代码的流程控制;在多线程的情况下,程序计数器用于记录当前线程执行的位置,从而当线程被切换回来的时候能够知道该线程上次运行到哪儿了。
  • 虚拟机栈:每个 Java 方法在执行之前会创建一个栈帧用于存储局部变量表、操作数栈、常量池引用等信息。从方法调用直至执行完成的过程,就对应着一个栈帧在 Java 虚拟机栈中入栈和出栈的过程。
  • 本地方法栈: 和虚拟机栈所发挥的作用非常相似,区别是:虚拟机栈为虚拟机执行 Java 方法 (也就是字节码)服务,而本地方法栈则为虚拟机使用到的 Native 方法服务。 在 HotSpot 虚拟机中和 Java 虚拟机栈合二为一。

JDK 1.2 之前,Java 线程是基于绿色线程(一种用户级线程),之后改成直接用原生的内核级线程。

现在的 Java 线程的本质其实就是操作系统的线程。Java线程模型在win和linux上为一对一。

2. 堆和方法区是什么?

堆和方法区是所有线程共享的资源。

是进程中最大的一块内存,主要用于存放新创建的对象实例和数组 (几乎所有对象都在这里分配内存)。

方法区 主要用于存放已被加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。

3. 如何创建线程?

使用new Thread().start()创建线程。

在 Java 代码中使用多线程的方法:继承Thread类、实现Runnable接口、实现Callable接口、使用线程池、使用CompletableFuture类等等。

4. ⭐线程的生命周期和状态有什么?

Java线程在生命周期中有6种状态,随着代码的执行在不同状态中切换。

  1. NEW:初始状态。线程被创建,但没有调用.start()。
  2. RUNNABLE:可运行状态。线程被调用了start()开始运行。
  3. WAITING:等待状态。线程需要等待其他线程的特定动作,如通知或中断。
  4. TIME_WAITING:超时等待。线程需要等待一定的时间才能回到RUNNABLE。
  5. BLOCKED:阻塞状态。线程需要等待其他线程释放锁之后才能运行。
  6. TERMINATED:终止状态。线程运行结束

thread.join()表示调用此方法的线程被阻塞,仅当thread完成以后,方法才能继续运行。

5. 什么是线程上下文切换?

线程切换时:线程从CPU状态中退出,需要保存当前线程的上下文(比如程序计数器、虚拟机栈、本地方法栈等),加载即将占用CPU的线程的上下文。

当前线程从CPU状态退出的几种情况:

  • 主动让出CPU。比如调用sleep、wait。
  • 时间片用完。操作系统防止一个线程或进程长时间占用CPU导致其他线程或进程饿死。
  • 调用了阻塞类型的CPU中断。比如请求IO,线程被阻塞。
  • 被终止或结束运行。
6. Thread.sleep()和Object.wait()的异同点?

同:都可以暂停线程的执行。都是native方法

不同:

  1. sleep()不释放锁,wait()释放锁。
  2. wait只能在同步代码块或同步控制块中使用,而sleep可以在任何位置使用。
  3. sleep()常被用于暂停执行,wait()常被用于线程通信。
  4. **唤醒方法不同。**sleep()和wait(long timeout)完成后,线程自动苏醒;而wait()调用后,需要别的线程调用同一个对象上的notify()或notifyAll()方法来唤醒正在等待的线程。
  5. sleep()时Thread类的static native方法,wait()是Object类的native方法。

wait()为什么是定义在Object中而不是Thread中?

每个对象都有对象锁,wait()是希望让获得对象锁的线程等待,会释放当前线程占有的对象锁,是需要操作对应的Object而不是Thread。

sleep()为什么定义在Thread中?

sleep()是希望当前线程暂停执行,不涉及对象。

7. 直接调用Thread类的run方法会怎么样?

线程从创建到运行,是在new之后,执行thread.start()方法之后进入RUNNABLE状态。start()方法进行线程的准备工作,之后自动执行run()方法,这是多线程运行模式。但是如果手动调用thread.run()而不执行start()的话只会把run()方法当作普通方法,而不会以多线程方式执行。

多线程

1. 并发与并行、同步和异步?

并发:多个作业在同一时间段执行。并行:多个作业在同一时刻执行。

同步:发出调用后,没有得到结果前,此调用不能返回。异步:发出调用后不用等待返回结果,调用可以直接返回。

2. ⭐为什么需要使用多线程?

**减少开销:**线程可以比作轻量级的进程,线程是程序执行的最小单位,线程切换和调度成本远低于进程,多核CPU时代多个线程可以同时运行,减少线程上下文开销。

**互联网发展的要求:**如今的系统很多需要百万级、千万级的并发,多线程并发编程时开发高并发系统的基础,利用多线程机制可以提升系统并发能力和性能。

从计算机底层来讲:

**单核时代:**多线程可以提高进程利用CPU和IO系统的效率。如果Java进程只有一个线程,当线程被IO阻塞时整个进程都被阻塞,CPU和IO设置只能有一个在运行,系统整体效率低。

**多核时代:**多线程可以提高进程利用多核CPU的能力。多个线程可以被映射到底层多个CPU核心上执行,提高效率。

3. ⭐单核CPU支持Java多线程吗?

支持。操作系统通过时间片轮转的方式,把CPU时间分配给不同线程,虽然单核CPU一次只能执行一个线程,但是通过多个线程间快速切换,可以让用户感觉多个任务同时进行。

Java使用的线程调度方式是抢占式调度。操作系统决定何时暂停当前正在运行的线程,并切换到另一个线程执行。这种切换通常是由系统时钟中断(时间片轮转)或其他高优先级事件(如 I/O 操作完成)触发的。

JVM本身不负责Java线程调度,而是交给操作系统。操作系统基于线程优先级和时间片来调度线程的执行。

4. ⭐单核CPU上运行多个线程效率一定高吗?

不一定,取决于线程的类型和任务性质,而且都要适度。

对于CPU密集型线程,进行计算和逻辑处理需要占用大量的CPU资源,多个线程运行时会导致频繁的线程切换,增加系统开销。

对于IO密集型线程,主要进行输入输出操作,多个线程同时运行可以利用CPU在等待IO时的空闲时间,提高了效率。

5. 并发编程可能带来的问题?什么是线程安全和不安全?

并发编程目的是提高程序执行效率,但可能会导致内存泄漏、死锁、线程不安全。

线程安全:多线程环境下,同一份数据,不管多少线程同时访问,都能够保证数据安全和一致性。

线程不安全:多线程环境下,同一份数据,多个线程同时访问,数据不一致、错误、丢失等。

👉死锁

1. 什么是线程死锁?Java如何检测死锁?

Java线程死锁指的是,多个线程同时阻塞,等待资源被释放。

死锁形成的4个条件:

  1. 互斥条件。一个资源任意时刻只能被一个线程占用。
  2. 请求与保持条件。一个线程请求新的资源阻塞时,对以获取的资源保持不放。
  3. 不可剥夺条件。线程已获取的资源在没有使用完之前不会被其他线程强行剥夺,只有自己使用完毕后才释放资源。
  4. 循环等待条件。多个线程之间形成首尾相接的循环等待资源关系。

Java如何检测死锁?

  • 使用jmap、jstack等查看JVM线程栈和堆内存的情况,死锁发生时jstack输出中通常包括"Found one Java-level deadlock:"字样,后面跟随死锁相关的线程信息。

    shell 复制代码
    # jps(Java Virtual Machine Process Status Tool)是Java自带的一个工具,它可以列出当前运行的所有Java进程及其相关信息。
    jps -l # 找到当前java进程的pid
    jstack <pid>
    • 实际项目中可以搭配top、df、free等命令查看操作系统信息,出现死锁可能导致CPU、内存资源消耗过高。
  • 采用VisualVM、JConsole等工具排查。

2. 如何预防和避免死锁?

预防死锁:破坏死锁产生的必要条件。

  • 破坏请求保持条件:一次性申请所有资源。
  • 破坏不可剥夺条件:如何线程申请资源申请不到,就释放持有的资源。
  • 破坏循环等待条件:申请资源时按照一定的线程顺序,释放资源时按照逆序。

避免死锁:资源分配时,借助算法(如银行家算法)对资源分配进行评估,使其进入安全状态。

银行家算法:

思想:当一个线程申请资源时,系统先试探性地分配,判断分配后系统是否处于安全状态,如果安全那么就分配,否则不分配让线程继续等待。

如何判断安全状态?假设线程P1申请资源R1,系统试探性分配后,判断剩余的资源中,能否满足队列中的某个线程执行完毕:

  1. 如果能,假设回收已分配给它的资源,标记线程已完成,再继续试探性的分配,直到所有的线程都能执行完毕,此时处于安全状态,给线程分配资源的顺序即为安全序列。
  2. 如果不能,则系统处于不安全状态。此时没有一个进程能够完成并释放资源,随着时间推移,系统会处于死锁状态。

一句话+一张图说清楚------银行家算法-CSDN博客

👉乐观锁、悲观锁

1. 什么是乐观锁?问题?使用场景?

乐观锁 :假设最好的情况,对共享资源的访问都不会产生问题,不用加锁不用等待,只需要在提交修改时验证(版本号机制、CAS算法)资源是否被其他线程修改了。例如AtomicInteger、LongAdder

可能产生的问题:高写入场景下,乐观锁可能导致 验证时 频繁的失败和重试,影响性能。(LongAdder以空间换时间的方式解决了大量失败重试的问题)

适用场景:优点是不存在锁竞争导致的现场阻塞,不会有死锁问题,性能更好,适用于多读场景。但是乐观锁主要针对对象是单个共享变量。

2. 什么是悲观锁?问题?使用场景?

悲观锁 :假设最坏的情况,对共享资源的每次访问都可能发生问题,因此每次请求就上锁,一个资源一次只能被一个线程使用,其他线程阻塞。例如synchronized和ReentrantLock

可能带来的问题:

  • 高并发场景下,激烈的锁竞争造成线程阻塞,大量阻塞时操作系统上下文切换会影响系统性能。
  • 可能发生死锁问题,影响代码正常运行。

使用场景:多写场景,竞争激烈时,可以采用悲观锁,从而避免乐观锁频繁失败和重试影响性能。但如果乐观锁用LongAdder解决了频繁失败和重试问题的话,也可以用乐观锁。

3. 乐观锁如何实现的?什么是CAS算法?

版本号法:数据表中添加版本号,每次修改都让版本号加1,读取数据时读取版本号,写入时比较版本号和目前数据库中的数据版本号是否一致,一致则更新,不一致则说明其他线程已经修改了,重试操作。

CAS算法:比较和交换算法。CAS 操作是比较并交换,先比较内存中的值与预期值是否相等,相等则更新为新值。不相等说明有其他线程修改了变量值,需要放弃或重试。

4. java中是如何实现CAS算法的?有什么问题?
  • CAS 操作是比较并交换,先比较内存中的值与预期值是否相等,相等则更新为新值。CAS是一个无锁的原子算法。多个线程同时使用CAS 操作一个变量时,只有一个会胜出,并成功更新,其余均会失败。

  • Java中CAS的实现依赖于Unsafe类。sun.misc包下的Unsafe类提供了compareAndSwapObjectcompareAndSwapIntcompareAndSwapLong方法来实现的对Objectintlong类型的 CAS 操作。

  • Unsafe类中的CAS方法是native方法,是通过本地C、C++代码实现而不是java实现的,这些方法直接调用底层硬件指令实现原子操作,也就是说Java中CAS 相关的实现是通过 C++ 内联汇编的形式实现的(JNI调用 Java Native Interface)。因此, CAS 的具体实现和操作系统以及 CPU 都有关系。

  • 可能的问题?

    • ABA问题:如果变量V的值最初为A,被改为B后再改回来变成A,CAS算法可能误认为变量没改变。解决思路是在变量前面追加上版本号或者时间戳。
    • 循环时间长开销大:CAS 算法在执行失败时,会一直循环尝试,直到成功为止。如果 CAS 操作一直失败,那么循环时间就会很长,从而导致开销增大。可能会导致线程饥饿:如果一个线程一直占用着 CAS 操作,那么其他线程就可能会一直等待,从而导致线程饥饿。
    • 只能保证一个共享变量的原子操作 :CAS 算法只能保证一个共享变量的原子操作,如果需要对多个共享变量进行原子操作,那么就需要加锁或者把多个变量封装在一个AtomicReference类(保证引用对象之间的原子性)中。

Unsafe类位于sun.misc包下,是一个提供低级别、不安全操作的类。由于其强大的功能和潜在的危险性,它通常用于 JVM 内部或一些需要极高性能和底层访问的库中,而不推荐普通开发者在应用程序中使用。

5. 什么是自旋锁?

什么是自旋锁机制?当一个线程尝试去获取某一把锁的时候,如果这个锁此时已经被别人获取(占用),那么此线程就无法获取到这把锁,该线程将会等待,间隔一段时间后会再次尝试获取。再比如CSA算法因为并发冲突失败时,会不断循环尝试直到成功。

👉线程池

1. ⭐ 什么是线程池,作用和优势?

线程池就是管理一系列线程的资源池。当有任务要处理时,直接从线程池中获取线程来处理,处理完之后线程并不会立即被销毁,而是等待下一个任务。

线程池的作用是管理和复用线程,避免频繁创建和销毁线程带来的开销。

优势:降低资源消耗(创建和销毁)、提高响应速度(性能,不需要等待线程创建)、提高线程可管理性、控制并发数量。

2. Java 中常见的线程池有哪些?如何创建?

创建方式:

  1. **ThreadPoolExecutor**构造函数
  2. Executor 框架的工具类 Executors

常见的线程池:

SingleThreadExecutor :创建一个单线程的线程池,所有任务都由一个唯一的线程来执行。++适用于任务需要按顺序执行的场景++ 。ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();

FixedThreadPool :创建一个固定大小的线程池,线程池中的线程数量是固定的。++适用于任务数已知且负载比较均衡的场景++ 。ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4);

CachedThreadPool :创建一个可缓存的线程池,线程池的大小会根据需要动态变化。若有空闲线程可以复用,则会优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。如果线程池中的线程空闲超过一定时间,线程会被回收。++适用于执行很多短期异步任务的场景++ 。ExecutorService cachedThreadPool = Executors.newCachedThreadPool();

ScheduledThreadPool :创建一个支持定时和周期性任务的线程池。++适用于定时任务或重复任务的执行++ 。ScheduledExecutorService scheduledExecutor = Executors.newScheduledThreadPool(4);

3. ⭐为什么不推荐使用内置线程池?

《阿里巴巴 Java 开发手册》中强制线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 构造函数的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。

Executors 返回线程池对象的弊端:

  • FixedThreadPoolSingleThreadExecutor:使用的是有界阻塞队列是 LinkedBlockingQueue ,其任务队列的最大长度为 Integer.MAX_VALUE ,可能堆积大量的请求,导致 OOM。
  • CachedThreadPool:使用的是同步队列 SynchronousQueue, 允许创建的线程数量为 Integer.MAX_VALUE ,如果任务数量过多且执行速度较慢,可能会创建大量的线程,从而导致 OOM。
  • ScheduledThreadPoolSingleThreadScheduledExecutor :使用的无界的延迟阻塞队列 DelayedWorkQueue ,任务队列最大长度为 Integer.MAX_VALUE ,可能堆积大量的请求,从而导致 OOM。
4. 线程池常见参数有哪些?如何解释?⭐线程池处理任务的流程了解吗?
java 复制代码
public ThreadPoolExecutor(
	int corePoolSize,		//核心线程数量 任务队列未达到队列容量时,最大可以同时运行的线程数量。
    //通常根据系统的硬件资源(例如 CPU 核心数)来配置。对于计算密集型任务,核心线程数可以设置为 CPU 核心数N+1;对于 I/O 密集型任务,可以设置2N。
    int maximumPoolSize,	//最大线程数量 任务队列中存放的任务达到队列容量的时候,当前可以同时运行的线程数量变为最大线程数。//设置合适的最大线程数可以防止线程池创建过多的线程,避免系统资源耗尽或过载。
    BlockingQueue<Runnable> workQueue,	//任务队列,存储等待执行任务的队列。新任务来的时候会先判断当前运行的线程数量是否达到核心线程数,如果达到的话,新任务就会被存放在队列中。
    
    long keepAliveTime,		//线程数大于核心线程数时,多余空闲线程最长存活时间
    TimeUnit unit,			//时间单位
    TreadFactory threadFactory,			//线程工厂,用于创建线程
    RejectedExecutionHandler handler,	//拒绝策略
){
    
}

活跃线程指正在处理任务的线程:

  1. 随着任务数量的增加,会增加活跃的线程数。
  2. 当活跃的线程数 = 核心线程数,此时不再增加活跃线程数,而是往任务队列里堆积。
  3. 当任务队列堆满了,随着任务数量的增加,会在核心线程数的基础上加开线程。
  4. 直到活跃线程数 = 最大线程数,就不能增加线程了。
  5. 如果此时任务还在增加,则: 任务数 > 最大线程数 + 队列长度 ,抛出异常RejectedExecutionException,拒绝任务。

ThreadPoolExecutor 默认不会回收核心线程,即使它们已经空闲了。但是如果线程池是被用于周期性使用的场景,且频率不高(周期之间有明显的空闲时间),可以考虑将 allowCoreThreadTimeOut(boolean value) 方法的参数设置为 true,这样就会回收空闲(时间间隔由 keepAliveTime 指定)的核心线程了。

线程池在提交任务前,可以提前创建线程吗?

可以,ThreadPoolExecutor提供了2个方法:

  • prestartCoreThread():启动一个线程,等待任务,如果已达到核心线程数,这个方法返回 false,否则返回 true;
  • prestartAllCoreThreads():启动所有的核心线程,并返回启动成功的核心线程数。
5. ⭐线程池的拒绝策略有哪些?

线程任务数 > 最大线程数 + 队列长度时,拒绝策略:

AbortPolicy (默认):抛出 RejectedExecutionException 异常,拒绝执行当前任务。

CallerRunsPolicy:由调用execute方法的线程处理任务,而不是交给线程池中的线程。如果调用执行的线程关闭了,就丢弃任务。优点:所有任务请求都执行;缺点:影响提交新任务的速度,可能导致主线程阻塞,影响性能。

DiscardPolicy:丢弃当前任务,不抛出异常。

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

CallerRunsPolicy 拒绝策略有什么风险?如何解决?

CallerRunsPolicy拒绝策略的风险主要在于它可能会导致提交任务的线程被阻塞,从而影响该线程后续的执行。

  • 合理调整线程池的其他参数,比如增加核心线程数、最大线程数或调整工作队列的大小,以尽量避免触发该拒绝策略。

  • 可以结合监控系统,实时关注线程池的状态和任务执行情况,以便及时发现并处理可能出现的问题。

  • 导致主线程卡死的本质是因为我们不希望任何一个任务被丢弃,可以进行任务持久化。设计一张任务表将任务存储到 MySQL 数据库中、Redis 缓存任务、将任务提交到消息队列中等。

    • MySQL:当任务有较高的状态记录要求,并且希望能通过数据库进行进一步分析或查询时,可以选择数据库持久化。①自定义拒绝策略,实现RejectedExecutionHandler接口,负责将线程池暂时无法处理(此时阻塞队列已满)的任务入库保存到 MySQL 中。②自定义阻塞队列,继承BlockingQueue,包含 JDK 自带的ArrayBlockingQueue,重写take()方法来修改取任务处理的逻辑,优先从数据库中读取最早的任务,数据库中无任务时再从 ArrayBlockingQueue中去取任务。
    • Redis:高并发、高流量场景,尤其是当任务数过大时,通过 Redis 实现任务缓冲,可以有效减轻系统压力。
    • 消息队列:当系统需要高可靠性、任务不可以丢失,并且希望能够处理瞬时大流量的任务时,消息队列是一个合适的选择。
    • 其他主流框架的做法:以 Netty 为例,它的拒绝策略则是直接创建一个线程池以外的线程处理这些任务,为了保证任务的实时处理,这种做法可能需要良好的硬件设备且临时创建的线程无法做到准确的监控。ActiveMQ 则是尝试在指定的时效内尽可能的争取将任务入队,以保证最大交付。
6. 线程池常见的阻塞队列有哪些?

LinkedBlockingQueue(有界阻塞队列):基于链表实现。FixedThreadPoolSingleThreadExecutor

ArrayBlockingQueue(有界阻塞队列):底层由数组实现,容量一旦创建,就不能修改。

SynchronousQueue(同步队列):没有容量,没有空闲线程就创建新线程处理任务。如CachedThreadPool 的最大线程数是 Integer.MAX_VALUE ,可以理解为线程数是可以无限扩展的,可能会创建大量线程,从而导致 OOM。

DelayedWorkQueue(延迟队列):按照延迟时间长短对任务进行排序(延迟时间指的是每个任务被添加到队列后需要等待多久才会被执行)。内部采用的是"堆"的数据结构,可以保证每次出队的任务都是当前队列中执行时间最靠前的。DelayedWorkQueue 添加元素满了之后会自动扩容,增加原来容量的 50%,即队列永远不会阻塞,最大扩容可达 Integer.MAX_VALUE,所以最多只能创建核心线程数的线程。ScheduledThreadPoolSingleThreadScheduledExecutor

7. ⭐线程池中线程异常后,销毁还是复用?

两种情况。

用**execute()**提交任务时,如果任务执行时抛出异常并且没有在任务内被捕获,异常会导致当前线程终止,异常被打印到控制台或日志中。线程池检测到线程终止就创建新的线程替代它,保证配置的线程数不变。

submit()提交任务时,异常不会打印出来,而是被封装到Future对象,再由submit()返回Future(后续Future.get()可以捕获到ExecutionException)。对于线程池来说,线程不会因为异常终止,所以是继续复用。

8. ⭐如何给线程池命名?

线程池中的线程命名方式,取决于定义线程池时,传入的线程工厂。

  • 可以自定义线程工厂。构造函数中参数为线程名字,自增的AtomicInteger去标记线程池中不同的线程编号,线程工厂newThread时用线程池名字+线程编号。
  • 可以采用现成的工具库guava。ThreadFactoryBuilder
9. 如何配置线程池的大小?

根据任务特性,计算密集型 N+1, IO密集型2N。N是cpu核心数。

10. ⭐如何动态修改线程池参数?

实现自定义配置线程池的核心参数corePoolSizemaximumPoolSizeworkQueue,通过ThreadPoolExecutor提供的setxxx方法配置。

  • 例如,程序运行期间,若调用setCorePoolSize(),线程如果判断当前工作线程数大于corePoolSize就会收回工作线程。
  • 但是ThreadPoolExecutor没有提供动态指定任务队列长度的方法。有界阻塞队列LinkedBlockingQueue中的capacity被final修饰,所以美团是自定义了一个队列ResizableCapacityLinkedBlockIngQueue,去掉final修饰。Java线程池实现原理及其在美团业务中的实践 - 美团技术团队
11. ⭐如何设计一个能根据任务优先级来执行的线程池?
  1. 使用 PriorityBlockingQueue 作为任务队列,通过 ThreadPoolExecutor 的构造函数的 workQueue 参数传入。
  2. 使提交到线程池的任务具备排序能力,有两种方式:一是任务实现 Comparable 接口并重写 compareTo 方法指定优先级比较规则;二是创建 PriorityBlockingQueue 时传入 Comparator 对象指定排序规则(推荐)
  3. 针对可能出现的 OOM 问题,可继承 PriorityBlockingQueue 并重写 offer 方法,当插入元素数量超指定容量返回 false。
  4. 对于饥饿问题,可通过优化设计解决,如等待时间过长的任务移除并重新添加到队列且提升优先级。
相关推荐
boonya1 分钟前
Python 量化金融框架及技术落地方案
开发语言·python·金融
梦想不只是梦与想1 分钟前
rag和agent的区别
人工智能·python·知识库·rag·智能体·agent‌
go不是csgo3 分钟前
从一个 while 循环开始,搭一个完整的 AI Agent(参考开源项目 learn claude code)
人工智能·python·ai
WL_Aurora4 分钟前
Python爬虫实战(一):图书网站API接口爬取
爬虫·python
沙振宇4 分钟前
【Python】使用YOLO8识别视频中的车与人物
python·yolo·音视频·状态模式·识别
Ulyanov6 分钟前
《从质点到位姿:基于Python与PyVista的导弹制导控制全栈仿真》: 基石——3-DOF质点弹道的高保真建模与数值稳定性分析
开发语言·python·算法·ui·系统仿真
源码之家6 分钟前
计算机毕业设计:Python医疗数据可视化系统 Flask框架 数据分析 可视化 医疗大数据 用户画像(建议收藏)✅
python·深度学习·信息可视化·数据分析·django·flask·课程设计
学习中.........7 分钟前
Java 并发容器深度解析:从早期遗留类到现代高并发架构
java·开发语言·架构
加号39 分钟前
【C#】 实现程序最小化后重新拉起并强制置顶显示的技术指南
开发语言·c#
无所事事O_o11 分钟前
你真的理解 volatile 关键字了吗?
java