Java面试篇(线程池相关专题)

文章目录

  • 1. 为什么要使用线程池
  • 2. 线程池的核心参数和线程池的执行原理
    • 2.1 线程池的核心参数
    • 2.2 线程池的执行原理
  • 3. 线程池中常见的阻塞队列
    • 3.1 常见的阻塞队列
    • 3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别
  • 4. 如何确定线程池的核心线程数
    • 4.1 应用程序中任务的类型
      • 4.1.1 IO 密集型任务
      • 4.1.2 CPU 密集型任务
    • 4.2 如何确定核心线程数
  • 5. 线程池的种类
    • 5.1 固定线程数的线程池
    • 5.2 单线程线程池
    • 5.3 可缓存线程池
    • 5.4 提供延迟功能和周期执行功能的线程池
  • 6. 为什么不建议使用 Executors 类提供的静态方法创建线程池
  • 7. 线程池(多线程)的使用场景
    • 7.1 CountDownLatch
    • 7.2 多线程使用场景一:ElasticSearch 批量导入数据
    • 7.3 多线程使用场景二:数据汇总
    • 7.4 多线程使用场景三:异步调用
  • 8. 控制某个方法允许线程并发访问的线程数量(Semaphore)
  • 9. ThreadLocal
    • 9.1 ThreadLocal的基本使用
    • 9.2 ThreadLocal的实现原理&源码分析
      • 9.2.1 set 方法
      • 9.2.2 get 方法
      • 9.2.3 remove 方法
    • 9.3 ThreadLocal 的内存泄漏问题
  • 10. 线程池的 execute 方法和 submit 方法有什么区别
    • 10.1 返回值
    • 10.2 异常处理
    • 10.3 任务类型
    • 10.4 使用场景

1. 为什么要使用线程池

我们为什么要使用线程池呢,主要有两个原因:

  1. 每次创建线程的时候都会占用一定的内存空间,如果要创建很多个线程,有可能会浪费内存,严重的情况下还有可能会导致内存溢出
  2. CPU 资源是有限的,同一时刻 CPU 只能处理一个线程,如果有大量的请求到达服务器,我们创建了大量的线程,那么很多线程都没有 CPU 的执行权,这些线程都需要等待获取 CPU 的执行权,在这个过程中会有非常多的切换线程操作,频繁的线程切换操作会导致上下文切换开销增大,CPU 花费在真正执行任务上的时间就会减少,从而导致程序的性能下降

在项目开发的过程中,一般都会用线程池来管理线程、创建线程

线程池相关的内容,跟实际开发是有很大关系的,面试官也是特别喜欢问

2. 线程池的核心参数和线程池的执行原理

在这里插入图片描述

2.1 线程池的核心参数

线程池的核心参数主要有七个,我们主要参考 ThreadPoolExecutor 类的具有七个参数的构造函数,这七个参数也是面试官提问的重点

在这里插入图片描述

  1. corePoolSize:核心线程数
  2. maximumPoolSize:最大线程数(核心线程数 + 救急线程数的最大值)
  3. keepAliveTime:救急线程的生存时间,如果生存时间内没有新任务,与救急线程相关的资源会被释放
  4. unit:救急线程的生存时间单位
  5. workQueue:当没有空闲的核心线程时,新来任务会加入到此队列中排队,如果队列满了会创建救急线程来执行任务
  6. threadFactory:线程工厂,可以定制如何线程对象,例如为线程设置名字、是否为守护线程等
  7. handler:拒绝策略,当所有线程都在繁忙、workQueue 中的线程也满了时,会触发拒绝策略

2.2 线程池的执行原理

下面为大家介绍一下线程池的执行原理,也就是线程池是怎么工作的

在这里插入图片描述

首先,当有一个新任务到来时,会先判断核心线程数是否已经满了,如果核心线程数没满,就将任务添加到工作线程中,执行这个任务

如果核心线程数已经满了,会判断阻塞队列是否满了,如果阻塞队列中还有空间,就把任务添加到阻塞队列中进行等待,

如果阻塞队列满了,会判断线程数是否小于最大线程数,如果线程数小于最大线程数,会创建救急线程来执行任务

当核心线程或救急线程处于空闲的时候,会去阻塞队列中检查一下是否有需要执行的任务,如果有,就会使用核心线程或救急线程来执行阻塞队列中的任务


