线程池详解(一)

1.线程池的使用

Exector框架提供了各种类型的线程池,主要有以下几种方法:

public static ExecutorService newFixedThreadPool(int nThreads)
public static ExecutorService newSingleThreadExecutor()
public static ExecutorService newCachedThreadPool() 
public static ScheduledExecutorService newSingleThreadScheduledExecutor() 
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize)
  1. newFixedThreadPool(int nThreads): 该方法返回一个固定线程数量的线程池。该线程池中的线程数量始终不变。当有一个新的任务提交时,线程池中若有空闲线程,则立即执行;如果没有,则新的任务会被暂存在一个任务队列(LinkedBlockingQueue有序)中,等到有线程空闲的时候,便处理任务队列中的任务。
    2.newSingleThreadExecutor():该方法返回一个只有一个线程的线程池。若多余一个任务被提交到线程池,任务会被保存到一个任务队列(LinkedBlockingQueue)中,等到线程空闲,按先入先出的顺序执行队列中的任务。
    3.newCachedThreadPool() :该方法返回一个可根据实际情况调整线程数量的线程池。线程池中的数量不确定,但若有空闲的线程可以复用,则优先使用可复用的线程。若所有线程均在工作,又有新的任务提交,则会创建新的线程处理任务。所有线程在当前任务执行完毕后,返回线程池进行复用。
    4.newSingleThreadScheduledExecutor() :该方法返回一个ScheduledExecutorService对象,线程池的大小为1。ScheduledExecutorService接口在ExecutorService接口之上扩展了在给定时间执行某任务的功能,如在某个固定的延时之后执行,或者周期性执行某个任务。
    5.newScheduledThreadPool(int corePoolSize):该方法也返回一个ScheduledExecutorService对象,但可以指定线程的数量。
    固定大小线程池使用实例
public class FixedThreadPoolDemo {
    public static void main(String[] args) {
        ExecutorService es = Executors.newFixedThreadPool(5);
        for (int i=0;i<5;i++){
            es.submit(()->{
                System.out.println(System.currentTimeMillis()+"ThreadID:"+Thread.currentThread().getId());
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }
    }
}

计划任务
另一个值得注意的方法是newScheduledThreadPool(),它返回一个ScheduledExecutorService对象,可以根据时间需要对线程进行调度。它的一些方法如下:

public ScheduledFuture<?> schedule(Runnable command,
                                       long delay, TimeUnit unit);
public ScheduledFuture<?>  scheduleAtFixedRate(Runnable command,
                                                  long initialDelay,
                                                  long period,
                                                  TimeUnit unit);
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,
                                                     long initialDelay,
                                                     long delay,
                                                     TimeUnit unit);

schedule会在给定时间,对任务进行一次调度。
scheduleAtFixedRate任务调度的频率是一定的,它是以上一个任务开始执行时间为起点,在之后的period时间调度下一次任务。
scheduleWithFixedDelay以上一次任务结束以后的时间为起点,再经过delay时间进行任务调度。

实例代码

public class ScheduleAtFixedRate {
    public static void main(String[] args) {
        ScheduledExecutorService ses = Executors.newScheduledThreadPool(10);
        ses.scheduleAtFixedRate(()->{
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getId()+System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        },0,2, TimeUnit.SECONDS);

    }
}

注意:如果任务的执行时间超过调度时间的话,比如调度周期是2S,任务执行时间是8S,那么并不会出现多个任务堆叠到一起的情况,如果将上述代码的Thread.sleep改为8000,那么任务的执行周期就不是2S,而是8S。

2.核心线程池的内部实现

无论是newFixedThreadPool方法还是newSingleThreadExecutor方法还是newCachedThreadPool方法,其内部实现均使用了ThreadPoolExecutor类。
下面给出这三个线程池的实现方式

 public static ExecutorService newFixedThreadPool(int nThreads) {
        return new ThreadPoolExecutor(nThreads, nThreads,
                                      0L, TimeUnit.MILLISECONDS,
                                      new LinkedBlockingQueue<Runnable>());
    }
public static ExecutorService newSingleThreadExecutor() {
        return new FinalizableDelegatedExecutorService
            (new ThreadPoolExecutor(1, 1,
                                    0L, TimeUnit.MILLISECONDS,
                                    new LinkedBlockingQueue<Runnable>()));
    }
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

创建线程池主要通过ThreadPoolExecutor类完成。ThreadPoolExcetor有很多重载方法,通过参数最多的构造方法来理解创建线程池需要配置那些参数,构造方法如下:

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler)

1.corePoolSize:指定了线程池中的线程数量。
2.maximumPoolSize:指定了线程池中的最大线程数量。。
3.keepAliveTime:空闲线程存活时间。如果当前线程池的线程个数已经超过了corePoolSize,并且线程空闲时间超过了keepAliveTime的话,就会将这些空闲线程销毁,这样可以尽可能降低系统资源消耗。
4.unit:为keepAliveTime指定时间单位

  1. workQueue:任务队列。被提交但尚未执行的任务。
    6.threadFactory:创建线程的工厂类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。一般用默认的就行。
    7.handler:拒绝策略,当任务太多来不及处理时,如何拒绝任务。

