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. 为什么要使用线程池
我们为什么要使用线程池呢,主要有两个原因:
- 每次创建线程的时候都会占用一定的内存空间,如果要创建很多个线程,有可能会浪费内存,严重的情况下还有可能会导致内存溢出
- CPU 资源是有限的,同一时刻 CPU 只能处理一个线程,如果有大量的请求到达服务器,我们创建了大量的线程,那么很多线程都没有 CPU 的执行权,这些线程都需要等待获取 CPU 的执行权,在这个过程中会有非常多的切换线程操作,频繁的线程切换操作会导致上下文切换开销增大,CPU 花费在真正执行任务上的时间就会减少,从而导致程序的性能下降
在项目开发的过程中,一般都会用线程池来管理线程、创建线程
线程池相关的内容,跟实际开发是有很大关系的,面试官也是特别喜欢问
2. 线程池的核心参数和线程池的执行原理
2.1 线程池的核心参数
线程池的核心参数主要有七个,我们主要参考 ThreadPoolExecutor
类的具有七个参数的构造函数,这七个参数也是面试官提问的重点
- corePoolSize:核心线程数
- maximumPoolSize:最大线程数(核心线程数 + 救急线程数的最大值)
- keepAliveTime:救急线程的生存时间,如果生存时间内没有新任务,与救急线程相关的资源会被释放
- unit:救急线程的生存时间单位
- workQueue:当没有空闲的核心线程时,新来任务会加入到此队列中排队,如果队列满了会创建救急线程来执行任务
- threadFactory:线程工厂,可以定制如何线程对象,例如为线程设置名字、是否为守护线程等
- handler:拒绝策略,当所有线程都在繁忙、workQueue 中的线程也满了时,会触发拒绝策略
2.2 线程池的执行原理
下面为大家介绍一下线程池的执行原理,也就是线程池是怎么工作的
首先,当有一个新任务到来时,会先判断核心线程数是否已经满了,如果核心线程数没满,就将任务添加到工作线程中,执行这个任务
如果核心线程数已经满了,会判断阻塞队列是否满了,如果阻塞队列中还有空间,就把任务添加到阻塞队列中进行等待,
如果阻塞队列满了,会判断线程数是否小于最大线程数,如果线程数小于最大线程数,会创建救急线程来执行任务
当核心线程或救急线程处于空闲的时候,会去阻塞队列中检查一下是否有需要执行的任务,如果有,就会使用核心线程或救急线程来执行阻塞队列中的任务
假如所有条件都不满足,线程池会有一个拒绝策略,拒绝策略有以下四种:
- AbortPolicy:直接抛出异常,默认使用的拒绝策略
- CallerRunsPolicy:用调用者所在的线程来执行任务
- DiscardOldestPolicy:丢弃阻塞队列中靠最前的任务,并执行当前任务
- 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):
- ArrayBlockingQueue:基于数组结构的有界阻塞队列,符合队列的先进先出原则
- LinkedBlockingQueue:基于链表结构的有界阻塞队列,符合队列的先进先出原则
- DelayedWorkQueue:优先级队列,能够保证每次出队的元素是队列中延迟时间最短的任务
- SynchronousQueue:不存储元素的阻塞队列,每次执行插入操作前都必须等待一个移出操作
补充:任何想要放入 DelayedWorkQueue 的对象都必须实现 Delayed 接口,Delayed 接口要求实现两个方法:
- getDelay(TimeUnit unit) 方法用于返回元素的剩余延迟时间,即距离执行时间还有多长时间
- compareTo(Delayed other) 方法用于比较两个 Delayed 对象的延迟时间
3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别
LinkedBlockingQueue(推荐使用) | ArrayBlockingQueue |
---|---|
默认无界(Integer 类型的最大值),支持有界 | 强制有界 |
底层的数据结构是链表 | 底层的数据结构是数组 |
是懒惰的,创建节点的时候添加数据 | 提前初始化 Node 数组 |
入队会生成新 Node | Node 需要提前创建好的 |
两把锁(链表的头部和尾部各一把锁) | 一把锁 |
4. 如何确定线程池的核心线程数
4.1 应用程序中任务的类型
应用程序中任务的类型主要可以分为两种:
- IO 密集型任务
- 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
类中提供了大量创建连接池的静态方法,常见的有四种:
- 创建使用固定线程数的线程池
- 创建单线程线程池
- 创建可缓存线程池
- 创建提供延迟功能和周期执行功能的线程池
5.1 固定线程数的线程池
我们可以查看创建固定线程数线程池的源码
固定线程数的线程池的特点是:
- 核心线程数与最大线程数一样,没有救急线程
- 阻塞队列是
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
- 阻塞队列是
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 可缓存线程池
我们可以查看创建可缓存线程池的源码
可缓存线程池的特点:
- 核心线程数为 0
- 最大线程数是
Integer.MAX VALUE
- 阻塞队列为
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:闭锁、倒计时锁,用来进行线程同步协作,等待所有线程完成倒计时(一个或者多个线程,等待其他多个线程完成某件事情之后才能执行)
- 构造参数用来初始化等待计数值
- await()方法用来等待计数归零
- 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的使用步骤:
- 创建 Semaphore 对象,并给定一个容量
- semaphore.acquire():请求一个信号量,这时候的信号量个数 - 1(一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
- 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 的基本使用:
- set(value):设置值
- get():获取值
- 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
方法的源码
注意事项:
- submit 方法是 ExecutorService 接口提供的,execute 方法是由 Executor 接口提供的
- ExecutorService 接口继承了 Executor 接口
submit
方法
execute
方法
10.2 异常处理
- 如果在
Runnable
任务中抛出了未捕获的异常,execute
方法不会做任何处理,异常将直接传递给线程的异常处理器(默认情况下,这将导致线程终止并打印堆栈跟踪) submit
方法会将异常封装在返回的Future
对象中。如果任务在执行过程中抛出了异常,调用Future
对象的get()
方法将会抛出ExecutionException
,该异常会包含原始的异常
10.3 任务类型
execute
方法只能接受Runnable
类型的任务submit
方法除了可以接受Runnable
类型的任务外,还可以接受Callable
类型的任务。Callable
与Runnable
类似,但它可以返回一个结果,并且可以抛出异常
10.4 使用场景
- 当你不需要知道任务执行的结果,也不关心任务执行过程中可能发生的异常时,可以使用
execute
方法 - 当你需要跟踪任务的执行状态、结果,或者需要在任务完成后进行一些操作(如清理资源、处理结果等),或者任务可能会抛出异常并且需要捕获处理时,应该使用
submit
方法
总的来说,submit
方法提供了比 execute
方法更丰富的功能,特别是在方法的返回值和异常处理方面
相关文章:

Java面试篇(线程池相关专题)
文章目录 1. 为什么要使用线程池2. 线程池的核心参数和线程池的执行原理2.1 线程池的核心参数2.2 线程池的执行原理 3. 线程池中常见的阻塞队列3.1 常见的阻塞队列3.2 ArrayBlockingQueue 和 LinkedBlockingQueue 的区别 4. 如何确定线程池的核心线程数4.1 应用程序中任务的类型…...

git推送错误-->远程分支比本地的分支更新,无法直接推送
每次上传本地修改好的代码的时候,十次有八次都会出现这样的问题!!(暴躁!!!) 现在写个帖子记录一下,这个问题目前我还没有解决,欢迎懂的佬指点一下. 情景: 我在本地仓库做了一些代码的修改,准备上传到远程仓库上,下边是上传步骤: git add . # 将所有的修改都提交到缓冲区git …...

【三维重建】SpotlessSplats:去除瞬态干扰物的三维高斯喷溅(3DGS)
代码:https://spotlesssplats.github.io 论文:https://arxiv.org/pdf/2406.20055 来源:DeepMind,多伦多大学,斯坦福大学,西蒙弗雷泽大学 提示:关注B站【方矩实验室】,查看视频讲解…...

28. 找出字符串中第一个匹配项的下标【 力扣(LeetCode) 】
一、题目描述 给你两个字符串 haystack 和 needle ,请你在 haystack 字符串中找出 needle 字符串的第一个匹配项的下标(下标从 0 开始)。如果 needle 不是 haystack 的一部分,则返回 -1 。 二、测试用例 示例 1: 输…...

邀请函 I 松下信息和望繁信科技邀您参加「数智时代下大数据应用的“道”与“术”」闭门会议
在数字化浪潮席卷全球的今天,大数据与智能化的结合成为企业成功的关键。为了深入探讨这一重要议题,松下信息系统(上海)有限公司(简称“松下信息”)与上海望繁信科技有限公司(简称“望繁信科技”…...

Node.js中的fs.watchFile与fs.unwatchFile:文件监控与取消监控
在Node.js中,对文件系统的操作是非常常见的需求。有时,我们需要对某个文件的变化进行实时监控,并在文件内容或元数据发生变化时执行相应的操作。Node.js的fs模块提供了watchFile和unwatchFile两个方法,用于实现文件的监控和取消监…...

Hadoop大集群配置文档-粗略版-3万字长文 (包括hive,zookeeper,hbase,flume等中间件和mysql等)
先填一下上次许诺的坑: (许诺的那篇文章链接如下) 如何用sql在1分钟从1T数据中精准定位查询?Hive离线数仓 Spark分析-CSDN博客文章浏览阅读1.2k次,点赞38次,收藏14次。在大数据-Hadoop体系中 ,…...

原生html+js播放flv直播视频流【vue等皆可用】
一、前言 最近着手了一个新需求:将某记录仪的实时视频在页面展现。 实现步骤: 通过WebRtc将直播视频转码为flv/rtsp格式流;通过Vlc或代码中的视频播放器播放视频。 常见播放flv直播视频流软件如:VLC、PotPlayer等,…...

初学java第一天:写一下熟悉的猜数字小游戏
初学java,不知道bug多不多,为了整理凌乱的思绪,写一个实践一下,跟C好像啊 简单来说,初学java确实有一点难度,但是大部分知识和思想和C语言和python相似,所以写起来还行,注意是对一些…...

【C++】如何判断类型
typeid的缺点 typeid对多态的情况不支持 #include <iostream>class Parent { public:Parent() {} private:int a 0; };class Child :public Parent { public :Child() {} private:int b 0; };int main() {Parent* obj1 new Child();Parent* pobj1 obj1;std::cout &…...

让一切发生皆有利于我,在人生的长河中,我们常常面临诸多的不确定性和变化
让一切发生皆有利于我,在人生的长河中,我们常常面临诸多的不确定性和变化。如何在这纷繁复杂的世界中保持内心的坚定,以积极的姿态应对生活的起伏,是我们一生都需要探索的课题。“一切发生皆有利于我”,这是一种心态;“让一切发生皆有利于我”,这是一种策略。这一深刻的…...

腾讯云AI代码助手:智能AI代码助手 ,新一代的高效代码开发辅助工具
前言 近些年是一个科技大爆发的时代,自从大模型发布以来越来越多的科技产品出现。例如去年的智能编码助手自出现以来,各大老牌大厂腾讯,百度 阿里也都紧随其后,智能编码助手的出现可以说大大的节省了我们写一些冗余代码的时间成本…...

C#:索引器 集合初始化器 事件访问器 枚举器 迭代器
1.索引器 就是有参属性 ,这个属性的get访问器接受 一个或多个参数 ,set访问器接受 两个或多个参数 <<via c#>>第10.2节 索引器可以被是被智能的数组 ,属性封装了类中的一个值,而索引器 封装了一组值,使用索引器时,语法和使用数组一样 <<c#从入门到精…...

css伪类选择器、盒子模型等
一、伪类选择器 1.1查找单个元素 根据元素的结构关系查找元素 查找第一个元素:标签名:first-child 查找最后一个元素:标签名:last-child 查找第n个元素:标签名:nth-child(n) 1.2查找多个元素 :nth-child(公式…...

opencv-python图像增强三:图像清晰度增强
文章目录 一、简介:二、图像清晰度增强方案:三、算法实现步骤3.1高反差保留实现3.2. usm锐化3.3 Overlay叠加 四:整体代码实现五:效果 一、简介: 你是否有过这样的烦恼,拍出来的照片总是不够清晰ÿ…...

第130天:内网安全-横向移动PTH哈希PTT 票据PTK密匙Kerberos密码喷射
环境搭建 这里这个环境继续上一篇文章搭建的环境 案例一:域横向移动-PTH-Mimikatz&NTLM 什么是pth? PTH Pass The Hash ,通过密码散列值 ( 通常是 NTLM Hash) 来进行攻击。在域环境中,用户登录计算机时使用的域账号&…...

SB3045LFCT-ASEMI无人机专用SB3045LFCT
编辑:ll SB3045LFCT-ASEMI无人机专用SB3045LFCT 型号:SB3045LFCT 品牌:ASEMI 封装:TO-220F 批号:最新 最大平均正向电流(IF):30A 最大循环峰值反向电压(VRRM&…...

RPA财务机器人是什么,RPA的具体应用场景有哪些?| 实在RPA研究
数字化转型关键期,越来越多的人工智能及超自动化技术在企业财务工作中得以普及应用,以提升财务工作效率,促进财务部门实现 RPA财务机器人是什么? RPA,即机器人流程自动化(Robotic Process Automation&#…...

滑动窗口 | Java | (hot100) 力扣 3
力扣 3.无重复字符的最长子串 暴力法:双层for循环,i-j的字符查重 滑动窗口:因为这题被分在这个类别里,那么已知要用滑动窗口,思路应该是什么。 反正我想不出来…… 看了别人的题解写出来的出错点:特别容易…...

【产品经理】竞品分析怎么理解?拆解一下
什么叫竞品?(研究的对象) 竞品看你怎么理解,有时候不一定是你的竞争对手,有可能是其他行业也做了这个功能,那你也可以学习,有类似的功能或者策略都可以学习,不过这个可能在管理学上…...

合规性导航:处理爬虫数据用于机器学习的最佳实践
在数据驱动的时代,机器学习已成为企业和研究者的重要工具。然而,使用爬虫技术抓取的数据进行机器学习时,合规性问题不容忽视。本文将详细探讨在使用爬虫抓取的数据进行机器学习时可能遇到的合规性问题,并提供相应的最佳实践。 一…...

spring中使用到的设计模式有哪些
Spring 框架是一个高度模块化和灵活的框架,广泛使用了各种设计模式来实现其核心功能和架构。这些设计模式帮助 Spring 提供了高可配置性、可扩展性和可维护性。以下是 Spring 框架中使用到的一些关键设计模式:...

splitcontainer控件设置固定大小
要设置SplitContainer控件以固定的大小,可以通过设置SplitContainer的FixedPanel属性来实现。您还需要设置IsSplitterFixed属性为true来锁定分割条的大小,并且通过设置SplitterWidth或SplitterLength属性来调整分割条的宽度或高度。 以下是一个示例代码…...

最近在写的支付模块
最近再写支付模块就到处借鉴 旨在回顾一下。 1.确认订单功能 使用场景是:用户在选择好购物车后,或者是直接选择商品后(选择商品封装为购物车) 这样做是根据尚硅谷来学习的 目前需要这些属性,原因是在确认订单页面后…...

解决域名加别名后再代理或者映射到fastadmin项目
如果遇到微应用不想再添加或者不方便添加单独的二级域名时,就需要用到代理或者映射来进入到我们的微应用项目中。 可以修改route.php路由文件的下面这个参数 __alias__ > [别名 > 模块/控制器] 如图 然后再修改config.php文件里面的view_replace_str参数…...

Armv9.5架构新增的关键扩展--精简版
Armv9.5架构扩展是对Armv9.4的扩展。它增加了强制性和可选的架构特性。有些特性必须一起实现。实现是符合Armv9.5规范,需要满足以下条件: 符合/兼容Armv9.4规范包含所有Armv9.5架构的强制性特性。符合Armv9.5规范的实现还可以包括: Armv9.5的可选特性以下是arm9.5架构中关键…...

STM32 GPIO 模块
B站视频地址:芯片内部GPIO模块细节 引脚 将 STM32 芯片,类比为【大脑】 而旁边的引脚,类比为【神经】 通过引脚,使得,STM32,可以和外部世界,进行交流 比如,当我们和别人说话时&am…...

网络剪枝——network-slimming 项目复现
目录 文章目录 目录网络剪枝——network-slimming 项目复现clone 存储库Baselinevgg训练结果 resnet训练结果 densenet训练结果 Sparsityvgg训练结果 resnet训练结果 densenet训练结果 Prunevgg命令结果 resnet命令结果 densenet命令结果 Fine-tunevgg训练结果 resnet训练结果 …...

Spring 懒加载的实际应用
引言 在 Spring 框架中,懒加载机制允许你在应用程序运行时延迟加载 Bean。这意味着 Bean 只会在第一次被请求时才实例化,而不是在应用程序启动时就立即创建。这种机制可以提高应用程序的启动速度,并节省内存资源。 Spring 的懒加载机制 懒…...

PyQT 串口改动每次点开时更新串口信息
class MainWindow(QWidget, Ui_Form):def __init__(self):super().__init__(parentNone)self.setupUi(self)self.comboBox.installEventFilter(self) # 加载事件过滤器self.comboBox.addItems(get_ports())def eventFilter(self, obj, event): # 定义事件过滤器if isinstance(o…...