为什么引入线程池?
创建线程池的主要目的是为了提高系统的性能和资源利用率。在多线程编程中,如果每个任务都创建一个新线程来执行,会导致系统资源的浪费和线程数量的过多,从而影响系统的性能和稳定性。而使用线程池可以避免这些问题,它可以通过复用线程来减少线程创建和销毁的开销,从而提高系统的性能和资源利用率。
创建线程需要调用系统内核,进一步由操作系统内核完成线程的创建,而从线程池中取一个线程是我提前做好需要在内核中进行的操作,并尽量减少这个在内核中进行的操作(线程复用),获取线程这个过程完全由用户代码完成,是纯用户态的动作,所以线程池能提高系统性能和资源利用率。
具体来说,线程池可以帮助我们实现以下功能:
线程复用:线程池中的线程可以被复用来执行多个任务,避免了频繁创建和销毁线程的开销。
线程数量控制:线程池可以根据实际需要动态调整线程数量,避免了线程数量过多或过少的问题。
任务队列管理:线程池可以使用任务队列来管理等待执行的任务,避免了任务过多导致系统崩溃的问题。
线程调度管理:线程池可以使用调度算法来管理线程的执行顺序和优先级,以提高系统的性能和响应速度。
创建一个线程池
使用Exectors工厂类创建线程池
ExecutorService表示一个线程池对象,service是自定义的线程池对象的名称。
Executors表示一个工厂类,提供了很多不同效果的工厂方法用于创建线程池对象
创建线程池对象的工厂方法介绍
1.Executors.newFixedThreadPool()
作用:创建一个固定线程数量的线程池。上述参数中的int即为自定义的线程池的固定线程数量。
至于ThreadFactory,是一个用于创建新线程的工厂。在创建线程池时,可以通过指定ThreadFactory参数来自定义线程的创建方式,从而实现更加灵活和个性化的线程池配置,这里就不展开说明(我不会用)。
在使用newFixedThreadPool()方法创建线程池时,需要根据实际情况合理设置线程数量,以保证系统的性能和稳定性。如果线程池中的线程数量过小,可能会导致任务处理速度过慢,影响系统的响应能力;如果线程池中的线程数量过大,可能会导致系统资源浪费和竞争,影响系统的稳定性
2.newCachedThreadPool
创建线程数目动态增长的线程池.
该方法创建的线程池可以根据需要自动创建和回收线程,线程池的大小可以根据任务数量动态调整。
newCachedThreadPool()方法创建的线程池大小是动态变化的,因此该线程池适用于处理大量短时任务的场景,例如网络编程、IO操作等。对于长时间运行的任务,使用该线程池可能会导致线程数过多,从而影响系统的性能和稳定性。
3.newSingleThreadExecutor
创建只包含单个线程的线程池.
newSingleThreadExecutor()方法创建的线程池中只有一个核心线程,因此该线程池适用于需要按照顺序执行任务的场景,例如文件读写、数据处理等。如果需要并发执行多个任务,则应该选择其他类型的线程池。
4.newScheduledThreadPool:
设定 延迟时间后执行命令,或者定期执行命令. 是进阶版的 Timer.
为什么使用工厂模式
构造方法的名字必须和我们的类名一致,我们要想创造不同的构造方法就只能利用方法重载,但是有一些场景我们对构造方法的要求更加的细致,使用方法重载不能满足我们的要求(我需要创建俩个参数数量类型都相同的构造方法),这时就需要使用工厂方法。
工厂类是一种创建型设计模式,其作用是封装对象的创建过程,使得客户端无需直接创建对象,而是通过工厂类来获取所需对象。工厂类可以隐藏对象的创建细节,使得客户端只需要关注对象的使用,而无需关注对象的创建过程。
在Java中,工厂类通常用于创建复杂对象或者对象组合,例如线程池、数据库连接池等。这些对象的创建可能涉及到多个步骤和复杂的逻辑,使用工厂类可以将这些步骤和逻辑封装起来,提供简单的接口给客户端使用。同时,工厂类也可以通过缓存等机制来提高对象的创建效率和性能。
在实际开发中,我们通常使用工厂方法或者抽象工厂来实现工厂模式。工厂方法是一种将对象创建延迟到子类中实现的方式,它通过定义一个抽象的工厂接口和多个具体的工厂类来实现。抽象工厂是一种用于创建一组相关或者相互依赖的对象的工厂模式,它通过定义一个抽象的工厂接口和多个具体的工厂类来实现。
总之,工厂类是一种非常重要的设计模式,它可以提高代码的可维护性、可扩展性和可测试性,同时也可以提高代码的复用性和性能。
使用ThreadPoolExecutor创建线程池
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
4, // 最大线程数
60, // 非核心线程的空闲时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(10) // 任务队列
);
ThreadPoolExecutor的参数介绍
以这个构造方法为例:
线程池的线程数量是可以动态变化的,会根据任务的情况自适应变化。
线程池中的线程可以分为核心线程和非核心线程,核心线程是我们线程池中一直会存在的线程,不管是任务量大或者没有任务,这些线程都一直存在,不会被销毁,除非线程池被销毁。当任务量大的时候线程池可以创建非核心线程,核心线程数和非核心线程数加起来不能超过最大线程数。非核心线程在一段时间后不执行任务就会被销毁,这个时间就是最长空闲时间。工厂模式之前提到过,拒绝方式就是当阻塞队列满了之后线程池的应对方式。
例如,在Exectors的工厂方法中实现固定线程池大小就是让线程池的最大线程数和核心线程数都等于我们自定义的那个数量。
RejectedExecutionHandler handle拒绝方式介绍
当阻塞队列满时,若继续添加任务线程池的处理方式:
创建线程池时,我们的参数是怎么来的?
由于线程池要执行的任务未知:io密集型任务或者CPU密集型任务,我们无法判断下结论式的规定核心线程数,最大线程数等这些参数。
我们需要结合实际生产,通过性能测试,找到那个综合了效率和开销的数值。即实践见真知。
ThreadPoolExecutor和Exectors的工厂方法创建线程有什么区别?
ThreadPoolExecutor是Java提供的一个底层的线程池实现类,相比于使用Executors工厂方法创建线程池,使用ThreadPoolExecutor可以更加灵活地配置线程池,满足不同场景下的需求。
具体来说,使用Executors工厂方法创建线程池时,虽然可以快速地创建一个线程池,但是其默认的配置可能并不适合所有场景。例如,newFixedThreadPool()方法创建的线程池大小是固定的,无法动态调整;newCachedThreadPool()方法创建的线程池大小是动态变化的,但可能会导致线程数过多,从而影响系统的性能和稳定性。而使用ThreadPoolExecutor创建线程池时,可以根据实际情况灵活地配置线程池参数,例如核心线程数、最大线程数、任务队列、拒绝策略等,以满足不同场景下的需求。
可以这样认为:Executors的工厂方法是对ThreadPoolExecutor的一个封装,更方便我们使用于一些固定场合。
实现一个简单的线程池
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;
class MyThreadPool {
private BlockingQueue<Runnable> blockingQueue = new LinkedBlockingQueue<>();
//submit负责添加任务
public void submit(Runnable runnable) throws InterruptedException {
blockingQueue.put(runnable);
}
//n为指定线程数量
public MyThreadPool(int n) {
for(int i = 0;i < n;i ++) {
Thread thread = new Thread( ()-> {
while(true) {
try {
//取出任务执行
Runnable runnable = blockingQueue.take();
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
thread.start();
}
}
}