workQueue和handler详解
workQueue
参数workQueue指被提交但未执行的任务队列,它是一个BlockingQueue接口的对象,仅用于存放Runnable对象。可以使用以下几种:

1.直接提交队列(SynchronousQueue)
SynchronousQueue是一种特殊的BlockingQueue。SynchronousQueue没有容量,每一个插入操作都要等待一个响应的删除操作,反之,每一个删除操作都要等待对应的插入操作。如果使用SynchronousQueue,提交的任务不会被真实保存,而总是将新任务提交给线程执行,如果没有空闲线程,则尝试创建新的线程,则尝试创建新的线程,如果线程数量达到最大值,则执行拒绝策略。因此,使用SynchronousQueue队列,通常要设置很大的maxumumPoolSize值,否则很容易执行拒绝策略

2.有界的任务队列(ArrayBlockingQueue)
ArrayBlockingQueue类的构造函数必须带一个容量参数,表示该队列的最大容量

public ArrayBlockingQueue(int capacity)

当使用有界的任务队列的时候,若有新的任务需要执行,如果线程池的实际线程数小于corePoolSize,则会优先创建新的线程;若大于corePoolSize,则会将新任务加入等待队列。若等待队列已满,无法加入,则在总线程数不大于maximumPoolSize的前提下,创建新的线程执行任务。若大于maximumPoolSize,则执行拒绝策略。可见,有界队列仅当任务队列装满时,才可能将线程数提升到corePoolSize以上。也就是说除非系统特别繁忙,否则要确保核心线程数维持在corePoolSize。

3.无界的任务队列 (LinkedBlockingQueue)
与有界任务队列相比,除非系统资源耗尽,否则无界的任务队列不存在任务入队失败的情况。当有新的任务到来时,系统线程数小于corePoolSize时,线程池会生成新的线程执行任务,当系统的线程数达到corePoolSize后,就不会继续增加了。若后续仍有新的任务加入,又没有空闲的线程资源,则任务直接进入队列等待,若任务创建和处理的速度差异很大,无界队列会保持一直增长,直到资源耗尽。
4.优先任务队列(PriorityBlockingQueue)
可以控制任务的执行先后顺序。它是一个特殊的无界队列。PriorityBlockingQueue类可以根据任务自身的优先级顺序先后执行,在确保系统性能的同时,也有很好额质量保证。

回顾newFixedThreadPool方法的实现,它返回了一个corePoolSize和maximumPoolSize大小一样的,并且使用LinkedBlockingQueue任务队列的线程池。对于固定大小的线程池而言,不存在线程数量动态变化,因此corePoolSize和maximumPoolSize大小相等,它使用无界队列存放无法执行的任务,当任务提交频繁时,该队列迅速增长,从而导致系统资源耗尽。
newSingleThreadExecutor方法返回的是单线程线程池,是newFixedThreadPool方法的一种退化,只是将线程池线程数量设为1。
newCachedThreadPool方法的corePoolSize为0,而maximumPoolSize为无穷大的线程池,这意味着没有任务时,该线程池内无线程,而当任务被提交时,该线程池会使用空闲线程执行任务,如果没有空闲线程,则将任务加入SynchronousQueue队列,而SynchronousQueue队列是一种直接提交队列,它会迫使线程池增加新的线程执行任务。当任务执行完毕后,由于corePoolSIze为0,空闲线程又会在指定60S被回收。
对于newChchedThreadPool方法,如果同时有大量任务被提交,而任务的执行又不是很快时,系统会开启等量的线程处理,这样很快会耗尽系统资源。
ThreadPoolExecutor线程池的核心调度代码如下

public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }


handler
handler指定了拒绝策略,也就是当任务数量超过系统实际承载能力时,就要用到拒绝策略。拒绝策略可以说是系统超负荷运行时的补救措施,通常由于压力太大而引起的。也就是当等待队列已满并且当前线程数量超过maximumPoolSize时,处理
的策略。
JDK内置4种策略
AbortPolicy策略:该策略会直接抛出异常,阻止系统正常工作。
CallerRunsPolicy策略:只要线程池未关闭,该策略直接在调用者线程中,运行当前被丢弃的任务,显然这样做不会真的丢弃任务,但是,任务提交线程的性能极有可能急速下降。
DiscardOldestPolicy策略:该策略丢弃最老的一个请求,也就是即将被执行的一个任务,并尝试再次提交当前任务
DiscardPolicy策略:该策略默默丢弃无法处理的任务,不予任何处理。如果允许任务丢弃,这是最好的方案。

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