目录
前言
本篇文章将介绍多线程案例 —— 线程池,这也是一个非常有用的案例~
在之前已经介绍过,进程本身已经能做到并发编程,但是我们仍然创建了线程,是因为进程太重量了,创建和销毁的成本都比较高(需要申请释放资源)~
线程,就是对上述问题的优化(共用同一组系统资源)~
虽然如此,但是在更频繁创建释放的情况下,线程也不一定那么轻量了~(有对比就有伤害)
因此,还需要去做进一步的优化,此时 可以有两种优化方式:
- 线程池
- 协程(又称 纤程,可以理解为 轻量级线程)
由于协程还没有被加入到java标准库中 ,所以本文不讲协程,协程Go中有,本篇博客 主要介绍的是 线程池~
一、线程池概述
线程池和字符串常量池、数据库连接池一样,都是为了提高程序的效率~
其解决问题的思路,就是把线程创建好了以后,放到池子里,当我们需要使用线程的时候,就可以直接从池子里取,而不是通过系统来创建;当线程用完了,就还到池子里,而不是通过系统来销毁线程~
从池里获取、还给池的操作要比创建/销毁更高效~
因此,上述操作 降低了创建/销毁线程的开销,又能够进一步提高效率了~
此时,就出现了一个关键问题:为什么把线程放到池子里,从池子里取线程就要比从系统这里创建线程 更高效呢?
原因是 从池子里去取 是纯用户态操作,通过系统来创建 涉及到内核态操作~
通常认为,牵扯到内核态的操作,就要比纯用户态的操作更低效!!!
内核态:操作系统内核执行的工作,如前面所介绍的 线程、进程、PCB 它们的一些相关的管理和调度,都是由 系统内核 来负责的~
当把任务交给内核态的时候,内核态不仅仅去完成交给它的工作,大概率还会伴随着其他的工作(内核态不会只为一个任务服务,还有其他的某些任务);而将工作交给用户态时,用户态仅仅完成交给它的工作~
所以说,内核态要做的事情多了(你交给它的任务的确可以干完,但是不确定是不是可以立即马上干你交给它的任务);但是,用户态可以立即马上 去执行你交给它的任务~
因此,用户态更为高效~
二、线程池的使用方式
2.1 线程池的初步认识
在java标准库中,就提供了现成的线程池,可以直接使用~~
这样就创建了一个线程池对象,创建了一个 固定线程个数的线程池(此处的固定数量是 10个)~
这里的new是方法名字的一部分,不是 new 关键字。
当然,Executors还可以创建其他方式的线程池~~
这些不同的 "工厂方法" 本质上是通过包装 ThreadPoolExextor 实现出来的(ThreadPoolExextor自身的构造方法太麻烦了,针对 ThreadPoolExextor这个类进行了 new,并且传入不同风格的参数,来达到构造不同种类线程池的目标,用着更简单了):
可以根据不同的需要,来选择不同的线程池~
其中,比较典型的两个线程池分别是:
//固定个数 的线程池 Executors.newFixedThreadPool(10); //线程数量 动态增加 的线程池 Executors.newCachedThreadPool();
2.1.1 工厂方法
Executors 是一个类,newFixedThreadPool() 是这个类的静态方法,使用这个静态方法 来构造对象(相当于是把new操作 给隐藏到这样的方法背后了),像这样的方法,称为 "工厂方法";提供这个工厂的类,就叫做“工厂类”;对应的设计模式,就叫做 "工厂模式"~
工厂模式:一句话解释:使用普通的静态方法来代替构造方法,创建对象~
为啥不用普通的构造方法?而要大费周章去搞个工厂方法?
通常情况下,创建对象 是借助 new,调用构造方法 来实现的~
但是,C++ / Java 里面的构造方法,有坑,在很多时候不方便使用~
因此就需要给构造方法 再包装一层,外面起到包装作用的方法 就是工厂方法~
构造方法如果只构造有一种对象,好办;如果要构造多种不同的对象,就比较难搞
构造方法的限制,在于 当前构造方法的名字 必须是和类名一样~
现在来列举一个关于 "工厂模式" 的例子:
//创建一个表示点的例子 class Point { //通过 横、纵坐标的方式 public Point(double x,double y) { } //通过 极坐标的方式 public Point(double r,double a) { } } //当直接构造对象的时候,就可以通过下面的来表示一个点: Point p = Point.makePointByXY(30,60);
很明显,上面的代码不可以编译运行,无法构成重载,编译错误!!!
重载的要求是方法名相同,参数个数或者类型不同
为了解决上述问题,就可以使用 工厂模式:
public static Point makePointByXY(double x,double y) { Ponit p = new Point(); p.setX(x); p.setY(y); return p; } public static Point makePointByRA(double r,double a) { Point p = new Point(); p.serR(r); p.setA(a); return p; }
普通方法,方法名字没有限制,因此有多种方式构造,就可以直接使用不同的方法名即可~
此时,方法的参数是否要区分,已经不重要了,
可以通过 上面的两种方法,来完成构造一个点的坐标~
2.2 线程池的使用
submit 方法
线程池提供了一个很重要的方法,submit,可以给线程池提交若干个任务。
package thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Demo25 {
public static void main(String[] args) {
//创建线程池对象
ExecutorService threadPool = Executors.newFixedThreadPool(10);
//安排任务
//并且把任务加到线程池里面去,由线程池里面的线程负责 执行其中的任务~
threadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
运行结果:
可以看见,程序一运行,"hello"就被打印出来了~
同时和定时器类似,线程池中的线程全是前台线程,会阻止了程序的退出,所以需要手动退出~
线程池存在的目的,就是让程序员不必创建新的线程,直接使用已有的线程完成想要进行的工作即可~
需要注意的是,虽然上面的线程池只有 10 个线程,但是并不是说 线程只执行 10 个任务~
比如说,有一个餐馆里面需要洗客人吃饭过后 剩下的盘子~
服务员会把用过的盘子收起来,放到一个非常大的盆里,需要洗碗工来洗盘子~
虽然说,每个洗碗工每一次只能洗一个盘子,但是 这并不是说,每一个洗碗工只能洗一个盘子,他们洗完一个会再去洗下一个~
当然,如果有 10个线程,给6个任务,那么也不一定10个线程都在工作(可能情况在 1~10 个之间)~
10个洗碗工, 6个碗~
那么 可能1个洗碗工洗1个,有4个在摸鱼;可能有一个洗碗工洗两个 ......
往线程里放1000个相同的任务,这1000个任务分给10个线程去做,平均下来,差不多是一个线程执行100个,但是注意这里并非是严格的平均,可能有的多一个,有的少一个,都很正常(每个线程都是执行完一个任务后再立即取下一个任务,由于每个任务执行的时间差不多,所以每个线程做的任务数量也就差不多)
线程之间的调度 是充满随机性的,但是 在更大的数据量级下,各个线程之间的工作是比较均衡的(相差的也不会太多)~
2.3 自己动手来模拟实现一个线程池
package thread;
import java.util.concurrent.BlockingDeque;
import java.util.concurrent.LinkedBlockingDeque;
//自己写的线程池类
//简单实现成,固定 10 个线程的线程池
class MyThreadPool {
//核心操作,往线程池里插入任务
//由于插入操作 一下就可以插入很多任务,那么就需要把当前尚未执行的任务都保存起来
//使用阻塞队列来保存
//这个队列就是 "任务队列",把当前线程池要完成的任务都放到这个队列中,
//再由线程池内部的工作线程负责完成它们
private BlockingDeque<Runnable> queue = new LinkedBlockingDeque<> ();
public void submit(Runnable runnable) {
//提交任务
try {
queue.put(runnable);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
public MyThreadPool(int n) {
//n 设定线程池里面有几个线程
//构造方法中,就需要创建一些线程,让这些线程负责完成上述执行任务的工作
for (int i = 0; i < n; i++) {
Thread t = new Thread(() -> {
//当前线程是否已经中断(中断:不执行 未中断:继续执行)
while(!Thread.currentThread().isInterrupted()) {
try {
//把任务给取出来
Runnable runnable = queue.take();
//取出一个任务就执行一个任务
runnable.run();
} catch (InterruptedException e) {
e.printStackTrace();
break;
}
}
});
t.start();
}
}
}
public class Demo26 {
//小小测试一下
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(10);
for (int i = 0; i < 100; i++) {
myThreadPool.submit(new Runnable() {
@Override
public void run() {
System.out.println("hello");
}
});
}
}
}
运行结果:
三、juc
java.util.concurrent ,这个包里放的很多类都是和并发编程(多线程编程)密切相关的。简称juc。
看看 ThreadPoolExextor 的构造方法~~
7个参数:
- corePoolSize 核心线程数
- maximumPoolSize 最大线程数
- keepAliveTime
- TimeUnit unti
描述了临时工可以摸鱼的最大时间,时间单位(s,ms,min....)
- BlockingQueue<Runnable> workQueue
线程池的任务队列,此处使用的是阻塞队列,想象一下,线程池中的线程都是在不停尝试take任务的,如果有任务就take成功,没有就阻塞。
- ThreadFactory threadFactory
用来创建线程,线程池里的线程要被创建出来
- RejectedExecutionHandler handler
描述了线程池的"拒绝策略",也是一个特殊的对象,描述了线程池任务队列满了的情况,如果继续添加任务会有啥行为
拒绝策略: