线程池的由来:
回顾服务器并发处理的发展史,其实就是一部不断“降本增效”的进化史。最开始,我们习惯为每个客户端请求单独创建一个进程,但这种“一人一岗”的模式开销实在太大。后来,更轻量的线程取代了进程,大大缓解了资源压力。不过,当系统对性能的要求达到极致时,哪怕是线程的创建与销毁也成了瓶颈。于是,线程池被推上了历史舞台,它通过提前创建并复用线程,彻底解决了频繁创建销毁的开销问题,让高并发处理变得更加从容。
线程池的优点:
·线程复用:在使用线程池时,ThreadPoolExecutor(线程池执行器)把线程提前创建好(默认情况下为懒汉模式,不会立马创建线程,等到我们向线程池提交(submit)第一个任务后才开始创建,当然也可以强制提前创建,这里我们不过多讨论),放到一个“池”中,需要用的时候,随时去取,用完了就还回到池子中
·方便管理:我们在创建线程池时,可以传入七大参数来定制我们需要的那一款线程池
为什么我们认为,直接创建线程开销比从池中取线程开销更大呢?
这里我们就要提到操作系统的用户态和内核态了
一个操作系统 = 内核 + 配套的应用程序
内核包含操作系统的各种核心功能:
(1)管理硬件设备
(2)给软件提供稳定的运行环境
我们认为:
从线程池中取现成的线程,纯应用程序代码就可以完成【可控】
从操作系统创建新线程,就需要操作系统内核配合完成【不可控】
我们通常认为,可控的过程要比不可控的过程更高效,所以说如果有一段代码,需要进入到内核中,由内核负责完成一系列工作,这个过程是不可控的,我们写的代码干预不了,效率也就更低,而使用线程池,就可以省下应用程序切换到内核中运行这样的开销
为什么说操作系统创建线程是不可控的?
假设有这样一个场景(操作系统就是柜员,身份证是我们要创建的线程)
假设我们要去办理业务,但是忘记带身份证了
(1)我们可以选择自己去旁边的自主复印机复印一张出来,全程都是自己操作的,可见的【可控】
(2)我们选择让柜员帮我们复印一张,那么这个柜员就会去后台帮我们复印,但是谁知道他会不会中途去上个厕所,或者接个电话,抽个烟后再帮我们复印呢?整个过程不可见,我们只能在大堂盯着窗口等着这个柜员【不可控】
所以说,操作系统通过内核负责完成的一系列工作(包括创建线程)是不可控的,无法干预的,效率低的
而使用线程池,就可以将不可控变为可控,省下应用程序切换到内核中这样的开销
如何创建线程池?
在实战中,强烈建议不要使用Executors工厂类来创建线程池,而是直接使用ThreadPoolExecutor类进行手动配置8。这是因为Executors内部默认使用的无界队列或无上限的最大线程数,极易在任务激增时引发内存溢出(OOM)或系统崩溃
代码实例:
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize,
maximumPoolSize,
keepAliveTime,
unit,
workQueue,
threadFactory,
handler
);
认识七大参数
这里我们将一个 线程池 比作一家公司
一、corePoolSize(核心线程数)
核心线程数就好比一家公司的正式员工,负责处理公司的各种业务,即使由于公司没有业务导致长时间处于空闲状态,也不会被踢(销毁)
二、maximumPoolSize(最大线程数)
最大线程数又包括核心线程数和非核心线程数,也就是公司允许存在的最大员工数量,我们将非核心线程比作实习生
三、keepAliveTime(允许实习生最大摸鱼时间)
如果公司长期没有业务,处于空闲时期,公司不会马上踢掉这些实习生,如果实习生在公司指定最大摸鱼时间后还是没有活干,公司就会踢掉这些实习生(非核心线程)
四、unit(摸鱼时间单位)
一个枚举类型,里面有指定摸鱼时间的时间的各种单位,比如秒,分钟,天...
五、workQueue(工作队列,公司处理业务的地方)
公司的大厅(处理业务的地方),员工们(线程们)会不断从中拿任务并执行
六、ThreadFactory(工厂模式,公司的人力资源部)
工厂模式也属于一种设计模式,地位关系与我们之前提到的单例模式这一设计模式是并列的,
公司通过人力资源部,统一高效创建不同部门和各种员工(发工牌,取名....),也方便以后排查谁在干活。
工厂模式分为很多种,在业务中我们根据实际情况自己实现并传参即可
这里我们写一个简单工厂模式:
我们假设一个点有两种表示方式,一个是普通的 xy 坐标表达式,一个是通过 半径r 和 角度a 的坐标表达式,但我们在创建Point这个类,我们发现构造方式只有一个,也就是只能通过一种方式来表达这个点,这里就可以引出我们的工厂设计模式(通过不同的静态方法实现不同的表达式创建对象)
class Point{ private double x; private double y; private Point(double x,double y){ this.x = x; this.y = y; } public static Point makePointByXY(double x,double y){ Point point = new Point(x,y); return point; } public static Point makePointByRA(double r,double a){ double x = r*Math.cos(a); double y = r* Math.sin(a); Point point = new Point(x,y); return point; } }这样我们就避免了构造方法单一的创建对象方式,转而使用静态方法实现多种创建对象方法
七、handle(拒绝策略,公司里的保安)
- 对应关系:大厅里的保安,或客满时的应对方案。
- 业务逻辑:当所有的正式员工都在忙,等候区也挤满了人,且实习生也招满了(达到了最大线程数),这时候如果还有新业务进来,保安就必须出面处理了。常见的保安策略有:
- AbortPolicy(直接赶走):拒绝接单,并大喊“客满啦,不接了!”(抛出异常)5。
- CallerRunsPolicy(老板自己干):让提交这个业务的人自己去处理,起到限流的作用(由调用线程自己执行)。
- DiscardPolicy(默默无视):保安直接把新客户请走,不作任何通知(静默丢弃任务)。
- DiscardOldestPolicy(把排队最久的赶走):保安把等候区排队时间最长的人请出去,让新客户补上(丢弃最老的任务)