目录

阶段性小结

8.3线程池

线程池是什么

线程池的工作原理

标准库中的线程池

[经典面试题]这些参数都是啥意思??

①int corePoolSize→核心线程数

②int maximumPoolSize→最大线程数

③long keepAliveTime→非核心线程允许空闲的最大时间(允许实习生摸鱼的最大时间~)

④TimeUnit unit→③的时间单位

⑤BlockingQueue workQueue→工作队列

⑥ThreadFcatory threadFactory→线程工厂

工厂设计模式(与单例模式一样,面试中常考的)

⑦RejectedExecutionHandler handler→拒绝策略

阶段性小结

实现线程池

1.线程池,最核心的就是 submit 这样的操作,往线程池中,添加任务(任务就是 runnable)

2.光把任务放到队列还不够,还得有线程来执行队列中的任务!!

3.接下来我们编写main方法来感受一下

答疑:那是不是不推荐用shutdown??

提个问题:为啥退出码不一样??


书接上文: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个任务

这个过程就不涉及 线程的创建、销毁了

后续网络编程再写这里的代码~~

像客户端这样的程序,除非是那种专门的生产力工具,很少涉及到大规模的计算,也不太需要用到线程池

更多推荐