假如所有条件都不满足,线程池会有一个拒绝策略,拒绝策略有以下四种:

  1. AbortPolicy:直接抛出异常,默认使用的拒绝策略
  2. CallerRunsPolicy:用调用者所在的线程来执行任务
  3. DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
  4. DiscardPolicy:直接丢弃任务

可以运行以下代码,查看控制台的输出,辅助理解线程池的执行原理

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.FutureTask;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;public class TestThreadPoolExecutor {static class MyTask implements Runnable {private final String name;private final long duration;public MyTask(String name) {this(name, 0);}public MyTask(String name, long duration) {this.name = name;this.duration = duration;}@Overridepublic void run() {try {System.out.println(Thread.currentThread().getName() + " Running..." + this);Thread.sleep(duration);} catch (InterruptedException exception) {exception.printStackTrace();}}@Overridepublic String toString() {return "MyTask(" + name + ")";}}public static void main(String[] args) {AtomicInteger atomicInteger = new AtomicInteger(1);ArrayBlockingQueue<Runnable> arrayBlockingQueue = new ArrayBlockingQueue<>(2);ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,3,0,TimeUnit.MILLISECONDS,arrayBlockingQueue,runnable -> new Thread(runnable, "myThread" + atomicInteger.getAndIncrement()),new ThreadPoolExecutor.AbortPolicy());// new ThreadPoolExecutor.CallerRunsPolicy());// new ThreadPoolExecutor.DiscardOldestPolicy());// new ThreadPoolExecutor.DiscardPolicy());showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("1", 3600000));showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("2", 3600000));showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("3"));showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("4"));showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("5", 3600000));showState(arrayBlockingQueue, threadPool);threadPool.submit(new MyTask("6"));showState(arrayBlockingQueue, threadPool);}private static void showState(ArrayBlockingQueue<Runnable> queue, ThreadPoolExecutor threadPool) {try {Thread.sleep(300);} catch (InterruptedException exception) {exception.printStackTrace();}List<Object> tasks = new ArrayList<>();for (Runnable runnable : queue) {try {Field callable = FutureTask.class.getDeclaredField("callable");callable.setAccessible(true);Object adapter = callable.get(runnable);Class<?> clazz = Class.forName("java.util.concurrent.Executors$RunnableAdapter");Field task = clazz.getDeclaredField("task");task.setAccessible(true);Object object = task.get(adapter);tasks.add(object);} catch (Exception exception) {exception.printStackTrace();}}System.err.println("pool size: " + threadPool.getPoolSize() + ", queue: " + tasks);}}

如果运行代码时遇到了以下错误,运行前可以添加以下 VM 参数

错误信息:

java.lang.reflect.InaccessibleObjectException: Unable to make field private java.util.concurrent.Callable java.util.concurrent.FutureTask.callable accessible: module java.base does not "opens java.util.concurrent" to unnamed module @4fca772dat java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)at java.base/java.lang.reflect.Field.checkCanSetAccessible(Field.java:178)at java.base/java.lang.reflect.Field.setAccessible(Field.java:172)

VM 参数的具体内容:

--add-opens java.base/java.util.concurrent=ALL-UNNAMED 

3. 线程池中常见的阻塞队列

在这里插入图片描述

workQueue,当没有空闲的核心线程时,新来任务会加入到阻塞队列,阻塞队列满了后会创建救急线程执行任务

3.1 常见的阻塞队列

常见的阻塞队列主要有以下四种(重点了解 ArrayBlockingQueue 和 LinkedBlockingQueue):

  1. ArrayBlockingQueue:基于数组结构的有界阻塞队列,符合队列的先进先出原则
  2. LinkedBlockingQueue:基于链表结构的有界阻塞队列,符合队列的先进先出原则
  3. DelayedWorkQueue:优先级队列,能够保证每次出队的元素是队列中延迟时间最短的任务
  4. SynchronousQueue:不存储元素的阻塞队列,每次执行插入操作前都必须等待一个移出操作

补充:任何想要放入 DelayedWorkQueue 的对象都必须实现 Delayed 接口,Delayed 接口要求实现两个方法:

  1. getDelay(TimeUnit unit) 方法用于返回元素的剩余延迟时间,即距离执行时间还有多长时间
  2. compareTo(Delayed other) 方法用于比较两个 Delayed 对象的延迟时间

