Java线程池,从使用到原理

转载自  Java线程池,从使用到原理

线程池的技术背景

在面向对象编程中,创建和销毁对象是很费时间的,因为创建一个对象要获取内存资源或者其它更多资源。在Java中更是如此,虚拟机将试图跟踪每一个对象,以便能够在对象销毁后进行垃圾回收。

所以提高服务程序效率的一个手段就是尽可能减少创建和销毁对象的次数,特别是一些很耗资源的对象创建和销毁。如何利用已有对象来服务就是一个需要解决的关键问题,其实这就是一些”池化资源”技术产生的原因。

例如Android中常见到的很多通用组件一般都离不开”池”的概念,如各种图片加载库,网络请求库,即使Android的消息传递机制中的Meaasge当使用Meaasge.obtain()就是使用的Meaasge池中的对象,因此这个概念很重要。本文将介绍的线程池技术同样符合这一思想。

线程池的优点:

1.重用线程池中的线程,减少因对象创建,销毁所带来的性能开销;

2.能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;

3.能够多线程进行简单的管理,使线程的使用简单、高效。

线程池框架Executor

java中的线程池是通过Executor框架实现的,Executor 框架包括类:Executor,Executors,ExecutorService,ThreadPoolExecutor ,Callable和Future、FutureTask的使用等。

Executor: 所有线程池的接口,只有一个方法。

public interface Executor {void execute(Runnable command);
}

ExecutorService: 增加Executor的行为,是Executor实现类的最直接接口。

Executors: 提供了一系列工厂方法用于创先线程池,返回的线程池都实现了ExecutorService 接口。

ThreadPoolExecutor:线程池的具体实现类,一般用的各种线程池都是基于这个类实现的。 构造方法如下:

public ThreadPoolExecutor(int corePoolSize,int maximumPoolSize,long keepAliveTime,TimeUnit unit,BlockingQueue<Runnable> workQueue) {this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory(), defaultHandler);
}

corePoolSize:线程池的核心线程数,线程池中运行的线程数也永远不会超过 corePoolSize个,默认情况下可以一直存活。可以通过设置allowCoreThreadTimeOut为True,此时核心线程数就是0,此时keepAliveTime控制所有线程的超时时间。

maximumPoolSize:线程池允许的最大线程数;

keepAliveTime: 指的是空闲线程结束的超时时间;

unit :是一个枚举,表示 keepAliveTime 的单位;

workQueue:表示存放任务的BlockingQueue<Runnable>队列。

BlockingQueue:阻塞队列(BlockingQueue)是java.util.concurrent下的主要用来控制线程同步的工具。如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间才会被唤醒继续操作。 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。具体的实现类有LinkedBlockingQueue,ArrayBlockingQueued等。一般其内部的都是通过Lock和Condition来实现阻塞和唤醒。

线程池的工作过程如下:

线程池刚创建时,里面没有一个线程。任务队列是作为参数传进来的。不过,就算队列里面有任务,线程池也不会马上执行它们。

当调用 execute() 方法添加一个任务时,线程池会做如下判断:

如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

当一个线程完成任务时,它会从队列中取下一个任务来执行。

当一个线程无事可做,超过一定的时间(keepAliveTime)时,线程池会判断,如果当前运行的线程数大于 corePoolSize,那么这个线程就被停掉。所以线程池的所有任务完成后,它最终会收缩到 corePoolSize 的大小。

线程池的创建和使用

生成线程池采用了工具类Executors的静态方法,以下是几种常见的线程池。

SingleThreadExecutor:单个后台线程 (其缓冲队列是无界的)

public static ExecutorService newSingleThreadExecutor() {return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1, 1,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}

创建一个单线程的线程池。这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

FixedThreadPool:只有核心线程的线程池,大小固定 (其缓冲队列是无界的) 。

public static ExecutorService newFixedThreadPool(int nThreads){return new ThreadPoolExecutor(nThreads, nThreads,0L, TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>());
}

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

CachedThreadPool:无界线程池,可以进行自动线程回收。

public static ExecutorService newCachedThreadPool() {return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}

如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue是一个是缓冲区为1的阻塞队列。

ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

