目录
1、int corePoolSize与int maximumPoolSize
2、long keepAlive和TimeUnit unit
5、RejectedExecutionHandler handler
一、线程池是什么
池(Pool)的引入本质是为了提高程序的运行效率。
线程池(Thread Pool),其核心思想就是提前创建一定数量的线程,形成线程池,当遇到高并发请求的时候,可以直接把任务放到线程池中,让线程池里已经有的线程执行这些任务,避免频繁的创建和销毁线程(系统频繁申请线程开销是比较大的),从而提高效率。
想象一个这样的场景:
你是一名经营着生意火爆的海鲜饭店老板,对新鲜水产品的需求量非常大。所以高效的水产品供应方式一定要想好。
如果直接去市场买,先不谈来回的时间成本,买回来的鱼啊、虾啊、贝壳啊这些可能就不太新鲜了,甚至有的已经嘎了,总之有很多不可控因素在里面。
为了解决这种情况,于是你在后厨装修了一个很大的池子,在这个池子中有很多区域被分隔开来,把不同的海鲜产品放到不同的隔间,从而实现更好的管理,最后把那些需要的水产品买回来放到对应隔间即可。等后厨需要的时候直接往池子拿即可。
二、为什么线程池中取线程会比直接向操作系统申请来的高效?
从饭店老板的例子中相信大家已经略知一二了。
这里涉及到操作系统中的概念:
内核态&用户态
操作系统实际上约等于操作系统内核+操作系统配套的应用程序接口。
对应的程序的执行很多需要用户态的代码和内核态的代码配合完成的。
但是,系统内核是统一调度管理程序的主体,当同时运行多个程序的时候系统内核的工作可能就非常繁忙,此时如果应用程序在去告诉内核它向创建线程,这个线程会被创建,但是这个线程什么时候创建,创建耗时多久就是一个未知数,是不可控制的,极大的影响效率!
如图,这样一个形象的例子,狗哥去银行办理业务,但是忘记带复印件了,于是去让柜台人员帮他办理(直接去操作系统申请)但是前面排队的人很多,当他把复印件给柜台人员后,具体什么时候证件复印完,是不知道的,效率就很低:
但是如果是纯用户态的操作,那么效率就会大大提高,如图,狗哥直接去自助银行服务区自行打印并办理业务:
三、JAVA标准库中的线程池
一下会介绍两个类,都是用于创建线程池的。
(1)类:ThreadPoolExecutor
官方文档描述:docs.oracle.com
这个类在路径java.util.concurrent这个路径上。
这个类的构造方法有一点复杂,一共有四个构造方法,其中介绍参数最全也是最多的这一个(介绍了这个其他的也就清楚了)
摘自官方文档的构造方法:
一共有7个参数,我们一个一个介绍:
1、int corePoolSize与int maximumPoolSize
corePoolSize描述的是线程池中至少有几个线程。
当任务请求过多的时候,线程池可以在已有线程的基础上再扩容多几个线程。maximumPoolSize代表的就是线程池最多含有几个线程。
简化记忆:maximumPoolSize=corePoolSize+新创建的线程
2、long keepAlive和TimeUnit unit
keepAlive描述的是线程池中一个线程的可以空闲的最大时间。
当请求不在繁忙,线程池可能创建了多个线程,对于多创建的且没有实际工作的线程会占用资源,所以要设置一个线程可以“摸鱼”的最大时间,当到达这个时间,那么就销毁这个线程。
而unit指的是时间的单位。
TimeUnit是一个枚举类型,包含多种时间类型:
3、BlockingQueue<Runnable> workQueue
线程池的使用实际上是典型的生产者消费者模型,用到生产者消费者模型那么就不得不涉及到阻塞队列。
线程池的大致工作流程就是用户端(生产者)通过型如submit的操作把需要完成的任务传递给workQueue(阻塞队列)中,然后线程池(消费者)也通过从workQueue(阻塞队列)拿到需要执行的任务,放入到合适的线程中执行。
4、ThreadFactory threadFactory
ThreadFactory是一个接口(不是函数式接口),定义如下:
它只有一个抽象方法newThread,这个方法是用来创建线程的,它这里用到了工厂设计模式,是一种常见的设计模式,这种设计模式是用来填构造方法的一个“坑”的。
举个例子,要写两个构造方法,确定平面上一个点的位置。这个我们高中学数学的都知道有两种表示方法,一个是用平面直角坐标系(x,y),一个是极坐标(r,θ),但是如果真的这样去定义,编译器就报错了:
通过工厂设计模式就可以避免这种情况发生。
最简单的工厂设计模式就是定义一个静态方法,在这个方法里直接线程创建一个Point类,然后设置类的每一个参数。
如图伪代码:
像这种创建一个对象的静态方法叫“工厂方法”,如果把这类方法单独放到一个类中,这个类就叫“工厂类”
当然在我们实际使用ThreadPoolExecutor类的时候一般不用自己写一个方法去实现这个ThreadFactor接口,一般使用Excutors类(等一下会介绍这个类)自带的 默认方法:Executors.defaultThreadFactory()即可。
5、RejectedExecutionHandler handler
这个参数描述了无法执行任务时的拒绝策略。
什么时候会无法执行任务呢?很简单就是任务队列满的时候。虽然阻塞队列满的时候,submit会进入阻塞状态,但这种方式是不够安全和高效的,因为无法具体控制submit的任务的具体状态。通过给出明确的拒绝策略,是一个明智的办法。
ThreadPoolExecutor提供了四种拒绝策略:1)AbortPolicy(终止策略)
当任务已满,调用者继续发送任务给到任务队列,那么就会抛出RejectedExecutionException。
2)CallerRunsPolicy(调用者运行策略)当任务已满,调用者继续提交任务给任务队列,那么这个任务会由提交这个任务的线程自行完成。
3)DiscardOldestPolicy(丢弃最旧任务策略)当任务已满,调用者继续提交任务给任务队列,那么任务队列中存在任务最旧的任务就会被丢弃(Discard)来优先执行新提交的任务。
4)DiscardPolicy(丢弃策略)如果任务队列已满,在提交新的任务,直接把刚才新提交的任务丢弃,而不抛出异常或者通知调用者。
(2)类:Excutors
1、提供的方法:
ThreadPoolExecutor功能很强大,但参数多,用起来麻烦。
为此,Excutors(工厂类)中提供了许多专门用于构造ThreadPoolExecutor的静态方法,对ThreadPoolExecutor进行了封装,从而简化线程池的使用。
这里我们只介绍两个比较常用的构造ThreadPoolExecutor的工厂方法:
- newFixedThreadPool(size):这个方法规定了创建的线程池固定只有size个线程,也就是核
心线程数等于最大线程数。
- newCachedThreadPool():这个方法设置了一个非常大的线程数,线程不够就可以扩容。
2、返回类类型
这些工厂方法都会返回一个接口类型:
这个接口含有许多重要的方法,其中核心的就是service.submit(Runnable r)和service.shutdown()。
1)service.submit(Runnable r)
这个方法可以把对应的任务放到线程池中去执行(重写run方法):
如下代码批量运行,这写任务:
public class ExecutorUse {
public static void main(String[] args) {
//蛇者这个线程池固定只有4个线程
ExecutorService service=Executors.newFixedThreadPool(4);
for (int i = 0; i <1000 ; i++) {
int id=i;a Thread cur= Thread.currentThread();
service.submit(()->{//直接用lambda表达式,也是可以的
System.out.println("执行任务id:"+id+","+cur.getName());
});
}
}
}
我们发现程序成功运行完了for循环,但是没有退出:
原因在于虽然提交的任务都运行完了但是ExecutorService默认情况下会一直等待新的任务提交,不会自动关闭。
底层解释就是线程池中所有的线程默认都是前台线程(只要有一个前台线程没有结束,程序就不会结束,即使main线程运行结束),这些前台线程虽然已经干完了活,但是都处于阻塞状态,没有真正终止。
2)service.shutdown()
如果没有需要提交运行的任务了,可以使用这个shutdown()方法关闭线程池中的所有线程:
public class ExecutorUse {
public static void main(String[] args) {
//蛇者这个线程池固定只有4个线程
ExecutorService service=Executors.newFixedThreadPool(4);
for (int i = 0; i <1000 ; i++) {
int id=i;
Thread cur= Thread.currentThread();
service.submit(()->{
System.out.println("执行任务id:"+id+","+cur.getName());
});
}
//把线程池所有线程终止
service.shutdown();
}
}
程序退出:
四、最简单线程池实现
实现一个含有submit的固定线程数目的线程池:
class MyThreadPool_ {
//定义一个工作队列,把submit的任务放到队列,线程池拿任务,也在这里区,容量顶一个1000
BlockingQueue<Runnable> workingQueue = new ArrayBlockingQueue<>(1000);
//设定固定容量的线程的构造方法
public MyThreadPool_(int capacity) throws InterruptedException {
for (int i = 0; i < capacity; i++) {
Thread t = new Thread(() -> {//线程池固定创建capacity个线程
while(true){//在线程中,死循环,去take任务
Runnable r = null;
try {
r = workingQueue.take();//从工作队列里面获取任务,没有任务会进入阻塞状态(WAITING)
} catch (InterruptedException e) {
e.printStackTrace();
}
r.run();//执行任务
}
});
t.start();//启动线程
}
}
public void submit(Runnable r) throws InterruptedException {
workingQueue.put(r);//把任务放到工作队列中
}
}
public class TestCSDNDemo {
public static void main(String[] args) throws InterruptedException {
//线程池的线程数设置成10个
MyThreadPool_ myThreadPool=new MyThreadPool_(10);
//测试,提交1000个任务
for (int i = 0; i <1000 ; i++) {
int id=i;
myThreadPool.submit(()->{
Thread currentThread= Thread.currentThread();
System.out.println("执行任务:"+id+",线程名字:"+currentThread.getName());
});
}
}
}
执行结果: