线程池API及使用

文章目录

    • 1.1首先思考一下线程是不是越多越好?
    • 1.2 相关概念
    • 1.3线程池API - 接口和实现类
      • 1.3.1 ExeutorService API
      • 1.3.2 ScheduledExecutorService API
    • 1.4 线程池 的使用
      • 1.4.1 线程池的创建
        • 1.4.1.1 构造方法(new)创建线程池
        • 1.4.1.2 Executors工具类创建线程池
        • 1.4.1.3 线程池执行原理分析
        • 1.4.1.4 等待队列类型
    • 1.5 不同类型等待队列的使用场景
    • 1.6 线程池可以接收的线程类型
    • 1.7 线程数量定义

1.1首先思考一下线程是不是越多越好?

1.线程在Java中是一个对象,更是操作系统的资源(本质上来源于底层操作系统),线程创建,销毁需要时间。如果创建时间+销毁时间>执行时间 就很不合算。

2.Java对象占用堆内存,操作系统线程占用系统内存,根据jvm规范,一个线程默认最大栈大小1M,这个栈空间是需要从系统内存中分配的。线程过多,会消耗很多的内存。

3.操作系统需要频繁切换线程上下文(每个线程都想被运行)

线程池的退出,就是为了方便的控制线程池数量

1.2 相关概念

1.线程池管理器:用于创建并管理线程池,包括创建线程池,销毁线程池,添加新任务;

2.工作线程:线程池中线程,在没有任务时处于等待状态,可以循环的执行任务;

3.任务接口:每个任务必须实现的接口,以供工作线程调度任务的执行,他主要规定了任务的入口,任务执行完后的收尾工作,任务的执行状态等;

4.任务队列:用于存放没有处理的任务。提供一种缓冲机制。

线程池API及使用_第1张图片

运行部分理解图(结合概念)

线程池API及使用_第2张图片

1.3线程池API - 接口和实现类

类型 名称 描述
接口 Exeutor 最上层的即接口,定义了执行任务的execute方法
接口 ExeutorService 继承了Executor接口,扩展了Callable、Future、关闭方法
接口 ScheduledExecutorService 继承了ExecutorService,增加了定时任务相关方法
实现类 ThreadPoolExecutor 基础、标准的线程池实现
实现类 ScheduledThreadPoolExecutor 继承了ThreadPoolExecutor,实现了 ScheduledExecutorService中相关定时任务的方法

可以认为ScheduledThreadPoolExecutor是最丰富的功能

1.3.1 ExeutorService API

方法名称 方法作用
awaitTermination(long timeout,TimeUnit unit) 监测ExecutorService是否已经关闭,直到所有任务完成执行,或超时发生,或当前线程被中断
invokeAll(Collection tasks) 执行给定的任务集合,执行完毕后,返回结果
invokeAll(Collection tasks, long timeout, TimeUnit unit) 执行给定的任务集合,执行完毕或者超时后,返回结果,其他任务终止
invokeAny(Collection tasks) 执行给定的任务,任意一个执行成功则返回结果,其他任务终止
invokeAny(Collection tasks, long timeout, TimeUnit unit) 执行给定的入伍,任意一个执行成功或者超时,则返回结果,其他任务终止
isShutdown() 如果此线程池已经关闭,则返回true
isTerminated() 如果关闭后所有任务都已经完成,则返回true
shutdown() 优雅关闭线程池,之前提交的任务将被执行。
shutdownNow() 尝试停止所有正在执行的任务,停止等待任务的处理,并返回等待执行任务的列表。
submit(Callable task) 提交一个用于执行的Callable返回任务,并返回一个Futura,用于获取Callable执行结果
submit(Runnable task) 提交可运行任务以执行,并返回一个Future对象,执行结果为null
submit(Runnable task, T result) 提交可运行任务以执行,并返回一个Future,执行结果为传入的result

ExeutorService 接口位于rt.jar包下的Java/util/concurrent/locks包下面,可以结合接口注释理解一波

1.3.2 ScheduledExecutorService API

