Java中的线程池用过吧?来说说你是怎么理解线程池吧?

线程池

使用线程池的目的

先说一下我们为什么要使用线程池?

  • 线程是稀缺资源,不能频繁的创建。而且创建和销毁线程也是比较占用系统开销的。
  • 为了做到解耦,线程的创建与执行任务分开,方便对线程进行维护。
  • 为了复用,前面也说了创建和销毁线程比较耗系统开销,那么创建出来线程放到一个池子里,可以给其他任务进行复用。

线程池是如何一步一步创建的

第一版

正常的我们在创建一个线程去执行任务的时候是这样的:

new Thread(r).start();

但是这是最基本的方式,我们的项目中有可能很多地方都需要创建一个新的线程。这个使用为了减少重复代码,我们会把这段创建线程的代码放的一个工具类里面,然后对外提供工具方法,使用的时候直接调用此方法即可。

第二版

/**
 * 先定义接口(任务执行器)
 */
public interface Executor {
    /**
     * 执行任务
     * @param runnable  线程任务
     */
    void execute(Runnable runnable);
}
/**
 * 实现:直接创建线程。
 */
class ExecutorImpl implements Executor {
    public void execute(Runnable r) {
        new Thread(r).start();
    }
}

这种方式实现了创建线程的代码的复用,但是并没有实现线程资源的复用,当有1000个地方需要线程的时候,会创建1000个线程。

第三版

为了实现资源也复用,增加一个阻塞队列,当来了创建线程的任务的时候,先放到队列里,然后再用一个线程(Worker),来处理任务。这样就完成了线程资源的复用了,全程只有一个线程在来回的复用,一直在处理队列中的任务。

通过上面的方式,实现了线程资源的复用,并且也起到提交任务和处理任务之间的解耦。但是只有一个线程处理任务,会有瓶颈的,所以具体需要多少线程来处理任务最好是根据具体的业务场景来确定,这样我们把这个值,设置成一个参数,当创建线程池的时候传入,就叫corePoolSize吧。

而且任务队列最好也要有容量,但也应该是根据业务场景来配置容量,而且任务队列还可以定制一些规则,例如:按照一定的规则出队。所以我们把任务队列也配置成参数,在创建线程池的时候传入。参数名称就叫:workQueue吧。

当队列中任务满了之后,任务就会被抛弃,但是如果是重要业务任务,还不能抛弃,所以,当队列中任务满了之后,在线程池没有资源处理任务的时候,拒绝策略,我们也根据业务场景来确定,这样也在创建的时候传入一种拒绝策略,参数名称就叫:rejectedExecutionHandler

继续优化

虽然多了上面的三个参数后效果优化了不少,但是还可以继续优化:

  • 并不用上来就创建corePoolSize数量的线程,我们可以增加了一个变量workCount,来记录已经创建出来了工作线程,这样在初始化的时候只有workCount<corePoolSize的时候,我们才创建线程来执行任务,当workCount>CorePoolSize的时候,再来了任务,就去进队列。
  • 在增加拒绝策略的时候,我定义一个接口:RejectedExecutionHandler,然后使用者可以自己去实现这个接口,来完成自己的拒绝策略。
  • 增加一个线程工厂的入参:ThreadFactory,这样保证每次创建线程的时候不用手动去创建线程了,而是通过ThreadFactory来获取线程,并且也可以增加一些线程的标识。

第四版

虽然说第三版的线程池已经可以应对日常工作中的情况了,但是还是不够有弹性,所谓的弹性就是指,在任务提交频繁时应该处理能力提高,任务提交不频繁时处理能力应该降低。

上面这版线程池就不够弹性。

如果某个时间段,任务提交量剧增,这个时候,corePoolSize和队列都满了,再来提交任务就只能走拒绝策略了。

你或许会想到,那我可以增大corePoolSize的值,这样就会创建出来更多的线程来处理任务,但是这个任务提交量剧增,只是某个时间段,过了这个时间段之后,创建出来这么多的线程,可以大部分都会是空闲的状态。这样也是浪费资源了。

这样就导致了一个两难的情况,corePoolSize的值设置太大了也不好,设置太小了也不好。