3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别

LinkedBlockingQueue(推荐使用)ArrayBlockingQueue
默认无界(Integer 类型的最大值),支持有界强制有界
底层的数据结构是链表底层的数据结构是数组
是懒惰的,创建节点的时候添加数据提前初始化 Node 数组
入队会生成新 NodeNode 需要提前创建好的
两把锁(链表的头部和尾部各一把锁)一把锁

在这里插入图片描述

4. 如何确定线程池的核心线程数

在这里插入图片描述

4.1 应用程序中任务的类型

应用程序中任务的类型主要可以分为两种:

  1. IO 密集型任务
  2. CPU 密集型任务

4.1.1 IO 密集型任务

常见的 IO 密集型任务主要有文件读写、数据库读写、网络请求等

4.1.2 CPU 密集型任务

常见的 CPU 密集型任务主要有计算较为密集的代码、BitMap 转换、JSON 转换等

4.2 如何确定核心线程数

对于 IO 密集型的任务来说,核心线程数可以设置为 2 * N + 1 (其中 N 为 CPU 的核数),因为 IO 密集型任务消耗的 CPU 资源较少

对于 CPU 密集型的任务来说,核心线程数可以设置为 N + 1 (其中 N 为 CPU 的核数),因为 CPU 密集型任务需要消耗大量的 CPU 资源,线程数少了,就能够减少 CPU 在不同线程之间切换所耗费的时间,充分地利用 CPU 资源

一般来说,用 Java 开发的应用程序,任务的类型大都为 IO 密集型


如何查看电脑的 CPU 核数呢,可以运行以下代码查看

public class ProcessorsDemo {public static void main(String[] args) {System.out.println(Runtime.getRuntime().availableProcessors());}}

5. 线程池的种类

在这里插入图片描述

java.util.concurrent.Executors 类中提供了大量创建连接池的静态方法,常见的有四种:

  1. 创建使用固定线程数的线程池
  2. 创建单线程线程池
  3. 创建可缓存线程池
  4. 创建提供延迟功能和周期执行功能的线程池

5.1 固定线程数的线程池

我们可以查看创建固定线程数线程池的源码

在这里插入图片描述

固定线程数的线程池的特点是:

  1. 核心线程数与最大线程数一样,没有救急线程
  2. 阻塞队列是 LinkedBlockingQueue ,最大容量为 Integer.MAX_VALUE