方法名称 方法作用
schedule(Runnable command,long delay, TimeUnit unit) 创建并执行一个一次性任务,过了delay的间隔就会自动执行,timeUnit指代的是delay的单位(时分秒或其他)
schedule(Callable callable,long delay, TimeUnit unit) 创建并执行一个一次性任务,过了delay的间隔就会自动执行,timeUnit指代的是delay的单位(时分秒或其他)
scheduleAtFixedRate(Runnable command,long initialDelay, long period,TimeUnit unit) 创建并执行一个周期性任务过了给定的初始延迟时间,会第一时间被执行,执行过程中发生了异常,那么任务时间就停止
scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit) 创建并执行一个周期性任务,过了初始延迟时间,第一次被执行,后续以给定的周期时间执行,执行过程发生了异常,那么任务就停止

scheduleAtFixedRate 与 scheduleWithFixedDelay 的区别在于

scheduleAtFixedRate 当有工作线程空闲时,会立刻将任务队列中的任务放入工作线程中

scheduleWithFixedDelay 当有工作线程空闲时,在空闲的那一刻开始计时延时指定的参数的时间执行下一个线程

1.4 线程池 的使用

1.4.1 线程池的创建

1.4.1.1 构造方法(new)创建线程池

ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue workQueue)

corePoolSize :核心线程池大小

maximumPoolSize : 最大线程池大小

keepAliveTime : 存活时间

unit :时间的单位类型(是 时/分/秒 还是其他)

workQueue :工作队列模式

1.4.1.2 Executors工具类创建线程池

Executors 类位于java.util.concurrent包下(可以 快速 创建线程的一个工具类)

其实就是通过调用 ThreadPoolExecutor 或者 ScheduledThreadPoolExecutor 构造方法,传递不同的参数,常用方法如下:

newFixedThreadPool(int nThreads)创建一个固定大小、任务队列容量无界的线程池。核心线程数=最大线程数。

newCachedThreadPool()创建的是一个大小无界的缓冲线程池。他的任务队列是一个同步队列。任务加入到池中,如果池中有空闲线程,则用空闲线程执行,如无则创建新线程执行。池中的线程空闲超过60秒,将被销毁释放。线程数随任务的多是变化。适用于执行耗时较小的异步任务。池的核心线程数=0,最大线程数=Integer.MAX_VALUE

newSingleThreadExecutor() 只有一个线程来执行无界任务队列的单一线程池。该线程池确保任务按加入的顺序一个一个依次执行。当唯一的线程因任务异常终止时,将创建一个新的线程来执行后续的任务。与newFixedThreadPool(1)的区别在于,单一线程池的池大小在newSingleThreadExecutor方法中硬编码,不能在改变。

newScheduledThreadPool(int corePoolSize)能定时执行任务的线程池。该池的核心线程数由参数指定,最大线程数=Integer.MAX_VALUE

1.4.1.3 线程池执行原理分析

结合 ThreadPoolExecutor 的 execute 方法 内 的注释 内容可以得出以下结论:

1、是否达到核心线程数量?没达到,创建一个工作线程来执行任务。

2、工作队列是否已满?美满,则将新提交的任务存储在任务队列中。

3、是否达到线程池最大数量?没达到,则创建一个新的工作线程来执行任务。

4、最后,执行拒绝策略来处理这个任务。

线程池API及使用_第3张图片

1.4.1.4 等待队列类型

线程池API及使用_第4张图片

该队列的实现有上述类型,常用的队列类型如下

1.LinkedBlockingQueue 当不传参数时候,是无限大小的等待队列,通过有参数的构造方法的时候,为参数大小的等待队列

2.SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列

1.5 不同类型等待队列的使用场景

  1. 创建测试类
  2. 将下列方法写入
	/** 
	* 测试: 提交15个执行时间需要3秒的任务,看线程池的状况 
	*  
	* @param threadPoolExecutor 传入不同的线程池,看不同的结果 
	* @throws Exception 
	*/
