Java EE:2.多线程-初阶(第八弹):多线程案例-线程池
目录
③long keepAliveTime→非核心线程允许空闲的最大时间(允许实习生摸鱼的最大时间~)
⑥ThreadFcatory threadFactory→线程工厂
⑦RejectedExecutionHandler handler→拒绝策略
1.线程池,最核心的就是 submit 这样的操作,往线程池中,添加任务(任务就是 runnable)
2.光把任务放到队列还不够,还得有线程来执行队列中的任务!!
书接上文:Java EE:2.多线程-初阶(第七弹)~~
阶段性小结
多线程
单例模式
1.单例模式的写法=>饿汉/懒汉
2.线程安全=>为啥要这么做
阻塞队列(特殊的队列)
1.线程安全(天然使用在多线程环境中)
2.阻塞特性
a)队列满了,入队列
b)队列空了,出队列
基于以上两个特性,可以实现一个生产者消费者模型
生产者、消费者 针对核心资源来划分
交易场所=>阻塞队列
优势:
1.解耦合
2.削峰填谷
劣势
1.服务器的集群结构更复杂
2.性能
把阻塞队列单独包装成服务器程序,并且使用单独的机器(集群)来部署,这样的队列成为“消息队列”(MQ)
8.3线程池
先回忆一下之前学过的常量池
常量池:字符串常量,在Java程序最初构造的时候,就已经准备好了,等程序运行的时候,这样的常量也就加载到内存中了,省下了构造/销毁 的开销
计算机中,池 这个词,就这一个意思,表示的含义都是一样的
之前的例子中,咱们举的是一个叫小美的妹子,同时谈了三个男朋友(茶花之后)
这次我们谈谈小美茶花之前的事儿,小妹也要谈男朋友~
但是谈恋爱这件事情,有个新鲜感很关键~~导致很多“青梅竹马”打不过“一见钟情”
一旦新鲜感过去了,感情就容易大降温了
过了一段时间,小美对于现在这个男票,没啥兴趣了
此时小美有个大胆的想法,想换个男朋友
1.想办法分手
想方设法的,zuo一zuo,时不时打打拳~~
反复打拳,让对方逐渐失去耐心,等到时机成熟,一锤定音~~
2.和下一个小哥哥培养感情~~
这个两个过程都很消耗时间,效率都比较低,那么小美就想办法节省时间
于是她就茶化了~~
在小美和上一个男票进行打拳的时候(当然也可以更早),就开始和下一个小哥哥培养感情了(搞暧昧)
感情培养到位,此时,只要前一个环节,分手完毕,此时下一个小哥哥就能立即上位(此时下一个小哥哥就称为“备胎”)
由于小美对于换小哥哥的需求比较高~~一个备胎不够用,就需要同时聊多个备胎,此时这多个备胎,就构成了“备胎池”~~
跟字符串常量池、线程池、进程池、内存池、数据库连接池……一样,就是一池子备胎,现在虽然还没用呢,但随时就能拿出来使用~~
线程池是什么
线程池,就是为了让我们高效的创建销毁线程的
最初引入线程的原因:频繁创建销毁进程,太慢了
随着互联网的发展,我们对性能的要求会更进一步~~
咱们现在觉得,频繁创建销毁线程,开销有些不能接受了~~
解决方案有两个:
1.线程池
2.协程(纤程,轻量级线程)
闲聊:
协程这个概念是Java 17 左右的时候,才开始引入到标准库中,目前在Java圈子里还没有普遍使用
因此这个东西先按下不表了~~
再说为啥Go能够威胁到Java的地位??
就是因为Go更擅长处理“并发编程”
1.代码更简单(CSP编程模型)
2.使用协程,相比于线程性能更高
线程池最大的好处就是减少每次启动、摧毁线程的损耗
把线程提前创建好,放到一个(类似数组的)地方,需要的时候,随时去取,用完了还回到池子中
为啥我们认为,直接创建线程开销比从池子里取线程更大呢??
这就涉及到了操作系统的 用户态 和 内核态
什么是内核呢?
一个操作系统=内核+配套的应用程序
这个内核就包含操作系统的各种核心功能:
1)管理硬件设备
2)给软件提供稳定的运行环境
一个操作系统,内核只有一份,而这一份内核,要给所有的应用程序提供服务支持~~
比如当你去银行办证件:
这时候,工作人员小姐姐让你搞一份复印件,有两个方式:
1.小姐姐直接给你复印
这个过程是不可控的~~
因为小姐姐去给你复印的过程中就离开你的视线了,可能就直接摸鱼去了,不知道啥时候才回来~~
2.去自助复印机自己印
这个过程是可控的~~
因为你自己立马就能去执行
如果有一段代码是在应用程序中自行完成的,这个执行过程就是可控的
如果有一段代码,需要进入到内核中,由内核负责完成一系列工作,这个过程就是不可控的,咱们程序员写的代码干预不了
因此,通常认为,可控的过程要比不可控的过程更高效~~
[可控]从线程池取现成的线程,纯应用程序代码就可以完成
[不可控]从操作系统创建新线程,就需要操作系统内核配合完成
使用线程池,就可以省下应用程序切换到内核中运行这样的开销
线程池的工作原理
标准库中的线程池
ThreadPoolExecutor=ThreadPool(线程池)+Executor(执行器)
这里的执行器:线程池里准备好一些线程,让这些线程执行一些任务
核心方法:submit(Runnable)
通过 Runnable 描述一段要执行的任务
通过 submit 把任务放到线程池中
此时线程池里的线程就会执行这样的任务
注意事项就体现在,构造这个类的时候,构造方法,比较麻烦(参数比较多)
[经典面试题]这些参数都是啥意思??
Java的线程池,里面包含几个线程,是可以动态调整的
任务多的时候,自动扩容成更多的线程(干更多的活)
任务少的时候,把额外的线程干掉,节省资源
①int corePoolSize→核心线程数
核心线程数:至少有多少个线程,线程池一创建,这些线程也要随之创建,直到整个线程池销毁,这些线程才会销毁
②int maximumPoolSize→最大线程数
最大线程数=核心线程+非核心线程
其中非核心线程(自适应):不繁忙就销毁,繁忙就再创建(因为线程也不是越多越好,之前讲过滑稽哥抢鸡腿~)
上述的核心线程可以理解为 CPU的 基频/默频,最慢的速度
非核心线程可以理解为 CPU的 睿频/加速频,可以动态调整(但也有上限,取决于CPU的型号……)
如果把线程池=>公司,那么核心线程=>正式员工,非核心线程=>实习生
需要的时候就招聘实习生,不需要的时候再裁掉
(正式员工不敢乱裁,如果赔偿不到位,劳动仲裁够喝一壶~~)
闲聊:实习生说裁就裁了,那我还要费劲找实习吗??
找实习还是有很多好处的:
1.转正
2.技能上的飞跃(技术,非技术的技能)
3.顺便挣点钱~~虽然实习工资一般不高,也比奖学金多很多
其实相比于其他行业,像程序员这种技术性强的岗位,没那么容易裁实习生的~
你进了公司实习,不是一下就能上手干活,需要有一个漫长的培养的阶段,公司培养你已经付出很多的成本了
一般在实习期间,不惹祸(不删库),大概率都能留用的(特别好的公司,留用竞争才会激烈),在中小公司,只要实习时间足够长,转正还是非常稳的~~
回答问题Q1:删库跑路~
真的会进去~~之前字节的投毒事件,北大的同学在字节实习,因为对领导不满(据说好像是3年都没有转正成功),最终往训练模型的集群里投放病毒~~
好像没进去,只是辞退了,但这个是看你造成损失多少来看的
回答问题Q2:35岁危机~45岁危机~49岁危机~
这个主要是程序员议价太高了,35岁时的薪资可能是天价了~
如果你能力不足以支撑起这样的价格,自然会有危机
因此,要么提升能力,撑住价格,要么降低一点薪资的要求~~(就算降了,大概率还是比其他行业赚的多的~~)
③long keepAliveTime→非核心线程允许空闲的最大时间(允许实习生摸鱼的最大时间~)
如果实习生连续一个月都没有啥活了,就可以考虑优化掉
闲聊:
之前有同学表示,实习期间非常轻松,天天摸鱼~~
但这其实是一个危险的信号~~公司不养闲人~~
回答问题:赶紧找下家!!
咱们要想办法改变现状~而不是“逃避”
尤其00后,出去实习,受不了半点委屈,稍微遇到点困难,就想着离职~~
这其实要想一想是不是你自己的问题??
①63班有个同学:(正面例子)
实习在字节,经常吐槽,这不好那不好~~惦记着要溜~~
溜了,转正机会就没了~~
最后他还是溜了~~又拿了个腾讯的实习~~
在腾讯干了一段时间之后,发现还不如之前的呢~~
他就赶紧联系一下之前的领导(领导对他印象还挺好),秋招直接给他留了个hc,秋招的时候没面试,直接给他了个字节的offer(相当于转正了)
②java 107的尹同学:(反面例子)
大三上学期,在网易实习~就各种不好~~
然后下学期去了B站实习~~
3月去的B站,实习到8月左右~~到了该转正的时候,转正失败了~
因为他们组有另一个大佬,能力强,实习时间比他久,所以人家能留下,他这就没机会了~~
他秋招目前还在挣扎中~~
其实如果当初在网易能够实习到9月,差不多1年的时间,转正还是非常稳的~~有点可惜
回答问题:这种学校那边没事吗?
看你自己能抗住多少压力~~(这个事情,本身也是一个筛选的过程),能扛得住,30w+的offer就在招手~~
所以实习的转正概率是和实习时间呈正相关的,实习时间越久,做的东西就越多,在领导眼中你的产出就越多~~
④TimeUnit unit→③的时间单位
这是一个枚举类型,包含了我们常见的单位:秒、微秒、毫秒、小时……
⑤BlockingQueue<Runnable> workQueue→工作队列
线程池,本质上也是 生产者消费者模型
调用 submit 就是在生产任务
线程池里的线程就是在消费任务
而workQueue就是起到一个阻塞队列的作用去传递任务
workQueue允许我们自己去指定队列,因此我们可以很灵活的使用:
1)选择使用数组版本/链表版本
2)指定capacity
3)指定是否要带有优先级/比较规则
给我更多的自由度,让我们能够更好地完成任务
⑥ThreadFcatory threadFactory→线程工厂
工厂设计模式(与单例模式一样,面试中常考的)
也是一种设计模式的体现:工厂模式(也是一种设计模式,和单例模式是并列关系)
用来弥补构造方法的缺陷的
为什么说构造方法有缺陷呢??
我们会发现这个代码就库库报错👇
为什么会库库报错呢?→重载
此时我们希望这两种写法能够构成重载的,但是重载要求:参数个数/参数类型 不同
但此处我们的 参数个数/参数类型 都是相同的,这就属于C++和Java共有的一个问题:
构造方法的名字是固定的
要想提供不同的版本,就需要通过重载
但有时候不一定能构成重载
而 工厂模式 就是解决上述的一个方式~~
package thread; //工厂设计模式 class Point{ } //工厂类 class PointFactory{ //工厂方法 public static Point makePointByXY(double x,double y){ Point p=new Point(); //通过 x 和 y 给 p 进行属性设置 return p; } public static Point makePointByRY(double r,double a){ Point p=new Point(); //通过 r 和 a 给 p 进行属性设置 return p; } } public class Demo33 { public static void main(String[] args) { Point p=PointFactory.makePointByXY(10,20); } }工厂方法的核心:通过静态方法,把构造对象new的过程,各种属性初始化的过程,给封装起来了
我们可以通过多组静态方法,实现不同情况的构造
提供工厂方法的类,就可以称为“工厂类”,整体的这么代码结构就称为“工厂设计模式”
而ThreadFcatory这个类就是标准库给线程类提供的工厂类
其实使用工厂类最核心的目的是:简化初始化的操作⬇️
线程中有一些属性可以设置,一个线程当然很好设置,但线程池是一组线程,ThreadFcatory就可以对其进行统一的构造并初始化线程
同时,ThreadFcatory也是一个接口,我们可以自己去进行实现,只要我们去实现一个newThread的方法就可以了,在这个方法里面,去构造这个Thread对象,把newThread重写一下,重写逻辑中,可以针对线程对象进行一些相关设置就可以了⬇️
其实我们自己去实现也不复杂,因为标准库中提供了 defaultThreadFactory⬇️,就属于人家帮我们设置好线程相关的属性了(设置成非后台线程、优先级……),相当于给我们提供了一个种更加简单的做法,可以让我们根据需要去调整线程怎样进行构造
⑦RejectedExecutionHandler handler→拒绝策略
这个参数是整个线程池7个参数中,最重要、最复杂的
面试官考察线程池的参数含义,最想听的就是你对于这个参数的理解
通过之前的知识,我们知道submit把任务添加到任务队列中,任务队列是阻塞队列
队列满了,再添加,就会阻塞~~但我们不希望程序阻塞太多
对于线程池来说,发现入队列操作时,队列满了,不会真的触发“入队列操作”,不会真阻塞,而是执行拒绝策略相关的代码
具体都有哪些拒绝策略呢??在文档上有所体现⬇️
这四个类就对应着四种不同的拒绝策略:
1️⃣AbortPolicy:线程池直接抛出异常(线程池就可能无法继续工作)
2️⃣CallerRunsPolicy:让调用submit的线程自行执行任务(你把活给我干,我不干,你自己爱干不干)
3️⃣Discard0ldestPolicy:丢弃队列中最老的任务(来了个新的,那旧的任务就不执行了,直接扔了)
4️⃣DiscardPolicy:丢弃最新的任务(当前submit的任务,就不会再执行了)
答疑:为什么不用生产者消费者模型呢?
这不就是生产者消费者模型嘛,队列满了,如果调用submit就阻塞(业务逻辑中的线程调用submit),就会使这个线程没法干别的事情了,不是一个好的选择
比如这个要响应用户请求的线程阻塞了,用户迟迟拿不到请求的响应,用户就要等很久,直观上看到的现象就是“卡了”,与其是“卡了”,还不如直接告诉我“失败”
下面举个例子来理解一下上述四个类的概念⬇️
比如我是个老师,我的任务就是上课,而且我的任务已经排满了(任务队列满了)
有一天,领导找我,让我去B站搞一场直播~~(给我submit了新的任务)
此时就有4种拒绝策略了:
1️⃣AbortPolicy
我哇的一声就哭出来了~~心态崩了~~
非但新的任务干不了了,甚至之前旧的任务也干不了了~~
2️⃣CallerRunsPolicy
领导给我进行的submit
于是我说:我这实在没时间,领导你还是自己去B站直播吧~~
闲聊:
submit方法里,暗藏玄机~~
①判定当前队列是否满
②判定当前拒绝策略是否是CallerRunsPolicy
如果都是,紧接着submit内部就会调用Runnable.run()
submit是调用者线程来调用的
也就是说submit整个方法里的代码,都是在这个调用者线程中执行的⬇️
//一个进程中可能有好几个线程 //假设:线程池里有4个线程:abcd //又有一个新线程x,调用submit方法 //此时x调用submit,就是执行下述逻辑 void submit(Runnable runnable){ if(队列满了&&拒绝策略是CallerRunsPolicy){ runnable.run();//自己执行 return ; } queue.put(runnable);//线程池里的一组线程来执行任务 return ; }3️⃣Discard0ldestPolicy
领导说,要不就把周内的课鸽了一节~~
去B站直播~~
4️⃣DiscardPolicy
领导说:咱B站的直播先不搞了,回头有空再说
答疑:这四个属性在一个进程中吗?
这些线程都是在一个进程中的
因为进程的内存空间是各自独立的
所以进程1的线程a无法直接和进程2的线程b进行数据交互的
进程1创建的阻塞队列,在进程2中也无法访问
如果非得进程之间通信,也有办法(其他办法)
后面重点讲两种方式:
1)文件
2)网络
C++会讲很多,比如说管道、共享内存、信号量、信号……(Java一般不用这些东西)
有同学说,这线程池用起来真费劲儿,有没有更简单的使用线程池的方法呢??
设计标准库的大佬也知道这玩意用起来麻烦,所以Java标准库,也提供了另一组类,针对 ThreadPoolExecutor 进行了进一步的封装(封装成 Executors ),简化线程池的使用(也是基于工厂设计模式)
Executors 创建线程池的几种方式 newFixedThreadPool 创建固定线程数的线程池 newCachedThreadPoll 创建线程数目动态增长的线程池 newSingleThreadExecutor 创建只包含单个线程的线程池 newScheduledThreadPool 设定延迟时间后执行命令,或者定期执行命令。是进阶版的Timer package thread; import java.util.concurrent.Executors; public class Demo34 { public static void main(String[] args) { //Executors:这个就是线程池的工厂类 //通过这类里提供的静态方法,我们就能创建一些具体的线程池 Executors.newFixedThreadPool(4);//创建一个固定数量的线程池 //固定数量:核心线程数和最大线程数一样~~上述固定线程数就是4 Executors.newCachedThreadPool();//不需要填写参数 //最大线程数是一个很大的数字(线程可以无限增加) } }上述两个是最关键的,当然,还有一些其他的👇
线程池的基本的使用,就简单了很多👇
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo34 { public static void main(String[] args) { ExecutorService threadPool=Executors.newFixedThreadPool(4); threadPool.submit(()->{ System.out.println("hello"); }); } }
一个任务太少,也可以添加更多的任务👇
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo34 { public static void main(String[] args) { ExecutorService threadPool=Executors.newFixedThreadPool(4); for(int i=0;i<1000;i++){ int id=i;//设成局部变量就能解决打印时 i 捕获不到的情况 threadPool.submit(()->{ System.out.println("hello"+id+","+Thread.currentThread().getName()+"正在办理业务"); }); } } }
我们把固定量线程池改成无限量线程池来看看效果👇
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo34 { public static void main(String[] args) { ExecutorService threadPool=Executors.newCachedThreadPool(); for(int i=0;i<1000;i++){ int id=i;//设成局部变量就能解决打印时 i 捕获不到的情况 threadPool.submit(()->{ System.out.println("hello"+id+","+Thread.currentThread().getName()+"正在办理业务"); }); } } }我们通过观察线程名字就会发现,此时的线程就不拘泥于4个了,而是有很多,甚至上百个来完成这打印1000次的任务~~
还有一个需要注意的点:业界流传了一份“武林秘籍”:
阿里巴巴Java编程规范手册
这份规范中,明确说:使用线程池,要用 ThreadPoolExecutor 这个版本,而不应该使用 Executors
理由是:使用 Executors 的话,线程数目/拒绝策略 等信息都是隐式的,可能不好控制
(比如创建200个线程,确实不是一件好事~)
闲聊:当然,这也跟阿里巴巴本身有关:项目规模复杂、稳定性要求高、相关的开发人员也多
未来我们在我们的公司里,可能有不同的规范~
阶段性小结
上面内容围绕着多线程代码案例,重点讨论了线程池这样的案例
所谓线程池,就是我们日常开发中非常关键的组件了,毕竟进行一个真正项目的开发中,多线程是广泛使用的,而使用多线程的过程中又会注意到,多线程的创建和销毁也是存在开销的,为了更好的优化程序的效率,于是我们又引入了线程池
相比于Thread.start()来创建,线程池就相当于把线程创建好了,我们随时用随时从池子里去取,不用的时候再还给池子里去,这个过程都是纯用户态、纯应用程序层面的操作,不涉及到操作系统内核起到作用,因此整个过程可认为是比较可控、比较高效的
线程池
ThreadPoolExecutor
①int corePoolSize 和 int maximumPoolSize:核心线程数 和 最大线程数
②long keepAliveTime 和 TimeUnit unit:非核心线程允许空闲的最大时间 及其 单位
③BlockingQueue<Runnable> workQueue:任务队列
④ThreadFactory threadFactory:线程工厂(本质是个接口,我们可以实现 newThread 方法来实现创建线程的相关操作)→Executors defaultThreadFactory():如果你不需要自定义的东西,那么就可以直接使用标准库这个默认的类
⑤RejectedExecutionHandler handler:拒绝策略
1)AbortPolicy:直接抛出异常
2)CallerRunsPolicy:调用submit的线程负责执行新任务
3)Discard0ldestPolicy:丢弃队列中最老的任务
4)DiscardPolicy:丢弃当前任务
线程池用起来确实复杂一些,因此标准库给我们提供了线程池的工厂类 Executors,创建出 固定线程数目 的线程池(newFixedThreadPool) / 自动扩容线程数 的线程池(newCachedThreadPool)
另外,需要结合这些,理解另外一个知识点:
工厂设计模式:解决构造方法自身的一个缺陷,当我们想提供多个版本的构造对象的时候,如果仅仅是基于构造方法的话,那么我们必须给它设计成构成重载的模式,但是并不是所有的都能设计成重载的模式,所以我们使用工厂方法就能很好的解决上述问题
另外,如果构造方法本身传的参数比较多,比较复杂,咱们也同样可以通过工厂模式进行设定
实现线程池
package thread; //实现一个固定线程个数的线程池 class MyThreadPool { public MyThreadPool(int n){ //初始化线程池,创建固定个数的线程 } } public class Demo35 { public static void main(String[] args) { } }1.线程池,最核心的就是 submit 这样的操作,往线程池中,添加任务(任务就是 runnable)
package thread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; //实现一个固定线程个数的线程池 class MyThreadPool { //任务队列 private BlockingQueue<Runnable> queue=null; public MyThreadPool(int n){ //初始化线程池,创建固定个数的线程 //这里使用ArrayBlockingQueue作为任务队列,容量为1000 queue=new ArrayBlockingQueue<>(1000); } public void submit(Runnable task) throws InterruptedException { //将任务放入任务队列 queue.put(task); } } public class Demo35 { public static void main(String[] args) { } }2.光把任务放到队列还不够,还得有线程来执行队列中的任务!!
在构造方法中,把线程创建出来
由于随时有新的任务要被添加进去
因此咱们的线程就需要持续不断的尝试读取任务~~
取到了就执行
没取到就阻塞等待
package thread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; //实现一个固定线程个数的线程池 class MyThreadPool { //任务队列 private BlockingQueue<Runnable> queue=null; public MyThreadPool(int n){ //初始化线程池,创建固定个数的线程 //这里使用ArrayBlockingQueue作为任务队列,容量为1000 queue=new ArrayBlockingQueue<>(1000); //创建N个线程 for(int i=0;i<n;i++){ Thread t=new Thread(()->{ try{ while(true){ Runnable task=queue.take(); task.run();//执行任务 } }catch (InterruptedException e){ //两种处理方式均可,咱们就简单打印一下得了 e.printStackTrace(); //throw new RuntimeException(e); } }); t.start();//启动线程 } } public void submit(Runnable task) throws InterruptedException { //将任务放入任务队列 queue.put(task); } } public class Demo35 { public static void main(String[] args) { } }3.接下来我们编写main方法来感受一下
package thread; import java.util.concurrent.ArrayBlockingQueue; import java.util.concurrent.BlockingQueue; //实现一个固定线程个数的线程池 class MyThreadPool { //任务队列 private BlockingQueue<Runnable> queue=null; public MyThreadPool(int n){ //初始化线程池,创建固定个数的线程 //这里使用ArrayBlockingQueue作为任务队列,容量为1000 queue=new ArrayBlockingQueue<>(1000); //创建N个线程(核心代码) for(int i=0;i<n;i++){ Thread t=new Thread(()->{ try{ while(true){ Runnable task=queue.take(); task.run();//执行任务 } }catch (InterruptedException e){ //两种处理方式均可,咱们就简单打印一下得了 e.printStackTrace(); //throw new RuntimeException(e); } }); t.start();//启动线程 } } public void submit(Runnable task) throws InterruptedException { //将任务放入任务队列 queue.put(task); } } public class Demo35 { public static void main(String[] args) throws InterruptedException { MyThreadPool pool=new MyThreadPool(10); //向线程池提交任务 for(int i=0;i<100;i++){ int id=i;//直接打印i编译不了,所以这里用局部变量id pool.submit(()->{ System.out.println(Thread.currentThread().getName()+"id ="+ id); }); } } }可以看到,这10个线程,它们有不同的名字,这10个线程就开始从队列种去取任务,哪个线程取到哪个任务,这个过程是完全不确定的,但是总体来说,咱们就能把这100个任务执行完毕了
我们发现代码写起来并不复杂,原因就是阻塞队列,已经完成了很多工作了,因为阻塞队列天然就是线程安全的
接下来不知道大家有没有发现,虽然100个任务已经结束了,但是进程仍然没有结束~~
这是为什么??
因为线程池里的这些线程由于队列为空 ,还在 take 这里阻塞着(等待新线程加入)
由此可见,线程池中的线程,是前台线程,能够组织进程结束!!(Java中自己创建的线程默认都是前台线程)
怎么设成后台呢??
很简单,只需要在 t.start(); 前加上一个 t.setDaemon(true); 即可
但是我们真的需要设置为后台线程吗??
其实在标准库的设定里面,它也是一个前台线程👇
package thread; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; public class Demo34 { public static void main(String[] args) { ExecutorService threadPool=Executors.newCachedThreadPool(); for(int i=0;i<1000;i++){ int id=i;//设成局部变量就能解决打印时 i 捕获不到的情况 threadPool.submit(()->{ System.out.println("hello"+id+","+Thread.currentThread().getName()+"正在办理业务"); }); } } }
所以我们也写成前台线程也算是理所应当,都还好~~
想设成后台线程,那就设不想设成后台线程,也是可以的
另一方面呢,标准库中还提供了 .shutdown() 方法,能够把线程池里的线程全部关闭,但是不能保证线程池内的任务一定能够执行完
而 shutdown 里面所做的事情其实就是我们之前学过的,线程终止的操作,循环调用每个线程的 Intterupted ,使线程终止
所以,如果需要等待线程池内的任务全部执行完毕再终止,需要调用 awaitTermination 方法
答疑:那是不是不推荐用shutdown??
得看需求,你这里的任务,到底使非常重要的任务,还是不关键的任务~~
提个问题:为啥退出码不一样??
其实C语言的第一节课就讲过:
int main(){ return 0; }main函数的返回值,就是进程的退出码~~
操作系统中,约定了,退出码为0,表示正常结束
非0表示异常结束(不同的数字表示不同的原因)
Java一般不写多进程程序,因此不太关注退出码~~
闲聊:
线程池有啥用途??
咱之前说过的:节省线程创建销毁的开销
最主要的还是解决服务器这边开发的问题
上古时期,服务器如何处理多个客户端的请求?
基于多进程的模型
每次有个客户端请求过来了,服务器这边都创建一个进程给这个客户端提供服务
(读取请求,解析请求,返回响应……)
后来,频繁创建销毁进程,效率比较低
引入线程
模型就成了,每个客户端都分配一个线程,提供服务
随着客户端数量越来越多,发现频繁创建销毁线程,这个事情也变得比较低效了
又引入了线程池
线程池里,提前准备好10个线程
有100个客户端把请求发过来~~
把这100个客户端的请求,封装成 任务(Runnable)添加到线程池里
线程池有着10个线程负责处理这100个任务
这个过程就不涉及 线程的创建、销毁了
后续网络编程再写这里的代码~~
像客户端这样的程序,除非是那种专门的生产力工具,很少涉及到大规模的计算,也不太需要用到线程池
更多推荐



















所有评论(0)