线程池的使用
- 线程池基础
- 线程池使用
- 线程池综合案例
- 线程池总结
1. 线程池基础
1.1 什么是线程池
线程池是存储线程的容器,线程事先创建好后放入线程池,当有任务需要执行时,直接从线程池拿空闲线程使用,使用完毕后归还给线程池.
1.2 为什么使用线程池
在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁,这就是”池化资源”技术产生的原因。线程池顾名思义就是事先创建若干个可执行的线程放入一个池(容器)中,需要的时候从池中获取线程不用自行创建,使用完毕不需要销毁线程而是放回池中,从而减少创建和销毁线程对象的开销。
1.3 线程池有哪些优势
a) 线程和任务分离,提升线程重用性;
b) 控制线程并发数量,降低服务器压力,统一管理所有线程;
c) 提升系统响应速度,加入创建线程用的回见为T1,执行任务用的时间为T2,销毁线程用的时间为T3,那么使用线程池就免去了T1和T3的时间。
1.4 线程池的引用场景
- 引用场景:
- 网购商品秒杀;
- 云盘文件上传与下载
- 12306网上购票系统等
- 总之
只要有并发的地方、任务数量大或小、每次任务执行时间长或端的都可以使用线程池;只不过在使用线程池的时候,注意一下设置合理的线程池大小即可。
2. 线程池使用
2.1 ThreadPoolExecutor类
java内置线程池类
corePoolSize(核心线程数): 当任务提交到线程池时,如果当前运行的线程数量没有达到核心线程池数,就会新开一个线程来执行这个任务。
maximumPoolSize(最大线程数):允许创建出的最大线程数量。
keepAliveTime(最大空闲时间):当一个线程不被适用,允许空闲的时间。
TimeUnit(时间单位):枚举类
workQueue(任务队列):一个集合,当线程数量达到核心线程数量,再提交新任务会被压到任务队列,一个临时的缓冲区。
threadFactory(线程工厂):允许创建自己的线程。
rejectedExecutionHandler(饱和处理机制):当任务数量达到核心线程总数、最大线程数量、队列已满,暂时无法再处理任务,则使用饱和处理机制处理新任务。
线程池工作流程:
2.2 自定义线程池-参数设计分析
- 通过观察Java中的内置线程池参数讲解和线程池工作流程总结,我们不难发现,要设计一个好的线程池,就必须合理的设置线程池的4个参数;那到底该如何合理的设计4个参数的值呢?
2.2.1 四个参数的设计
-
- 核心线程数(corePoolSize)
核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,例如:执行一个任务需要0.1秒,系统百分之80的时间每秒会产生100个任务,那么想在1秒内处理完这100个任务,就需要10个线程,此时我们就可以设计核心线程数为10,当然实际情况不可能那么平均,所以一般按照8020原则设计即可,即按照百分之80的情况设计核心线程,剩下的百分之20利用最大线程数处理。
- 核心线程数(corePoolSize)
-
- 任务队列长度(workQueue)
任务队列长度一般设计为:核心线程数、单个任务执行时间* 2即可;例如:在上面的场景中,核心线程设计为10,单个任务执行时间为0.1,则队列长度可以设计为200.
- 任务队列长度(workQueue)
-
- 最大线程数(maximumPoolSzie)
最大线程数的设计粗了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定。例如:上述环境中,如果系统每秒产生的任务是1000个,那么最大线程数=(最大任务数-任务队列长度)* 单个任务执行时间。即:最大线程数=(1000-200)* 0.1=80个
- 最大线程数(maximumPoolSzie)
-
- 最大空闲时间(keepAliveTime)
这个参数的设计完全参考系统运行环境和硬件压力设定,没有固定的参考值,用户可以根据经验和系统产生任务的时间间隔合理设计一个值即可。
- 最大空闲时间(keepAliveTime)
2.2.2 实现步骤
- 编写任务类(MyTask),实现Runnable接口;
- 编写线程类(MyWorker),用于执行任务,需要持有所有任务;
- 编写线程池类(MyThreadPool),包含提交任务,执行任务的能力;
- 编写测试类(MyTest),创建线程池丢向,提交多个任务测试;
/**
* 需求:自定义线程池练习,这是任务类,需要实现Runnable接口
* 包含任务编号,每个任务执行时间为0.2秒
*/
public class MyTask implements Runnable {
private int id;
//由于run方法是重写接口中的方法,因此id这个属性初始化可以利用构造方法完成
public MyTask(int id) {
this.id = id;
}
@Override
public void run() {
String name=Thread.currentThread().getName();
System.out.println("线程:"+name+"即将执行任务"+id);
try {
Thread.sleep(200);
} catch (Exception e) {}
System.out.println("线程:"+name+"完成了任务"+id);
}
@Override
public String toString() {
return "MyTask [id=" + id + "]";
}
}
/**
* 需求:编写一个线程类,继承Thread类,设计一个属性用于保护线程的名字
* 设计一个集合,用于保存所有的任务
*/
public class MyWorker extends Thread{
private String name; //保存线程的名字
private List<Runnable> tasks;
//利用构造方法给成员变量赋值
public MyWorker(String name, List<Runnable> tasks) {
this.name = name;
this.tasks = tasks;
}
@Override
public void run() {
//判断集合中是否有任务,只要有就一直执行
while(tasks.size()>0) {
Runnable r=tasks.remove(0);
r.run();
}
}
}
/**
* 自定义的线程池类
* 成员变量:
* 1. 任务队列 --集合,线程安全问题
* 2. 当前线程数量
* 3. 核心线程数
* 4. 最大线程数
* 5. 任务队列长度
* 成员方法:
* 1.提交任务:直接将任务添加到集合中,需要判断是否超出总长度
* 2.执行任务:判断当前线程的数量,决定核心线程还是非核心线程
*/
public class MyThreadPool {
// 1.任务队列 --集合,线程安全问题
private List<Runnable> tasks=Collections.synchronizedList(new
LinkedList<>());
// 2.当前线程数量
private int num;
// 3.核心线程数
private int corePoolSize;
// 4.最大线程数
private int maxSize;
// 5.任务队列长度
private int workSize;
public MyThreadPool(int corePoolSize, int maxSize, int workSize) {
this.corePoolSize = corePoolSize;
this.maxSize = maxSize;
this.workSize = workSize;
}
public void submit(Runnable r) {
//判断当前集合中任务的数量是否超出了最大任务数量
if(tasks.size()>=workSize) {
System.out.println("任务:"+r+"被丢弃了、、、");
}else {
tasks.add(r);
execTask(r);
}
}
// 2. 执行任务
public void execTask(Runnable r) {
//判断当前线程池中的线程总数量,是否超出了核心数
if(num<corePoolSize) {
new MyWorker("核心线程:"+num,tasks).start();
num++;
}else if(num<maxSize){
new MyWorker("非核心线程:"+num,tasks).start();
num++;
}else {
System.out.println("任务"+r+"被缓存来了");
}
}
}
/**
* 测试类:
* 1. 创建线程池类对象;
* 2. 提交多个任务
*/
public class MyTest {
public static void main(String[] args) {
// 1. 创建线程池类对象;
MyThreadPool myThreadPool=new MyThreadPool(2,4,10);
// 2. 提交多个任务
for (int i = 0; i < 20; i++) {
MyTask my=new MyTask(i);
myThreadPool.submit(my);
}
}
}
输出结果:
线程:Thread-0即将执行任务0
线程:Thread-1即将执行任务1
线程:Thread-2即将执行任务2
任务MyTask [id=4]被缓存来了
线程:Thread-3即将执行任务3
任务MyTask [id=5]被缓存来了
…
任务MyTask [id=12]被缓存来了
任务MyTask [id=13]被缓存来了
任务:MyTask [id=14]被丢弃了、、、
…
任务:MyTask [id=19]被丢弃了、、、
线程:Thread-1完成了任务1
线程:Thread-2完成了任务2
线程:Thread-2即将执行任务5
线程:Thread-0完成了任务0
…
线程:Thread-1完成了任务10
线程:Thread-3完成了任务13
线程:Thread-2完成了任务12
2.3 Java内置线程池-ExecutorService
- ExecutorService接口是java内置的线程池接口,源码如下:
public interface ExecutorService extends Executor {
// 启动一次顺序关闭,执行以前提交的任务,但不接受新任务。
void shutdown();
// 停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的恩物列表。
List<Runnable> shutdownNow();
boolean isShutdown();
boolean isTerminated();
boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException;
// 执行但返回值的任务,返回一个Future对象
<T> Future<T> submit(Callable<T> task);
// 执行Runnable任务,并返回一个表示该任务的Future
<T> Future<T> submit(Runnable task, T result);
// 执行Runnable任务,并返回一个表示该任务的Future
Future<?> submit(Runnable task);
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks)
throws InterruptedException;
<T> List<Future<T>> invokeAll(Collection<? extends Callable<T>> tasks,
<T> T invokeAny(Collection<? extends Callable<T>> tasks)
throws InterruptedException, ExecutionException;
<T> T invokeAny(Collection<? extends Callable<T>> tasks,
long timeout, TimeUnit unit)
throws InterruptedException, ExecutionException, TimeoutException;
}
- 既然ExecutorService是一个接口,接口是无法直接创建对象的,那么如何获取ExecutorService对象呢?
2.3.1 ExecutorService获取
获取ExecutorService可以利用JDK中的Executors类中的静态方法,常用的获取方式如下:
- static ExecutorService newCachedThreadPool() :创建一个默认的线程池对象,里面的线程可重用,且在第一次使用时才创建
- static ExecutorService newCachedThreadPool(ThreadFactory threadFactory) :线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行
- static ExecutorService newFixedThreadPool(int nThreads) :创建一个可重用固定线程数的线程池
- static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) :创建一个可重用固定线程数的线程且线程池中的所有线程都使用ThreadFactory来创建。
- static ExecutorService newSingleThreadExecutor() :创建一个使用单个worker线程的Executor,以无解队列方式来运行该线程。
- static ExecutorService newSingleThreadExecutor(ThreadFactorry threadFactory) :创建一个使用单个worker线程的Executor,且线程池中的所有线程都使用ThreadFactory来创建。
2.4 Java内置线程池-ScheduledExecutorService
ScheduledExecutorService是ExecutorService的子接口,具备了延迟运行或定期执行任务的能力
获取方式如下:
- static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) 创建一个可重用固定线程数的线程池且允许延迟运行或定期执行任务;
- static ScheduledExecutorService newScheduledThreadPool(int corePoolSize, ThreadFactory threadFactory) 创建一个可重用固定线程数的线程池且线程池中的所有线程都是用ThreadFactory来创建,且允许延迟运行或定期执行任务;
- static ScheduledExecutorService newSingleThreadScheduledExecutor() : 创建一个单线程执行程序,它允许在给定延迟后运行命令或者定期地执行。
- static ScheduledExecutorService newSingleThreadScheduledExecutor(ThreadFactory threadFactory) : 创建一个单线程执行程序,它可安排在给定延迟后运行命令或者定期地执行。
2.4.1 ScheduledExecutorService的常用方法
- ScheduledFuture schedule( Callable callable, long delay, TimeUnit unit) : 延迟时间单位是unit,数量是delay的时间后执行callable。
- ScheduledFuture<?> schedule( Runnable command, long delay, TimeUnit unit) : 延迟时间单位是unit,数量是delay的时间后执行command。
- ScheduledFuture<?> scheduleAtFixedRate( Runnable command, long initialDelay, long period, TimeUnit unit) :延迟时间单位是unit,数量是initialDelay的时间后,每间隔period时间重复执行一次command。
- ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay, long period, TimeUnit unit) :创建并执行一个在给定初始延迟后首次启动的定期操作,随后,在每一次执行终止和下一次执行开始之间都存在给定的延迟。
public class ScheduledExecutorServiceDemo1 {
public static void main(String[] args) {
// 1.获取一个具备延迟执行任务的线程池对象
//ScheduledExecutorService es = Executors.newScheduledThreadPool(4)
// ScheduledExecutorService es =
// Executors.newScheduledThreadPool(5,
// new ThreadFactory() {
// int n;
// @Override
// public Thread newThread(Runnable r) {
// return new Thread(r,"自定义的线程名:"+n++);
// }
// });
ScheduledExecutorService es =Executors.newSingleThreadScheduledExecutor(new ThreadFactory() {
int n;
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"自定义的线程名:"+n++);
}
});
// 2.创建多个任务对象,提交任务,每个任务延迟2秒执行
// for (int i = 0; i <10 ; i++) {
// es.schedule(new MyRunnable(i),2, TimeUnit.SECONDS);
// }
//es.scheduleAtFixedRate(new MyRunnable(1),1,2,TimeUnit.SECONDS);
es.scheduleWithFixedDelay(new MyRunnable(1),1,2,TimeUnit.SECONDS);
System.out.println("over");
}
}
class MyRunnable implements Runnable{
private int id;
public MyRunnable(int id) {
this.id = id;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+"执行了任务"+id);
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
2.5 Java内置线程池-异步计算结果(Future)
我们刚刚在学习java内置线程池时,没有考虑线程计算的结果,但开发中,我们有时需要利用线程进行一些计算,然后获取这些计算的结果,而java中的Future接口就是专门用于描述异步计算结果的,我们可以通过Future对象获取线程计算的结果;
Future的常用方法如下:
boolean cancel(boolean mayInterruptIfRunning) : 试图取消对此任务的执行。
V get() :如有必要,等待计算完成,然后获取其结果。
V get(long timeout,TimeUnit unit) :如有必要,最多等待为使计算完成所给的时间之后,获取其结果(如果结果可用)。
boolean isCancelled() 如果在任务正常完成前将其取消,则返回true。
boolean isDone() :如果任务已完成,则返回true。
public class Test2 {
public static void main(String[] args) throws Exception{
// 1.获取线程池对象
ExecutorService es=Executors.newCachedThreadPool();
// 2.创建<u>Callable</u>类型的任务对象
Future<Integer> <u>future</u>=es.submit(new MyCall(1, 3));
// 3.判断任务是否已经完成
//test1(future);
// boolean b=future.cancel(true);
// System.out.println("取消任务执行的结果:"+b);
// Integer i=future.get(1,TimeUnit.SECONDS); //由于等待时间过短,任务来不及完成,会报异常
// System.out.println("任务执行的结果是:"+i);
}
//正常测试流程
public void test1(Future<Integer> future) throws Exception{
boolean done=future.isDone();
System.out.println("第一次判断任务是否已经完成:"+done);
boolean cancel=future.isCancelled();
System.out.println("第一次判断任务是否已经取消:"+cancel);
Integer i=future.get();//一直等待任务的执行,直到完成为止
System.out.println("任务执行的结果是:"+i);
boolean done2=future.isDone();
System.out.println("第二次判断任务是否已经完成:"+done2);
boolean cancel2=future.isCancelled();
System.out.println("第二次判断任务是否已经取消:"+cancel2);
}
}
class MyCall implements Callable<Integer>{
private int a;
private int b;
public MyCall(int a,int b){
this.a=a;
this.b=b;
}
@Override
public Integer call() throws Exception {
String name=Thread.currentThread().getName();
System.out.println(name+"准备开始计算...");
Thread.sleep(2000);
System.out.println(name+"计算完成...");
return a+b;
}
}
3 线程池综合案例
3.1 综合案例-秒杀商品
- 案例介绍
假如某网上商城推出活动,新上架10部新手机免费送客户体验,要求所有参与活动的人员在规定的时间同时参与秒杀争抢,假如有20人同时参加了该活动,请使用线程池模拟这个场景,保证前10人秒杀成功,后10人秒杀失败。 - 要求
- 使用线程池创建线程
- 解决线程安全问题
- 思路提示:
- 既然商品总数量是10个,name我们可以在创建线程池的时候初始化线程数是10个及以下,设计线程池最大数量为10个;
- 当某个线程执行完任务之后,可以让其他秒杀的人继续使用该县城参与秒杀;
- 使用synchronized控制线程安全,放置出现错误数据;
- 代码步骤:
- 编写任务类,主要是送出手机给秒杀成功的客户;
- 编写主程序类,创建20个任务(模拟20个客户);
- 创建线程池对象,并接受20个任务,开始执行任务。
/**
* 任务类,包含了商品数量、客户名称、送手机的行为
*/
public class MyTask implements Runnable{
// 设计一个变量,用于表示上商品的数量
private static int id=10 ;
// 表示客户名称的变量
private String userName;
public MyTask (String userName){
this.userName=userName;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(userName+"正在参与秒杀任务..");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
synchronized (MyTask.class) {
if(id>0){
System.out.println(userName+"使用"+name+"秒杀"+id--+"号商品成功");
}else{
System.out.println(userName+"使用"+name+"秒杀"+id--+"号商品失败");
}
}
}
}
/**
* 主程序类
* @author Administrator
*
*/
public class MyTest {
public static void main(String[] args) {
//1. 创建一个线程池对象
ThreadPoolExecutor pool=new ThreadPoolExecutor(3,5,1,TimeUnit.MINUTES,new LinkedBlockingQueue<Runnable>(15));
// 2. 循环创建任务对象
for (int i = 0; i < 20; i++) {
MyTask myTask=new MyTask("客户"+i);
pool.submit(myTask);
}
// 3.关闭线程池
pool.shutdown();
}
}
3.2 综合案例-取款业务
- 案例介绍
设计一个程序,使用两个线程模拟在两个地点同时从一个账号曲线,假如卡中一共有1000元,每个线程取800元,要求演示结果一个线程取款成功,剩余200元,另一个线程取款失败,余额不足。 - 要求
- 使用线程池创建对象
- 解决线程安全问题
- 思路提示:
- 线程池可以利用Executors工厂类的静态方法,创建线程池对象;
- 解决线程安全问题可以使用synchronized关方法控制取钱的操作;
- 在取款前,先判断余额是否足够,且保证余额判断和取钱行为的原子性。
public class MyTest {
public static void main(String[] args) {
//1. 创建线程池对象
ExecutorService es=Executors.newFixedThreadPool(2,new
ThreadFactory() {
int id=1;
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"ATM:"+id++);
}
});
// 2.创建量词任务提交
es.submit(new MyTask("客户1",800));
es.submit(new MyTask("客户2",800));
es.shutdown();
}
}
public class MyTask implements Runnable{
//用户姓名
private String userName;
// 取款金额
private double money;
// 总金额
private static double total =1000;
public MyTask(String userName, double money) {
this.userName = userName;
this.money = money;
}
@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元");
try {
Thread.sleep(200);
} catch (InterruptedException e) {
}
synchronized (MyTask.class) {
if(total-money>0){
System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元成功,余额:"+(total-money)+"元");
total-=money;
}else{
System.out.println(userName+"正在准备使用"+name+"取款:"+money+"元失败");
}
}
}
}
4. 总结:
线程池的使用步骤可以归纳总结为五步:
- 利用Executors工厂类的静态方法,创建线程池对象;
- 编写Runnable或Callable实现类的实例对象;
- 利用ExecutorService的submit方法或ScheduledExecutorService的schedule方法提交并执行线程任务;
- 如果有执行结果,则处理异步执行结果;
- 调用shutdown()方法,关闭线程池。