共计 4876 个字符,预计需要花费 13 分钟才能阅读完成。
前言
前两天趁着假期在整理粉丝私信的时候看到一个粉丝朋友的私信跟我说自己现在正在复习准备面试,自己在复习到线程池这一块的时候有点卡壳,总感觉自己差了点什么。想要我帮他指导一下。这不趁着假期我也有时间我把自己这么多年的理解和从网上找的资料放在一块整理了一下都放在下面了!
1. 什么是线程池
线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位,我们的程序最终都是由线程进行运作。在 Java 中,创建和销毁线程的动作是很消耗资源的,因此就出现了所谓“池化资源”技术。线程池是池化资源技术的一个应用,所谓线程池,顾名思义就是预先按某个规定创建若干个可执行线程放入一个容器中(线程池),需要使用的时候从线程池中去取,用完之后不销毁而是放回去,从而减少了线程创建和销毁的次数,达到节约资源的目的。
2. 为什么要使用线程池
2.1 降低资源消耗
前面已经讲到线程池的出现减少了线程创建和销毁的次数,每个线程都可以被重复利用,可执行多个任务。
2.2 提高系统的响应速度
每当有任务到来时,直接复用线程池中的线程,而不需要等待新线程的创建,这个动作可以带来响应速度的提升
2.3 防止过多的线程搞坏系统
可以根据系统的承受能力,调整线程池中的工作线程的数量,防止因为线程过多服务器变慢或死机。java 一个线程默认占用空间为 1M,可以想象一旦手动创建线程过多极有可能导致内存溢出。
3. 线程池主要参数
我们可以用 Executors 类来创建一些常用的线程池,但是像阿里是禁止直接通过 Executors 类直接创建线程池的,具体的原因稍后再谈。
在了解 Executors 类所提供的几个线程池前,我们首先来了解一下 ThreadPoolExecutor 的主要参数,ThreadPoolExecutor 是创建线程池的类,我们选取参数最多的构造方法来看一下:
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
3.1 corePoolSize
当向线程池提交一个任务时,如果线程池中已创建的线程数量小于 corePoolSIze,即便存在空闲线程,也会创建一个新线程来执行任务,直到创建的线程数大于或等于 corePoolSIze。
3.2 maximumPoolSize
线程池所允许的最大线程个数,当队列满了且已经创建的线程数小于 maximumPoolSize 时,会创建新的线程执行任务。
3.3 keepAliveTime
当线程中的线程数大于 corePoolSIze 时,如果线程空闲时间大于 keepAliveTime,该线程就会被销毁。
3.4 unit
keepAliveTime 的时间单位
3.5 workQueue
用于保存等待执行任务的队列
3.6 threadFactory
用于创建新线程。threadFactory 创建的线程也是采用 new Thread() 方式,threadFactory 创建的线程名都具有统一的风格:pool-m-thread-n
3.7 handler
拒绝策略,当线程池和队列满了之后,再加入新线程后会执行此策略。
下面是四种线程池的拒绝策略:
AbortPolicy:中断任务并抛出异常
DiscardPolicy:中段任务但是不抛出异常
DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务
CallerRunsPolicy:由调用线程处理该任务
4. 线程池执行流程
当我们了解了 ThreadPoolExecutor 的七个参数后,我们就可以很快的理解线程池的流程:
当提交任务后,首先判断当前线程数是否超过核心线程数,如果没超过则创建新线程执行任务,否则判断工作队列是否已满,如果未满则将任务添加到队列中,否则判断线程数是否超过最大线程数,如果未超过则创建线程执行任务,否则执行拒绝策略。
5.Executors 提供的线程池
executors 提供了许多种线程池供用户使用,虽然很多公司禁止使用 executors 创建线程池,但是对于刚开始解除线程池的人来说,Executors 类所提供的线程池能很好的带你进入多线程的世界。
5.1 newSingleThreadExecutor
ExecutorService executorService = Executors.newSingleThreadExecutor();
听名字就可以知道这是一个单线程的线程池,在这个线程池中只有一个线程在工作,相当于单线程执行所有任务,此线程可以保证所有任务的执行顺序按照提交顺序执行,看构造方法也可以看出,corePoolSize 和 maximumPoolSize 都是 1。
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue()));
}
5.2 newFixedThreadPool
ExecutorService executorService = Executors.newFixedThreadPool(2);
固定长度的线程池,线程池的长度在创建时通过变量传入。下面是 newFixedThreadPool 的构造方法,corePoolSize 和 maximumPoolSize 都是传入的参数值
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue());
}
5.3 newCachedThreadPool
ExecutorService executorService = Executors.newCachedThreadPool();
可缓存线程池,这个线程池设定 keepAliveTime 为 60 秒,并且对最大线程数量几乎不做控制。
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue());
}
观察构造方法,corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制。设定 keepAliveTime 为 60 秒,线程空闲 60 秒后自动结束,因为该线程池创建无限制,不会有队列等待,所以使用 SynchronousQueue 同步队列。
5.4 newScheduledThreadPool
创建一个定时的线程池。此线程池支持定时以及周期性执行任务的需求。下面是 newScheduledThreadPool 的用法:
Thread thread1=new Thread(new Runnable() {
@Override
public void run() {System.out.println(Thread.currentThread().getName()+"thread1");
}
});
Thread thread2=new Thread(new Runnable() {
@Override
public void run() {System.out.println(Thread.currentThread().getName()+"thread2");
}
});
Thread thread3=new Thread(new Runnable() {
@Override
public void run() {System.out.println(Thread.currentThread().getName()+"thread3");
}
});
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
// 在 1000ms 后执行 thread1
scheduledExecutorService.schedule(thread1,1000,TimeUnit.MILLISECONDS);
// 在 1000ms 后每隔 1000ms 执行一次 thread2, 如果任务执行时间比间隔时间长,则延迟执行
scheduledExecutorService.scheduleAtFixedRate(thread2,1000,1000,TimeUnit.MILLISECONDS);
// 和第二种方式类似,但下一次任务开始的时间为:上一次任务结束时间(而不是开始时间)+ delay 时间
scheduledExecutorService.scheduleWithFixedDelay(thread3,1000,1000,TimeUnit.MILLISECONDS);
6. 为什么阿里巴巴禁止程序员用 Exectors 创建线程池
如果你的 idea 装了 Alibaba Java Codeing Guidelines 插件(推荐大家使用,有助于让你的代码更加规范),那么当你写了 Exectors 创建线程池后会看到提示:
并且阿里将这个用法定义为 Blocker,即不允许使用,而是让人们用 ThreadPoolExecutor 的方式创建线程池。原因是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的人更加明确线程池的运行规则,规避资源耗尽的风险。
Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致 OOM。
2)CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致 OOM。
下面是 ThreadPoolExecutor 创建线程池的简单例子
int corePoolSize=5;
int maximumPoolSize=10;
long keepAliveTime=30;
BlockingQueue blockingQueue=new ArrayBlockingQueue(2);
RejectedExecutionHandler handler=new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, blockingQueue, handler);
threadPoolExecutor.execute(thread1);
7. 总结
那么今天这篇关于线程池的文章就到这里了,大家看完有什么不懂的欢迎在下方留言讨论,有什么不懂的也可以私信问我。另外在这里说一下私信回复的问题,因为我平时跟大家一样也要上班,私信一般是晚上会抽空看一下,一般看到大家问的问题我都会帮忙回复解决,有时间忙的时候也可能几天没时间看,这点希望大家可以见谅。
最后
欢迎关注公众号:【前程有光】,领取一套 227 页的 2020 最新的 Java 面试宝典