三:线程池的相关总结
1.线程池的意义:
线程虽然好用,但是如果创建了大量的线程,会拖垮整个程序,甚至可能会出现OOM异常;另一方面,大量的线程被GC的时候,也会产生巨大的压力,延长GC的停顿时间。
其次,线程在被创建和销毁的时候,也是会消耗系统的内存和资源的。
说的简单一点,你们JDBC连接是不是用连接池的,现在基本上不会有人去创建JDBC连接,然后去使用它了,基本都是利用连接池,取得池内的连接。线程池也是一样的概念。我们不再创建线程,而是从线程池中去取得,由线程池去控制和管理线程。
2.最基本的线程池:
JDK对于多线程的控制,专门提供了一套Executor框架。java.util.concurrent是java的并发包。其中Executors类是线程池工厂,通过Executors类我们可以得到不同功能的线程池。
1)newFixedThreadPool:构造方法时必须传入线程数(Int),这是一个固定传入参数的线程数的线程池
2)newSingleThreadPool:这是一个固定只有一个线程的线程池
3)newCacheThreadPool:这是一个最大线程数无上限的线程池
这三个线程池,看似功能不同,但是其实他们都是ThreadPoolExecutor类的封装,我们可以看一下,关于这个ThreadPoolExecutor这个类的构造方法;
最完整的构造方法里面构造参数有7个,分别是corePoolSize,maximumPoolSize,keepActiveTime,unit,workQueue,threadFactory,RejectedExecutionHandler。
corePoolSize 表示线程池的核心线程数,
maximumPoolSize 表示线程池最大线程数
keepActiveTime 表示在线程池中,空闲线程的存活时间
Unit 表示上面存活时间的时间单位
workQueue 表示线程池中的任务队列
threadFactory 表示线程池中的线程工厂,基本上都是选择默认的,关于这个东西,阿里云的代码规范对这个线程工厂做了一个补充,大家可以去看看
RejectedExecutionHandler 表示的是这个线程池中的拒绝策略,JDK默认的拒绝策略是AbortPolicy
我觉得知道了连接池,再去看线程池,其实都是差不多的,所以什么核心线程数,最大线程数,存活时间的我就不多说了,觉得可以说一下的是workQueue和RejectedExecutionHandler。
一般来说,任务队列(workQueue)有以下的几种,
1.SynchronousQueue(直接提交队列),这个任务队列,他没有容量,基本就是属于有了任务,就将任务提交给线程池,让线程池创建新的线程,直至线程池中的活跃线程大于最大线程数,这个时候,线程池就直接执行拒绝策略(RejectedExecutionHandler)
2.ArrayBlockingQueue(有界队列),如果要使用这个任务队列,就必须在初始化的时候,传入容量参数,代表这个任务队列的最大容量。对于这个任务队列而言,当线程池中的活跃线程数等于核心线程数之后,这个队列就不提交任务了,直到任务堆积过多,超过了容量参数,这个时候,任务队列才会继续提交任务,让线程池开始生成新的线程,直到活跃线程数大于最大线程数,而这个时候,线程池就开始执行拒绝策略了
3.LinkedBlockingQueue(无界队列),这个队列,他的容量是无限的,他可以不断的增长,直到吃完系统的所有资源。对于这个队列而言,当线程池中的活跃线程数等于核心线程数之后,他也同样不会提交任务,但是和有界队列不同,因为他的容量是无限的,所以不存在会使用最大线程数的时候,也不存在拒绝策略,除非你的系统先挂掉
4.PriorityBlockingQueue(优先队列),这个队列,是一个特殊的无界队列,所以在对待线程池内的线程数的控制方面,是和无界队列一致的。对于有界队列和无界队列而言,他们其实都是按照先进先算,后进后算的方法来向线程池传递任务的。但是优先队列正如名字一样,他是会根据任务自己的优先级来决定先后执行顺序的。所以换句话说,如果你要使用优先队列,那么就要让你的任务去实现Comparable接口,重写里面的compareTo方法,具体可以参考下面的代码
package com.liu.javathread.priorityblockingqueue;
public class MyThread implements Runnable, Comparable<MyThread>{
protected String name;
public MyThread(String name) {
super();
this.name = name;
}
public MyThread() {
}
@Override
public int compareTo(MyThread o) {
// 这是自身这个线程的数值
int me = Integer.parseInt(this.name.split("_")[1]);
// 传参的线程的数据
int other = Integer.parseInt(o.name.split("_")[1]);
if(me > other){
return 1;
}else if(me < other){
return -1;
}else{
return 0;
}
}
@Override
public void run() {
try {
// 系统沉睡模拟工作任务
Thread.sleep(1000);
System.out.println(name+" ");
} catch (Exception e) {
e.printStackTrace();
}
}
}
package com.liu.javathread.priorityblockingqueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.PriorityBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadMain {
public static void main(String[] args) {
ExecutorService executor = new ThreadPoolExecutor(10, 100, 2, TimeUnit.SECONDS, new PriorityBlockingQueue<Runnable>());
for(int i=0;i<1000;i++){
executor.execute(new MyThread("testThread_"+Integer.toString(999-i)));
}
}
}
而关于拒绝策略,我觉得一般来说,选择默认的AbortPolicy就可以了,这个拒绝策略意思是直接抛出异常,阻止系统继续工作
另外的拒绝策略,CallerRunsPolicy,DiscardOledestPolicy,DiscardPolicy这些策略,大家有兴趣可以自己研究一下,另外如果说,你对于这些拒绝策略都不满意,可以自己去重新实现。RejectedExecutionHandler 这个东西其实他是一个接口,上述的那些都是他的实现。
最后大家可以去看一下,之前我们说的三个JDK定义好的线程池,他们的核心线程数,最大线程数,存活时间,包括任务队列分别都是些什么。这对于我们已经选择JDK自带的线程池有很大的帮助
3.扩展线程池:
阿里巴巴的代码规范中,建议我们使用线程池,而且最好不是使用JDK自带的线程池,而是利用最基本的线程池ThreadPoolExecutor,不仅仅是因为我们可以自由的选择任务队列,拒绝策略,更多的是因为,这个线程池具有可扩展性
首先具体的来说,最具代表性的就是beforeExecutor()和afterExecutor()这两个方法,这两个方法看方法名就知道,他们分别是在线程池内线程启动之前和线程结束之后分别去调用的。
我们可以通过继承ThreadPoolExecutor这个类,然后重写这两个方法,去实现我们的业务,我个人觉得这个在调试的时候比较有用,至少你可以知道你的多个线程在执行前和执行后的状态。具体可以参考MyThreadPool这个文件。另外如果大家有兴趣的话,可以去找一下为什么线程池内部在调用工作线程的时候,会调用这两个方法