实现多线程的方式
- 继承Thread,实现run方法;
- 实现Runnable接口,实现run方法;
- 实现callable接口,实现call方法;
- 线程池
线程池是干啥的?
线程池的主要工作是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了规定的最大数,则超出的线程排队等候,等其他线程执行完毕,再从队列中去除任务来执行。
线程池有什么特点?
- 线程复用
- 控制最大并发数
- 管理线程
为什么要用线程池?有什么好处?
- 降低资源消耗:通过复用已创建的线程来降低线程创建和销毁造成的消耗
- 提高响应速度:当任务到达时,不需要等待线程创建即可立即执行
- 提高线程的可管理性:线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配、调优和监控
都有哪些线程池?
- ScheduledThreadPool:执行定时任务和具有固定周期的重复任务
- WorkStealingPool:适合使用在很耗时的操作,但 WorkStealingPool 不是 ThreadPoolExecutor 的扩展,它是新的线程池类 ForkJoinPool 的扩展
- FixedThreadPool:执行长期任务,性能较好
- CachedThreadPool:适用于执行很多短期异步的小程序或负载量小的任务
- SingleThreadExecutor:适用于多个任务串行执行的场景
线程池简单用法
public class MyThreadPoolDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService threadPool = Executors.newFixedThreadPool(5);
ExecutorService threadPool2 = Executors.newSingleThreadExecutor();
ExecutorService threadPool3 = Executors.newCachedThreadPool();
for (int i = 0; i < 10; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 办理业务");
}
});
TimeUnit.MILLISECONDS.sleep(300);
}
}
}
-------------------------
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
pool-1-thread-5 办理业务
pool-1-thread-1 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
pool-1-thread-5 办理业务
线程池的七大参数
public static ExecutorService newFixedThreadPool(int nThreads) {
return new ThreadPoolExecutor(nThreads, nThreads,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>());
}
public static ExecutorService newSingleThreadExecutor() {
return new FinalizableDelegatedExecutorService
(new ThreadPoolExecutor(1, 1,
0L, TimeUnit.MILLISECONDS,
new LinkedBlockingQueue<Runnable>()));
}
public static ExecutorService newCachedThreadPool() {
return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
60L, TimeUnit.SECONDS,
new SynchronousQueue<Runnable>());
}
以上三种JDK自带的线程池,其具体实现类是:ThreadPoolExecutor,它有 5 个参数
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
Executors.defaultThreadFactory(), defaultHandler);
}
#this
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
if (corePoolSize < 0 ||
maximumPoolSize <= 0 ||
maximumPoolSize < corePoolSize ||
keepAliveTime < 0)
throw new IllegalArgumentException();
if (workQueue == null || threadFactory == null || handler == null)
throw new NullPointerException();
this.corePoolSize = corePoolSize;
this.maximumPoolSize = maximumPoolSize;
this.workQueue = workQueue;
this.keepAliveTime = unit.toNanos(keepAliveTime);
this.threadFactory = threadFactory;
this.handler = handler;
}
查看 ThreadPoolExecutor 内部的构造方法,可看到有 7 个参数。
重点来了!!!那这 7 大参数的具体意义是什么?这也是各大厂面试中的高频率面试题!
- corePoolSize:线程池中的常驻线核心程数
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,必大于 1
- keepAliveTime:多余空闲线程的存活时间。当池中线程数大于 corePoolSize,且空闲线程的存活时间达到keepAliveTime时,多余的空闲线程将被销毁,直到只剩下corePoolSize个
- unit:keepAliveTime 的单位
- workQueue:任务队列,被提交但尚未被执行的任务
- threadFactory:线程工厂,用于生产线程池中的工作线程,一般用默认的即可
- handler:拒绝策略,表示当前队列满了并且工作线程大于等于线程池的最大线程数 maximumPoolSize
线程池工作原理
1、新建线程池,等待执行任务
2、调用execute() 方法添加一个请求任务,线程池会判断:
2.1、如果当前线程数小于corePoolSize,则新建线程执行任务
2.2、如果当前线程数大于等于corePoolSize,则将任务放入阻塞队列
2.3、如果阻塞队列已满,且当前线程数小于 maximunPoolSize,则创建非核心线程执行任务
2.4、如果阻塞队列已满且当前线程数等于 maximumPoolSize ,则线程池会执行饱和拒绝策略
3、当一个线程完成任务时,它会从队列中取下一个任务来执行
4、当一个线程无事可做且又达到 keepAliveTime 时,线程池会判断:如果当前线程数大于corePoolSize,则会销毁该线程,也 就是说,当线程池执行完所有任务后,最后始终在运行的线程数就是corePoolSize数。
JDK内置的拒绝策略
- AbortPolicy(默认):直接抛出 RejectedExecutionException 异常阻止系统正常运行。
- CallerRunsPolicy:“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOldestPolicy:抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy:直接丢弃任务,不予任何处理也不抛异常。如果允许任务丢失,这是最好的一种方案。
面试题:这几种线程池:Fixed、Single、Cached,你在工作中用的哪个最多?(大坑)
✦正确答案:JDK提供的这几种线程池一个都没用过,我们工作中只允许使用自定义的。
为什么???
因为:《阿里巴巴Java开发手册》并发模块中有 强制 要求:
1、【强制】线程资源必须通过线程池提供,不允许在应用中自行显式创建线程。(也就是说不能 new Thread().start();)
说明:使用线程池的好处是减少在创建和销毁线程上所消耗的时间和系统资源的开销,解决资源不足的问题。如果不使 用线程池,有可能造成系统创建大量同类线程而导致消耗完内存或者“过度切换”的问题。
2、【强制】线程池不允许使用Executors创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确 线程池的运行规则,规避资源耗尽的风险。
说明:Executors 返回的线程池对象的弊端如下:
1)FixedThreadPool 和 SingleTheadExecutor:(其内部阻塞队列为LinkedBlockingQueue)
允许的请求队列的长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。
2)CachedThreadPool 和 ScheduledThreadPool:
允许创建的线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。
如何手写一个线程池
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(3, //核心线程数为 3
5, //最大线程数为 5
1L, //空闲线程存活时长为 1 秒
TimeUnit.SECONDS, //单位秒
new LinkedBlockingDeque(3), //阻塞队列,设置长度为 3
Executors.defaultThreadFactory(), //使用默认的线程工厂
new ThreadPoolExecutor.AbortPolicy()); //设置饱和拒绝策略
try {
for (int i = 1; i <=9; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 办理业务");
}
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
-----------------
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-1 办理业务
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
java.util.concurrent.RejectedExecutionException: Task
以上代码,设置的最大线程数为 5,阻塞队列长度为 3,即允许存在的最大任务数为 8,这里设置了 9 个,再加上饱和拒绝策略使用的默认的,所以最后执行到第 9 个任务时直接抛出异常。
再试试其他几种饱和拒绝策略的效果
public static void main(String[] args) {
ExecutorService threadPool = new ThreadPoolExecutor(3, //核心线程数为 3
5, //最大线程数为 5
1L, //空闲线程存活时长为 1 秒
TimeUnit.SECONDS, //单位秒
new LinkedBlockingDeque(3), //阻塞队列,设置长度为 3
Executors.defaultThreadFactory(), //使用默认的线程工厂
new ThreadPoolExecutor.CallerRunsPolicy()); //设置饱和拒绝策略
try {
for (int i = 1; i <=9; i++) {
threadPool.execute(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " 办理业务");
}
});
}
// TimeUnit.MILLISECONDS.sleep(300);
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
--------------------
pool-1-thread-1 办理业务
pool-1-thread-4 办理业务
pool-1-thread-1 办理业务
main 办理业务
pool-1-thread-3 办理业务
pool-1-thread-2 办理业务
pool-1-thread-1 办理业务
pool-1-thread-4 办理业务
pool-1-thread-5 办理业务
这次使用的是 CallerRunsPolicy :“调用者运行”一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
根据打印结果来看,是main线程提交的任务,最后任务又退回给main线程执行。
另外两种策略(DiscardOldestPolicy 和 DiscardPolicy)的打印结果为:
pool-1-thread-3 办理业务
pool-1-thread-2 办理业务
pool-1-thread-5 办理业务
pool-1-thread-1 办理业务
pool-1-thread-5 办理业务
pool-1-thread-2 办理业务
pool-1-thread-3 办理业务
pool-1-thread-4 办理业务
只处理 8 个任务,其他的丢掉。DiscardOldestPolicy 是丢掉等的最久的那个;DiscardPolicy 是丢掉新来的。
如何合理配置线程池数量
大致有两种类型可设置:
CPU 密集型
- 该任务需要大量的计算而没有阻塞,CPU一直全速运行
- CPU密集任务只有在真正的多核CPU上才可能通过多线程得到加速,而在单核CPU上,无论开几个模拟的多线程,该任务都不可能得到加速,受限于CPU总的运算能力
- CPU密集型任务配置尽可能少的线程数量,一般公式:CPU核数 + 1 个线程
IO 密集型
- 该任务需要大量的IO操作,即大量的阻塞。
- 在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力,浪费在等待上。
- 所以IO密集型任务中使用多线程可以大大的加速程序的运行,即便是在单核的CPU上。这种加速主要是利用了被浪费掉的阻塞时间。
-
IO密集型时大部分线程都阻塞,故需要多配置线程数。
参考公式:CPU核数 / (1 - 阻塞系数) 阻塞系数在 0.8 ~ 0.9之间
比如 8 核 CPU: 8 / (1 - 0.9 ) = 80,即,可设置最大线程数为 80。
这两种类型的选择方法:
- 首先要熟悉自己的硬件,通过 System.out.println(Runtime.getRuntime().availableProcessors()); 来查看阿里云服务器或者自己公司的服务器是几核的。(当然目前的服务器已经没有单核的了,知道就好)
- 然后根据自己的业务来判断是 CPU密集型 还是 IO密集型。涉及到大量算法而又不会有阻塞就选CPU密集型;涉及到大量IO操作或者会造成阻塞的就选IO密集型