前言
池化技术指的是提前准备一些资源,在需要时可以重复使用这些预先准备的资源。是一种优化资源管理和提高系统性能的技术,广泛应用于需要频繁创建、使用和销毁资源的场景。其核心思想是预先创建一定数量的资源对象,并将这些对象保存在一个“池”(如线程池、连接池或对象池)中,以供重复使用,而不是每次需要时都重新创建和销毁资源。
线程池
1.什么是线程池
线程池其实就是一种多线程处理形式,处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面java基础学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象。
2.为什么使用线程池
使用线程池的好处是减少在创建和销毁线程上所消耗的时间以及系统资源开销,解决资源不足的问题。如果不使用线程池,有可能会造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
- 降低资源消耗
- 提高响应速度,方便管理
- 线程可以复用、可以控制最大并发数、可以管理线程
3.有哪些线程池
- Executors.newFixedThreadPool:创建固定线程数量的线程池
- Executors.newSingleThreadExecutor():创建一个只有一个线程的线程池
- Executors.newCachedThreadPool():创建一个可根据实际情况调整线程数量的线程池
- Executors.newScheduledThreadPool():创建可定期执行
FixedThreadPool 和 SingleThreadExecutor : 允许请求的队列长度为 Integer.MAX_VALUE,可能堆积大量的请求,从而导致 OOM。
CachedThreadPool 和 ScheduledThreadPool : 允许创建的线程数量为 Integer.MAX_VALUE ,可能会创建大量线程,从而导致 OOM。
4.线程池+7大参数
ThreadPoolExecutor的参数
- int corePoolSize:核心线程数量
- int maximumPoolSize:最大线程数
- long keepAliveTime: 最大空闲时间
- TimeUnit unit:时间单位
- BlockingQueue<Runnable> workQueue:任务队列
- ThreadFactory threadFactory:线程工厂
- RejectedExecutionHandler handler: 饱和处理机制
工作原理:
- 线程池在初始化的同时,会自动创建一定数量的线程(核心线程)并存放起来。这些线程在没有任务时处于空闲状态,等待任务的到来。
- 当有新的任务需要执行时,可以通过调用线程池的execute()或submit()方法将任务提交到线程池。execute()方法用于提交无返回值的Runnable任务,而submit()方法则可以提交有返回值的Callable任务。
- 判断核心线程是否空闲:线程池会首先判断核心线程数量是否已满。
- 如果未满,且有空闲的核心线程,则直接分配一个空闲的核心线程来执行任务。
- 如果核心线程都在忙碌,线程池会检查工作队列(一个阻塞队列)是否已满。
- 如果工作队列未满,则将新任务添加到工作队列中等待执行。空闲的核心线程会从工作队列中按照先进先出(FIFO)的规则取出任务并执行。
- 如果工作队列已满,且当前存活线程数未达到线程池的最大线程数(maximumPoolSize),线程池会创建一个新的非核心线程来执行任务
- 如果当前存活线程数已达到最大线程数,且工作队列也已满,此时再有新任务提交,线程池会根据预设的拒绝策略来处理新任务。常见的拒绝策略包括
- AbortPolicy:直接抛出RejectedExecutionException异常,阻止新任务的提交。
- CallerRunsPolicy:由提交任务的线程(调用者线程)来处理该任务。
- DiscardPolicy:直接丢弃新提交的任务,不做任何处理。
- DiscardOldestPolicy:丢弃队列中最旧的未处理任务,然后尝试重新提交被拒绝的任务。
- 自定义拒绝策略:通过实现RejectedExecutionHandler接口来自定义任务拒绝的处理方式。
简而言之:
使用银行案例理解:例如银行有5个窗口,对应有5个员工(最大线程数),银行大厅只有20座位(任务队列)。
重点来了:一天早上银行只开了2个窗口(2个核心线程数量),刚开始办理人比较少,突然来一波办理业务的人,大约30人。大厅的座位已经满了(任务队列满了)。行长见状不妙,处理不过来,另外安排了3名员工开了剩下的三个窗口,目前一共5个窗口(达到最大线程数)。此时5个窗口有人办理,另外座位满了。银行已经塞不下人了,里面挂牌当前办理人太多,11点过后再来(拒绝策略)
如何判断是CPU密集型任务还是IO密集型任务
- CPU 密集型:利用 CPU 计算能力的任务比如你在内存中对大量数据进行排序。
- IO 密集型:单凡涉及到网络读取,文件读取这类都是 IO 密集型,这类任务的特点是 CPU 计算耗费时间相比于等待 IO 操作完成的时间来说很少,大部分时间都花在了等待 IO 操作完成上。
工作中使用线程池
工作中使用线程池,采用自定义的方式。Spring中实现多线程,其实非常简单,只需要在配置类中添加@EnableAsync就可以使用多线程。在希望执行的并发方法中使用@Async就可以定义一个线程任务。
- 定义线程池配置(使用参数有可以进行写在配置文件yml里)
@Configuration
@EnableAsync
public class ThreadPoolConfig {
private final int core = Runtime.getRuntime().availableProcessors();
@Bean(name = "testTaskExecutor")
public ThreadPoolTaskExecutor testTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
// 配置核心线程数
executor.setCorePoolSize(this.core);
// 配置最大线程数
executor.setMaxPoolSize(this.core * 2);
//配置队列大小
executor.setQueueCapacity(100);
//线程池维护线程所允许的空闲时间
executor.setKeepAliveSeconds(60);
// 配置线程池中的线程的名称前缀
executor.setThreadNamePrefix("test-progress-task-");
//拒绝策略
executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
return executor;
}
}
- 使用线程池
@Slf4j
@Component
public class TestSupport {
@Async("testTaskExecutor")
public void testTask() {
System.out.println("异步执行")
}
}
注意事项
多线程可以提高程序的处理能力,实现对共享资源的并发访问以及实现异步操作。在多线程编程中,常见的问题包括线程安全问题、死锁问题、上下文切换问题、数据同步问题和过度创建线程问题。
1. 为了解决线程安全问题,可以使用同步机制(如synchronized关键字、Lock对象)、使用线程安全的数据结构或避免共享状态。
2. 为了解决死锁问题,需要避免循环等待资源、按照固定顺序获取资源或设置超时时间等。
3. 为了解决上下文切换问题,可以合理设计线程数量、减少线程间的竞争或使用线程池等。
4. 为了解决数据同步问题,可以使用锁来保证数据的原子性、使用volatile关键字保证可见性或使用线程安全的数据结构等。
5. 为了解决过度创建线程问题,可以使用线程池来复用线程、合理设置线程池大小等。