线程池
随着现代计算机的发展,任务越来越多,线程创建也逐渐增加,每次让操作系统创建线程这个开销就有点大,因此,我们诞生了线程池的概念,线程池里面有很多线程,这些线程可以被用户去调用执行任务,这样我们就不用每次有任务执行的时候就让操作系统创建线程了,线程池一次性就创建好好几个线程供我们使用。
为什么让操作系统创建线程还是觉得性能不够好???
CPU 有两种状态,一种是用户态,另一种是内核态,内核态就是此时 CPU 执行的是 操作系统的内核代码(操作系统是由内核和配套的应用程序构成的,操作系统的内核包含操作系统的核心功能:管理硬件设备和给程序提供稳定的运行环境),用户态就是CPU 执行除操作系统之外的代码。
如果让操作系统频繁地创建线程,此时操作系统不只是会创建线程,可能在中间还会干其他的事情,所以这里是有一定的开销的。
因此我们直接从线程池里获取线程的开销是小于让操作系统创建一个线程的开销要小的。
ThreadPoolExecutor
Java 标准库里提供了线程池的类,我们可以直接使用,这里先介绍 ThreadPoolExecutor
Java 在 ThreadPoolExecutor 提供了四个构造方法,这里以最长的参数的构造方法开始进行介绍,上面三个构造方法和最长的构造方法的参数含义是一致的。
corePoolSize 这里是核心线程数,就是这个线程池最少应该由多少线程数量。
maximunPoolSize 这是最大线程数,这个数量包括**核心线程数 加 非核心线程数,**非核心线程数可以根据具体的任务量来创建和销毁,当任务量多的时候,线程池就会创建 非核心线程来 接收这些任务,当任务量少的时候,线程池可能会把非核心的线程给销毁掉。
keepAliveTime 是存活时间,也就是这些 非核心线程 处于空余状态可以存活多久
TimeUnit unit 这个是设置时间单位的,我们在前面设置好了 keepAliveTime 之后,要搭配时间单位,这个是枚举类,时间单位如下:
workQueue 是阻塞队列,这个阻塞队列存放的就是任务,调用 submit 的线程就是生产者,消费这些任务的线程池里的线程就是消费者。
threadFactory 线程工厂,这个是用来统一构造和初始化线程的。工厂模式放在下面讲解
Java 给我们提供了ThreadFactory 接口,让我们可以自定设置创建的线程的属性,当然如果你不想这么麻烦,你就是想用用线程来处理任务,这里Java 给你提供了一个默认的工厂方法defaultThreadFactory()
,这是静态方法,你直接调用就可以使用了,这个方法在import static java.util.concurrent.Executors.defaultThreadFactory;
handler 拒绝政策,当此时阻塞队列已满,但是又有线程进行了 submit 的调用,这时候 线程提供了 四种策略来应对:
AbortPolicy : 如果遇到阻塞队列放心不下任务的情况下会直接抛出异常 RejectedExecutionException.
CallerRunsPolicy : 是让谁调用了submit,那这个线程自己来执行 任务,举个例子:
public static void main(String[] args) { ThreadPoolExecutor threadPoolExcutor = new ThreadPoolExecutor(10,10,10, TimeUnit.MICROSECONDS,new ArrayBlockingQueue<>(1000), defaultThreadFactory(),new ThreadPoolExecutor.AbortPolicy()); threadPoolExcutor.submit(() -> { System.out.println("hello"); }); }
如果此时队列已满,这个打印 hello 的任务就会交由调用 submit 的线程来自己执行,也就 main 线程自己执行
DiscardOldestPolicy : 是让队列抛弃最老的任务,把新的任务加入进来。
DiscardPolicy : 让队列抛弃最新的任务,也就是 submit 新传入的任务直接抛弃,老子不干了~~
工厂模式
工厂模式和之前的单例模式、阻塞队列都是一种设计模式
在Java 中我们知道方法是有重载的,但是构造方法的重载能力是有限的,例如我们有一个 类 点(Point),我们可以使用平面直角坐标系的 x 和 y 坐标进行定位并创建这个点,我们也可以使用极坐标 L 和 a 来创建点,但是这里就是两个构造方法,我们是无法进行创建,因为重载的要求 参数个数 / 参数类型不同,恰好这里的构造方法的参数个数都是两个,并且都是 double 类型:
为此,这里 Java 使用工厂模式来解决这个棘手的问题:通过不同的静态方法来进行对象的属性的设置。
class Point { private double x; private double y; private double l; private double a; public void setX(double x) { this.x = x; } public void setY(double y) { this.y = y; } public void setL(double l) { this.l = l; } public void setA(double a) { this.a = a; }}
class PointFactory{ public static Point PointByXY(double x, double y) { Point point = new Point(); point.setX(x); point.setY(y); return point; } public static Point PointByLA(double l, double a) { Point point = new Point(); point.setL(l); point.setA(a); return point; }}
拥有工厂方法的类叫做工厂类,上面写的工厂类实现了不同构造需求的满足。
Executors
上面提到的 ThreadPoolExecutor 可能传递的参数有些麻烦,于是 Java 还给我们提供了 创建线程池的另一个类 Executors,它是对 ThreadPoolExecutor 进行了进一步的封装。
newFixedThreadPoll 就是创建固定线程数量的线程池,也就说核心线程数等于最大线程数
newWorkStealingPool 是创建一个足够大的线程池,这些线程数量能支持任务的处理,实际线程的数量是会动态的增加或者减少的。
这个是创建单线程的线程池,当这个线程挂掉的时候,线程池会再次创建一个新的线程来代替他继续完成工作
newCachedThreadPool 最大线程数可以无限大,也就是可以创建无限多的线程出来。
newScheduledThreadPool : 这是一个带有定时功能的线程池,也就是这个线程池会延期执行这些任务。
模拟实现
class MyThreadPoolExecutor { private BlockingQueue<Runnable> queue; private int poolSize; private Thread[] executors; public MyThreadPoolExecutor(int poolSize) { this.poolSize = poolSize; executors = new Thread[poolSize]; queue = new ArrayBlockingQueue<>(1000); for (int i = 0; i < poolSize; i++) { Thread t = new Thread(() -> { while(true) { try { queue.take().run(); } catch (InterruptedException e) { throw new RuntimeException(e); } } }); executors[i] = t; t.start(); } } public void submit(Runnable task) throws InterruptedException { queue.put(task); }}
shutDown
当你向线程池 submit 一个任务的时候,线程池就开始工作了,线程池的线程默认的前台线程,所以你最后会发现即使任务完成了,但是程序还是没有关闭。
要想关闭线程池所有的线程的话,我们可以使用 shutDown 方法
public class Demo2 { public static void main(String[] args) { ThreadPoolExecutor threadPoolExcutor = new ThreadPoolExecutor(10, 10, 10, TimeUnit.MICROSECONDS, new ArrayBlockingQueue<>(1000), defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy()); threadPoolExcutor.submit(() -> { System.out.println("hello"); }); threadPoolExcutor.shutdown(); }}