线程池做的工作主要是控制运行的线程的数量,处理过程中将任务放入队列,然后在线程创建后启动这些任务,如果线程数量超过了最大数量,超出数量的线程排队等候,等其它线程执行完毕,再从队列中取出任务来执行。
它的主要特点为:线程复用,控制最大并发数,管理线程。
- 降低资源消耗。通过重复利用已创建的线程,降低线程创建和销毁造成的消耗。
- 提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。
- 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控
Java中的线程池通过Executor框架实现的,该框架中用到了Executor, Executors, ExecutorService, ThreadPoolExecutor这几个类。
执行长期的任务,性能好很多
package com.jak.demo.TreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors() + "个处理器");
//一池5个处理线程,比如一个银行网点,5个处理窗口
ExecutorService threadPool = Executors.newFixedThreadPool(5);
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程, 线程池可以复用
try {
for (int i=0; i {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
一个任务一个任务执行的场景
package com.jak.demo.TreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors() + "个处理器");
//一池1个处理线程,比如一个银行网点,只有一个窗口
ExecutorService threadPool = Executors.newSingleThreadExecutor();
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程, 线程池可以复用
try {
for (int i=0; i {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
适用:执行很多短期异步的小程序或者负载较轻的服务器
package com.jak.demo.TreadPool;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
System.out.println(Runtime.getRuntime().availableProcessors() + "个处理器");
//一池N个处理线程,看情况。一个银行网点,来了10个人办理业务,如果10个蜂拥而至,这时候开启5个办理窗口,如果一个一个来,那么一个窗口慢慢处理
ExecutorService threadPool = Executors.newCachedThreadPool();
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程, 线程池可以复用
try {
for (int i=0; i {
System.out.println(Thread.currentThread().getName() + " 办理业务");
});
//暂停一会儿线程
try {
TimeUnit.MICROSECONDS.sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
- corePoolSize:线程池的常驻核心线程数
- maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
- keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
- unit:keepAliveTime的单位。
- workQueue:任务队列,被提交但尚未被执行的任务。
- threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程。一般用默认的即可。
- handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)时如何来拒绝
corePoolSize:线程池的常驻核心线程数
在创建了线程池后,当有请求任务来之后,就会安排池中的线程去执行请求任务,近似理解为今日当值线程。
通俗来的讲,线程池类似于一个银行网点,可以办理各种各样的业务。corePoolSize就类似于银行网点今日当值的处理窗口。
maximumPoolSize:线程池能够容纳同时执行的最大线程数,此值必须大于等于1
maximumPoolSize就是物理上可能达到的受理窗口
workQueue:任务队列,被提交但尚未被执行的任务
当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中。
workQueue阻塞队列,就类似于银行网点的等候区。1,2,3,4,5来银行网点办理业务,1,2分别在今日当值窗口,3,4,5来了只能在网点等候区等待叫号。
假如,在原来的基础上又来了三个人,6,7,8。那现在,1,2占着2个今日当值窗口,3,4,5占满了等候区,此时,业务量就比较大了。这时,行长会通知后面的三个窗口,今天业务量比较大,你们三个过来临时加一下班,此时就达到了,线程池里面的最大线程数。此时,3,4,5,进入处理窗口,6,7,8进入等候区等待。
threadFactory:表示生成线程池中工作线程的线程工厂,用于创建线程。一般用默认的即可。
ThreadFactory就类似于银行网点的Logo,制服,一种标配
handler:拒绝策略,表示当队列满了并且工作线程大于等于线程池的最大线程数(maxmumPoolSize)时如何来拒绝
现在,银行网点的今日当值窗口,等候区和加班窗口都已经满了,此时,大堂经理,就会拒绝再进入人来办理业务,这就是拒绝策略。
keepAliveTime:多余的空闲线程的存活时间。当前线程池数量超过corePoolSize时,当空闲时间达到keepAliveTime值时,多余空闲线程会被销毁直到只剩下corePoolSize个线程为止。
unit:keepAliveTime的单位。
默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize.
随着时间的推移,业务量逐渐变小了,3,4,5,6,7,8都办理完走了,而后面三个窗口本身就是加班窗口,假设在一定时间内,业务量始终上不来,等了1个小时了,都没有新的请求过来,就会商量,那么我们仨现在就先撤了,只剩下2个今日当值窗口。
线程池的等待队列(加班窗口)也已经,再也塞不下新任务了,同时,线程池中的max线程也达到了,无法继续为新任务服务。这时候我们就需要拒绝策略机制合理的处理这个问题。
6.2 JDK内置的拒绝策略- AbortPolicy(默认): 直接抛出RejectedExecutionException异常阻止系统正常运行。
- CallerRunsPolicy: "调用者运行"一种调节机制,该策略既不会抛弃任务,也不会抛出异常,而是将某些任务回退到调用者,从而降低新任务的流量。
- DiscardOlderstPolicy: 抛弃队列中等待最久的任务,然后把当前任务加入队列中尝试再次提交当前任务。
- DiscardPolicy: 直接丢弃任务,不予任何处理也不抛出异常。如果允许任务丢失,这是最好的一种方案。
以上内置拒绝策略均实现了RejectedExecutionHandler接口
七、常见面试题 7.1 你在工作中单一的/固定数的/可变的三种创建线程池的方法,你用的哪个多?答案是一个都不用,我们生产上只能使用自定义的
7.2 Executors中JDK已经给你提供了,为什么不用?package com.jak.java.test.ThreadPool;
import java.util.concurrent.Executors;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
public class MyThreadPoolDemo {
public static void main(String[] args) {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2,
5,
1L,
TimeUnit.SECONDS,
new LinkedBlockingDeque(3),
Executors.defaultThreadFactory(),
new ThreadPoolExecutor.AbortPolicy());
try {
//模拟10个用户来办理业务,每个用户就是一个来自外部的请求线程
for (int i=1; i System.out.println(Thread.currentThread().getName() + " 办理业务"));
}
} catch (Exception e) {
e.printStackTrace();
} finally {
threadPool.shutdown();
}
}
}
7.3.1 ThreadPoolExecutor.AbortPolicy
CPU密集的意思是该任务需要大量的运算,而没有阻塞,CPU一直全速运行。
CPU密集任务只有在真正的多核CPU上才可能得到加速(通过多线程)。
而在单核CPU上,无论你开几个模拟的多线程,该任务都不可能得到加速,因为CPU总的运算能力就那些。
CPU密集型任务配置尽可能少的线程数量:
一般公式:CPU核数+1个线程的线程池
8.2 IO密集型 8.2.1 公式一由于IO密集型任务线程并不是一直在执行任务,则应配置尽可能多的线程,如CPU核数*2
8.2.2 公式二IO密集型,即该任务需要大量的IO,即大量的阻塞。
在单线程上运行IO密集型的任务会导致浪费大量的CPU运算能力,浪费在等待。
所以在IO密集型任务中使用多线程可以大大的加速程序运行,即使在单核CPU上,这种加速主要就是利用了被浪费掉的阻塞时间。
IO密集型时,大部分线程都阻塞,故需要多配置线程数:
参考公式:CPU核数/(1 - 阻塞系数) 阻塞系数在 0.8-0.9之间
比如8核CPU: 8/(1 -0.9)= 80个线程数
九、Callable接口 9.1 Runnable接口Thread类的构造方法传参为Runnable类型
对应Runnable接口我们可以这样实现
class MyThread implements Runnable {
@Override
public void run() {
System.out.println("Runnable 接口");
}
}
public class Test {
public static void main(String[] args) {
new Thread(new MyThread()).start;
}
}
9.2 Callable接口
9.2.1 代码
对于Callable接口要怎么传?Thread类构造函数要求类型为Runnable接口
注意: FutureTask是可以传入Callable类型的,而FutureTask又实现了RunnableFuture,RunnableFuture又继承了Runnable接口,所以,我们可以通过FutrueTask来适配Callable
class MyThread implements Callable {
@Override
public Integer call() throws Exception {
System.out.println(Thread.currentThread().getName() + "**********come in Callable");
//暂停一会儿线程
try {
TimeUnit.SECONDS.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
return 1024;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
FutureTask futureTask = new FutureTask(new MyThread());
new Thread(futureTask, "AA").start();
System.out.println(Thread.currentThread().getName() + "***********main");
int result01 = 100;
// while (!futureTask.isDone()){}
int result02 = futureTask.get();
System.out.println("*******result: " + (result01 + result02));
}
}
9.2.2 图解
Callable接口可以得到一个值,比如,新起的线程AA要计算一个值r1,最后,要和main线程的r2要求和,main线程可能会因为AA线程的计算时长,而阻塞
视频教程,源码1,源码2