public void testCommon(ThreadPoolExecutor threadPoolExecutor) throws Exception {   
    // 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况   
    for (int i = 0; i < 15; i++) {      
        int n = i;      
        threadPoolExecutor.submit(new Runnable() {         
            @Override         
            public void run() {            
                try {               
                    System.out.println("开始执行:" + n);               
                    Thread.sleep(3000L);               
                    System.err.println("执行结束:" + n);            
                } catch (InterruptedException e) {               
                    e.printStackTrace();            
                }         
            }      
        });      
        System.out.println("任务提交成功 :" + i);   
    }   
    // 查看线程数量,查看队列等待数量   
    Thread.sleep(500L);   
    System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
    System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());   
    // 等待15秒,查看线程数量和队列数量(理论上,会被超出核心线程数量的线程自动销毁)
    Thread.sleep(15000L);   
    System.out.println("当前线程池线程数量为:" + threadPoolExecutor.getPoolSize());
    System.out.println("当前线程池等待的数量为:" + threadPoolExecutor.getQueue().size());
}
/**
* 1、线程池信息: 核心线程数量5,最大数量10,无界队列,超出核心线程数量的线程存活时间:5秒
* 
* @throws Exception
*/
private void threadPoolExecutorTest1() throws Exception {
    ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
   		new LinkedBlockingQueue<Runnable>());
    testCommon(threadPoolExecutor);
    // 预计结果:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}
// 总结
// 严格控制核心线程数量,最大线程数量,超出的线程会放入等待队列中,允许大量任务堆积的时候
/**
 * 2、 线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 * 
 * @throws Exception
 */
private void threadPoolExecutorTest2() throws Exception {
	// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
	// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
	ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
			new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
				@Override
				public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
					System.err.println("有任务被拒绝执行了");
				}
			});
	testCommon(threadPoolExecutor);
	// 预计结果:
	// 1、 5个任务直接分配线程开始执行
	// 2、 3个任务进入等待队列
	// 3、 队列不够用,临时加开5个线程来执行任务(5秒没活干就销毁)
	// 4、 队列和线程池都满了,剩下2个任务,没资源了,被拒绝执行。
	// 5、 任务执行,5秒后,如果无任务可执行,销毁临时创建的5个线程
}
// 总结
// 严格控制线程数量,等待队列数量,超出等待队列数量,会执行回调策略
/**
 * 3、 线程池信息: 核心线程数量5,最大数量5,无界队列,超出核心线程数量的线程存活时间:5秒
 * 
 * @throws Exception
 */
private void threadPoolExecutorTest3() throws Exception {
	ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 5, 0L, TimeUnit.MILLISECONDS,
			new LinkedBlockingQueue<Runnable>());
	testCommon(threadPoolExecutor);
	// 预计结:线程池线程数量为:5,超出数量的任务,其他的进入队列中等待被执行
}
// 总结
// 模拟实现 Executors.newFixedThreadPool(int nThreads) 一样的 线程池
// 与第一种的区别在于,是否可以自定义最大线程池数量
// 通过构造方法可以自己随便封装最大线程池数量的线程
/**
 * 4、 线程池信息:
 * 核心线程数量0,最大数量Integer.MAX_VALUE,SynchronousQueue队列,超出核心线程数量的线程存活时间:60秒
 * 
 * @throws Exception
 */
