JDK中的线程池
Posted CSBase
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JDK中的线程池相关的知识,希望对你有一定的参考价值。
概述
大家都知道阻塞队列,趁着这个机会正好看看它的一个应用案例ThreadPoolExecutor,同样是出自Doug Lea之手。
JDK中的线程池
通常一些复杂或者构造代价比较大的对象我们都会放到一个“池子”中缓存来重用。很多时候在服务端一请求一线程的处理模型中,需要大量的线程来处理来自客户端的请求,如果每次都创建线程然后用完就销毁这显然是一种浪费。JVM并不负责线程调度,它的线程和OS的线程是一种1对1的关系,也就是实际上是OS线程的一个映射。启动一个线程不仅仅有JNI调用,还有系统调用,这本身就是一个很消耗资源的操作。所以通常的做法是创建一个线程池里面缓存很多线程,同时配置一个任务队列,由各个线程不断的消费队列里的任务,当然这是一个大概的描述,实际上里面有很多控制细节。
线程池种核心的几个参数是coreSize、maxSize、queueSize、keepAliveTime、allowCoreThreadTimeOut,这几个参数控制线程池的行为。假设我们新创建了一个线程池并且业务陆续到来,如果当前线程池里的线程数未达到coreSize,不管现有的线程是否空闲都会优先创建线程直到达到coreSize,这是线程池的第一阶段。当然了如果要构造的线程数比较多,系统刚刚开始工作的时候可能会受此影响,所以它提供了一个prestartAllCoreThreads的方法用于提前初始化好coreSize个线程来达到一个预热的效果。如果线程数达到coreSize的时候仍然有大量的任务到来,此时任务队列就起作用了,新的任务全部会入到队列中,这是线程池的第二个阶段,此时的状态如下图所示:
假设我们的系统业务量很大,仍然有源源不断的任务提交过来以至于队列达到了上限,这个时候入队会失败,然后线程池便会再次创建线程直到达到maxSize,这是第三个阶段。当线程数达到了maxSize并且队列也满了的时候继续添加任务便会触发回调RejectedExecutionHandler,也就是交给用户来处理这种异常情况,而JDK默认也提供了几种失败处理策略,大概有丢弃当前任务,由提交线程处理任务等等。
线程池里的线程并不是有增无减的,当系统比较空闲的时候,线程池也会缩减当前的线程数,这里系统空闲的标准由keepAliveTime参数来控制。当系统中的线程数大于coreSize的时候,如果有线程idle时间超过keepAliveTime则会销毁该线程直到达到coreSize。但是ThreadPoolExecutor也提供了销毁核心线程的方法,由allowCoreThreadTimeOut来控制,注意这个参数是一个布尔类型表示是否允许销毁核心线程,默认是false也就是不销毁。
Tomcat线程池
tomcat中的线程池和JDK线程池的策略稍微有些不同。仔细推敲JDK线程池实现的方式可能会觉得并不是太完美,当队列里有多余的任务并且无空闲线程的时候,这个时候比较好的做法可能是继续增加线程直到达到maxSize而不是等到队列满了以后再做此操作。因此Tomcat自己实现了优先增加线程的策略,它的实现方式其实并不复杂(并没有重写ThreadPoolExecutor),核心的思想是构造一个任务队列去控制offer的状态(成功或者失败),因为ThreadPoolExecutor是根据offer的状态来控制是否要增加线程的(达到coreSize以后)。除此之外还增加了一个统计当前空闲线程个数的属性submittedCount(JDK提供的方法持有锁的时间比较长,不适合高并发)。
线程池的问题
由于线程也是需要消耗资源的,所以并不是可以无限制的创建,而且如果有大量线程的话会给OS调度带来很大压力,直接带来的问题就是系统的吞吐量上不来,现象就是sys CPU和cs值很高。所以线程池也跟GC一样需要一个调优的过程。另外一个问题是线程共享一个队列,造成热点锁,也会影响系统的性能。我们有个系统为了避免这个问题底层的架构模型采用的是一个线程一个队列的方式,同时配合异步和事件机制,这种处理方式效率还是很高的(有点类似Netty的做法)。但有个问题是线程一定不能处理耗时的操作比如磁盘或者网络IO,否则会出现事件拥堵,因为它不像传统的线程池那样即使有几个线程处理慢也没关系,还有其他线程会同步消费队列里的任务。
以上是关于JDK中的线程池的主要内容,如果未能解决你的问题,请参考以下文章