您当前的位置:首页 > 电脑百科 > 程序开发 > 语言 > JAVA

Java线程池

时间:2022-11-17 14:11:39  来源:今日头条  作者:Esgoon

JAVA多线程的实现方式

Java程序中,常见有4种方式实现多线程

①继承Thread类

②实现Runnable接口

③实现Callable接口

④使用Executor框架

在JDK5之前,创建线程有2种方式,一种是继承Thread类,另外一种是实现Runnable接口。这2种方式在执行完任务之后都无法获取执行结果,如果需要获取执行结果,就必须通过共享变量或者使用线程通信的方式来达到效果,这样使用起来就比较麻烦。自Java 5起,就提供了Callable和Future,通过它们可以在任务执行完毕之后得到任务执行结果。

方式①举例:继承Thread类,实现run()方法,调用start()方法启动线程

public class ThreadSample extends Thread {
	@Override
	public void run() {
		System.out.println(this.getName() + " do some work...");
	}

	public static void mAIn(String[] args) {
		ThreadSample threadSample = new ThreadSample();
		threadSample.setName("thread-a");
		threadSample.start();
	}
}
start()方法调用后并不是立即执行多线程代码,而是使得该线程变为Ready状态,等待CPU分配执行时间。

方式②举例:实现Runnable接口,实现run()方法,将实例对象传入Thread构造方法

public class ThreadSample implements Runnable {
	@Override
	public void run() {
		System.out.println(Thread.currentThread().getName() + " do some work...");
	}

	public static void main(String[] args) {
		Thread threadSample = new Thread(new ThreadSample(), "thread-b");
		threadSample.start();
	}
}

方式③举例:实现Callable接口和FutureTask对象组合

public class ThreadSample implements Callable<Integer> {
	@Override
	public Integer call() {
		int result = 0;
		for (int i = 0; i <= 10; i++) {
			result++;
		}
		return result;
	}

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		//1、实例化Callable对象
		ThreadSample callableSample = new ThreadSample();
		//2、创建装载线程的FutureTask对象
		FutureTask<Integer> ft = new FutureTask<Integer>(callableSample);
		//3、启动线程
		Thread thread = new Thread(ft, "thread-callable");
		thread.start();
		//4、获取返回结果
		Integer result = ft.get();
		System.out.println("result = " + result);
	}
}

与使用Runnable相比,Callable功能更强大

  • 可以有返回值,支持泛型的返回值,借助FutureTask类获取返回值;
  • 可以捕获程序执行过程中的异常。

方式④举例:线程池实现

public class ThreadSample implements Callable<Integer> {
	@Override
	public Integer call() {
		int result = 0;
		for (int i = 0; i <= 10; i++) {
			result++;
		}
		return result;
	}

	public static void main(String[] args) throws InterruptedException, ExecutionException {
		ExecutorService services = Executors.newSingleThreadExecutor();
		Future<Integer> future = services.submit(new ThreadSample());
		System.out.println("result = " + future.get());
		services.shutdown();
	}
}

以上继承Thread类、实现Runnable接口、实现Callable接口三种方式中,理论上优先选用Runnable接口和Callable接口,如果有需要返回值则选用Callable接口实现方式。此外,无论何时,当看到这种形式的代码:

new Thread(runnable).start()

并且最终希望有一个更加灵活的执行策略时,都可以认真考虑使用Executor代替Thread。

使用线程池的好处

在线程池中执行任务线程,比起每个任务创建一个线程,有很多优势。

  • 减少系统开销。重用存在的线程,而不是创建新的线程,这可以在处理多请求时抵消线程创建、销毁产生的开销。
  • 提升请求响应性。在请求到达时,工作者线程已经存在,可以立即执行,因此提高了响应性。
  • 增强线程的可管理性。通过调整线程池的大小,可以充分利用CPU资源,同时可以防止过多的线程相互竞争资源,导致应用程序耗尽内存或者失败。线程池可以对线程资源进行统一分配、调优和监控。

线程池的工作流程

Java线程池的核心实现类是ThreadPoolExecutor类,任务提交到线程池时,具体处理由ThreadPoolExecutor类的execute()方法执行。当一个新任务提交到线程池时,线程池的处理流程如下:

①判断核心线程池里的线程是否都在执行任务,如果不是,创建一个新的工作线程来执行任务。如果是,则进行下一步流程。

②判断阻塞队列是否已满,如果没满,则将新提交的任务存储在阻塞队列中。如果满,则进行下一步流程。

