上篇文章我为大家归纳总结什么是阻塞队列,如何使用阻塞队列并且我们自己手动模拟实现了阻塞队列~~~这边文章,我将继续归纳总结在多线程中,关于线程池的概念,线程池如何使用,然后我们也继续自己手动模拟实现一个简单版本的线程池~~~
1.什么是线程池
2.刨析线程池构造方法中的对象
3.线程池的使用
4.手动模拟实现线程池
什么是线程池
我们可以想象现在建一个池子,建好池子后,我们创建数个线程,放在池子里,当我们任务队列中有任务的时候,就不需要创建线程,可以直接让线程池中创建好的线程直接去执行任务,就节省了一部分时间上的开销,提高效率,避免短时间内大量的进行线程的创建和销毁,
采用线程池的方案,提前把线程通过操作系统的API创建好,放在一个集合类中,后续如果需要使用,就直接从集合类中获取
总结来说:把线程提前的创建好,放在一个固定的地方,需要用到的时候就可以直接获取
这样的操作比操作系统创建来的就更快~~~避免创建线程和销毁线程的频繁开销
Java官方标准库中也提供了现成的线程池,我们需要对官方提供的线程池中的构造对象进行了解~


刨析线程池中构造方法的对象
1.)核心线程数

当线程池被创建的时候,就已经创建好了的线程,不会被销毁---- 核心线程数
后续发现线程池中的线程数不够了,但又由于任务太多,就会额外创建线程,可能会被销毁 ---- 非核心线程数
所以在线程池中涉及到俩类线程 ,并且会自动扩容(通过非核心线程数)
当任务多的时候,线程就多创建点;当任务少的时候,线程就少创建点~~~~
2.)最大线程数

最大线程数 = 核心线程数(线程池创建的时候就创建的) + 非核心线程数(后来创建的)
3.)非核心线程允许空闲的最大时间

当任务减少时,非核心线程就可能处于空闲的状态,这时,操作系统并不会立即将这些空闲的非核心线程销毁掉,而是观察一定的时间,如果这段时间内这些非核心线程确实还是处于空闲状态,那么就会将这些非核心线程销毁掉~~~
KeepAliveTime --- 允许空间最大时间
unit ---- 时间单位(s,ns,ms,day,hour ....)
4.)任务队列

当有任务时,任务就分配到任务队列中,让线程池中的线程去取,当任务队列为空时,那么线程池中的线程就阻塞等地~~~
我们可以指定任务队列的容量capaci和数据结构(数组,链表,堆)
5.)线程工厂

线程工厂是为了解决构造方法的弊端
比如当我们如何确定一个点时,可以有俩种方法 1.)通过直角坐标系,由x轴和y轴确定一个点
2.)可以通过半径和角度确定一个点

我们写一个伪代码:
java
public class Point {
// 通过x轴和y轴来确定一个点
public Point(double x, double y) {
//...
}
// 通过半径和角度来确定一个点
public Point(double r, doube a) {
//...
}
}
这个代码是会编译报错的,因为这俩个构造方法无法构成重载,而构造方法的名字又需要固定~~~
即使变量名不同,但是有多种方法,但是无法通过构造方法去构建线程对象。
所以我们引进线程工厂,就是为了解决上述问题,参数是相同的,但是构造方法名称无法改变,无法通过多种方法去创建线程对象~~~
下面是线程工厂的伪代码:
java
clas Point {
public Point() {
}
// 通过set方法来设置不同参数的方法创建Thread对象
public void set() {
}
}
//这个类就是Point的工厂类
class PointFactor {
//这样就可以通过不同名称的方法,但是参数类型个数又相同的方法来设置Thread对象~~~~
public static Point BuildByXY(double x, double y) {
Point p = new Point();
//通过set方法将x和y设置进去
p.set();
reutrn p;
}
public static Point BuildByRA(double r, double a) {
Point p = new Point();
//通过set方法将r和a设置进去
p.set();
return p;
}
}
这种工厂模式,就可以很好的对线程池中大量的线程进行统一的设置管理,设置统一属性~~~
比如设置统一的名称
比如设置统一的优先级
比如设置统一的后台线程
......
6.)拒绝策略(四种)


1.)抛出异常
当任务队列满了的时候,如果继续往任务队列中添加任务,就抛出异常~~~
如果异常没有catch住,那程序就会崩溃
2.)让调用submit的线程自己执行
当某个线程调用submit方法,往任务队列中添加任务,但是由于任务队列满了,新的任务无法添加到任务队列,此时可以让调用submit的线程自己去执行这个新任务~~
3.)抛弃任务队列中最老的任务
当任务队满的时候,可以把任务队列中早加入的,但还未被执行的任务抛弃掉,给新的任务腾出空间,将这个位置留给新的任务~~~
4.)抛弃任务队列中最新的任务
当任务队列满的时候,将最慢被加入任务队列的,但还未被执行的任务抛弃掉,给新的任务腾出空间,将这个位置留给新的任务~~~
总而言之,在实际的开发过程中,我们认为新来的任务是比较重要的~~所以我们会想方设法让新来的任务加入到任务队列中
线程池的使用
因为标准库的线程池使用起来还是比较麻烦的,所以Java将线程池进一步进行封装!!
Java对线程池封装之后,也提供很多个版本,我们可以了解了解:
1.)有核心线程和非核心线程的线程池

2.)只有给定核心线程数,没有非核心线程的线程池

3.)只包含一个核心线程的线程池

4.)定时器线程池,当未来某个时间点执行

5.)只包含一个核心线程的定时器线程池

java
public class Demo {
public static void main(String[] args) {
//创建只有5个核心线程的线程池,没有非核心线程
ExecutorService executorService = Executors.newFixedThreadPool(5);
for(int i = 1; i <= 1000; i++) {
int id = i;
//调用submit方法让线程池中的某个线程去执行任务
executorService.submit(new Runnable() {
public void run() {
//执行任务
String name = Thread.currentThread.getName();
System.out.println(name + "拿到任务:" + id);
}
})
}
}
}

因为线程池内部的线程默认是前台线程,所以main函数结束了,线程池内的线程不会被销毁,直到整个线程池被销毁了~~~~
手动模拟实现线程池
java
class MyThreadPool {
//固定线程数的线程池
//使用阻塞队列来入队列和出队列---- 当为空的时候就会阻塞
private BlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();
public MyThreadPool(int n) {
//创建n个线程
for(int i = 1; i <= n; i++) {
Thread t = new Thread(() -> {
try {
//让线程不断地在while死循环中从任务队列获取任务,获取不到就阻塞等待任务到来
while(true) {
Runnable task = queue.take();
task.run();//执行任务
}
}catch(InterruptedException e) {
throw new RuntimeException(e);
}
});
t.setDaemon(true);//设置为后台线程
t.start();
}
}
//提供submit方法
public void submit(Runnable task) throws InterruptedException{
queue.put(task);//往队列中添加任务
}
}
public class Demo {
public static void main(String[] args) throws InterruptedException{
MyThreadPool myThreadpool = new MyThreadPool(5);//给定5个核心线程
for(int i = 1; i <= 1000; i++) {
int id = i;
myThreadPool.submit(() -> {
String name = Thread.currentThread.getName();
System.out.println(name + "拿到任务:" + id);
});
}
Thread.sleep(1000);//因为线程池中的线程被我们设置为后台线程,所以要让main线程等待
}
}