public static ScheduledExecutorServicenewScheduledThreadPool(int corePoolSize) {return new ScheduledThreadPoolExecutor(corePoolSize);
}

创建一个周期性执行任务的线程池。如果闲置,非核心线程池会在DEFAULT_KEEPALIVEMILLIS时间内回收。

线程池最常用的提交任务的方法有两种:

execute:

ExecutorService.execute(Runnable runable);

submit:

FutureTask task = ExecutorService.submit(Runnable runnable);
FutureTask<T> task = ExecutorService.submit(Runnable runnable,T Result);
FutureTask<T> task = ExecutorService.submit(Callable<T> callable);

submit(Callable callable)的实现,submit(Runnable runnable)同理。(AbstractExecutorService.java)

public <T> Future<T> submit(Callable<T> task) {if (task == null) throw new NullPointerException();RunnableFuture<T> ftask = newTaskFor(task);execute(ftask);return ftask;
}

可以看出submit开启的是有返回结果的任务,会返回一个FutureTask对象,这样就能通过get()方法得到结果。submit最终调用的也是execute(Runnable runable),submit只是将Callable对象或Runnable封装成一个FutureTask对象,因为FutureTask是个Runnable,所以可以在execute中执行。关于Callable对象和Runnable怎么封装成FutureTask对象,见Callable和Future、FutureTask的使用。

线程池实现的原理

如果只讲线程池的使用,那这篇博客没有什么大的价值,充其量也就是熟悉Executor相关API的过程。线程池的实现过程没有用到Synchronized关键字,用的都是Volatile,Lock和同步(阻塞)队列,Atomic相关类,FutureTask等等,因为后者的性能更优。理解的过程可以很好的学习源码中并发控制的思想。

在开篇提到过线程池的优点是可总结为以下三点:

线程复用

控制最大并发数

管理线程

1.线程复用过程

理解线程复用原理首先应了解线程生命周期。

在线程的生命周期中,它要经过新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)和死亡(Dead)5种状态。

Thread通过new来新建一个线程,这个过程是是初始化一些线程信息,如线程名,id,线程所属group等,可以认为只是个普通的对象。调用Thread的start()后Java虚拟机会为其创建方法调用栈和程序计数器,同时将hasBeenStarted为true,之后调用start方法就会有异常。

处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。当线程获取CPU后,run()方法会被调用。不要自己去调用Thread的run()方法。之后根据CPU的调度在就绪——运行——阻塞间切换,直到run()方法结束或其他方式停止线程,进入dead状态。

所以实现线程复用的原理应该就是要保持线程处于存活状态(就绪,运行或阻塞)。接下来来看下ThreadPoolExecutor是怎么实现线程复用的。

在ThreadPoolExecutor主要Worker类来控制线程的复用。看下Worker类简化后的代码,这样方便理解:

private final class Worker implements Runnable{final Thread thread;Runnable firstTask;Worker(Runnable firstTask) {this.firstTask = firstTask;this.thread = getThreadFactory().newThread(this);}public void run() {runWorker(this);}final void runWorker(Worker w) {Runnable task = w.firstTask;w.firstTask = null;while (task != null || (task = getTask()) != null) {       task.run();        }}
}

Worker是一个Runnable,同时拥有一个thread,这个thread就是要开启的线程,在新建Worker对象时同时新建一个Thread对象,同时将Worker自己作为参数传入TThread,这样当Thread的start()方法调用时,运行的实际上是Worker的run()方法,接着到runWorker()中,有个while循环,一直从getTask()里得到Runnable对象,顺序执行。getTask()又是怎么得到Runnable对象的呢?

依旧是简化后的代码:

private Runnable getTask() {if (一些特殊情况){return null;}Runnable r = workQueue.take();return r;
}

这个workQueue就是初始化ThreadPoolExecutor时存放任务的BlockingQueue队列,这个队列里的存放的都是将要执行的Runnable任务。因为BlockingQueue是个阻塞队列,BlockingQueue.take()得到如果是空,则进入等待状态直到BlockingQueue有新的对象被加入时唤醒阻塞的线程。所以一般情况Thread的run()方法就不会结束,而是不断执行从workQueue里的Runnable任务,这就达到了线程复用的原理了。

2.控制最大并发数

那Runnable是什么时候放入workQueue?Worker又是什么时候创建,Worker里的Thread的又是什么时候调用start()开启新线程来执行Worker的run()方法的呢?有上面的分析看出Worker里的runWorker()执行任务时是一个接一个,串行进行的,那并发是怎么体现的呢?

很容易想到是在execute(Runnable runnable)时会做上面的一些任务。看下execute里是怎么做的。

execute:

简化后的代码

public void execute(Runnable command) {if (command == null)throw new NullPointerException();int c = ctl.get();//当前线程数 < corePoolSizeif (workerCountOf(c) < corePoolSize) {//直接启动新的线程if (addWorker(command, true))return;c = ctl.get();}// 活动线程数 >= corePoolSize// runState为RUNNING && 队列未满if (isRunning(c) && workQueue.offer(command)) {int recheck = ctl.get();//再次检验是否为RUNNING状态//非RUNNING状态,则从workQueue中移除任务并拒绝if (!isRunning(recheck) && remove(command)){reject(command);//采用线程池指定的策略拒绝任务 // 两种情况:// 1.非RUNNING状态拒绝新的任务// 2.队列满了启动新的线程失败(workCount > maximumPoolSize                }else if (workerCountOf(recheck) == 0){addWorker(null, false);}}else if (!addWorker(command, false))reject(command);
}

addWorker:

简化后的代码

private boolean addWorker(Runnable firstTask, boolean core) {int wc = workerCountOf(c);if (wc >= (core ? corePoolSize : maximumPoolSize)){return false;}Worker w = new Worker(firstTask);final Thread t = w.thread;t.start();
}

根据代码再来看上面提到的线程池工作过程中的添加任务的情况:

* 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;
* 如果正在运行的线程数量大于或等于corePoolSize,那么将这个任务放入队列;
* 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;
* 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

通过addWorker如果成功创建新的线程成功,则通过start()开启新线程,同时将firstTask作为这个Worker里的run()中执行的第一个任务。

虽然每个Worker的任务是串行处理,但如果创建了多个Worker,因为共用一个workQueue,所以就会并行处理了。

所以根据corePoolSize和maximumPoolSize来控制最大并发数。大致过程可用下图表示。

上面的讲解和图来可以很好的理解的这个过程。

3.管理线程

通过线程池可以很好的管理线程的复用,控制并发数,以及销毁等过程,线程的复用和控制并发上面已经讲了,而线程的管理过程已经穿插在其中了,也很好理解。

在ThreadPoolExecutor有个ctl的AtomicInteger变量。通过这一个变量保存了两个内容:

所有线程的数量、每个线程所处的状态,其中低29位存线程数,高3位存runState,通过位运算来得到不同的值。

private final AtomicInteger ctl = newAtomicInteger(ctlOf(RUNNING, 0));//得到线程的状态
private static int runStateOf(int c){return c & ~CAPACITY; 
}//得到Worker的数量
private static int workerCountOf(int c)  {return c & CAPACITY; 
}//判断线程是否在运行
private static boolean isRunning(int c) {return c < SHUTDOWN;
}

这里主要通过shutdown和shutdownNow()来分析线程池的关闭过程。首先线程池有五种状态来控制任务添加与执行。主要介绍以下三种:

RUNNING状态:线程池正常运行,可以接受新的任务并处理队列中的任务;

SHUTDOWN状态:不再接受新的任务,但是会执行队列中的任务;

STOP状态:不再接受新任务,不处理队列中的任务。

shutdown这个方法会将runState置为SHUTDOWN,会终止所有空闲的线程,而仍在工作的线程不受影响,所以队列中的任务仍会被执行。

shutdownNow方法将runState置为STOP。和shutdown方法的区别,这个方法会终止所有的线程,所以队列中的任务也不会被执行了。

总结

通过对ThreadPoolExecutor源码的分析,从总体上了解了线程池的创建,任务的添加,执行等过程,熟悉这些过程,使用线程池就会更轻松了。

而从中学到的一些对并发控制,以及生产者——消费者模型任务处理的使用,对以后理解或解决其他相关问题会有很大的帮助。



本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/329133.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

聊聊HTTPS和SSL/TLS协议

要说清楚 HTTPS 协议的实现原理&#xff0c;至少需要如下几个背景知识。1. 大致了解几个基本术语&#xff08;HTTPS、SSL、TLS&#xff09;的含义2. 大致了解 HTTP 和 TCP 的关系&#xff08;尤其是“短连接”VS“长连接”&#xff09;3. 大致了解加密算法的概念&#xff08;尤…

php事件编程,PHP相应button中onclick事件的案例分析

PHP相应button中onclick事件的案例分析发布时间&#xff1a;2020-11-10 11:28:31来源&#xff1a;亿速云阅读&#xff1a;71作者&#xff1a;小新小编给大家分享一下PHP相应button中onclick事件的案例分析&#xff0c;相信大部分人都还不怎么了解&#xff0c;因此分享这篇文章给…

Java 中Timer和TimerTask 定时器和定时任务使用的例子

转载自 Java 中Timer和TimerTask 定时器和定时任务使用的例子 这两个类使用起来非常方便&#xff0c;可以完成我们对定时器的绝大多数需求 Timer类是用来执行任务的类&#xff0c;它接受一个TimerTask做参数 Timer有两种执行任务的模式,最常用的是schedule,它可以以两种方式执…

复制一个5G文件只需要两秒,全网最牛方法!

文章来至 微信公众号&#xff1a;中国黑客联盟 很多时候我们在复制比较大的文件的时候是一件多么痛苦的事情&#xff0c;因为少的几分钟多则十几分钟&#xff0c;这样的等待是我们无法容忍的&#xff01;那么今天我们就教大家如何快速的复制电脑大文件&#xff01; 首先我…

.NET Core也可以使用MongoDB了

可能是由于.NET Core还不是正式版的缘故吧&#xff0c;MongoDB的官方Driver(http://mongodb.github.io/mongo-csharp-driver/)一直不支持.NET Core&#xff0c;这给想在.NET Core上尝试MongoDB带来了不便&#xff0c;本人就是其中之一 &#xff1a;&#xff09; 于是Fork了官方…

Java多线程系列--“JUC线程池”06之 Callable和Future

转载自 Java多线程系列--“JUC线程池”06之 Callable和FutureCallable 和 Future 简介Callable 和 Future 是比较有趣的一对组合。当我们需要获取线程的执行结果时&#xff0c;就需要用到它们。Callable用于产生结果&#xff0c;Future用于获取结果。 1. Callable Callable 是…

php array分组,php数组分组简单例子

在php网站开发过程中有时候需要把结果集进行分组&#xff0c;使用php的内置函数array_chunk就可以实现 代码如下复制代码$teamsarray(1,2,3,4,5,6,7,8,9);$teamsarray_chunk($teams,2);print_r($teams);/*Array([0] > Array([0] > 1[1] > 2)[1] > Array([0] > 3…

SuperSocket源码解析之开篇

一 简介 官方介绍&#xff1a;SuperSocket 是一个轻量级, 跨平台而且可扩展的 .Net/Mono Socket 服务器程序框架。你无须了解如何使用 Socket, 如何维护 Socket 连接和 Socket 如何工作&#xff0c;但是你却可以使用 SuperSocket 很容易的开发出一款 Socket 服务器端软件&#…

Java守护线程概述

转载自 Java守护线程概述Java的线程分为两种&#xff1a;User Thread(用户线程)、DaemonThread(守护线程)。 只要当前JVM实例中尚存任何一个非守护线程没有结束&#xff0c;守护线程就全部工作&#xff1b;只有当最后一个非守护线程结束是&#xff0c;守护线程随着JVM一同结束…

php while循环次数,php while循环得到循环次数