③判断线程池中的线程是否都处于工作状态,如果没有,则创建一个新的工作线程来执行任务。如果线程池中所有的线程都处于工作状态,则交给饱和策略来处理这个任务。

 

ThreadPoolExecutor通用的构造函数为:

 

参数说明如下

corePoolSize

  • 线程池基本大小。提交一个任务到线程池时,线程池会创建一个新的线程来执行任务。新任务提交时,当目前线程数小于corePoolSize,即使有空闲的线程可以执行该任务,也会创建新的线程。
  • 如果线程池中的线程数已经大于或等于corePoolSize,则不会创建新的线程。
  • 当一个ThreadPoolExecutor被初始创建后,所有核心线程并非立即开始,而是等到有任务提交的时刻。但如果调用了prestartAllCoreThreads()方法,所有核心线程会立即启动。

maximuPoolSize

  • 线程池允许创建的最大线程数。当阻塞队列满,且线程数小于maximumPoolSize时,便可以创建新的线程执行任务。
  • 如果使用无界阻塞队列,该参数无效。

keepAliveTime

  • 线程池的工作线程空闲后,保持存活的时间。如果任务多且任务执行时间较短,可以调大该值,提高线程利用率。
  • 如果一个线程已经闲置的时间超过了存活时间,它将成为一个被回收的候选者,如果当前的池的大小超过了的corePoolSize,线程池会终止它。

unit

  • 与keepAliveTime相关联的时间单位。可选值有DAYS、HOURS、MINUTES、毫秒、微妙、纳秒。

workQueue

  • 用于保存等待执行的任务的阻塞队列。ThreadPoolExecutor允许提供一个BlockingQueue来持有等待执行的任务。任务排队有3种基本方法:无限队列、有限队列、同步移交。队列的选择和很多其他的配置参数都有关系,比如池的大小等。ThreadPoolExecutor推荐以下几种阻塞队列。
  • LinkedBlockingQueue:线程安全的阻塞队列,先进先出(FIFO)。可以指定容量(有限队列),也可以不指定(无限队列),不指定的话默认最大是Integer.MAX_VALUE。如果所有的工作者线程都处于忙碌状态,新提交的任务将会在队列中等候。如果新的任务持续快速到达,超过了它们被执行的速度,队列会无限制地增加。线程池中能创建的最大线程数为corePoolSize指定的值。
  • ArrayBlockingQueue:数组实现的有界阻塞队列,先进先出(FIFO)。线程池中能创建的最大线程数为maximumPoolSize指定的值。有界队列有助于避免资源耗尽,当队列满时,如果还有新的任务到达,将根据饱和策略(也称拒绝策略)进行处理。对于一个有界队列,队列的长度与池的长度必须一起调节。一个大队列和一个小池,可以控制对内存和CPU的使用,也可以减少上下文切换,但相应的吞吐量也会减小。
  • SynchronousQueue:SynchronousQueue并不是一个真正的队列,而是一种管理直接在线程间移交信息的机制。当新任务到达时,如果所有工作线程都处于忙碌状态,且线程池数量小于maximumPoolSize,就会创建一个新的线程。否则根据饱和策略处理。只有池是无限的,或者可以接受任务被拒绝,SynchronousQueue才是一个有实际价值的选择。
  • PriorityBlokingQueue: 一个支持优先级的无界阻塞队列 。使用该队列,线程池中能创建的最大线程数为corePoolSize。

threadFactory

  • 线程池创建线程时使用的线程工厂,可以不指定该参数,使用默认的线程工厂Executors.defaultThreadFactory()。

handler

饱和策略,也称拒绝策略。当有限队列满且线程池满的情况下,新的任务到达后,饱和策略将进行处理。有以下几种:

  • ThreadPoolExecutor.AbortPolicy()

抛出RejectedExecutionException异常。默认策略。调用者可以捕获抛出的异常,进行相应的处理。

  • ThreadPoolExecutor.CallerRunsPolicy()

不会丢弃任务,也不会抛出异常,由向线程池提交任务的线程来执行该任务。

  • ThreadPoolExecutor.DiscardPolicy()

丢弃当前的任务

  • ThreadPoolExecutor.DiscardOldestPolicy()

丢弃最旧的任务(最先提交而没有得到执行的任务),并执行当前任务。

 

ThreadPoolExecutor执行流程如下

 

①当新任务提交时,如果当前线程池中的线程数小于corePoolSize,则创建新的线程处理。

②如果线程池中的线程大于或等于corePoolSize,且BlockingQueue未满,则将新任务加入BlockingQueue。

