线程池
简介
Java多线程的另一种实现方式。此文介绍方式比较贴近人学习的自然流程,而不是生硬的介绍api
1. 线程池开始的地方 Executor
复盘线程创建的方式,不外乎就是显示的创建出多线程任务对象,比如继承Thread,比如实现 Runnable.最后显示的调用 *start( )*来开启线程。 线程池则改变了这种线程创建的方式,把程序员手动创建多线程的方式变成了把任务当做一个一个的对象,传入我们预先创建好的线程池中,然后由这个池子来负责多线程任务的创建。 我们可以把线程池类比为一种队列,吧任务类比成一个队列中的元素。
在Java中,每一个Thread的创建都对应OS的线程的创建,而线程的创建是非常昂贵的操作,假设我们使用*new Thread()*的方式去完成任务,那么1000个任务或许就要创建1000个OS线程。线程池就是一个非常好的替代,允许我们通过参数来控制线程的数量,比如创建10个线程来完成1000个任务。
1.1 Executor 的引入
- an object that executes submitted Runnable tasks.
- This interface provides a way of decoupling task submission from the mechanics of how each task will be run, including details of thread use,scheduling, etc.
- An Executor is normally used instead of explicitly creating threads.
我们可以根据文档原文总结出 Executor 接口的使命:
- 负责运行提交的 Runnable 任务。
- 提供了一个解耦合的任务提交方式。
- 主要用于代替显示的创建线程方式。
例子:
-
显示的创建线程
new Thread(new Runnable(){ //执行一些任务 }).start;//手动new了一个Thread对象
-
线程池的方式
Executor executor = anExecutor; //把任务交给线程池 executor.execute(new RunnableTask1()); executor.execute(new RunnableTask2()); ...
2. 使用线程池来干一些简单的事情
学习开车,不能首先去学习变速箱的设计方式,而是学会踩离合挂挡到1档(完美),或者2档(正常),3档(有问题)把车开起来。
- FixedThreadPool
简单介绍一下 FixedThreadPool ,这是Java异步编程框架给我们提供的一个创建线程池的方式,创建线程有多种方式,这里只需要把这种创建方式理解为一种方便的自动挡的创建方式(相对于自己设置一些必要的参数,此类线程池只需要确认线程池的大小)
此类型的线程池方法有2个重载方法,参数的含义是
- 线程池线程的数量
- 线程工厂 表明了线程创建的方式
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
public static ExecutorService newFixedThreadPool(int nThreads)
public class FixedPoolDemo {
public static void main(String[] args) {
/**
* FixedThreadPool,是异步框架为我们提供的一个自动挡的线程池,
* 此方法有2个参数,分别是指定线程的数量,和线程工厂
* 示例代码创建了一个包含5个线程的线程池。
*/
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//使用线程池来完成一些事情
for (int i = 0; i < 100; i++) {
//线程池调用execute方法执行任务。
threadPool.execute(new Runnable() {
@Override
public void run() {
//通过获取线程的名字来观察是否是多线程的在完成任务。
System.out.println(Thread.currentThread().getName()+"===============");
}
});
}
//关闭线程池
threadPool.shutdown();
}
}
- 运行结果
pool-1-thread-1===============
pool-1-thread-5===============
pool-1-thread-3===============
pool-1-thread-3===============
pool-1-thread-4===============
pool-1-thread-2===============
pool-1-thread-4===============
pool-1-thread-3===============
pool-1-thread-3===============
pool-1-thread-3===============
pool-1-thread-5===============
pool-1-thread-5===============
pool-1-thread-1===============
pool-1-thread-3===============
pool-1-thread-4===============
pool-1-thread-2===============
pool-1-thread-2===============
pool-1-thread-2===============
pool-1-thread-2===============
pool-1-thread-4===============
pool-1-thread-3===============
pool-1-thread-3===============
pool-1-thread-3===============
pool-1-thread-5===============
pool-1-thread-5===============
pool-1-thread-1===============
pool-1-thread-5===============
pool-1-thread-3===============
pool-1-thread-4===============
pool-1-thread-2===============
Process finished with exit code 0
我们可以观察到一共有5个不同的线程同时在处理我们的任务。这里我们就实现了一个简单的使用线程池的多线程demo。
-
这里请思考一个问题,已知我们的最大线程是5个,如果我们的任务非常多,有10000个甚至更多,那么当这些任务高并发的被execute()方法执行,会不会导致线程池满了?会不会因为任务的执行时间比较长,从而导致任务堆积呢?
答案是会的。线程池提供了一个用于存放被提交但是尚未被执行的任务缓存容器。这个容器被叫做 阻塞队列, Blocking Queue
而且,这个自动挡线程池类型 FixedThreadPool搭配的阻塞队列是 LinkedBlocking Queue,是一种链表结构的队列。我们知道数据结构中有几种基本的类型,链表,数组。这里使用链表结构的队列原因就是因为:我们的线程数量被固定到了一个具体的值,当任务数量不可控的增大时,我们需要一个可变长的容器来存放任务,链表结构的容器在添加长度未知的元素时,插入数据的时间复杂度是O(1)。
3. Java提供的(自动挡)线程池种类
- 线程池框架给我们提供了4种可选择的类型。
3.1 FixedThreadPool
3.2 CachedThreadPool
- 同样,先让我们把车开起来,体验一下 CacheThreadPool的使用。
- 这里一共有2个重载的创建方法。而且不需要我们手动指定线程的大小!
public static ExecutorService newCachedThreadPool()
public static ExecutorService newCachedThreadPool(ThreadFactory threadFactory)
public class CachedThreadPoolDemo {
public static void main(String[] args) {
/**
* 缓存线程池的初始化不需要我们指定线程的数量,让我们看看为什么是这样?
* 推测一定是线程的创建被自动化了?
*
*/
ExecutorService threadPool = Executors.newCachedThreadPool();
//使用线程池来完成一些事情
for (int i = 0; i < 100; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
//通过获取线程的名字来观察是否是多线程的在完成任务。
System.out.println(Thread.currentThread().getName()+"===============");
}
});
}
//关闭线程池
threadPool.shutdown();
}
}
pool-1-thread-1===============
pool-1-thread-4===============
pool-1-thread-2===============
pool-1-thread-3===============
pool-1-thread-5===============
pool-1-thread-6===============
pool-1-thread-7===============
pool-1-thread-8===============
pool-1-thread-9===============
pool-1-thread-10===============
pool-1-thread-10===============
pool-1-thread-6===============
pool-1-thread-3===============
pool-1-thread-5===============
pool-1-thread-7===============
pool-1-thread-10===============
pool-1-thread-4===============
pool-1-thread-8===============
pool-1-thread-3===============
pool-1-thread-1===============
pool-1-thread-8===============
pool-1-thread-2===============
pool-1-thread-8===============
pool-1-thread-8===============
pool-1-thread-3===============
pool-1-thread-4===============
pool-1-thread-5===============
pool-1-thread-12===============
pool-1-thread-7===============
pool-1-thread-6===============
pool-1-thread-5===============
pool-1-thread-12===============
pool-1-thread-16===============
pool-1-thread-7===============
pool-1-thread-14===============
pool-1-thread-8===============
pool-1-thread-13===============
pool-1-thread-14===============
pool-1-thread-2===============
pool-1-thread-11===============
pool-1-thread-2===============
pool-1-thread-7===============
pool-1-thread-14===============
pool-1-thread-7===============
pool-1-thread-11===============
pool-1-thread-16===============
pool-1-thread-17===============
pool-1-thread-6===============
pool-1-thread-15===============
pool-1-thread-6===============
pool-1-thread-21===============
pool-1-thread-22===============
pool-1-thread-19===============
pool-1-thread-7===============
pool-1-thread-14===============
pool-1-thread-11===============
pool-1-thread-25===============
pool-1-thread-14===============
pool-1-thread-25===============
pool-1-thread-25===============
pool-1-thread-21===============
pool-1-thread-23===============
pool-1-thread-15===============
pool-1-thread-20===============
pool-1-thread-31===============
pool-1-thread-14===============
pool-1-thread-7===============
pool-1-thread-27===============
pool-1-thread-11===============
pool-1-thread-9===============
pool-1-thread-29===============
pool-1-thread-26===============
pool-1-thread-19===============
pool-1-thread-25===============
pool-1-thread-32===============
pool-1-thread-16===============
pool-1-thread-18===============
pool-1-thread-2===============
pool-1-thread-37===============
pool-1-thread-24===============
pool-1-thread-30===============
pool-1-thread-23===============
pool-1-thread-33===============
pool-1-thread-28===============
pool-1-thread-36===============
pool-1-thread-10===============
pool-1-thread-1===============
-
通过观察结果,我们发现了线程创建的数量是自我管理的,自动创建的。
-
如果高并发场景下,突然涌入了特别多的任务,意味着此类型的线程池或许会自我创建很多的线程来加快任务的处理。那么就必须考虑使用过的线程的回收。
-
没有使用blocking queue 来保存任务(底层其实使用了 Synchrous Queue,此类型的队列实际上只有一个位置,当任务等待时,会自动增加线程数)。这种情形当(假设有10个线程)所有线程都被占用仍然有任务,会自动创建新的线程来执行任务,通过设置线程空闲时间来回收空闲的线程。
-
我们无法控制线程的增加或减少。
public static ExecutorService newCachedThreadPool() {
//引入线程池构造器,当我们调用CachedThreadPool时,实际上是调用了这个方法创建的线程池。
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
- 我们可以发现,当使用空参的newCachedThreadPool()时,底层实际上调用了new ThreadPoolExecutor()方法。如果这里对其他类型的线程池不感兴趣,可以直接跳到ThreadPoolExecutor。
3.3 SchedualedThreadPool
3.4 SingleThreadedPool
3.5 线程池类型小结
Type | Type Of Queue | why |
---|---|---|
FixedThreadPool | LinkedBlockingQueue | 线程池线程大小固定不变,导致阻塞队列的大小不能固定,从而使用链表结构的队列。 |
SingleThreadExecutor | LinkedBlockingQueue | 线程池线程大小固定不变,导致阻塞队列的大小不能固定,从而使用链表结构的队列。 |
CachedThreadPool | SynchrousQueue | 线程的大小无上限,因此不用保存任务,SynchrousQueue是一种只有一个slot的队列 |
SchedualedThreadPool | DelayedWorkQueue | 特殊的调度线程池,用于处理和时间有关的任务 |
4. ThreadPoolExecutor
-
现在开了自动挡的汽车,来研究一下如何开手动挡的车(手动创建线程池)
-
这是Java java.util.concurrent 包下的一个线程池类
-
参数详解
构造方法: public ThreadPoolExecutor(int corePoolSize, //核心线程数量 int maximumPoolSize,// 最大线程数 long keepAliveTime, // 最大空闲时间 TimeUnit unit, // 时间单位 BlockingQueue<Runnable> workQueue, // 任务队列 ThreadFactory threadFactory, // 线程工厂 RejectedExecutionHandler handler // 饱和处理机制 ) { ... }
public class ThreadPoolDemo { public static void main(String[] args) throws InterruptedException { /** * 线程池的创建 * 线程池的参数: * 1.corePoolSize 核心线程数, * 2.maxPoolSize * 3.KeepAliveTime * 4.阻塞队列 * 5.线程工厂 * 6.拒绝策略 */ ThreadPoolExecutor threadPool = new ThreadPoolExecutor(3, 5, 1, TimeUnit.SECONDS, new ArrayBlockingQueue<>(10)); for (int i = 0; i < 15; i++) { threadPool.execute(() -> { System.out.println(Thread.currentThread().getName() + "=====办理业务"); }); } threadPoolExecutor.shutdown(); // System.out.println(Runtime.getRuntime().availableProcessors()); } }
线程池最大线程数决策
-
CPU密集型任务:获取服务器的CPU核心数和线程数。(最大线程数和机器的核心数相同)
Runtime.getRuntime().availableProcessors()
-
IO密集型任务
如果任务会频繁的查看数据库,或者要做RPC call,那么意味着此线程需要等待数据库IO完成,或者RPC 响应,这样就可能造成线程一直等待,这样即使你把全部核心都用来完成线程,可能造成所有线程同时等待,失去响应。这种解决方案推荐是:
-
设置更大的线程数来减小等待响应的线程所占的比例。
Executor.newFixedThreadPool(100)//尝试使用更大的线程
-
阻塞队列的种类
- ArrayBlockingQueue
- LinkedBlockingQueue