这个时候,为让线程池做到弹性伸缩,我们可以为他再添加一个参数:maximumPoolSize,这个参数代表的意思是最大线程数。

corePoolSizeworkQueue都满了的时候,新提交的任务仍然可以创建新线程来进行处理,这些超过corePoolSize创建出来的线程,被称为非核心线程。当corePoolSize与非核心线程数量的和等于maximumPoolSize再执行拒绝策略。

通过这样的方式,corePoolSize,负责平时情况的线程使用量,maximumPoolSize负责提交任务高峰时的,临时扩充容量。

但是目前这样的方式只是考虑到了提交任务量高峰时期的扩充,但这个高峰期只是暂时的,过了这个高峰期,非核心线程一直放着也是浪费资源,所以我们再设定一个非核心线程的空闲活跃时间的参数:keepAliveTime,这样当非核心线程数,空闲时间超过这个值就销毁线程,释放资源。

这一版的线程池,做到了在提交任务高峰时可临时扩容,低谷时又可及时回收非核心线程,从而节省资源。真正的做到了收放自如。

通过上面几版线程池的改进,最终改进成了和Java中的线程池原理基本相似了。这样也能更透彻的理解创建线程池时要传入的这几个关键参数的意义了。

下面说几个线程池常见的考察点

Java中的线程池的阻塞队列都有哪几种

ArrayBlockingQueue : 有界队列,按照阻塞的先后顺序访问队列,默认情况下不保证线程公平的访问队列~如果要保证公平性,会降低一定的吞吐量。底层是靠ReentrantLock来实现的,每一个方法中,都是靠ReentrantLock加锁来完成阻塞。

 public ArrayBlockingQueue(int capacity) {
     this(capacity, false);
 }
 public ArrayBlockingQueue(int capacity, boolean fair) {
     if (capacity <= 0)
         throw new IllegalArgumentException();
     this.items = new Object[capacity];
     lock = new ReentrantLock(fair);
     notEmpty = lock.newCondition();
     notFull =  lock.newCondition();
 }
  • LinkedBlockingQueue:基于链表的阻塞队列,按照先进先出的顺序排列,在不设置队列长度的时候默认Integer.MAX_VALUE。所以认为当不设置队列长度时,LinkedBlockingQueue为无界队列。当指定了队列长度后变为有界队列,通常LinkedBlockingQueue的吞吐量要高于ArrayBlockingQueue;
  • SynchronousQueue:一个不存储元素的阻塞队列,每个插入操作必须等到另一个线程调用移除操作,否则插入操作一直处于阻塞状态。在不允许任务在队列中等待的时候可以使用此队列。
  • DelayQueue:延迟获取元素队列,按照指定时间后获取,为无界阻塞队列。
  • PriorityBlockingQueue:优先级排序队列,按照一定的优先级对任务进行排序,默认是小顶堆。
  • LinkedBlockingDeque:基于链表的双端阻塞队列。

Java提供了哪几个默认的线程池,为什么实际开发中不建议直接使用?

  • Executors.newCachedThreadPool();:阻塞队列采用的SynchronousQueue,所以是不存储等待任务的,并且最大线程数的值是Integer.MAX_VALUE。所以当任务提交量高峰时,相当于无限制的创建线程。并且空闲时间是60秒,QPS高峰期最终会将服务器资源耗尽,所以真正实际应用中不建议使用。
public static ExecutorService newCachedThreadPool() {
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                  60L, TimeUnit.SECONDS,
                                  new SynchronousQueue<Runnable>());
}
  • Executors.newFixedThreadPool(int nThreads);:可重用固定线程数的线程池,源码如下:
public static ExecutorService newFixedThreadPool(int nThreads, ThreadFactory threadFactory) {
    return new ThreadPoolExecutor(nThreads, nThreads,
                                  0L, TimeUnit.MILLISECONDS,
                                  new LinkedBlockingQueue<Runnable>(),
                                  threadFactory);
}

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