③如果BlockingQueue已满,且线程池中的线程数小于maximumPoolSize,则创建新的工作线程来执行任务。

④如果当前运行的线程大于或等于maximumPoolSize,将执行饱和策略。即调用
RejectedExecutionHandler.rejectExecution()方法。

几种常见的线程池

Executors提供了一些静态工厂方法创建的常见线程池。

  • newFixedThreadPool

创建一个固定长度的线程池,每当提交一个任务就创建一个线程,直到达到线程池最大长度,这时线程池长度不再变化。如果一个线程异常退出,线程池会补充一个新的线程。

  • newCachedThreadPool

创建一个可缓存的线程池,不会对池的长度做限制。如果线程池长度超过需求,它可以灵活地回收空闲的线程;当需求增加时,它可以灵活地添加新的线程。

  • newSingleThreadExecutor

创建一个单线程的executor,只创建唯一的工作者线程来执行任务,如果这个线程异常结束,会有一个新的取代它。

  • newScheduledThreadPool

创建一个定长的线程池,支持定时执行任务。

这几种线程池中,newFixedThreadPool和newSingleThreadExecutor默认使用无线队列LinkedBlockingQueue。newCachedThreadPool使用了同步移交队列SynchronousQueue。newScheduledThreadPool使用了DelayedWorkQueue阻塞队列。

newCachedThreadPool的corePoolSize为0,maximumPoolSize为Integer.MAX_VALUE,其他几种线程池corePoolSize与maximumPoolSize一样大。

线程池的状态与生命周期

线程池有5种状态,在ThreadPoolExecutor 源码中有定义。

  • RUNNING : 线程池最初创建后的初始状态,该状态的线程池既能接受新提交的任务 ,又能处理阻塞队列中任务。
  • SHUTDOWN: 调用shutdown()方法后进入该状态。该状态的线程池不能接收新提交的任务 ,但是能处理阻塞队列中的任务。
  • STOP: 调用shutdownNow()方法后进入该状态。该状态的线程池不接受新提交的任务 ,也不处理在阻塞队列中的任务 ,还会中断正在执行的任务。
  • TIDYING: 当所有的任务都已终止,工作线程数为0的状态。线程池进入该状态后会调用 terminated() 钩子方法进入TERMINATED 状态。
  • TERMINATED: 在terminated()钩子方法执行完后进入该状态。

 

调用线程池的shutdown()或者shutdownNow()方法可以关闭线程池,遍历线程池中工作线程,逐个调用interrupt方法来中断线程。

Shutdown()方法与shutdownNow()的特点:

Shutdown()方法将线程池的状态设置为SHUTDOWN状态,只会中断空闲的工作线程。

shutdownNow()方法将线程池的状态设置为STOP状态,会中断所有工作线程,不管工作线程是否空闲。

调用两者中任何一种方法,都会使isShutdown()方法的返回值为true;线程池中所有的任务都关闭后,isTerminated()方法的返回值为true。

通常使用shutdown()方法关闭线程池,如果不要求任务一定要执行完,则可以调用shutdownNow()方法。

确定线程池的大小

线程池合理的长度取决于所要执行的任务特征以及程序所部署的系统环境,一般根据这二者因素使用配置文件提供或者通过CPU核数N:

N = Runtime.getRuntime().availableProcessors();

动态计算而不是硬编码在代码中。主要是避免线程池过大或过小这两种极端情况。如果线程池过大,会导致CPU和内存资源竞争,频繁的上下文切换,任务延迟,甚至资源耗尽。如果线程池过小,会造成CPU和内存资源未充分利用,任务处理的吞吐量减小。

对于任务特征来说,需要分清楚是计算密集型任务,还是IO密集型任务或是混合型任务。

计算密集型也称为CPU密集型,意思就是该任务需要大量运算,而没有阻塞,CPU一直全速运行。CPU密集型任务只有在多核CPU上才可能得到加速,即scale up,通过多线程程序享受到增加CPU核数带来的好处。

IO密集型,即该任务需要大量的IO操作,例如网络连接、数据库连接等等,执行任务过程中会有大量的阻塞。在单线程上运行IO密集型任务会导致浪费大量的CPU运算能力浪费在等待。

对于计算密集型任务,原则是配置尽可能少的线程数,通常建议以下计算方式设置线程池大小来获得最优利用率:

N(线程数) = N(CPU核数)+ 1

对于IO密集型任务,考虑的因素会多一些,原则是因为较多的时间处于IO阻塞,不能处理新的任务,所有线程数尽可能大一些,通常建议是:

