目录
5.1 ArrayBlockingQueue 数组组成的有界阻塞队列
5.2 LinkedBlockingQueue 链表组成的有界阻塞队列 参数为Integer.MAX_VALUE
5.3 LinkedTransferQueue 链表组成的无界阻塞队列
5.4 LinkedBlockingDeque 链表组成的双向阻塞队列
5.5 PriorityBlockingQueue 支持优先级排序的无界阻塞队列
5.6 DelayQueue 优先队列实现的延迟无界阻塞队列
5.7 SynchronousQueue 不存储元素的队列 又称单元素队列
7.rejectedExecutionHahdler:拒绝策略
一、什么是线程池?
线程池是多线程处理方式(继承Thread类,实现Runnable接口,实现Callable接口(通过FutureTask构造器),线程池)中的一种,处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。线程池线程都是后台线程,每个线程池都使用默认的堆栈大小(-Xss参数,在linux、os、oracle系统中默认1024kb,windows系统中,大小取决于实际物理内存),以默认的优先级运行,并处于多线程单元中,如果某个线程在托管代码中空闲(如正在等待某个事件),则线程池将插入另一个辅助线程来使所有处理器保持繁忙。如果线程池中所有线程都保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程,但数目永远不会超过最大值(maximumPoolSize),超过最大值的线程可以排队,但它们要等到其他线程完成后才启动。
线程池的作用:
线程池的主要作用是控制线程数量,处理过程中将任务放入队列,然后在创建线程后启动这些任务,如果线程数量超过了最大数,超出数量的线程排队等候,等其他线程执行完毕,再从队列中取得任务来执行。
主要特点:线程复用,控制最大线程数,管理线程。
1).通过复用已经创建的线程,降低线程反复创建和回收对资源的消耗。
2).提高响应速度,当任务到达时不需要等待线程创建而现有的线程可立刻执行。
3).提高线程的可管理性,线程是稀缺资源,如果无限制创建,除了消耗系统资源,还会降低系统的稳定性,通过线程池可以实现对线程的统一管理、监控和调优。
线程池框架:
Java中线程池是通过Executor框架实现的,该框架用到了Executor,Executors,ExecutorService,AbstractExecutorService,ScheduleExecutorService,ThreadPoolExecutor,ScheduleThreadPoolExecutor.
二、线程池的参数
1.corePoolSize:核心线程数
线程池中常驻的线程数量,创建了线程以后,当有任务请求进来,就会安排池中常驻线程去执行任务,当线程池中线程数量达到corePoolSize后,就会把任务放到缓存队列中(wordQueue)
2.maximumPoolSize:最大线程数
线程池中能够容纳的同时执行的最大线程数,此值必须大于等于1,当前任务数量达到corePoolSize并且workQueue队列的数量也满了以后,就会创建新的线程去执行任务。
3.keepAliveTime:最大空闲时间
当线程数量超过corePoolSize后,其中的线程空闲时间达到keepAliveTime后,多余的线程就会被销毁直到剩下的线程数量为corePoolSize为止
4.timeUnit:keepAliveTime的时间单位
最大空闲时间的计量单位。
5.workQueue:任务队列
工作队列,存放被提交但尚未执行的任务。
5.1 ArrayBlockingQueue 数组组成的有界阻塞队列
此队列按照先进先出(FIFO)的原则对元素进行排序。默认情况下不保证访问者公平的访问对列,所谓公平是指阻塞堆列线程可按照阻塞的顺序访问队列。非公平性对先等待的线程是不公平的,当队列可用时,阻塞队列的线程都可以竞争访问队列的资格。
5.2 LinkedBlockingQueue 链表组成的有界阻塞队列 参数为Integer.MAX_VALUE
此队列按照先进先出的原则对元素进行排序。
5.3 LinkedTransferQueue 链表组成的无界阻塞队列
相比于其他队列,LinkedTransferQueue多了tryTransfer和transfer方法
5.4 LinkedBlockingDeque 链表组成的双向阻塞队列
5.5 PriorityBlockingQueue 支持优先级排序的无界阻塞队列
此队列逻辑上是无界的,但是资源耗尽时试图执行add也将失败,导致OOM
5.6 DelayQueue 优先队列实现的延迟无界阻塞队列
只有在延迟期满之后才能从中提取元素。
5.7 SynchronousQueue 不存储元素的队列 又称单元素队列
其中一个线程的插入操作必须等待另一个线程的对应移除操作,反之亦然。
线程池中最常用的是ArrayBlockingQueue,LinkedBlockingQueue,SynchronousQueue
队列中常用的方法:
add() 向队列添加一个元素,如果队列已满,抛出IllegalStageException
remove() 移除队列头节点元素,如果对列为空,抛出NoSuchElementException
element() 检查队列,由元素就返回头节点,否则抛出NoSuchElementException
offset() 向队列中添加一个元素,成功返回true,失败返回false
poll() 删除队列中的头节点,成功返回元素,失败返回null
peek() 检查队列,由元素返回头节点,对列为空则返回null
put() 向队列中添加元素,如果队列满了就阻塞
take() 从队列中删除并返回头节点,如果列队中没有元素就阻塞
6.threadFactory:线程工厂
表是线程池中生成工作线程的线程工厂,一般使用默认即可(Executors.defaultThreadFactory())。它主要是为了给线程池起一个标识,也就是为线程池起一个具有意义的名称。
也可以自己实现ThreadFactory接口:
public class MyThreadFactory implements ThreadFactory {
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(r);
thread.setName("一个全新的线程");
return thread;
}
}
7.rejectedExecutionHahdler:拒绝策略
7.1 AbortPolicy
线程池满了直接抛出RejectedExecutionHandlerException,并中断任务执行。这是线程池默认的拒绝策略,可及时反馈程序运行的状态。如果是比较关键的业务,推荐使用这个策略,这样的话如果系统不能承受更大的并发量的时候,就可以通过异常及时发现。
7.2 DiscardPolicy
直接抛弃当前任务,不抛异常。如果允许任务被抛弃,这是性能最好的一种拒绝策略。
7.3 DiscardOldestPolicy
抛弃任务队列中等待最久的任务,并把当前任务添加到任务队列尝试提交。不建议使用。
7.4 CallerRunPolicy
不抛异常也不丢弃任务,而是尝试将任务返回给调用者,也即将任务交给main(调用executor方法线)线程执行。
如果要自定义拒绝策略,可以通过实现rejectedExecution()方法实现
public class MyExecutionHandler implements RejectedExecutionHandler {
@Override
public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
//这里可以写自己的拒绝策略逻辑
}
}
三、线程池工作原理
1.工作原理流程
1).创建线程以后,等待提交过来的任务请求
2).当调用executor()或者submit()方法添加一个请求任务时,线程池会做如下判断:
如果正在运行的线程数量小于corePoolSize,那么马上安排线程执行这个任务。
如果正在运行的线程数量大于等于corePoolSize,那么就将任务放到任务队列中
如果任务队列没满就成功放入队列
如果任务队列满了并且正在运行的线程数小于maximumPoolSize,就创建非核心线程立刻执行这个任务(这里创建的线程不是首先去执行队列中的任务)。
如果队列满了且正在运行的线程数量大于等于maximumPoolSize,那么线程池就会启动饱和拒绝策略;
3).当一个线程执行完当前任务后,它会从队列中取下一个任务来执行。
4).当一个线程无事可做,空闲时间超过keepAliveTime,线程池就会判断:
如果当前线程池中线程数量大于corePoolSize,那么这个线程就会被回收
直到线程池中线程数量收缩到corePoolSize。
2.实现线程池的方式:
1).通过Executors工具类创建:
带有时间调度的线程池 (每秒执行等)
ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(5);
指定线程数的线程池 适用于处理长期任务
ExecutorService fixedThreadPool = Executors.newFixedThreadPool(5);
一池一线程 适用于一个任务接一个任务的场景
ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
一池多线程,带缓存 适用于处理量较大的短期任务
ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
使用目前机器上可用的处理器作为它的并行级别。 Java8之后的
ExecutorService workStealingPool = Executors.newWorkStealingPool();
上述的前四种线程池创建,最终构造方法都是用了ThreadPoolExecutor类的同一个构造器,只不过是所传参数的区别,构造器源码如下
package java.util.concurrent;
public class ThreadPoolExecutor extends AbstractExecutorService {
//略...
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.acc = System.getSecurityManager() == null ?
null :
AccessController.getContext();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
}
但是,在在阿里巴巴Java开发手册中,不建议直接使用Executors去创建:
线程池不允许使用Executors去创建,而是通过ThreadPoolExecutors的方式,
这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险
Executors返回的线程池对象的弊端如下:
(1).Executors.newFixedThreadPool()和Executors.newSIngleThreadExecutor()方法在调用构造器的时候传入的workQueue参数为new LinkedBlockingQueue<Runnable>(),此队列长度为Integer.MAX_VALUE,源码如下:
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public LinkedBlockingQueue() {
this(Integer.MAX_VALUE);
}
这样做可能导致大量堆积请求到队列,造成OOM。
(2).Executors.newScheduledThreadPool()和Executors.newCachedThreadPool方法在调用构造器的时候,传入的参数maximumPoolSize为Integer.MAX_VALUE,源码如下:
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue());
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
这样做可能导致大量线程被创建,造成OOM。
2).推荐的线程池实现方式的自己手写线程池,通过newThreadPoolExecutor实现:
ExecutorService threadPool = new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingQueue<Runnable>(4),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.DiscardPolicy());
手写线程池时,根据实际业务合理配置线程池参数,是最稳妥的做法。
四、任务提交方式
1).executor方法
void execute(Runnable command);
executorService.execute(() -> {
System.out.println("ThreadPoolDemo.execute");
});
2).submit()方法
Future<?> future = threadPool.submit(() -> {
System.out.println(Thread.currentThread().getName() + "\t 办理业务");
});
Object o = future.get();
总结execute()方法和submit()方法的区别:
(1).execute()只能提交Runnalbe类型的任务,无返回值。
(2).submit()既可以提交Runnable类型的任务也可以提交Callable类型的任务,会有一个类型为Future的返回值,如果提交的是Runnable类型的任务,future.get()得到的是null。
(3).execute()提交的任务在执行中遇到异常会直接抛出,而submit()不直接抛出,只有在Future的get方法获取返回值时,才会获得异常抛出。
(4).future在的get方法在为获得返回值之前会一直阻塞,我们可以在while循环中使用Future的isDone方法来判断任务是否执行完,从而去控制get方法,使得submit方法可以正确使用。
while(true) {
/idDone:如果任务已完成,则返回 true。 可能由于正常终止、异常或取消而完成,在所有这些情况中,此方法都将返回 true。
if(future.isDone()) {
System.out.println("任务执行完成:" + future.get());
break;
}
}
es.shutdown();
五、线程池如何关闭
如何优雅的关闭线程池是一个头疼的问题,开启线程是简单的,但想要停止却不是那么容易。通常大部分程序都是使用jdk提供的俩个方法来关闭线程池,分别是shutdown和shutdownNow;
通过调用线程池的shutdown()或shutdownNow()方法来关闭线程池。它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt()方法来中断线程(中断仅仅是给线程打上一个标记,并不代表这个线程真正停止了,如果线程不去响应中断,那么这个标记将毫无作用),所以无法响应中断的线程可能永远无法停止。
但是它们存在一定区别,shutdownNow首先将线程池状态设置为STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将只是将线程池的状态设置成shutdown状态,然后中断所有没有正在执行任务的线程。
只要调用了这俩个关闭方法中的任意一个,isShutdown方法就会返回true,当所有的任务都已关闭后,才表示线程池关闭成功,这时候调用isTerminaed方法会返回true,至于应该调用哪一种方法来关闭线程池,应该交由提交到线程池的任务特性决定,通常调用shutdown方法来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow方法。
六、怎么合理配置线程数量
在实际开发中,要根据任务的性质来决定我们创建的核心线程数的大小,可以从以下角度分析:
·任务的性质:CPU密集型、IO密集型和混合型
·任务优先级:高、中、低
·任务执行时间:长、中、短
·任务依赖性:是否依赖于系统其它资源,例如:数据库连接
对于CPU密集的任务,由于任务需要大量的运算而没有阻塞(没有多线程IO操作就不会阻塞),CPU一直在高速运算,一般配置CPU核数+1的线程池数量。多出来的一个线程是作为其他线程出现问题的备用线程。
但是对于单核CPU来说,其运算能力总共就那么多,设置多的线程数也不可能使程序的到加速。
由此可见,在多核CPU环境下,设置合理的线程数量其实本质就是合理的运用了CPU的空闲时间,以此来提高了效率。
IO密集型再多线程环境,由于存在锁的竞争以及阻塞,所以线程会有等待并不是一直在执行任务,因此就尽可能多的配置线程数,通常的公式是CPU核数*2
还有一种情况是IO密集型会有大量的IO以及阻塞,导致大部分线程阻塞,这时候为了更充分利用阻塞的时间(而不是因为阻塞,这段时间被浪费),就设置更多的线程数量,这时可以参考公式:
cpu核数/(1-阻塞系数)
阻塞系数一般在0.8~0.9之间
例如:CPU核数为8,阻塞系数为0.9。那么设置的线程数为: 8/(1-0.9) = 80