在之前的章节中我们就提到过池,例如字符串常量池,数据库连接池。
池是为了提升我们代码的效率的。由于有些东西我们频繁的创建销毁过于消耗资源,因此我们用池将暂时用不到的资源存储起来,等到以后有需要了再从池中拿出来
例如我们在招聘时被通知进入了公司的人才池,这并不说明你是个人才,而是说明公司嫌审核简历太麻烦了,现在还用不到你,等公司实在招不到人了才会调用这个人才池
而我们的线程池虽然相对进程来说已经轻量化了,但是多次创建和销毁仍是一种低效的操作,因此有了线程池。在线程使用完成后放到线程池中,等到需要新的线程了再从池子中取出来
这是因为我们的计算机由多个部分组成:硬件,驱动,内核,系统调用,应用程序。而应用程序就属于用户态,内核属于内核态。
当我们创建线程时,就需要创建一个PCB,其本质是一个内核中的数据结构,因此我们就需要从用户态切换到内核态来创建
而当我们从线程池中拿一个线程时,这是用户态自己就可以实现的,因此效率和开销更小
首先创建一个池对象
ExecutorService pool = Executors.newCachedThreadPool();
我们的这个写法不是构造方法,而是因为构造方法中的参数太多,进行优化后的一种写法——工厂方法
工厂方法是为了弥补构造方法的不足而产生的,由于构造实例时我们可能要传入多个参数,因此就要写多个构造方法,但是这几个构造方法可能参数类型和个数都一样,不构成重载,因此就会出现语法错误。因此我们用新的方法将这些构造方法封装起来,就可以创建不同的对象了
public class Factory {
static class Point{
private double r;
private double a;
private double x;
private double y;
public static Point makePointByXY(double x, double y){
Point p = new Point();
p.setX(x);
p.setY(y);
return p;
}
private void setY(double y) {
this.y = y;
}
private void setX(double x) {
this.x = x;
}
public static Point makePointByRA(double r, double a){
Point p = new Point();
p.setR(r);
p.setA(a);
return p;
}
private void setA(double a) {
this.a = a;
}
private void setR(double r) {
this.r = r;
}
}
}
创建完池任务后就可以加入任务,让线程池中的线程来完成这些任务
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("任务");
}
});
标准库中的ThreadPool有一系列参数,让我们可以构造出不同的线程池
前者代表核心线程数,也就是线程池中的主力,就算空闲了也留在线程池中,后者是最大线程池,也就是主力加上替补
当我们任务过多时,就会让主力和替补都上
当我们没多少任务,这时就可以开除几个替补了
也就是如果我们的替补多长时间不上场,就可以把他开除了,unit是时间的单位
我们在创建线程池之前有可能自己就有一个队列,因此我们可以通过这个参数来为线程池传入自己的队列,如果不传入,线程池会自己创建一个队列
这个参数描述了线程如何创建,通过这个参数可以指定线程的创建的方法
由于我们线程池中的线程个数有限,超出数量就要阻塞等待,因此如果有过多的任务要完成,就可以通过这个参数来实现拒绝任务的策略
创建一个核心线程数为5,最大线程数为10,任务队列为100,3秒的空闲开除替补,拒绝策略为忽略最新任务的线程池
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class demo3 {
public static void main(String[] args) {
ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(5,10,3,
TimeUnit.SECONDS,new ArrayBlockingQueue<>(100), new ThreadPoolExecutor.DiscardOldestPolicy());
}
}
我们要实现一个将用户发给我们的X个任务,分配给我们开发好的Y个线程。要做到当线程不够用时就让任务阻塞等待,当任务没了就让线程池阻塞等待
因此我们可以用阻塞队列那节的消费者生产者模型来解决这个问题
static class MyThreadPool{
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public void submit(Runnable runnable) throws InterruptedException {
queue.put(runnable);
}
public MyThreadPool(int m){
for (int i = 0; i < m; i++) {
Thread t = new Thread(() -> {
while(true){
Runnable runnable = null;
try {
runnable = queue.take();
} catch (InterruptedException e) {
e.printStackTrace();
}
runnable.run();
}
});
t.start();
}
}
}
我们的MyThreadPool中有一个阻塞队列参数,
实现了submit方法——将传入的任务放到队列中。
在构造方法中,我们传入了一个数量值,代表要线程池中的线程个数,然后用一个for循环将这些线程创建出来
在每个线程中,都有一个循环的while,使之持续扫描阻塞队列中是否有新的任务需要完成
我们自己定义的线程池线程个数究竟多少合适???
这个问题并没有一个确切的答案,因为这和cpu的性能,任务的执行特点都是有关联的
例如如果是CPU密集型任务,也就是有大量的算术运算和逻辑判断的任务,就会大量消耗cpu资源,也就应该少安排一些任务
如果是IO密集型任务,也就是有很多读写任务,那么线程多了也没关系,对cpu的消耗没有那么大
因此,我们应该通过实验的方式来确定多少线程数合适,通过设定不同的数目,测定程序的性能