日升时奋斗,日落时自省
目录
1、线程池解释
线程池可以从字面意思理解就是一个能装多个线程的池子,是的其实也就是这么理解。
认识他从问题开始 ,为什么要用它,我们已经可以自己实现多线程了,还要它创建是不是多此一举,实则不是的,凡是存在必有道理
了解线程的友友们都知道,线程的出现就是因为进程实现并发编程的时候,太重了(创建和销毁资源开销太大)
这个时候我们还是用了线程来代替进程操作,线程也叫做“轻量级进程” ,创建和销毁线程比创建和销毁进程更高效,此时使用多线程实现并发编程也就更高效了。
但是我们希望趋向于更好的并发编程,较少资源开销,线程上还需要更精进的。
第一种就是 :“轻量级线程” 已经有了,叫做“协程” 也可以叫做“纤程” 但是还没有更进到java标准库中,所以我们还用不了,其他部分语言是有的(java以后会不会有不知道,越更新肯定是越好的)
第二种就是 : 使用线程池 来降低线程创建和销毁的开销的
注:之所以说线程是 轻量级进程 是因为与进程相对比,减小了开销,但是不代表线程真的开销就很小。
需要将事先创建好的线程,放到“池”中,后来需要来再从里面取,用完了再还给池,这样创建和销毁线程就更家高效了。
这里简单的叙述可能一点点的模糊,先理论上简述一下,创建线程和销毁线程都是由操作系统内核完成的,如果从池子里获取和归还给池子,是算自己用户代码就能实现的,不必交给操作系统,因为操作系统慢啊,开销还大,池子更快。(图解一下)
2、线程池使用
2.1、代码解析
在java标准库中,也给咱们提供了现成的线程池,可以直接使用(这里先那一种创建方法进行解释)
ExecutorService pool= Executors.newFixedThreadPool(10);
这里使用ExecutorService来定义接收,线程池数目固定10个,注意这个操作是使用了某个静态方法(newFixedThreadPool),直接构造出一个对象来(这个方法的背后会有new操作,就不需要我们去写了)
这样的方法称为“工厂方法” ,提供这个工厂方法的类,也就是“工厂类”,这个行代码就使用了“工厂模式”(一种设计模式)
是不是感觉这种不用new 的还要搞一个方法来创建对象的有点多此一举,不是哈,是因为构造方法有缺陷,还记得重载吧,构造方法能重载,但是参数类型和个数是不能完全相同的。
这里也是因为创建线程池使用了种方法调用,所以在这里提一下工厂模式。
2.2、创建方式
创建方式有多种,这里写四种创建方法,其实也都是大体相同。为了创建出来而已
线程池创建后会有任务调度,那就需要一个方法来传这个任务,线程池提供了一个方法submit(任务)可以题给线程池提交若干任务
第一种 刚刚见过的
创建方法(newFixedThreadPool)
创建一个固定线程 线程数量可控根据不同任务情况 设置线程数量
public static void main1(String[] args) {
//创建线程池
ExecutorService pool= Executors.newFixedThreadPool(10);
for (int i = 0; i < 100; i++) {
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("第一种线程池创建方式");
}
});
}
}
第二种
创建方法 (newCachedThreadPool)
适用于短时间大量任务情况,线程数量动态变化,任务多了就多几个线程,如果任务少了,就少些线程
public static void main2(String[] args) {
ExecutorService pool=Executors.newCachedThreadPool();
for (int i = 0; i < 50; i++) {
int n=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("方法二");
}
});
}
}
第三种
创建方法(newSingleThreadExecutor)
(1)复用线程 不需要频繁创建销毁
(2)提供了任务队列 和 拒绝策略
public static void main3(String[] args) {
ExecutorService pool=Executors.newSingleThreadExecutor();
for (int i = 0; i < 20; i++) {
int n=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("方法三");
}
});
}
}
第四种
创建方法(newScheduledThreadPool)
类似于定时器,也是让任务延时执行,只不过执行的时候不是由扫描线程自己执行了,而是由单独的线程池来执行
public static void main(String[] args) {
ExecutorService pool=Executors.newScheduledThreadPool(10);
for (int i = 0; i < 50; i++) {
int n=i;
pool.submit(new Runnable() {
@Override
public void run() {
System.out.println("方法六"+n);
}
});
}
}
以上所有的创建方法都是通过包装ThreadPoolExecutor来实现出来的
其实这里有一个小问题可以提一下?(就是创建线程池的代码)
为什么在这里要int n=i 一下再打印出来,直接打印i可以吗?
答案:不可以, 因为i是主线程的局部变量(在主线程的栈上)随着主线程的执行结束就会销毁,很可能主线程这里for循环执行完了,当前run的任务在线程池还没有排到呢此时就已经销毁了,那剩下的任务谁做
这里的是n是一个变量捕获 run方法属于Runnable这个方法的执行时机,不是立刻马上执行而是再未来的某个节点执行(线程池里的线程就像是在排队等待,对应到谁了谁去执行任务)
为了避免线程之间生命周期不同,作用域差异,导致后序执行run的时候i已经销毁,于是这里采用了变量捕获,这里相当于run方法把当前主线程的i拷贝了一份,这时的n就是在执行run的一个局部变量
针对捕获变量:在java中,JDK1.8之前,要求变量捕获,只能捕获final修饰的变量,其实就是为了捕获不会改变的变量,在JDK1.8开始,有所更正,要求不一定非得带final关键字,只要满足条件,代码没有修改这个变量也可以进行捕获如当前场景
2.3、ThreadPoolExecutor解析
ThreadPoolExecutor里面有啥呢,有的挺多的,可以在javaAPI8官方文档里面找java.util.concurrent包里面找,可以找到,下面是对该类的参数进行一个解析
说到线程池的线程数量,其实都是根据情况而定,没有一定的答案,只有尝试才知道那种是最好的,在测试中选择。
不同的程序特点不同,设置线程的数量也是不同的
分为两种极端类型:
(1)CPU密集型: 每个线程要执行的任务都是需要CPU进行一系列的算术运算,此时线程池线程数,最多也不应该超过CPU核数,因为没有CPU就这么多核,设置更大也得空着,线程数多了也没有用不是嘛(这里说的核数是逻辑核心个数 )
注:现在电脑的CPU一般是8核16线程8个物理核心,16个逻辑核心,每个逻辑核心都可以执行一个线程,一个物理核心可以有多个逻辑核心
(2)IO密集型 每个线程干的工作就等待IO(读写硬盘,网卡,等待用户输入等),不吃CPU,此时这样的线程就处于阻塞状态,不参与CPU调度,是不是感觉线程一下不归CPU核心数管了,这不多多益善嘛,说是这么说的,计算机资源有限,所以不太可能,但仍然可以创建多个线程超过核心数
当然了以上提及都是理想化,因为大项目不会就CPU密集型,或者就IO密集型;真实情况,一部分需要CPU,一部分是IO密集型 ,这就取决于是那种类型的更多,CPU密集型多,线程数就少一点,反之线程数就多一点(两种类型相辅相成,要想知道设置多少个线程数为做优,需要针对程序进行测试,测试结果说最优才是最优,没有具体的衡量标准)
2.4、拒绝策略
拒绝策略在java标准库中专门提供了,四种方法下面是一些例子作为解释,拒绝策略像是我们生活中不同拒绝方法。
3、自主实现简单线程池
实现简单的线程池不会有太大的代码量
思路:
(1)需要一个队列(阻塞队列)(这篇博客中的生产者消费者模型解释了阻塞队列)前面解释过线程池的任务像是排队一样,没有接到任务就会先阻塞这
(2)该类的构造方法中创建n线程并执行任务
(3)写一个submit方法进行提交任务(阻塞队列入队列操作)
以上三点就可以写一个简单的线程池,当然就是简单实现,所以没有拒绝策略等多中方法
自定义一个类 我自定义叫做 MyThreadPool 后面的所有代码都是包括在这个类里面的
首先创建一个阻塞队列
//此处不涉及到时间 此处只有任务 就直接 使用Runable ,在定时器中是因为还有个是时间,所以需要进行自定义一个类来表示
private BlockingQueue<Runnable> queue=new LinkedBlockingQueue<>();
这里可以创建一个对象 原因: 防止后面出现测试的时候,出现对象不一致的问题(后面在解释)
private Object locker=new Object(); //直接每次都可以获得这个this 那就是一定的相同
然后写啥,该出创建的都已经创建了,该写这个我们线程池的构造方法
(1)构造方法的参数是线程数
(2)任务如何来如何执行,需要从阻塞队列中出队列
//n 表示线程的数量
public MyThreadPool(int n){
//构造方法 中 创建线程
for (int i = 0; i < n; i++) {
Thread t=new Thread(()->{
while(true){
try {
//任务出队列 + 接收任务
Runnable runnable=queue.take();
//这里做个打印可以看是不是同一个对象
System.out.println(locker);
//任务执行
runnable.run();
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
});
t.start();
}
}
就剩下最后一步,写一个submit方法 用来提交任务,上面为什么能接收到任务(阻塞队列中为什么有任务),就是因为在该方法中进行任务提交,放入阻塞队列中。
public void submit(Runnable runnable){
//其实这里可以理解为获取先线程任务 放到线程池中
try {
//阻塞队列存放任务
queue.put(runnable);
System.out.println(locker);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
}
把以上四个代码放在一个我们自定义的类(MyThreadPool)中,就是一个简单的线程池
现在所有代码都走完了,开始解释为什么要创建一个对象,还要打印出来看是不是相同的对象,这也是因为我看到这样的问题了,所以放到这里写一下。
有友友一定认为什么情况这个对象都是相同的,因为在一个类里面不都一样吗,如果这里没有创建这个对象的话,直接打印this对象,这里也是一样的,是的,但是有友友会在这里习惯性的使用匿名内部类结果就不一样了,匿名内部类搭配this在这里就会导致对象不一致