private void threadPoolExecutorTest4() throws Exception {

	// SynchronousQueue,实际上它不是一个真正的队列,因为它不会为队列中元素维护存储空间。与其他队列不同的是,它维护一组线程,这些线程在等待着把元素加入或移出队列。
	// 在使用SynchronousQueue作为工作队列的前提下,客户端代码向线程池提交任务时,
	// 而线程池中又没有空闲的线程能够从SynchronousQueue队列实例中取一个任务,
	// 那么相应的offer方法调用就会失败(即任务没有被存入工作队列)。
	// 此时,ThreadPoolExecutor会新建一个新的工作者线程用于对这个入队列失败的任务进行处理(假设此时线程池的大小还未达到其最大线程池大小maximumPoolSize)。

	// 和Executors.newCachedThreadPool() 的实现是一样的
	ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(0, Integer.MAX_VALUE, 60L, TimeUnit.SECONDS,
			new SynchronousQueue<Runnable>());
	testCommon(threadPoolExecutor);
	// 预计结果:
	// 1、 线程池线程数量为:15,超出数量的任务,其他的进入队列中等待被执行
	// 2、 所有任务执行结束,60秒后,如果无任务可执行,所有线程全部被销毁,池的大小恢复为0
	Thread.sleep(60000L);
	System.out.println("60秒后,再看线程池中的数量:" + threadPoolExecutor.getPoolSize());
}
// 总结
// 核心线程数量为0,最大线程数量为自定义,缓存队列为同步队列,每有一个任务,都会创建一个临时线程,超出自定义的最大线程数量的时候会报错
// 针对不可控的,无法预估线程数量的时候使用,但是注意,当线程开启过多依然会造成卡顿
// SynchronousQueue对列的特点是,没有空闲的就新开,再向队列加入新任务的时候,有已经执行完成的就会复用已有的空闲线程,当超过等待时间的时候,会销毁
// 尽量别这么使用因为当设定的最大线程数过小,而最大线程数量过小的时候会产生异常
/**
 * 5、 定时执行线程池信息:3秒后执行,一次性任务,到点就执行 
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒 * * @throws Exception */
private void threadPoolExecutorTest5() throws Exception { // 和 Executors.newScheduledThreadPool() 构建的线程池是一样的 ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5); threadPoolExecutor.schedule(new Runnable() { @Override public void run() { System.out.println("任务被执行,现在时间:" + System.currentTimeMillis()); } }, 3000, TimeUnit.MILLISECONDS); System.out.println("定时任务,提交成功,时间是:" + System.currentTimeMillis() + ", 当前线程池中线程数量:" + threadPoolExecutor.getPoolSize()); // 预计结果:任务在3秒后被执行一次 } // ScheduledThreadPoolExecutor想要实现定时或者推迟执行时,需要调用ScheduledThreadPoolExecutor类自己实现的方法,而不是继承至ThreadPoolExecutor的方法 // 例如 // schedule() 可以延时一段时间,执行一次,延时时间取决于delay参数
/**
 * 6、 定时执行线程池信息:线程固定数量5 ,
* 核心线程数量5,最大数量Integer.MAX_VALUE,DelayedWorkQueue延时队列,超出核心线程数量的线程存活时间:0秒 * * @throws Exception */
private void threadPoolExecutorTest6() throws Exception { ScheduledThreadPoolExecutor threadPoolExecutor = new ScheduledThreadPoolExecutor(5); // 周期性执行某一个任务,线程池提供了两种调度方式,这里单独演示一下。测试场景一样。 // 测试场景:提交的任务需要3秒才能执行完毕。看两种不同调度方式的区别 // 效果1: // 提交后,2秒后开始第一次执行,之后每间隔1秒, // 固定执行一次(如果发现上次执行还未完毕,则等待完毕,完毕后立刻执行)。 // 这里会分成两种情况 // 第一种是 任务的执行时间(run方法的执行时间) < period(ScheduledThreadPoolExecutor构造方法的第三个参 // 数) 此时的效果是 固定时间间隔 // 第二种是 任务的执行时间(run方法的执行时间) > period(ScheduledThreadPoolExecutor构造方法的第三个参 // 数) 此时的效果是 固定时间间隔失效,在任务执行结束后立即再次执行任务 threadPoolExecutor.scheduleAtFixedRate(new Runnable() { @Override public void run() { try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务-1 被执行,现在时间:" + System.currentTimeMillis()); } }, 2000, 1000, TimeUnit.MILLISECONDS); // 效果2: // 使用scheduleWithFixedDelay()方法的时候 // delay 参数代表的是固定延时多久,无论任务执行时间是多长,均需要等待任务执行结束后,在延迟delay的时间后 // 开始下次任务的执行 threadPoolExecutor.scheduleWithFixedDelay(new Runnable() { @Override public void run() { try { Thread.sleep(3000L); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("任务-2 被执行,现在时间:" + System.currentTimeMillis()); } }, 2000, 1000, TimeUnit.MILLISECONDS); }
/**
 * 7、 终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 * 
 * @throws Exception
 */
private void threadPoolExecutorTest7() throws Exception {
	// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
	// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
	ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
			new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
				@Override
				public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
					System.err.println("有任务被拒绝执行了");
				}
			});
    
	// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
	for (int i = 0; i < 15; i++) {
		int n = i;
		threadPoolExecutor.submit(new Runnable() {
			@Override
			public void run() {
				try {
					System.out.println("开始执行:" + n);
					Thread.sleep(3000L);
					System.err.println("执行结束:" + n);
				} catch (InterruptedException e) {
					System.out.println("异常:" + e.getMessage());
				}
			}
		});
		System.out.println("任务提交成功 :" + i);
	}
    
	// 1秒后终止线程池
	Thread.sleep(1000L);
	threadPoolExecutor.shutdown();
    
    
	// 再次提交提示失败
	threadPoolExecutor.submit(new Runnable() {
		@Override
		public void run() {
			System.out.println("追加一个任务");
		}
	});
    
	// 结果分析
	// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
	// 2、 调用shutdown后,不接收新的任务,等待13任务执行结束
	// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}