N(线程数) = 2 x N(CPU核数) + 1

或者更精确的:

N(线程数) = N(CPU核数) x U x (1 + W/C)

其中U表示CPU使用率,W/C表示IO等待时间与计算时间的比率,这个不需要太精确,只需要一个估算值。例如,4核CPU,CPU使用率80%,IO等待时间1秒,计算时间0.1秒,那么线程数为:4.8 x 11≈53。一些文章中还提到这种计算方式:

N(线程数) = N(CPU核数) x U / (1 - f)

其中U表示CPU使用率,f表示阻塞系数,即IO等待时间与任务执行总时间的比率:W/(W + C)。根据上面的例子计算出线程数为:4.8/0.09≈53。两种计算方式的结果是很相近的。

 

以上的计算方式和建议尽可以作为理论参考值,实际业务中可能并不完全按照这个计算值来设置。可以根据对线程池各项参数的监控,来确定一个合理的值。ThreadPoolExecutor提供的一些可用于获取监控的参数方法如下:

  • getTaskCount():线程池需要执行的任务数量,包括已经执行完的、未执行的和正在执行的。
  • getCompletedTaskCount():线程池在运行过程中已完成的任务数量 ,completedTaskCount <= taskCount。
  • getLargestPoolSize():线程池曾经创建过的最大线程数量 ,通过这个数据可以知道线程池是否满过。如等于线程池的最大大小 ,则表示线程池曾经满了。
  • getPoolSize(): 线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以线程池的线程数量只增不减 。
  • getActiveCount():获取活动的线程数。

此外,可以通过继承ThreadPoolExecutor并重写它的 beforeExecute(),afterExecute() 和 terminated()方法,我们可以在任务执行前,执行后和线程池关闭前做一些统计、日志输出等等操作,以帮助我们更好地监控到线程池的运行状态。



