1. 什么是线程池
线程这个概念我们已经十分熟悉了,但是对于"池"这个概念,我们还要讲解一下."池"其实并不少,例如我们之前见过字符串常量池,所谓的"池"就是利用一些数据结构把相同的数据内容管理起来.这里的"线程池"就是把线程通过一些数据结构给管理存储起来.
2. 使用线程池的好处
2.1 提高效率
创建和销毁一个线程,是需要用户态+内核态共同实现的.但是如果我们使用线程池,就可以直接通过用户态进行创建和销毁线程,不需要通过内核态.由于内核态中指令执行的不稳定性,可能会极大地增加线程创建和销毁的时间,因此使用线程池可以提高效率.
2.2 降低消耗
通过反复利用创建的线程,可以避免多次创建和销毁作用相同的线程,可以降低线程创建和销毁造成的消耗.
2.3 管理线程
线程是一种十分珍贵的系统资源,使用线程池可以将线程很好地管理起来,对线程进行状态的监视,提高系统的稳定性.
3. 标准库线程池的参数
在java中,也封装了线程池ThreadPoolExecutor,这个类在java.util.concurrent包中.其中包含了7个参数.ctrl+右键点入这个类,观察其构造方法:
3.1 corePoolSize
这个参数代表线程池中含有的核心线程数量.
线程池中的线程分为核心线程和非核心线程,对于核心线程,是在线程池创建时就被创建.而对于非核心线程,会根据实际情况进行创建,而在不需要时被回收.这样也就达到了我们上述所谈到的动态创建和销毁非核心线程,能够提高运行的效率.
3.2 maximuPoolSize
这个参数代表线程池中允许创建的最大线程数量.
虽然在线程池中可以自动创建非核心线程来提高效率,但是不能无止境地创建线程,这十分消耗系统的资源.规定允许创建的最大线程数量,相当于设置了一个上限.如果核心线程数量等于允许创建的最大线程数,就说明不会有非核心线程被创建.通常来说,maximuPoolSize的值会大于corePoolSize的值.
3.3 keepAliveTime
这个参数代表非核心线程创建使用后到被回收之间能够存活的时间.
在非核心线程执行完它的任务之后,一般并不会直接回收,而是等待存活一段时间,如果在这段时间内,它被分配了新的任务,它就会继续执行被分配的新任务;反之,则被回收.这个参数的设置能够避免在短时间内反复创建回收线程,提高了线程的利用率,节省了系统的资源消耗.
3.4 unit
这个参数代表keepAliveTime参数的单位.
可以看到,keepAliveTime这个参数的类型是long,具体的单位需要通过unit参数进行设置.
3.5 workQueue
这个参数代表存放需要执行任务的工作队列.
在上图中可以看到,workQueue是一个阻塞队列.它的泛型类型是Runnable对象,只要对这些Runnable对象调用run方法,就可以开始执行任务.这里不过多阐述.
(阻塞队列详细介绍可以看(阻塞队列和基本实现-优快云博客))
3.6 threadFactory
这个参数代表创建线程的工厂模式.
工厂模式是一种设计模式,主要是用来解决构造方法不易于"重载"的问题.
我们举一个简单的例子:
我现在要在二维坐标系中创建一个点.这个点既可以通过(x,y)在笛卡尔坐标系下表示,也可以通过(r,a)在极坐标系中表示.于是,我们调用构造方法就会出现问题:
class Point {
public double x;
public double y;
public Point(double x,double y) {
this.x = x;
this.y = y;
}
public Point(double r,double a) {
this.x = r;
this.y = a;
}
}
这个代码一定会编译报错,因为这两个构造方法参数类型一样,参数个数一样,无法构成重载.而构造方法的名字必须和类名一样,不能随意更改.这就导致了我们无法通过构造方法区分出这个点是用哪种形式表示的.这时,我们就可以选择手动进行设置.代码如下:
public class PointBuilder {
public static Point makePointByXY(double x,double y) {
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
public static Point makePointByRA(double r,double a) {
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
}
我们通过自己手动设置,调用不同的方法就进行了表示方式的区分.相当于给构造方法又加上了一层分装,增加了灵活性.
3.7 handler
这个参数代表任务队列满时再添加新的任务的拒绝策略.虽然任务队列满了之后,调用put方法会直接进入阻塞状态,但是在此处直接阻塞不太合适.因此采用引入拒绝策略这个枚举类型参数.这个美剧类型中一共有四个对象.
ThreadPoolExecutor.AbortPolicy
队列满了,直接报错抛出异常(罢工).
ThreadPoolExecutor.CallerRunsPolicy
哪个线程添加这个任务,哪个线程自己执行.
ThreadPoolExecutor.DiscardOldestPolicy
丢弃队列首的任务,把最新的任务添加入任务队列.
ThreadPoolExecutor.DiscardPolicy
丢弃最新的任务,仍然执行原来的任务.
4. 线程池的创建方式
总体上来说,线程池的创建方式共可分为两类:
使用ThreadPoolExecutor创建线程池和使用Executors创建线程池.
4.1 使用ThreadPoolExecutor创建线程池
public static void main(String[] args) {
ThreadPoolExecutor t = new ThreadPoolExecutor(//上述7个参数);
}
这种创建方式不推荐,因为这里面需要自己操作实现的东西比较多,使用起来不是十分便捷.因此,我们通常使用Executor创建线程池.
4.2 使用Executors创建线程池
使用Executor去创建一个线程池,我们首先需要一个ExecutorService对象,我们把它叫做service.这个service去接收使用Executors中的方法创建的线程池.
我们直接输入Executors.new就会出现下图中的补全信息,这里面就有我们许多十分常用的线程池.
随便选取其中的一个,代码如下:
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
}
这样我们就使用Executors创建了一个线程池.
5. 使用Executors创建常用线程池
5.1 创建一个线程自动扩容的线程池
public class CreateCachedThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 1000; i++) {
int id = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+" "+Thread.currentThread().getName());
}
});
}
}
}
运行效果如下:
5.2 创建一个固定线程数目的线程池
public class CreateFixedThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(10);
for (int i = 0; i < 1000; i++) {
int id = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+" "+Thread.currentThread().getName());
}
});
}
}
}
运行效果如下:
5.3 创建一个只包含单个线程的线程池
public class CreateSingleThreadExecutor {
public static void main(String[] args) {
ExecutorService service = Executors.newSingleThreadExecutor();
for (int i = 0; i < 1000; i++) {
int id = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+" "+Thread.currentThread().getName());
}
});
}
}
}
运行效果如下:
5.4 创建一个固定线程个数,但是任务延时执行的线程池
public class CreateScheduledThreadPool {
public static void main(String[] args) {
ExecutorService service = Executors.newScheduledThreadPool(10);
for (int i = 0; i < 1000; i++) {
int id = i;
service.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+" "+Thread.currentThread().getName());
}
});
}
}
}
运行效果如下:
6. 线程池的简要模拟实现
我们首先来思考一下:一个线程池需要包含哪些东西?
首先,一个线程池需要有若干个线程;其次,一个线程池需要有任务队列;最后,一个线程池需要提供submit方法.
6.1 参数设置
int maxPoolSize = 0;
BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(1000);
List<Thread> threadList = new ArrayList<>();
一共三个参数:允许创建的最大线程数量,任务队列,用来存放线程的线程列表.
6.2 构造方法
还记得我们之前说过:在创建线程池的时候,就创建了核心线程,因此在构造方法中,我们直接创建出核心线程;并且直接开启这些线程,让这些线程成为"消费者",即从任务队列中取出Runnable对象,开始执行任务.最后再把这些线程添加入线程列表中.
public MyThreadPoolExecutor(int corePoolSize,int maxPoolSizedSize) {
this.maxPoolSize = maxPoolSize;
for(int i=0;i<corePoolSize;i++) {
Thread t = new Thread(()->{
try {
while(true) {
Runnable runnable = q.take();
runnable.run();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
});
t.start();
threadList.add(t);
}
}
6.3 submit方法
submit方法相当于"生产者",不断地将Runnable对象添加进入任务队列.同时,对任务队列中任务的数量和线程列表中线程的数量进行判断,如果超过了某个阈值,就在submit方法中独立地创建一个非核心线程来执行任务.
void submit(Runnable runnable) throws InterruptedException{
q.put(runnable);
if(q.size()>500 && threadList.size()<maxPoolSize) {
Thread t = new Thread(() -> {
try {
while (true) {
Runnable task = q.take();
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
threadList.add(t);
}
}
完整代码:
public class MyThreadPoolExecutor {
int maxPoolSize = 0;
BlockingQueue<Runnable> q = new ArrayBlockingQueue<>(1000);
List<Thread> threadList = new ArrayList<>();
public MyThreadPoolExecutor(int corePoolSize,int maxPoolSizedSize) {
this.maxPoolSize = maxPoolSize;
for(int i=0;i<corePoolSize;i++) {
Thread t = new Thread(()->{
try {
while(true) {
Runnable runnable = q.take();
runnable.run();
}
}catch(InterruptedException e) {
e.printStackTrace();
}
});
t.start();
threadList.add(t);
}
}
void submit(Runnable runnable) throws InterruptedException{
q.put(runnable);
if(q.size()>500 && threadList.size()<maxPoolSize) {
Thread t = new Thread(() -> {
try {
while (true) {
Runnable task = q.take();
task.run();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
t.start();
threadList.add(t);
}
}
public static void main(String[] args) throws InterruptedException {
MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(5,10);
for (int i = 0; i < 1000; i++) {
int id = i;
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello "+id+" "+Thread.currentThread().getName());
}
});
}
}
}
最后,我们测试一下上述的代码,效果如下: