从线程池走到dubbo源码

title: dubbo源码阅读线程池感想

date: 2018-05-28 22:51:10

tags:

- Java

- Dubbo

- Netty

categories: 后端

copyright: true

引言

合理利用线程池能够带来三个好处。

第一:降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。

第二:提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行。

第三:提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。但是要做到合理的利用线程池,必须对其原理了如执掌。

<!-- more -->

线程池的使用

线程池的创建

我们可以通过ThreadPoolExecutor来创建一个线程池

newThreadPoolExecutor(corePoolSize,maximumPoolSize,keepAliveTime,milliseconds,runnableTaskQueue,handler);

创建一个线程池需要输入几个参数:

corePoolSize(线程池的基本大小):当提交一个任务到线程池时,线程池会创建一个线程来执行任务,即使其他空闲的基本线程能够执行新任务也会创建线程,等到需要执行的任务数大于线程池基本大小时就不再创建。如果调用了线程池的prestartAllCoreThreads方法,线程池会提前创建并启动所有基本线程。

runnableTaskQueue(任务队列):用于保存等待执行的任务的阻塞队列。 可以选择以下几个阻塞队列。

ArrayBlockingQueue:是一个基于数组结构的有界阻塞队列,此队列按 FIFO(先进先出)原则对元素进行排序。