Tags:Java   点击:()  评论:()
声明:本站部分内容及图片来自互联网,转载是出于传递更多信息之目的,内容观点仅代表作者本人,不构成投资建议。投资者据此操作,风险自担。如有任何标注错误或版权侵犯请与我们联系,我们将及时更正、删除。
▌相关推荐
JavaScript的异步编程常见模式
在JavaScript中,异步编程是一种处理长时间运行操作(如网络请求或I/O操作)的常见方式。它允许程序在等待这些操作完成时继续执行其他任务,从而提高应用程序的响应性和性能。JavaS...【详细内容】
2024-04-12  Search: Java  点击:(2)  评论:(0)  加入收藏
17 个你需要知道的 JavaScript 优化技巧
你可能一直在使用JavaScript搞开发,但很多时候你可能对它提供的最新功能并不感冒,尽管这些功能在无需编写额外代码的情况下就可以解决你的问题。作为前端开发人员,我们必须了解...【详细内容】
2024-04-03  Search: Java  点击:(6)  评论:(0)  加入收藏
你不可不知的 15 个 JavaScript 小贴士
在掌握如何编写JavaScript代码之后,那么就进阶到实践&mdash;&mdash;如何真正地解决问题。我们需要更改JS代码使其更简单、更易于阅读,因为这样的程序更易于团队成员之间紧密协...【详细内容】
2024-03-21  Search: Java  点击:(27)  评论:(0)  加入收藏
Oracle正式发布Java 22
Oracle 正式发布 Java 22,这是备受欢迎的编程语言和开发平台推出的全新版本。Java 22 (Oracle JDK 22) 在性能、稳定性和安全性方面进行了数千种改进,包括对Java 语言、其API...【详细内容】
2024-03-21  Search: Java  点击:(10)  评论:(0)  加入收藏
构建一个通用灵活的JavaScript插件系统?看完你也会!
在软件开发中,插件系统为应用程序提供了巨大的灵活性和可扩展性。它们允许开发者在不修改核心代码的情况下扩展和定制应用程序的功能。本文将详细介绍如何构建一个灵活的Java...【详细内容】
2024-03-20  Search: Java  点击:(20)  评论:(0)  加入收藏
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  Search: Java  点击:(15)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20  Search: Java  点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18  Search: Java  点击:(27)  评论:(0)  加入收藏
对JavaScript代码压缩有什么好处?
对JavaScript代码进行压缩主要带来以下好处: 减小文件大小:通过移除代码中的空白符、换行符、注释,以及缩短变量名等方式,可以显著减小JavaScript文件的大小。这有助于减少网页...【详细内容】
2024-03-13  Search: Java  点击:(2)  评论:(0)  加入收藏
跨端轻量JavaScript引擎的实现与探索
一、JavaScript 1.JavaScript语言JavaScript是ECMAScript的实现,由ECMA 39(欧洲计算机制造商协会39号技术委员会)负责制定ECMAScript标准。ECMAScript发展史: 2.JavaScript...【详细内容】
2024-03-12  Search: Java  点击:(2)  评论:(0)  加入收藏
▌简易百科推荐
Java 8 内存管理原理解析及内存故障排查实践
本文介绍Java8虚拟机的内存区域划分、内存垃圾回收工作原理解析、虚拟机内存分配配置,以及各垃圾收集器优缺点及场景应用、实践内存故障场景排查诊断,方便读者面临内存故障时...【详细内容】
2024-03-20  vivo互联网技术    Tags:Java 8   点击:(15)  评论:(0)  加入收藏
如何编写高性能的Java代码
作者 | 波哥审校 | 重楼在当今软件开发领域,编写高性能的Java代码是至关重要的。Java作为一种流行的编程语言,拥有强大的生态系统和丰富的工具链,但是要写出性能优异的Java代码...【详细内容】
2024-03-20    51CTO  Tags:Java代码   点击:(24)  评论:(0)  加入收藏
在Java应用程序中释放峰值性能:配置文件引导优化(PGO)概述
译者 | 李睿审校 | 重楼在Java开发领域,优化应用程序的性能是开发人员的持续追求。配置文件引导优化(Profile-Guided Optimization,PGO)是一种功能强大的技术,能够显著地提高Ja...【详细内容】
2024-03-18    51CTO  Tags:Java   点击:(27)  评论:(0)  加入收藏
Java生产环境下性能监控与调优详解
堆是 JVM 内存中最大的一块内存空间,该内存被所有线程共享,几乎所有对象和数组都被分配到了堆内存中。堆被划分为新生代和老年代,新生代又被进一步划分为 Eden 和 Survivor 区,...【详细内容】
2024-02-04  大雷家吃饭    Tags:Java   点击:(57)  评论:(0)  加入收藏
在项目中如何避免和解决Java内存泄漏问题
在Java中,内存泄漏通常指的是程序中存在一些不再使用的对象或数据结构仍然保持对内存的引用,从而导致这些对象无法被垃圾回收器回收,最终导致内存占用不断增加,进而影响程序的性...【详细内容】
2024-02-01  编程技术汇  今日头条  Tags:Java   点击:(70)  评论:(0)  加入收藏
Java中的缓存技术及其使用场景
Java中的缓存技术是一种优化手段,用于提高应用程序的性能和响应速度。缓存技术通过将计算结果或者经常访问的数据存储在快速访问的存储介质中,以便下次需要时可以更快地获取。...【详细内容】
2024-01-30  编程技术汇    Tags:Java   点击:(73)  评论:(0)  加入收藏
JDK17 与 JDK11 特性差异浅谈
从 JDK11 到 JDK17 ,Java 的发展经历了一系列重要的里程碑。其中最重要的是 JDK17 的发布,这是一个长期支持(LTS)版本,它将获得长期的更新和支持,有助于保持程序的稳定性和可靠性...【详细内容】
2024-01-26  政采云技术  51CTO  Tags:JDK17   点击:(90)  评论:(0)  加入收藏
Java并发编程高阶技术
随着计算机硬件的发展,多核处理器的普及和内存容量的增加,利用多线程实现异步并发成为提升程序性能的重要途径。在Java中,多线程的使用能够更好地发挥硬件资源,提高程序的响应...【详细内容】
2024-01-19  大雷家吃饭    Tags:Java   点击:(107)  评论:(0)  加入收藏
这篇文章彻底让你了解Java与RPA
前段时间更新系统的时候,发现多了一个名为Power Automate的应用,打开了解后发现是一个自动化应用,根据其描述,可以自动执行所有日常任务,说的还是比较夸张,简单用了下,对于office、...【详细内容】
2024-01-17  Java技术指北  微信公众号  Tags:Java   点击:(99)  评论:(0)  加入收藏
Java 在 2023 年仍然流行的 25 个原因
译者 | 刘汪洋审校 | 重楼学习 Java 的过程中,我意识到在 90 年代末 OOP 正值鼎盛时期,Java 作为能够真正实现这些概念的语言显得尤为突出(尽管我此前学过 C++,但相比 Java 影响...【详细内容】
2024-01-10  刘汪洋  51CTO  Tags:Java   点击:(78)  评论:(0)  加入收藏
站内最新
站内热门
站内头条