线程池
什么是线程池
线程池其实就是一种多线程处理形式, 处理过程中可以将任务添加到队列中,然后在创建线程后自动启动这些任务。这里的线程就是我们前面学过的线程,这里的任务就是我们前面学过的实现了Runnable或Callable接口的实例对象;
线程池原理:预先启动一些线程,线程无限循环从任务队列中获取一个任务进行执行,直到线程池被关闭。如果某个线程因为执行某个任务发生异常而终止,那么重新创建一个新的线程而已,如此反复。
线程池优点
- 降低资源消耗:通过重复利用已创建的线程降低线程创建和销毁带来的消耗。
- 提高响应速度:当任务到达时,任务可以不需要等待线程创建就能立即执行。
- 提高线程的可管理性:使用线程池可以统进行线程分配、调度和监控。
ThreadPoolExecutor构造方法
ExecutorService pool = new ThreadPoolExecutor(//线程池---快递公司
3,// 核心线程数(正式员工):创建好线程池,正式员工就开始取快递
// 临时工雇佣:正式员工忙不过来,就会创建临时工
// 临时工解雇:空闲时间超出设置的时间范围,就解雇
5,// 最大线程数(最多数量的员工:正式员工+临时工)
30,// 空闲时间:空闲多长时间后就会被回收
TimeUnit.SECONDS,// 时间单位(时间数量+时间单位表示一定范围的时间)
// 阻塞队列:当任务数量大于核心线程数之后,会将任务放入队列,队列放满后,才会创建新线程(没达到最大线程数)-------零时缓冲区
new ArrayBlockingQueue<>(1000),
// (了解)线程池创建Thread线程的工厂类。没有提供的话,就使用线程池内部默认的创建线程的方式----是个接口,可以自己写实现类
// new ThreadFactory() {
// @Override
// public Thread newThread(Runnable r) {
// return null;
// }
// },
// 拒绝策略:线程池处于饱和后,采取的措施
// CallerRunsPolicy:谁(execute代码行所在的线程)让我(快递公司)送快递,不好意思,你自己去送
// AbortPolicy:直接抛出异常RejectedExecutionException
// DiscardPolicy:从阻塞队列丢弃最新的任务(队尾)
// DiscardOldestPolicy:从阻塞队列丢弃最旧的任务(队首)
new ThreadPoolExecutor.DiscardOldestPolicy()
);
参数详解:
a客户(任务)去银行(线程池)办理业务,但银行刚开始营业窗口服务员还未就位(相当于线程池中初始线程数量为0),于是经理(线程池管理者)就安排1号工作人员(创建1号线程执行任务)接待a客户(创建线程);
在a客户业务还没办完时,b客户(任务)又来了,于是经理(线程池管理者)就安排2号工作人员(创建2号线程执行任务)接待b客户(又创建了-个新的线程);
假设该银行总共就2个窗口(核心线程数量是2);
紧接着在a,b客户都没有结束的情况下c客户来了,于是经理(线程池管理者)就安排c客户先坐到银行大厅的座位上(空位相当于是任务队列等候并告知他:如果1、2号工作人员空出,c就可以前去办理业务;
此时d客户又到了银行,(工作人员都在忙,大厅座位也满了)于是经理赶紧安排临时工(新创建的线程)在大堂站着,手持pad设备给d客户办理业务;
假如前面的业务都没有结束的时候e客户又来了,此时正式工作人员都上了,临时工也上了,座位也满了(临时工加正式员工的总数量就是最大线程数),
于是经理只能按《超出银行最大接待能力处理办法》(饱和处理机制拒接接待e客户;
最后,进来办业务的人少了,大厅的临时工空闲时间也超过了1个小时(最大空闲时间),经理就会让这部分空闲的员工人下班(销毁线程)
但是为了保证银行银行正常工作(有一个allowCoreThreadTimeout变量控制是否允许销毁核心线程默认false),即使正式工闲着,也不得提前 下班所以1.2号工作人员继续待着(池内保持核心线程数量);
线程池工作流程
1、判断线程池里的核心线程是否都在执行任务,如果不是(核心线程空闲或者还有核心线程没有被创建)则创建一个新的工作线程来执行任务。如果核心线程都在执行任务,则进入下个流程。
2、线程池判断工作队列是否已满,如果工作队列没有满,则将新提交的任务存储在这个工作队列里。如果工作队列满了,则进入下个流程。
3、判断线程池里的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果已经满了,则交给饱和策略来处理这个任务。
自定义线程池
参数设计
1.核心线程数
核心线程数的设计需要依据任务的处理时间和每秒产生的任务数量来确定,
2.任务队列长度
任务队列长度一般设计为:核心线程数/单个任务执行时间*2即可;
3.最大线程数
最大线程数的设计除了需要参照核心线程数的条件外,还需要参照系统每秒产生的最大任务数决定:最大线程数=(最大任务数-任务队列长度)*单个任务执行时间:
4.最大空闲时间
这个参数的设计完全参考系统运行环境和硬件压力设定没有固定的参考值用户可以根据经验和系统产生任务的时间间隔合理设置一个值即可;
实现步骤
-
编写任务类(MyTask),实现Runnable接口;
/* 需求: 自定义线程池练习,这是任务类,需要实现Runnable; 包含任务编号,每一个任务执行时间设计为0.2秒 */ public class MyTask implements Runnable{ //由于run方法是重写接口中的方法,因此id这个属性初始化可以利用构造方法完成 private int 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 (InterruptedException e) { e.printStackTrace(); } System.out.println("线程:"+name+"完成了任务"+id); } @Override public String toString() { return "MyTask{" + "id=" + id + '}'; } }
-
编写线程类(MyWorker),用于执行任务,需要持有所有任务;
/* 需求: 编写一个线程类,需要继承Thread类,设计个属性,用于保存线程的名字; 设计一个集合,用于保存所有的任务; */ import java.util.List; public class MyWork extends Thread{ private String name; private List<Runnable> tasks; //利用构造方法,给成员变量赋值 public MyWork(String name, List<Runnable> tasks) { super(name); this.tasks = tasks; } @Override public void run() { //判断集合中是否有任务,只要有,就一直执行任务 while (tasks.size()>0){ Runnable r=tasks.remove(0); r.run(); } } }
-
编写线程池类(MyThreadPool),包含提交任务,执行任务的能力;
import javafx.concurrent.Worker; import javax.sound.midi.Soundbank; import java.util.Collections; import java.util.LinkedList; import java.util.List; /* 这是自定义的线程池类; 成员变量: 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 corPoolSize; //4.最大线程数量 private int maxSize; //5.任务队列长度 private int workSize; public MyThreadPool(int corPoolSize, int maxSize, int workSize) { this.corPoolSize = corPoolSize; this.maxSize = maxSize; this.workSize = workSize; } //1:提交任务; public void submit(Runnable r){ //判断当前集合中任务的数量,书否超出了最大任务数量 if(tasks.size()>=workSize){ System.out.println("任务:"+r+"被丢弃了"); }else { tasks.add(r); execTask(r); } } //2.执行任务 private void execTask(Runnable r) { //判断当前线程池中的线程总数量,是否超出了核心数, if(num<corPoolSize){ new MyWork("核心线程:"+num,tasks).start(); num++; }else if(num<maxSize){ new MyWork("非核心线程:"+num,tasks).start(); num++; }else { System.out.println("任务:"+r+"被缓存了"); } } }
-
编写测试类(MyTest),创建线程池对象,提交多个任务测试;
/* 测试类: 1:创建线程池类对象; 2:提交多个任务 */ public class MyTest { public static void main(String[] args) { //1.创建线程池类对象; MyThreadPool pool=new MyThreadPool(2,4,20); //2:提交多个任务 for(int i=0;i<30;i++){ //3.创建任务对象,并提交给线程池 MyTask my=new MyTask(i); pool.submit(my); } } }
内置线程池
ExecutorService
ExecutorService接口是java内置的线程池接口,通过学习接口中的方法,可以快速的掌握java内置线程池的基本使用
常用方法:
- void shutdown()启动一次顺序关闭,执行以前提交的任务,但不接受新任务
- List shutdownNow(停止所有正在执行的任务,暂停处理正在等待的任务,并返回等待执行的任务列表
- Future submit(allable task) 执行带返回值的任务,返回-个Future对象。
- Future<?> submit(Runnable task)执行Runnable任务,并返回-一个表示该任务的Future.
- Future submit(Runnable task, T result)执行Runnable任务,并返回-一个表示该任务的Future.
获取ExecutorService可以利用JDK中的Executors类中的静态方法,常用获取方式如下:
-
static ExecutorService newCachedThreadPool0)创建一个默认的线程池对象, 里面的线程可重用,且在第一次使用时才创建---------创建线程数量不做限制,追求性能优先,服务器压力大
public static void main1(String[] args) { //1.使用工厂类获取线程池对象 ExecutorService es= Executors.newCachedThreadPool(); //2.提交任务 for (int i=1;i<=10;i++){ es.submit(new MyRunnable(i)); } } 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); } }
-
static ExecutorService newCachedThreadPool(ThreadF{story threadFactory)
线程池中的所有线程都使用ThreadFactory来创建,这样的线程无需手动启动,自动执行;public static void main(String[] args) { //1.使用工厂类获取线程池对象 ExecutorService es= Executors.newCachedThreadPool(new ThreadFactory() { int n=1; @Override public Thread newThread(Runnable r) { return new Thread(r,"自定义的线程名称"+n++); } }); //2.提交任务 for (int i=1;i<=10;i++){ es.submit(new MyRunnable(i)); } } 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); } }
-
static ExecutorService newFixedThreadPol(int nThreads)创建一 个可重用固定线程数的线程池------服务器压力不大,可以规定线程数量,达到一定值,不会再增加线程
-
static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory)
创建一个可重用固定线程数的线程池且线程池中的所有线程都使用ThreadFactory来创建。 -
static ExecutorService newSingleThreadExecutor()创建一个使用单个worker线程的Executor,以无界队列方式来运行该线程------单线程方式,追求绝对安全时使用
-
static ExecutorService newSingleThreadExecutor(ThreadFactory threadFactory)
创建-一个使用单个worker线程的Executor,且线程池中的所有线程都使用ThreadFactory来创建。
异步计算结果(Future)
开发中,我们有时需要利用线程进行一些计算然后获取这些计算的结果,而java中的Future接口就是专门用于描述异步计算结果的,我们可以通过Future对象获取线程计算的结果;
Future的常用方法如下:
- boolean cancel(boolean mayInterruptlfRunning) 试图取消对此任务的执行。
- V get() 如有必要,等待计算完成,然后获取其结果。
- V get(long timeout, TimeUnit unit)如有必要,最多等待为使计算完成所给定的时间之后,获取其结果(如果结果可用)。
- **boolean isCancelled()**如果在任务正常完成前将其取消,则返回true.
- **boolean isDone()**如果任务已完成,则返回true。
代码练习:
import java.util.concurrent.*;
public class FutureDome {
public static void main1(String[] args) throws ExecutionException, InterruptedException {
//1.
ExecutorService es= Executors.newCachedThreadPool();
Future<Integer> f=es.submit(new MyCall(1,1));
//判断任务是否已经完成
boolean done=f.isDone();
System.out.println("第一次判断任务是否完成"+done);
boolean cancelled=f.isCancelled();
System.out.println("第一次判断任务是否取消"+cancelled);
Integer val= f.get();//一直等待任务的执行,知道完成为止
System.out.println("任务执行的结果是:"+val);
boolean done2=f.isDone();
System.out.println("第er次判断任务是否完成"+done2);
boolean cancelled2=f.isCancelled();
System.out.println("第二判断任务是否取消"+cancelled2);
}
public static void main(String[] args) throws InterruptedException, ExecutionException, TimeoutException {
ExecutorService es= Executors.newCachedThreadPool();
Future<Integer> f=es.submit(new MyCall(1,1));
boolean b=f.cancel(true);
System.out.println("取消任务执行的结果"+b);
Integer v=f.get(1,TimeUnit.SECONDS);//由于等待时间过短,任务来不及执行
}
}
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;
}
}
线程池的使用步骤可以归纳总结为五步:
- 利用ExecutorsI厂类的静态方法,创建线程池对象;
- 编写Runnable或Callable实现类的实例对象;
- 利用ExecutorService的submit方法或ScheduledExecutorService的schedule方法提交并执行线程任务
- 如果有执行结果,则处理异步执行结果(Future)
- 调用shutdown()方法,关闭线程池