目录
一.什么是线程池
二.线程池的实现原理
为什么要有工厂模式?
三.线程池的构造方法解读
线程池的拒绝策略
四.自己实现一个线程池
简单来说,线程池就好比一块鱼塘,鱼塘中的每条鱼就是一个线程。那么为什么要有这个线程池呢?就好比 一个“渣女\渣男”,当他和A在一起的时候,如果想和B在一起,那么就需要先想办法和A分手,再和B搞好关系,最终和B在一起。如果她和A谈的时候,已经找好了B C D,此时就可以直接拿来无缝衔接~~
其实线程池也就大概这个作用,里面存放一些线程,需要用的时候直接拿来使用。
我们先来看线程池是如何创建的:
package Pool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class threadPoolDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newSingleThreadExecutor();
}
}
很明显,此处的线程池竟然不是 new 出来的,那么它是如何被实例化的呢?其实这里就使用了一种设计模式:工厂模式。
其实工厂模式就时给Java中的构造方法填坑的,我们的构造方法其实时有很大缺陷的,我们来看以下例子。
我们期望这个Point类,初始化的时候能够传入 double x,double y,用来构造笛卡尔坐标系
我们又希望在不创建其他类的情况下,这个Point类, 初始化的时候能够传入 double r,double a,用来构造极坐标系。
但是构造方法也是方法,此时的方法由于参数个数和参数的类型都相同,就会编译失败,那么就没办法满足期望!
此时我们可以再写一个类:
此时这个类中又两个静态方法,一个是构造笛卡尔坐标系,并且返回。一个是构造及坐标系,并且返回对象。
那么我们就可以使用以下语句来分别调用:
Point p1 = PointFactory.makePintByXY(10,20)
Point p2 = PointFactory.makePintByRA(12,63)
此时通过PointFactory类,来给Point类传入需要的值就可以了。
那么线程池也是通过这样的方式来进行创建的:
从Excutor这个工厂类的源码中可以得到以下:
其实线程的创建又被封装到了一个叫做ThreadPoolExecutor的类中
点开ThreadPoolExecutor,可以得到如下图片:
其中的每个参数的意思是这样的:
第一个是核心线程数, 第二个是最大线程数
线程池中的线程数目是可以动态变化的
范围就是【int corPoolSize. ~ int maxmumPoolSize】
什么是核心线程:
就好比一个公司中有正式员工(核心线程)和实习生,总的员工数目不能超过一定的值。当人手不够用就招实习生,这样既可以满足效率的需求,又可以避免过多的开销。
第三个是线程的可存活时间
第四个TimeUnit unit是用来设置非核心线程闲置超时时长(keepAliveTime)的单位。当一个非核心线程的闲置时间超过这个参数所设定的时长时,该线程就会被销毁掉。
第五个比较重要
第六个是线程池的拒绝策略,也就是当所有线程都处于忙碌状态,如果还往线程池中添加元素,线程池所做的操作。
这些构造方法,第一个和第五个以及第六个是需要重点掌握的。
下面来单独讲讲第六个参数:拒绝策略
所谓的拒绝策略,其实就是如果线程池中每个线程都是处于忙碌的状态,如何应对新来的线程任务。
举个例子:如果我周一到周五都是满课,此时我一朋友让我给他去代课,那么此时我如何应对?此时就会有相应的应对策略:
实现线程池一个最关键的步骤就是拒绝策略,那么说明拒绝策略呢?
由于之前学过阻塞队列的知识,这里就先用阻塞队列来实现以下。
那么这个就是一种自己定义的新的拒绝策略,那就是一直等待~
package Pool; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; class MyPoolDemo { //一个队列 BlockingQueue
queue = new ArrayBlockingQueue<>(1000); //通过这个方法,把任务添加到队列当中 public void submit(Runnable task) throws InterruptedException { queue.put(task); //往阻塞队列中放入元素 } public MyPoolDemo(int n) throws InterruptedException { //构造方法 for (int i = 0; i < n; i++) { Thread t = new Thread(() -> { //让这个线程从队列中消费任务并且进行执行 try { //如果队列中没有元素,那就阻塞等待 //一旦队列中有了任务,那么就立即执行take方法获取到任务并且开始执行 Runnable task = task = queue.take(); task.run(); } catch (InterruptedException e) { throw new RuntimeException(e); } }); t.start(); } } } public class RunDemo { public static void main(String[] args) throws InterruptedException { /** * 步骤理解: * 1.创建线程池并且指定线程数目是 20 ,在实例化线程池的时候已经创建好了20个线程 * 2.这20个线程都在等待 take 获取到任务队列中的任务 * 3.for 语句 循环 1000次,每次循环都会提交任务到任务队列 * 4.一但任务队列里面有元素,这20个线程就会立马获取到,并且执行 */ MyPoolDemo myPoolDemo = new MyPoolDemo(20); int taskCount = 1000; while (true) { for (int i = 0; i < taskCount; i++) { int id = i; myPoolDemo.submit(new Runnable() { @Override public void run() { System.out.println("执行任务:" + id); } }); } } } } 运行结果:
代码解读:
1.线程池里的线程是需要执行任务的,这个任务可以放到 BlockingQueue 这个阻塞队列中。为什么要使用阻塞队列呢?当线程池中的线程都在工作,此时就直接等待阻塞。
2.submit 方法接受一个实例化好的Runnable类型的任务,负责往队列中添加元素
3. MyPoolDemo(int n) 是这个类的构造放法,当实例化这个类的时候,被指定的 n 就是要创建的线程数量。
4. 由运行结果可以得出:当线程数量为20的时候,可以看到任务被随即执行完了。
总结:Java线程池是Java并发编程中一个重要的概念,它用于管理和控制线程的创建、销毁,以及任务提交和执行。线程池的主要目的是减少创建和销毁线程的开销,提高性能。