我们主要是要知道然后去创建并使用Java标准库中的线程池。
提供的类是:ThreadPoolExecutor。
参数解析
上面是这个类的构造方法,从上到下参数的个数是增多的。我们是要清楚构造方法的全部参数的。
下面是对最详细版的构造方法的参数解释:
注意:
1、 只有当核心线程数全部在工作时,如果这时需要处理新的任务,才会去创建新的线程。就好比一个公司,当内部员工足以处理这些业务时,就没必要花钱请外包,但是当内部员工已经忙不过来时,这时候才会需要外包来干新的任务。
2、当公司的业务过了旺季,到了淡季,这时候公司内部的员工都可能是出于空闲的状态,那外部更加没事干,因此公司就会考虑和外包解除合同。
3、线程工厂,这里使用了一种设计模式:工厂模式,其与我们前面学习的单例模式是出于同一级别的。工厂模式主要弥补构造方法的缺陷。例如,现在有一个类是用来描述平面直角坐标系中的一个点,描述的方式有两种:1、使用 (x,y) 坐标的方式;2、使用极坐标(用三角函数来实现)的方式;
因此,解决这样的问题,我们就可以使用工厂模式,将构造方法改为使用静态的方法,这样最终就不用通过构造方法来实现了。
代码演示:
class Point {
private double x = 0;
private double y = 0;
// 1、使用(x,y)的方式
public Point(double x, double y) {
this.x = x;
this.y = y;
}
// 2、使用极坐标的方式
/*public Point(double r, double a) {
this.x = r * Math.cos(a);
this.y = r * Math.cos(a);
}*/
public static Point getInstanceByXY(double x, double y) {
return new Point(x,y);
}
public static Point getInstanceByRA(double r, double a) {
return new Point(r*Math.cos(a), r*Math.sin(a));
}
}
4、拒绝策略:
对于这四种拒绝策略,1、3、4 应该是很好理解的,但对于第二种来说,可能有点模糊。第二种方式,是告诉需要执行任务的线程:这个任务,我现在没空,你自己去把这个任务完成吧。然后需要执行这个任务的线程,就会自己把这个任务执行完。
我们先来了解一下,任务是什么?通过前面的学习,我们已经知道了,线程就是轻量级进程,也就是一段需要执行的指令。任务同样也可以看作是一段需要执行的指令,并且任务所需要执行的代码都是用 Runnable给包装起来的。给线程池去执行的话,就是让线程池对象调用 submit 方法,然后把包含任务代码的Runnable给作为参数扔给 sumbit 去执行。这就是线程池执行任务的过程。
从上面的分析,我们也可以得出一个结论:交给线程池执行的任务,而让自己(需要执行该任务的线程)执行任务 的区别在于:线程池会利用多线程的方式去执行该任务,而自己只会去串行执行,这样就影响了程序最终的效率。而让自己去执行,其实就是底层让 Runnable 去调用 run 方法。
1)有小伙伴可能会对 3、4有疑惑:丢弃最新的任务和让需要执行该任务的线程自己去执行 的区别是不是前者根本就没有线程去执行,而后者是调用submit的线程(也就是需要执行该任务的线程)去执行。
2)也有小伙伴可能会遇到这种说法:4 是丢弃当前任务。这种说法也是正确的,这里最新的任务和当前的任务都是指需要被执行的任务。当前任务不就是需要被执行的任务嘛,最新的任务不也是需要被执行的任务嘛,对叭,细细品味一下。
使用线程池
上面就是对构造方法的参数的解析,下面我们就来使用一下这个线程池。由于原本的类参数过多,因此JVM又对其进行了部分封装,最终我们使用的类是 ExecutorSever 。
public class Test {
public static void main(String[] args) throws InterruptedException {
// 创建一个线程数目固定的线程池
ExecutorService service = Executors.newFixedThreadPool(2);
// 创建一个很大的线程池,最大线程的数目是Integer.MAX_VALUE
// ExecutorService service = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
int id = i;
// 创建任务
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println("Hello task"+id+"--->"+Thread.currentThread().getName());
}
};
// 调用线程池中的线程执行任务
service.submit(task);
Thread.sleep(1000);
}
}
}
我们去运行就会发现,上面的任务执行完成后,也就是所有的代码全部执行完成后,进程没有停下,还在继续。这是因为 线程池中的线程是前台线程的。
我们也可以去看这两个类的源码:
注意:上述代码的打印语句中不能使用 i ,因为在匿名内部类中,访问外部类的局部变量,采用的是变量捕获的方式,而这个方式固定了我们访问的变量必须是 final修饰的或者是事实 final(和我们上面一样,虽然没有用 final 修饰,但是最终的值并没有发生变化),i 在创建之后,还进行了 i++ 的操作,使得其发生了变化。