php while循环得到循环次数复制代码 代码如下:$link mysql_connect(localhost,root,pwd);mysql_select_db(db);$sql "select region_id,local_name from regions where region_grade1";$result mysql_query($sql);$i 0;while ($row mysql_fetch_assoc($result)) {…

EntityFramework和EntityFramework.Extended使用说明——性能,语法和产生的sql

环境说明:EntityFramework 6.1.3和.Net Framework4.5性能注意事项:https://msdn.microsoft.com/zh-cn/library/cc853327.aspx比较精髓的一点:查询执行的各个阶段中的准备查询,每个唯一查询一次。包括编写查询命令、基于模型和映射元数据生成命令树和定义所返回数据的形状的成本…

三个水桶(看了三遍,想了五遍!)

转载至&#xff1a;微信公众号&#xff1a; 创业励志网 一 有位木匠砍了一树&#xff0c;把它做了三个木桶。 一个装粪&#xff0c;就叫粪桶&#xff0c;众人躲着&#xff1b; 一个装水&#xff0c;就叫水桶&#xff0c;众人用着&#xff1b; 一个装酒&#xff0c;就叫酒桶&…

nginx解析php失败,为什么nginx不能解析php?

只运行过这些代码yum install -y vim wget zip unzip git httpd php php-mysql php-odbc php-ldap php-gd php-mbstring php-xml php-xml-rpc php-bcmath libjpeg* mariadb ;rpm -Uvh http://nginx.org/packages/centos/7/noarch/RPMS/nginx-release-centos-7-0.el7.ngx.noarc…

.NET Core amp; ASP.NET Core 1.0在Redhat峰会上正式发布

众所周知&#xff0c;Red Hat和微软正在努力使.NET Core成为Red Hat企业版Linux (RHEL)系统上的一流开发平台选项。这个团队已经一起工作好几个月了,RHEL对.NET有许多需求。今天在RedHat 峰会DevNation 上宣布了.NET Core & ASP.NET Core 1.0 RTM。Red Hat有一个新的关于在…

任务调度(三)——Timer的替代品ScheduledExecutorService简介

转载自 任务调度(三)——Timer的替代品ScheduledExecutorService简介先前的两篇博文《任务调度(一)——jdk自带的Timer》和《任务调度(二)——jdk自带的Timer 动态修改任务执行计划》中&#xff0c;简单介绍了一下Timer&#xff0c;可以实现几本的功能&#xff0c;但是在多线程…

jquery实现动态五角星评分

先上代码&#xff0c;最后附属上我的实现思路&#xff0c;新手做的bug多&#xff0c;大牛勿喷&#xff1a;请看代码&#xff1a; ☆☆☆☆☆<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-trans…

php中时间轴,PHP时间轴函数

26 04 2013PHP时间轴函数Yinchiang | 0我们会经常看见现在许多网站的留言或者评论的时间变得十分有趣&#xff0c;刚刚、3分钟前、1天前等等人性化的时间轴好吧&#xff0c;我承认这个函数是转载的&#xff0c;部分按照自己的需求修改了一点点。/*** 时间轴函数&#xff0c;单位…

开源Asp.Net Core小型社区系统

前言 盼星星盼月亮&#xff0c;Asp.Net Core终于发布啦&#xff01;&#xff01; Asp.Net发布时我还在上初中&#xff0c;没有赶上。但是Asp.Net Core我从beta版本便一直关注。最初项目名叫Asp.Net VNext&#xff0c;然后改名叫Asp.Net 5。最煎熬的是RC1发布后&#xff0c;官方…

Java多线程干货系列(1):Java多线程基础

转载自 Java多线程干货系列&#xff08;1&#xff09;&#xff1a;Java多线程基础前言 多线程并发编程是Java编程中重要的一块内容&#xff0c;也是面试重点覆盖区域&#xff0c;所以学好多线程并发编程对我们来说极其重要&#xff0c;下面跟我一起开启本次的学习之旅吧。 正文…

php accesscontrolalloworigin,设置Access-Control-Allow-Origin实现跨域访问

这篇文章主要介绍了Ajax 设置Access-Control-Allow-Origin实现跨域访问,非常不错&#xff0c;具有参考借鉴价值&#xff0c;需要的朋友可以参考下ajax跨域访问是一个老问题了&#xff0c;解决方法很多&#xff0c;比较常用的是JSONP方法&#xff0c;JSONP方法是一种非官方方法&…