// 总结 
// shutdown() 优雅关闭线程池,已经提交的任务,会继续执行,等待线程池内全部线程执行完毕后,关闭线程池
// 在shutdown() 后 追加新的任务,会执行拒绝策略
/**
 * 8、 立刻终止线程:线程池信息: 核心线程数量5,最大数量10,队列大小3,超出核心线程数量的线程存活时间:5秒, 指定拒绝策略的
 * 
 * @throws Exception
 */
private void threadPoolExecutorTest8() throws Exception {
	// 创建一个 核心线程数量为5,最大数量为10,等待队列最大是3 的线程池,也就是最大容纳13个任务。
	// 默认的策略是抛出RejectedExecutionException异常,java.util.concurrent.ThreadPoolExecutor.AbortPolicy
	ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(5, 10, 5, TimeUnit.SECONDS,
			new LinkedBlockingQueue<Runnable>(3), new RejectedExecutionHandler() {
				@Override
				public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {
					System.err.println("有任务被拒绝执行了");
				}
			});
	// 测试: 提交15个执行时间需要3秒的任务,看超过大小的2个,对应的处理情况
	for (int i = 0; i < 15; i++) {
		int n = i;
		threadPoolExecutor.submit(new Runnable() {
			@Override
			public void run() {
				try {
					System.out.println("开始执行:" + n);
					Thread.sleep(3000L);
					System.err.println("执行结束:" + n);
				} catch (InterruptedException e) {
					System.out.println("异常:" + e.getMessage());
				}
			}
		});
		System.out.println("任务提交成功 :" + i);
	}
	// 1秒后终止线程池
	Thread.sleep(1000L);
	List<Runnable> shutdownNow = threadPoolExecutor.shutdownNow();
	// 再次提交提示失败
	threadPoolExecutor.submit(new Runnable() {
		@Override
		public void run() {
			System.out.println("追加一个任务");
		}
	});
	System.out.println("未结束的任务有:" + shutdownNow.size());

	// 结果分析
	// 1、 10个任务被执行,3个任务进入队列等待,2个任务被拒绝执行
	// 2、 调用shutdownnow后,队列中的3个线程不再执行,10个线程被终止
	// 3、 追加的任务在线程池关闭后,无法再提交,会被拒绝执行
}

// 总结 
// shutdownnow() 后,处于等待队列中的会被拒绝执行,并且可以通过 threadPoolExecutor.shutdownNow() 方法获取到被拒绝执行的任务线程,通过集合的size() 可以获取到线程的数量
// 而出处于正在执行的线程,会使用intterrut()尝试去终止线程  (也是等线程任务正常执行完毕后退出线程)

1.6 线程池可以接收的线程类型

1.Callable类型的接口任务

2.Runable类型的接口任务

1.7 线程数量定义

如何确定合适的数量的线程?

计算型:CPU数量的1-2倍 (纯内存的运算,加减乘除等任务)

IO型任务:相对比计算型任务,需多开一些线程,要根据具体的IO阻塞线程数量进行考量决定的(例如读取数据库数据较慢的时候,还有就是通过网络接口请求也会消耗很多时间)

在正式环境当CPU达到80%的时候,就已经达到了饱和值,如果超过此数据值说明需要注意新线程的开启

你可能感兴趣的:(Java线程,线程池API及使用)