核心线程数和最大线程数相等的线程池,并且阻塞任务队列还是一个无界队列,这样,当处理任务的线程数量达到核心线程数时,再提交的任务都会进行到阻塞队列里,但是阻塞队列是无界的,这样就提交任务高峰期有可能会造成任务一直堆积在队列里,超出内存容量最终导致内存溢出。

  • Executors.newScheduledThreadPool(int corePoolSize);:一个定长线程池,支持定时及周期性任务执行,这个线程池的最大线程数也是Integer.MAX_VALUE,可以理解为会无限创建线程。存在将资源耗尽的风险,所以一般场景下不建议使用。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
    return new ScheduledThreadPoolExecutor(corePoolSize);
}
public ScheduledThreadPoolExecutor(int corePoolSize) {
    super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
          new DelayedWorkQueue());
}
  • Executors.newSingleThreadExecutor();
    这种线程池,会创建一个线程数固定是1的线程池,并且任务队列是无界的LinkedBlockingQueue,存在任务队列无限添加造成OOM的风险。
public static ExecutorService newSingleThreadExecutor() {
    return new FinalizableDelegatedExecutorService
        (new ThreadPoolExecutor(1, 1,
                                0L, TimeUnit.MILLISECONDS,
                                new LinkedBlockingQueue<Runnable>()));
}
  • Executors.newWorkStealingPool();:一个具有抢占式操作的线程池。
    参数中传入的是一个线程并发的数量,这里和之前就有很明显的区别,前面4种线程池都有核心线程数、最大线程数等等,而这就使用了一个并发线程数解决问题。这个线程池不会保证任务的顺序执行,也就是 WorkStealing 的意思,抢占式的工作,哪个线程抢到任务就执行。
public static ExecutorService newWorkStealingPool(int parallelism) {
    return new ForkJoinPool
        (parallelism,
         ForkJoinPool.defaultForkJoinWorkerThreadFactory,
         null, true);
}

Java中的线程池提供了哪几种拒绝策略

  • AbortPolicy:该策略默认是饱和策略。当不能在处理提交的任务时,直接抛出RejectedExecutionException,使用者可以自行捕获此异常。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    throw new RejectedExecutionException("Task " + r.toString() +
                                      " rejected from " +
                                      e.toString());
}
  • CallerRunsPolicy:该策略是在线程池处理不了任务时,交给提交任务的主线程去处理任务,主线程在处理任务的时候,不能在提交任务了,这样线程池就可以有时间去处理堆积的任务了。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
    if (!e.isShutdown()) {
        r.run();
    }
}
  • DiscardOldestPolicy:该策略是,抛弃最老的任务,然后再尝试提交任务,若阻塞队列使用PriorityBlockingQueue优先级队列,将会导致优先级最高的任务被抛弃,所以在阻塞队列为PriorityBlockingQueue时,不建议使用此策略。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
     if (!e.isShutdown()) {
         e.getQueue().poll();
         e.execute(r);
     }
 }
  • DiscardPolicy:这是一个比较任性的策略,当线程池处理不了任务时,直接抛弃,再来了新任务也直接抛弃。
public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
}
  • RejectHandler
    直接抛拒绝异常。
public void rejectedExecution(Runnable r, java.util.concurrent.ThreadPoolExecutor executor) {
    throw new RejectedExecutionException();
}

Java中线程池核心线程数与最大线程数该如何配置

可以根据提交的任务不同,将线程池分开。

  • 处理CPU密集型任务,线程数量应该较少,可为N(CPU核数)+1N(CPU核数) * 2,因为此时线程一定调度到某个CPU执行,若任务本身是CPU绑定的任务,那么过多的线程只会增加线程切换的开销,而不能提升吞吐量,但可能需要较长队列做缓冲。
  • I/O密集型任务,执行较慢、数量不大的IO任务,要考虑更多线程数,而无需太大队列。相比计算型任务,需多一些线程,要结合具体的 I/O 阻塞时长考虑。

但是实际情况下,有些任务是既耗CPU资源,又占用I/O资源的。所以这个时候可以采用类似美团技术提出方案,实时的监控线程池状态信息,然后对线程池的数据进行调整。

在监控线程池的时候可以使用如下几个线程池属性:

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

推荐阅读更多精彩内容