固定线程数的线程池适用于任务量已知、耗时较长的任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class FixedThreadPoolCase {static class FixedThreadDemo implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();for (int i = 0; i < 2; i++) {System.out.println(threadName + ":" + i);}}}public static void main(String[] args) throws InterruptedException {// 创建一个固定大小的线程池,核心线程数和最大线程数都是3ExecutorService executorService = Executors.newFixedThreadPool(3);for (int i = 0; i < 5; i++) {executorService.submit(new FixedThreadDemo());Thread.sleep(10);}executorService.shutdown();}}

5.2 单线程线程池

单线程线程池只会用唯一的工作线程来执行任务,保证所有任务按照先进先出的顺序(FIFO)执行

我们可以查看创建单线程线程池的源码

在这里插入图片描述

单线程线程池的特点是:

  1. 核心线程数和最大线程数都是1
  2. 阻塞队列是LinkedBlockingQueue ,最大容量为Integer.MAX VALUE

单线程线程池适用于需要严格按照顺序执行的任务

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class SingleThreadCase {static int count = 0;static class Demo implements Runnable {@Overridepublic void run() {count++;System.out.println(Thread.currentThread().getName() + ":" + count);}}public static void main(String[] args) throws InterruptedException {// 单个线程池,核心线程数和最大线程数都是1ExecutorService executorService = Executors.newSingleThreadExecutor();for (int i = 0; i < 10; i++) {executorService.execute(new Demo());Thread.sleep(5);}executorService.shutdown();}}

5.3 可缓存线程池

我们可以查看创建可缓存线程池的源码

在这里插入图片描述

可缓存线程池的特点:

  1. 核心线程数为 0
  2. 最大线程数是 Integer.MAX VALUE
  3. 阻塞队列为 SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作

可缓存线程池适用于任务数比较密集,但每个任务执行时间较短的情况

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;public class CachedThreadPoolCase {static class Demo implements Runnable {@Overridepublic void run() {String threadName = Thread.currentThread().getName();try {// 修改睡眠时间,模拟线程执行需要花费的时间Thread.sleep(100);System.out.println(threadName + "执行完了");} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 创建一个缓存的线程,没有核心线程数,最大线程数为Integer.MAX_VALUEExecutorService executorService = Executors.newCachedThreadPool();for (int i = 0; i < 10; i++) {executorService.execute(new Demo());Thread.sleep(1);}executorService.shutdown();}}

5.4 提供延迟功能和周期执行功能的线程池

我们可以查看创建提供延迟功能和周期执行功能的线程池的源码

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


顾名思义,提供延迟功能和周期执行功能的线程池适用于需要延迟执行或周期执行的任务

import java.util.Date;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;public class ScheduledThreadPoolCase {static class Task implements Runnable {@Overridepublic void run() {try {String threadName = Thread.currentThread().getName();System.out.println(threadName + ", 开始:" + new Date());Thread.sleep(1000);System.out.println(threadName + ", 结束:" + new Date());} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}}}public static void main(String[] args) throws InterruptedException {// 按照周期执行的线程池,核心线程数为2,最大线程数为Integer.MAX_VALUEScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(2);System.out.println("程序开始:" + new Date());/** schedule方法:提交任务到线程池中* 第一个参数:提交的任务* 第二个参数:任务执行的延迟时间* 第三个参数:时间单位*/scheduledThreadPool.schedule(new Task(), 0, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 1, TimeUnit.SECONDS);scheduledThreadPool.schedule(new Task(), 5, TimeUnit.SECONDS);Thread.sleep(5000);// 关闭线程池scheduledThreadPool.shutdown();}}

6. 为什么不建议使用 Executors 类提供的静态方法创建线程池

在这里插入图片描述

阿里开发手册《Java开发手册-嵩山版》中指出

在这里插入图片描述

7. 线程池(多线程)的使用场景

在这里插入图片描述

7.1 CountDownLatch

CountDownLatch:闭锁、倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)

  1. 构造参数用来初始化等待计数值
  2. await()方法用来等待计数归零
  3. countDown()方法用来让计数减一

在这里插入图片描述

import java.util.concurrent.CountDownLatch;public class CountDownLatchDemo {public static void main(String[] args) throws InterruptedException {// 初始化了一个参数为 3 的倒计时锁CountDownLatch countDownLatch = new CountDownLatch(3);new Thread(() -> {System.out.println(Thread.currentThread().getName() + "-begin...");try {Thread.sleep(1000);} catch (InterruptedException e) {throw new RuntimeException(e);}countDownLatch.countDown();System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());}).start();new Thread(() -> {System.out.println(Thread.currentThread().getName() + "-begin...");try {Thread.sleep(2000);} catch (InterruptedException e) {throw new RuntimeException(e);}countDownLatch.countDown();System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());}).start();new Thread(() -> {System.out.println(Thread.currentThread().getName() + "-begin...");try {Thread.sleep(1500);} catch (InterruptedException e) {throw new RuntimeException(e);}countDownLatch.countDown();System.out.println(Thread.currentThread().getName() + "-end..." + countDownLatch.getCount());}).start();String threadName = Thread.currentThread().getName();System.out.println(threadName + "-waiting...");// 等待其他线程完成countDownLatch.await();System.out.println(threadName + "-wait end...");}}

7.2 多线程使用场景一:ElasticSearch 批量导入数据

在项目上线之前,我们需要把数据库中的数据一次性的同步到 ElasticSearch 索引库中,但数据量达到百万级别时,一次性读取数据的做法是不可取的(Out Of Memory)

可以使用线程池的方式导入,利用 CountDownLatch 来控制就能避免一次性加载过多,防止内存溢出

在这里插入图片描述

7.3 多线程使用场景二:数据汇总

在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息

这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢


我们先来看一下常规的方案,先查询订单信息、再查询商品信息、最后查询物流信息,整个流程中每个部分是串行化执行的

在这里插入图片描述

我们先来看使用多线程的方案,查询订单信息、查询商品信息、查询物流信息三个操作相当于同时进行,整个流程中每个部分是并发执行的

在这里插入图片描述

当然,如果采用多线程的方案,需要使用 Future 接口(execute方法执行后会返回一个结果,结果的类型为 Future 接口)

7.4 多线程使用场景三:异步调用

在很多软件中都会有搜索功能,比如电商网站、地图软件等,并且这些软件会保存你的搜索记录

我们在实现搜索功能的时候,往往不会让保存搜索记录的操作影响到用户的正常搜索

在这里插入图片描述

我们可以选择采用异步线程来完成搜索记录的保存操作,具体要怎么操作呢?当用户开始搜索以后,我们正常返回与用户搜索内容相关的数据,用另一个线程去保存客户的搜索记录


那在代码中该如何实现呢(在 SpringBoot 项目中),只需要在保存用户搜索记录的具体方法上添加 @Async 注解

/*** 保存用户的搜索记录** @param userId  Integer* @param keyword String*/
@Async("taskExecutor")
@Override
public void insert(Integer userId, String keyword) {// 保存用户的搜索记录log.info("用户搜索记录保存成功,用户id:{},关键字:{}", userId, keyword);
}

同时 @Async 注解还能指定使用哪个线程池(该线程池需要由 Spring 管理)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.ExecutorService;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;@Configuration
public class ThreadPoolConfig {/*** 核心线程池大小*/private static final int CORE_POOL_SIZE = 25;/*** 最大可创建的线程数*/private static final int MAX_POOL_SIZE = 50;/*** 队列最大长度*/private static final int QUEUE_CAPACITY = 1000;/*** 线程池维护线程所允许的空闲时间*/private static final int KEEP_ALIVE_SECONDS = 500;@Bean("taskExecutor")public ExecutorService executorService() {AtomicInteger atomicInteger = new AtomicInteger(1);LinkedBlockingQueue<Runnable> linkedBlockingQueue = new LinkedBlockingQueue<Runnable>(QUEUE_CAPACITY);return new ThreadPoolExecutor(CORE_POOL_SIZE,MAX_POOL_SIZE,KEEP_ALIVE_SECONDS,TimeUnit.MILLISECONDS,linkedBlockingQueue,runnable -> new Thread(runnable, "wuyanzu-pool-" + atomicInteger.getAndIncrement()),new ThreadPoolExecutor.DiscardPolicy());}}

其中 ExecutorService 类是与线程池相关的类的顶层接口(IDEA 中按下 CTRL + H 快捷键可查看继承结构)

在这里插入图片描述

注意事项:如果想让 @Async 注解生效,需要在 SpringBoot 的启动类上添加 @EnableAsync 注解

8. 控制某个方法允许线程并发访问的线程数量(Semaphore)

在这里插入图片描述

Semaphore:信号量,JUC 包下的一个工具类,实现原理基于 AQS,可以通过 Semaphore 类限制执行的线程数量

Semaphore 通常用于那些资源有明确访问数量限制的场景,常用于限流

在这里插入图片描述

Semaphore的使用步骤:

  1. 创建 Semaphore 对象,并给定一个容量
  2. semaphore.acquire():请求一个信号量,这时候的信号量个数 - 1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
  3. semaphore.release():释放一个信号量,此时信号量个数 + 1

代码示例:

import java.util.concurrent.Semaphore;public class SemaphoreCase {public static void main(String[] args) {// 1.创建 semaphore 对象Semaphore semaphore = new Semaphore(3);// 2.让 10 个线程同时运行for (int i = 0; i < 10; i++) {new Thread(() -> {try {// 3. 获取许可,计数 - 1semaphore.acquire();} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}try {System.out.println("running...");try {Thread.sleep(1000);} catch (InterruptedException interruptedException) {interruptedException.printStackTrace();}System.out.println("end...");} finally {// 4. 释放许可 计数 + 1semaphore.release();}}).start();}}}

9. ThreadLocal

在这里插入图片描述

ThreadLocal 是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本,从而解决了变量并发访问冲突的问题,ThreadLocal 同时实现了线程内的资源共享

例如,使用 JDBC 操作数据库时,会将每一个线程的 Connection 对象放入各自的 ThreadLocal 中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免 A 线程关闭了 B 线程的连接

9.1 ThreadLocal的基本使用

ThreadLocal 的基本使用:

  1. set(value):设置值
  2. get():获取值
  3. remove():清除值

代码示例:

public class ThreadLocalTest {static ThreadLocal<String> threadLocal = new ThreadLocal<>();public static void main(String[] args) {new Thread(() -> {String threadName = Thread.currentThread().getName();threadLocal.set("Tom");removeAfterPrint(threadName);System.out.println(threadName + "-after remove : " + threadLocal.get());}, "t1").start();new Thread(() -> {String threadName = Thread.currentThread().getName();threadLocal.set("Jerry");removeAfterPrint(threadName);System.out.println(threadName + "-after remove : " + threadLocal.get());}, "t2").start();}static void removeAfterPrint(String str) {// 打印当前线程中本地内存中本地变量的值System.out.println(str + " : " + threadLocal.get());// 清除本地内存中的本地变量threadLocal.remove();}}

9.2 ThreadLocal的实现原理&源码分析

ThreadLocal 本质来说就是一个线程内部存储类,让每个线程只操作自己内部的值,从而实现线程数据隔离

在这里插入图片描述

9.2.1 set 方法

在这里插入图片描述

9.2.2 get 方法

在这里插入图片描述

9.2.3 remove 方法

整体逻辑与 get 方法类似,找到目标元素后将其清除

在这里插入图片描述

在这里插入图片描述

9.3 ThreadLocal 的内存泄漏问题

在分析 ThreadLocal 的内存泄漏问题前,我们先来简单了解一下 Java 中的强引用和弱引用

强引用:最普通的引用方式,表示一个对象处于有用且必须的状态,如果一个对象具有强引用,则 GC 并不会回收它。即使堆内存不足,宁可抛出 OOM(Out Of Memory) 错误,也不会对其进行回收

在这里插入图片描述

弱引用:表示一个对象处于可能有用且非必须的状态,在 GC 线程扫描内存区域时,一旦发现弱引用,就会回收与弱引用相关联的对象。对于弱引用的回收,无论内存区域是否足够,一旦发现就会回收

在这里插入图片描述

每一个 Thread内部都维护一个了 ThreadLocalMap ,在 ThreadLocalMap 中的 Entry 对象继承了 WeakReference ,其中 key 为使用弱引用的 ThreadLocal 实例,value 为线程变量的副本


以下是 ThreadLocal 类的部分源码

在这里插入图片描述

那怎么样防止内存泄漏呢,非常简单,就是在用完 ThreadLocal 类之后,主动调用 ThreadLocal 类的 remove 方法,把数据清理掉,就能避免内存泄露的情况了

10. 线程池的 execute 方法和 submit 方法有什么区别

在 Java 的ExecutorService接口中,execute() 方法和submit(Runnable task)方法是都用来提交任务以供异步执行的,但它们之间有一些关键的区别:

10.1 返回值

  • execute(Runnable command)方法没有返回值。它简单地执行给定的Runnable任务,不提供关于任务执行状态或结果的信息
  • submit(Runnable task)方法返回一个Future对象,通过这个对象可以检查任务是否执行完成,并且可以取消任务的执行。但是,由于Runnable接口不返回结果,所以返回的Future对象在get()方法调用时将返回null

以下是 submit 方法和 execute 方法的源码

注意事项:

  1. submit 方法是 ExecutorService 接口提供的,execute 方法是由 Executor 接口提供的
  2. ExecutorService 接口继承了 Executor 接口

submit 方法

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

execute 方法

在这里插入图片描述

10.2 异常处理

  • 如果在Runnable任务中抛出了未捕获的异常,execute方法不会做任何处理,异常将直接传递给线程的异常处理器(默认情况下,这将导致线程终止并打印堆栈跟踪)
  • submit方法会将异常封装在返回的Future对象中。如果任务在执行过程中抛出了异常,调用Future对象的get()方法将会抛出ExecutionException,该异常会包含原始的异常

10.3 任务类型

  • execute方法只能接受Runnable类型的任务
  • submit方法除了可以接受Runnable类型的任务外,还可以接受Callable类型的任务。CallableRunnable类似,但它可以返回一个结果,并且可以抛出异常

10.4 使用场景

  • 当你不需要知道任务执行的结果,也不关心任务执行过程中可能发生的异常时,可以使用execute方法
  • 当你需要跟踪任务的执行状态、结果,或者需要在任务完成后进行一些操作(如清理资源、处理结果等),或者任务可能会抛出异常并且需要捕获处理时,应该使用submit方法

总的来说,submit 方法提供了比 execute 方法更丰富的功能,特别是在方法的返回值和异常处理方面

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/397235.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【代码随想录】长度最小的子数组——滑动窗口

本博文为《代码随想录》的学习笔记。原文链接&#xff1a;代码随想录 题目 原题链接&#xff1a;长度最小的子数组 给定一个含有 n 个正整数的数组和一个正整数 target 。 找出该数组中满足其总和大于等于 target 的长度最小的 子数组 [numsl, numsl1, ..., numsr-1, nums…

历史库,成本与性能如何兼得?| OceanBase应用实践

随着数据量的迅猛增长&#xff0c;企业和组织在数据库管理方面遭遇的挑战愈发凸显。数据库性能逐渐下滑、存储成本节节攀升&#xff0c;以及数据运维复杂性的增加&#xff0c;这些挑战使得DBA和开发者在数据管理上面临更大的压力。 为了应对这些挑战&#xff0c;对数据生命周期…

uni-app学习笔记

一、下载HBuilder https://www.dcloud.io/hbuilderx.html 上述网址下载对应版本&#xff0c;下载完成后进行解压&#xff0c;不需要安装&#xff0c;解压完成后&#xff0c;点击HBuilder X.exe文件进行运行程序 二、创建uni-app项目 此处我是按照文档创建的uni-ui项目模板…

DWG图纸识别工作

DWG图纸识别工作 目的&#xff1a;完成从DWG图纸中数据的提取&#xff0c;在数据提取之前先要对DWG图纸进行识别。得到某个图层的数据。 最终完成图纸建筑外轮廓线坐标数据的提取。 1、 DWG图纸&#xff0c;通过AutoCAD软件导出 DXF文件 2、 DXF文件上传到服务端&#xff0c;…

Java设计模式(适配器模式)

定义 将一个类的接口转换成客户希望的另一个接口。适配器模式让那些接口不兼容的类可以一起工作。 角色 目标抽象类&#xff08;Target&#xff09;&#xff1a;目标抽象类定义客户所需的接口&#xff08;在类适配器中&#xff0c;目标抽象类只能是接口&#xff09;。 适配器类…

XJTUSE-离散数学-图论

概述 图的定义 几个定义&#xff0c;不赘述 多重图&#xff1a;有平行边存在 简单图&#xff1a;无平行边 无自环 子图 and 补图 完全图的概念 结点的度 入度&#xff0c;出度 奇结点、偶结点 定理&#xff1a;对于无向图&#xff0c;奇结点的个数为偶数 图的同构 必…

Golang 并发编程

Golang 并发编程 Goroutine 什么是协程 创建 Goroutine 主 goroutine &#xff08;main函数&#xff09;退出后&#xff0c;其它的工作 goroutine 也会自动退出 package mainimport ("fmt""time" )func myFunc() {i : 0for {ifmt.Println("func: …

MySQL:表的设计原则和聚合函数

所属专栏&#xff1a;MySQL学习 &#x1f48e;1. 表的设计原则 1. 从需求中找到类&#xff0c;类对应到数据库中的实体&#xff0c;实体在数据库中表现为一张一张的表&#xff0c;类中的属性对应着表中的字段 2. 确定类与类的对应关系 3. 使用SQL去创建具体的表 范式&#xff1…

从“抠图”到“抠视频”,Meta上新AI工具SAM 2。

继2023年4月首次推出SAM&#xff0c;实现对图像的精准分割后&#xff0c;Meta于北京时间2024年7月30日推出了能够分割视频的新模型SAM 2&#xff08;Segment Anything Model 2&#xff09;。SAM 2将图像分割和视频分割功能整合到一个模型中。所谓“分割”&#xff0c;是指区别视…

API 签名认证:AK(Access Key 访问密钥)和 SK(Secret Key 私密密钥)

API签名认证 在当今的互联网时代&#xff0c;API作为服务与服务、应用程序与应用程序之间通信的重要手段&#xff0c;其安全性不容忽视。你是否遇到过需要在HTTP请求中加入访问密钥(ak)和私密密钥(sk)的情况&#xff1f;是不是担心这些敏感信息会被拦截或者泄露&#xff1f;本…

【多线程】乐观/悲观锁、重量级/轻量级锁、挂起等待/自旋锁、公平/非公锁、可重入/不可重入锁、读写锁

文章目录 乐观锁和悲观锁重量级锁和轻量级锁挂起等待锁和自旋锁公平锁和非公平锁可重入锁和不可重入锁读写锁相关面试题 锁&#xff1a;非常广义的概念&#xff0c;不是指某个具体的锁&#xff0c;所有的锁都可以往这些策略中套 synchronized&#xff1a;只是市面上五花八门的锁…

[独家原创]基于分位数回归的Bayes-GRU多变量时序预测【区间预测】 (多输入单输出)Matlab代码

[独家原创]基于分位数回归的Bayes-GRU多变量时序预测【区间预测】 &#xff08;多输入单输出&#xff09;Matlab代码 目录 [独家原创]基于分位数回归的Bayes-GRU多变量时序预测【区间预测】 &#xff08;多输入单输出&#xff09;Matlab代码效果一览基本介绍程序设计参考资料 效…

RM麦轮控制以及底盘解算

一个典型的RM机器人四轮底盘由电机&#xff0c;底板&#xff0c;悬挂等构成&#xff0c;底盘安装在底盘的四角&#xff0c;呈矩形分布&#xff0c;麦克纳姆轮的辊子方向会影响其运动性能&#xff0c;一般采用如下图所示&#xff0c;四个麦轮的辊子延长线都过底盘中心的安装方法…

c语言学习,atoi()函数分析

1&#xff1a;atoi() 函数说明&#xff1a; 检查参数*ptr&#xff0c;子串中数字或正负号&#xff0c;遇到非数字或结束符停止 2&#xff1a;函数原型&#xff1a; int atoi(const char *ptr) 3&#xff1a;函数参数&#xff1a; 参数c&#xff0c;为检测子串 4&#xff1a;…

MyBatis 配置与测试方式

目录 一&#xff0c;什么是MyBatis 二&#xff0c;准备工作 创建项目 配置数据库连接 持久层代码 单元测试 一&#xff0c;什么是MyBatis 简单来说&#xff0c;MyBatis 是一款优秀的持久层框架&#xff0c;用于简化JDBC的开发&#xff0c;能更简单完成程序与数据库之间…

从0到1,AI我来了- (5)大模型-本地知识库-I

一、下载&安装Ollama Ollama下载地址&#xff1a; Download Ollama on macOS Github地址&#xff1a;GitHub - ollama/ollama: Get up and running with Llama 3.1, Mistral, Gemma 2, and other large language models. Ollama 是啥&#xff1f; 是一个人工智能和机器学习…

一文搞懂后端面试之不停机数据迁移【中间件 | 数据库 | MySQL | 数据一致性】

数据迁移方面的工作&#xff1a; 重构老系统&#xff1a;使用新的表结构来存储数据单库拆分分库分表、分库分表扩容大表修改表结构定义 数据备份工具 MySQL上常用的两款数据备份工具&#xff1a;mysqldump和XtraBackup mysqldump&#xff1a;一个用于备份和恢复数据库的命令…

Redis中的set类型

set的含义 集合设置&#xff08;和get相对应&#xff09; 集合就是把一些有关联的数据放到一起 集合中的元素是无序的&#xff08;和list的有序是对应的-顺序很重要&#xff0c;这里的无序就是顺序不重要&#xff09;&#xff1b;在list中[]1,2,3],[1,3,2]&#xff0c;是两个…

Java开发工具IDEA

IDEA概述 Intellij IDEA IDEA全称Intellij IDEA&#xff0c;是用于Java语言开发的集成环境&#xff0c;它是业界公认的目前用于Java程序开发最好的工具。 集成环境 把代码编写&#xff0c;编译&#xff0c;执行&#xff0c;调试等多种功能综合到一起的开发工具。 IDEA下载和安…

PDF在线预览实现:如何使用vue-pdf-embed实现前端PDF在线阅读

目录 PDF在线预览实现&#xff1a;如何使用vue-pdf-embed实现前端PDF在线阅读 一、前言 二、vue-pdf-embed是什么 1、作用与场景 2、vue-pdf-embed的优点 三、项目初始化与依赖安装 1、初始化Vue项目 2、安装依赖 3、集成vue-pdf-embed插件 四、一个实际的应用demo …