LinkedBlockingQueue:一个基于链表结构的阻塞队列,此队列按FIFO (先进先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。静态工厂方法Executors.newFixedThreadPool()使用了这个队列。

SynchronousQueue:一个不存储元素的阻塞队列。每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态,吞吐量通常要高于LinkedBlockingQueue,静态工厂方法Executors.newCachedThreadPool使用了这个队列。

PriorityBlockingQueue:一个具有优先级的无限阻塞队列。

maximumPoolSize(线程池最大大小):线程池允许创建的最大线程数。如果队列满了,并且已创建的线程数小于最大线程数,则线程池会再创建新的线程执行任务。值得注意的是如果使用了无界的任务队列这个参数就没什么效果。

ThreadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。

RejectedExecutionHandler(饱和策略):当队列和线程池都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是AbortPolicy,表示无法处理新任务时抛出异常。以下是JDK1.5提供的四种策略。

AbortPolicy:直接抛出异常。

CallerRunsPolicy:只用调用者所在线程来运行任务。

DiscardOldestPolicy:丢弃队列里最近的一个任务,并执行当前任务。

DiscardPolicy:不处理,丢弃掉。

当然也可以根据应用场景需要来实现RejectedExecutionHandler接口自定义策略。如记录日志或持久化不能处理的任务。

keepAliveTime(线程活动保持时间):线程池的工作线程空闲后,保持存活的时间。所以如果任务很多,并且每个任务执行的时间比较短,可以调大这个时间,提高线程的利用率。

TimeUnit(线程活动保持时间的单位):可选的单位有天(DAYS),小时(HOURS),分钟(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

向线程池提交任务

我们可以使用execute提交的任务,但是execute方法没有返回值,所以无法判断任务是否被线程池执行成功。通过以下代码可知execute方法输入的任务是一个Runnable类的实例。

threadsPool.execute(newRunnable() {

@Override

publicvoidrun() {

// TODO Auto-generated method stub

           }

       });

我们也可以使用submit 方法来提交任务,它会返回一个future,那么我们可以通过这个future来判断任务是否执行成功,通过future的get方法来获取返回值,get方法会阻塞住直到任务完成,而使用get(long timeout, TimeUnit unit)方法则会阻塞一段时间后立即返回,这时有可能任务没有执行完。

Future<Object>future=executor.submit(harReturnValuetask);

try{

Objects=future.get();

}catch(InterruptedExceptione) {

// 处理中断异常

}catch(ExecutionExceptione) {

// 处理无法执行任务异常

}finally{

// 关闭线程池

executor.shutdown();

}

线程池的关闭

我们可以通过调用线程池的shutdown或shutdownNow方法来关闭线程池,它们的原理是遍历线程池中的工作线程,然后逐个调用线程的interrupt方法来中断线程,所以无法响应中断的任务可能永远无法终止。但是它们存在一定的区别,shutdownNow首先将线程池的状态设置成STOP,然后尝试停止所有的正在执行或暂停任务的线程,并返回等待执行任务的列表,而shutdown只是将线程池的状态设置成SHUTDOWN状态,然后中断所有没有正在执行任务的线程。

只要调用了这两个关闭方法的其中一个,isShutdown方法就会返回true。当所有的任务都已关闭后,才表示线程池关闭成功,这时调用isTerminaed方法会返回true。至于我们应该调用哪一种方法来关闭线程池,应该由提交到线程池的任务特性决定,通常调用shutdown来关闭线程池,如果任务不一定要执行完,则可以调用shutdownNow。

线程池的分析

流程分析: 线程池的主要工作流程如下图:

)

从上图我们可以看出,当提交一个新任务到线程池时,线程池的处理流程如下:

首先线程池判断基本线程池是否已满?没满,创建一个工作线程来执行任务。满了,则进入下个流程。

其次线程池判断工作队列是否已满?没满,则将新提交的任务存储在工作队列里。满了,则进入下个流程。

最后线程池判断整个线程池是否已满?没满,则创建一个新的工作线程来执行任务,满了,则交给饱和策略来处理这个任务。

源码分析。上面的流程分析让我们很直观的了解了线程池的工作原理,让我们再通过源代码来看看是如何实现的。线程池执行任务的方法如下:

publicvoidexecute(Runnablecommand) {

if(command==null)

thrownewNullPointerException();

//如果线程数小于基本线程数,则创建线程并执行当前任务

if(poolSize>=corePoolSize||!addIfUnderCorePoolSize(command)) {

//如线程数大于等于基本线程数或线程创建失败,则将当前任务放到工作队列中。

if(runState==RUNNING&&workQueue.offer(command)) {

if(runState!=RUNNING||poolSize==0)

ensureQueuedTaskHandled(command);

       }

//如果线程池不处于运行中或任务无法放入队列,并且当前线程数量小于最大允许的线程数量,

则创建一个线程执行任务。

elseif(!addIfUnderMaximumPoolSize(command))

//抛出RejectedExecutionException异常

reject(command);// is shutdown or saturated

   }

}

工作线程。线程池创建线程时,会将线程封装成工作线程Worker,Worker在执行完任务后,还会无限循环获取工作队列里的任务来执行。我们可以从Worker的run方法里看到这点:

publicvoidrun() {

try{

Runnabletask=firstTask;

firstTask=null;

while(task!=null||(task=getTask())!=null) {

runTask(task);

task=null;

           }

}finally{

workerDone(this);

     }

}

合理的配置线程池

要想合理的配置线程池,就必须首先分析任务特性,可以从以下几个角度来进行分析:

任务的性质:CPU密集型任务,IO密集型任务和混合型任务。

任务的优先级:高,中和低。

任务的执行时间:长,中和短。

任务的依赖性:是否依赖其他系统资源,如数据库连接。

任务性质不同的任务可以用不同规模的线程池分开处理。CPU密集型任务配置尽可能小的线程,如配置Ncpu+1个线程的线程池。IO密集型任务则由于线程并不是一直在执行任务,则配置尽可能多的线程,如2*Ncpu。混合型的任务,如果可以拆分,则将其拆分成一个CPU密集型任务和一个IO密集型任务,只要这两个任务执行的时间相差不是太大,那么分解后执行的吞吐率要高于串行执行的吞吐率,如果这两个任务执行时间相差太大,则没必要进行分解。我们可以通过Runtime.getRuntime().availableProcessors()方法获得当前设备的CPU个数。

优先级不同的任务可以使用优先级队列PriorityBlockingQueue来处理。它可以让优先级高的任务先得到执行,需要注意的是如果一直有优先级高的任务提交到队列里,那么优先级低的任务可能永远不能执行。

执行时间不同的任务可以交给不同规模的线程池来处理,或者也可以使用优先级队列,让执行时间短的任务先执行。

依赖数据库连接池的任务,因为线程提交SQL后需要等待数据库返回结果,如果等待的时间越长CPU空闲时间就越长,那么线程数应该设置越大,这样才能更好的利用CPU。

建议使用有界队列,有界队列能增加系统的稳定性和预警能力,可以根据需要设大一点,比如几千。有一次我们组使用的后台任务线程池的队列和线程池全满了,不断的抛出抛弃任务的异常,通过排查发现是数据库出现了问题,导致执行SQL变得非常缓慢,因为后台任务线程池里的任务全是需要向数据库查询和插入数据的,所以导致线程池里的工作线程全部阻塞住,任务积压在线程池里。如果当时我们设置成无界队列,线程池的队列就会越来越多,有可能会撑满内存,导致整个系统不可用,而不只是后台任务出现问题。当然我们的系统所有的任务是用的单独的服务器部署的,而我们使用不同规模的线程池跑不同类型的任务,但是出现这样问题时也会影响到其他任务。

线程池的监控

通过线程池提供的参数进行监控。线程池里有一些属性在监控线程池的时候可以使用

taskCount:线程池需要执行的任务数量。

completedTaskCount:线程池在运行过程中已完成的任务数量。小于或等于taskCount。

largestPoolSize:线程池曾经创建过的最大线程数量。通过这个数据可以知道线程池是否满过。如等于线程池的最大大小,则表示线程池曾经满了。

getPoolSize:线程池的线程数量。如果线程池不销毁的话,池里的线程不会自动销毁,所以这个大小只增不+ getActiveCount:获取活动的线程数。

通过扩展线程池进行监控。通过继承线程池并重写线程池的beforeExecute,afterExecute和terminated方法,我们可以在任务执行前,执行后和线程池关闭前干一些事情。如监控任务的平均执行时间,最大执行时间和最小执行时间等。这几个方法在线程池里是空方法。如下:

protectedvoidbeforeExecute(Threadt,Runnabler) { }

dubbo对线程池的使用

dubbo-common 模块的threadpool包下体现,如下图所示:

ThreadPool

com.alibaba.dubbo.common.threadpool.ThreadPool ,线程池接口。代码如下:

//@SPI("fixed")注解,Dubbo SPI扩展点,默认为"fixed"。

@SPI("fixed")

publicinterfaceThreadPool{

/**

* @Adaptive({Constants.THREADPOOL_KEY}) 注解,基于Dubbo SPI Adaptive机制,加载对应的线程池实现,使用URL.threadpool属性。

* getExecutor(url)方法,获得对应的线程池的执行器

*

*/

@Adaptive({Constants.THREADPOOL_KEY})

ExecutorgetExecutor(URLurl);

}

FixedThreadPool

com.alibaba.dubbo.common.threadpool.support.fixed.FixedThreadPool ,实现ThreadPool接口,固定大小线程池,启动时建立线程,不关闭,一直持有。代码如下:

publicclassFixedThreadPoolimplementsThreadPool{

@Override

publicExecutorgetExecutor(URLurl) {

//线程名

Stringname=url.getParameter(Constants.THREAD_NAME_KEY,Constants.DEFAULT_THREAD_NAME);

//线程数

intthreads=url.getParameter(Constants.THREADS_KEY,Constants.DEFAULT_THREADS);

//队列数

intqueues=url.getParameter(Constants.QUEUES_KEY,Constants.DEFAULT_QUEUES);

//创建执行器

returnnewThreadPoolExecutor(threads,threads,0,TimeUnit.MILLISECONDS,

/**

* 根据不同的队列数,使用不同的队列实现:

* queues == 0,SynchronousQueue对象。

* queues < 0,LinkedBlockingQueue对象。

* queues > 0,带队列数的LinkedBlockingQueue对象。

*/

queues==0?newSynchronousQueue<Runnable>() :

(queues<0?newLinkedBlockingQueue<Runnable>()

:newLinkedBlockingQueue<Runnable>(queues)),

/**

* 创建NamedThreadFactory对象,用于生成线程名

* 创建AbortPolicyWithReport对象,用于当任务添加到线程池中被拒绝时。

*/

newNamedInternalThreadFactory(name,true),newAbortPolicyWithReport(name,url));

   }

}

推荐阅读:

《Java并发包中的同步队列SynchronousQueue实现原理》

《Java阻塞队列ArrayBlockingQueue和LinkedBlockingQueue实现原理分析》

《聊聊并发(七) -- Java中的阻塞队列》

CachedThreadPool

com.alibaba.dubbo.common.threadpool.support.cached.CachedThreadPool ,实现ThreadPool接口,缓存线程池,空闲一定时长,自动删除,需要时重建。代码如下:

publicclassCachedThreadPoolimplementsThreadPool{

@Override

publicExecutorgetExecutor(URLurl) {

//线程池名

Stringname=url.getParameter(Constants.THREAD_NAME_KEY,Constants.DEFAULT_THREAD_NAME);

//核心线程数

intcores=url.getParameter(Constants.CORE_THREADS_KEY,Constants.DEFAULT_CORE_THREADS);

//最大线程数

intthreads=url.getParameter(Constants.THREADS_KEY,Integer.MAX_VALUE);

//队列数

intqueues=url.getParameter(Constants.QUEUES_KEY,Constants.DEFAULT_QUEUES);

//线程存活时长

intalive=url.getParameter(Constants.ALIVE_KEY,Constants.DEFAULT_ALIVE);

//创建执行器

returnnewThreadPoolExecutor(cores,threads,alive,TimeUnit.MILLISECONDS,

queues==0?newSynchronousQueue<Runnable>() :

(queues<0?newLinkedBlockingQueue<Runnable>()

:newLinkedBlockingQueue<Runnable>(queues)),

newNamedInternalThreadFactory(name,true),newAbortPolicyWithReport(name,url));

   }

}

LimitedThreadPool

com.alibaba.dubbo.common.threadpool.support.limited.LimitedThreadPool ,实现ThreadPool接口,可伸缩线程池,但池中的线程池只会增长不会收缩。只增长不收缩的目的是为了避免收缩时突然来了大流量引起的性能问题。代码如下:

publicclassLimitedThreadPoolimplementsThreadPool{

@Override

publicExecutorgetExecutor(URLurl) {

//线程池名

Stringname=url.getParameter(Constants.THREAD_NAME_KEY,Constants.DEFAULT_THREAD_NAME);

//核心线程数

intcores=url.getParameter(Constants.CORE_THREADS_KEY,Constants.DEFAULT_CORE_THREADS);

//最大线程数

intthreads=url.getParameter(Constants.THREADS_KEY,Constants.DEFAULT_THREADS);

//队列数

intqueues=url.getParameter(Constants.QUEUES_KEY,Constants.DEFAULT_QUEUES);

/**

* 和CachedThreadPool实现是基本一致的,差异点在alive == Integer.MAX_VALUE,空闲时间无限大,即不会删除。

*/

returnnewThreadPoolExecutor(cores,threads,Long.MAX_VALUE,TimeUnit.MILLISECONDS,

queues==0?newSynchronousQueue<Runnable>() :

(queues<0?newLinkedBlockingQueue<Runnable>()

:newLinkedBlockingQueue<Runnable>(queues)),

newNamedInternalThreadFactory(name,true),newAbortPolicyWithReport(name,url));

   }

}

AbortPolicyWithReport

com.alibaba.dubbo.common.threadpool.support.AbortPolicyWithReport ,实现 java.util.concurrent.ThreadPoolExecutor.AbortPolicy,拒绝策略实现类。打印JStack,分析线程状态 代码如下:

/**

* AbortPolicyWithReport实现自ThreadPoolExecutor.AbortPolicy,拒绝策略实现类,

* 打印JStack,分析线程状态。

*/

publicclassAbortPolicyWithReportextendsThreadPoolExecutor.AbortPolicy{

protectedstaticfinalLoggerlogger=LoggerFactory.getLogger(AbortPolicyWithReport.class);

/**

* 线程名

*/

privatefinalStringthreadName;

/**

* URL 对象

*/

privatefinalURLurl;

/**

* 最后打印时间

*/

privatestaticvolatilelonglastPrintTime=0;

/**

* 信号量,大小为1。

*/

privatestaticSemaphoreguard=newSemaphore(1);

publicAbortPolicyWithReport(StringthreadName,URLurl) {

this.threadName=threadName;

this.url=url;

   }

@Override

publicvoidrejectedExecution(Runnabler,ThreadPoolExecutore) {

/**

* 打印告警日志

*/

Stringmsg=String.format("Thread pool is EXHAUSTED!"+

" Thread Name: %s, Pool Size: %d (active: %d, core: %d, max: %d, largest: %d), Task: %d (completed: %d),"+

" Executor status:(isShutdown:%s, isTerminated:%s, isTerminating:%s), in %s://%s:%d!",

threadName,e.getPoolSize(),e.getActiveCount(),e.getCorePoolSize(),e.getMaximumPoolSize(),e.getLargestPoolSize(),

e.getTaskCount(),e.getCompletedTaskCount(),e.isShutdown(),e.isTerminated(),e.isTerminating(),

url.getProtocol(),url.getIp(),url.getPort());

logger.warn(msg);

// 打印 JStack,分析线程状态。

dumpJStack();

//抛出 RejectedExecutionException 异常

thrownewRejectedExecutionException(msg);

   }

privatevoiddumpJStack() {

longnow=System.currentTimeMillis();

//每 10 分钟,打印一次。

//dump every 10 minutes

if(now-lastPrintTime<10*60*1000) {

return;

       }

//获得信号量

if(!guard.tryAcquire()) {

return;

       }

//创建线程池,后台执行打印JStack

Executors.newSingleThreadExecutor().execute(newRunnable() {

@Override

publicvoidrun() {

//获得路径

StringdumpPath=url.getParameter(Constants.DUMP_DIRECTORY,System.getProperty("user.home"));

SimpleDateFormatsdf;

//获得系统

StringOS=System.getProperty("os.name").toLowerCase();

// window system don't support ":" in file name

if(OS.contains("win")){

sdf=newSimpleDateFormat("yyyy-MM-dd_HH-mm-ss");

}else{

sdf=newSimpleDateFormat("yyyy-MM-dd_HH:mm:ss");

               }

StringdateStr=sdf.format(newDate());

//获得输出流

FileOutputStreamjstackStream=null;

try{

jstackStream=newFileOutputStream(newFile(dumpPath,"Dubbo_JStack.log"+"."+dateStr));

//打印JStack

JVMUtil.jstack(jstackStream);

}catch(Throwablet) {

logger.error("dump jstack error",t);

}finally{

//释放信号量

guard.release();

//释放输出流

if(jstackStream!=null) {

try{

jstackStream.flush();

jstackStream.close();

}catch(IOExceptione) {

                       }

                   }

               }

//记录最后打印时间

lastPrintTime=System.currentTimeMillis();

           }

       });

   }

}

推荐阅读:

《如何使用jstack分析线程状态》

《控制并发线程数的Semaphore》

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343

推荐阅读更多精彩内容

  • 线程是每个应用都必须关系的事情,毕竟任何服务器的资源都是有限的,服务线程过少的容易发生阻塞,服务线程过多的话上下文...
    此鱼不得水阅读 5,993评论 3 7
  • 为什么使用线程池 当我们在使用线程时,如果每次需要一个线程时都去创建一个线程,这样实现起来很简单,但是会有一个问题...
    闽越布衣阅读 4,278评论 10 45
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,598评论 18 139
  • 先看官网两张图【引用来自官网】:image.png 官网说明: 1.首先 ReferenceConfig 类的 i...
    致虑阅读 1,021评论 0 2
  • 案例: ...
    郭颜阅读 828评论 0 2