Java并发之线程池

一.使用线程池的好处

与“为每一个任务分配一个线程”相比,线程池有一些好处。

  • 重用已经创建的线程,减少了创建、销毁线程的开销。
  • 任务到达时,可能线程池中已经有创建好的线程供使用了,避免了等待线程创建的时间开销。

二.Java线程池实现原理

在Java中创建线程池可以使用Executors提供的四个静态方法创建适用于特定情况的几种线程池。但这些构造方法还是根据需求直接传入特定参数实例化了ThreadPoolExecutor类。所以,我们要想从原理上理解线程池,还是要先学习一下ThreadPoolExecutor的构造方法,看看都有哪些参数。这些参数其实可以理解为线程池的配置信息,根据自己的需求传入不同的参数就能构造出不同的线程池。

ThreadPollExector类有4个构造方法,其它三个的参数较少,使用了一些但归根结底是传入的7个关键参数决定了这个线程池是什么样的。所以关键是这七个参数。

2.1 ThreadPoolExecutor构造方法的7个参数

方法声明如下:

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

corePoolSize :
核心线程数。当线程池刚刚创建时线程数是0,这时如果每来一个任务就会创建一个新的线程,直到已创建的线程数等于核心线程数后就不再创建了,而是把新任务放入阻塞队里去。

关于初始线程数:如果调用了prestartAllCoreThreads()会直接创建出corePoolSize个线程;如果调用了prestartCoreThread()则会提前创建出一个线程。

关于线程存活时间:核心线程不受keepAliveTime的影响,创建后会一直存在,直到线程池关闭。除非调用了allowCoreThreadTimeOut(true)方法

这里的疑问是,如果现有线程是有空闲的,但没达到核心线程数,来了新任务会创建新的线程吗?
答:这个疑问如果理解了线程池的工作过程就不会问了。详见下面的线程池工作流程。

maximumPoolSize
最大线程数。是线程池最多允许存在的线程总数。如果当前线程数已经达到corePoolSize。那么就将任务放入队列,如果队列也满了。就判断一下当前存在线程数是否小于maximumPoolSize,如果是,则创建新的线程执行任务。这里创建的线程处于核心线程池外,受keepAliveTime的影响,如果空闲到达执行时间就会销毁。

*keepAliveTime
控制非核心线程在空闲状态下的存活时间。如果调用allowCoreThreadTimedOut方法,核心线程也可以受它的影响。

unit
keepAliveTime的时间单位,有秒、毫秒、分钟、小时、天等。

workQueue
指定缓存队列

threadFactory
线程工厂,用来创建线程。

handler
表示当前拒绝处理任务时的策略,当缓存队列已满,线程数也达到maximumPoolSize时就按指定的策略处理新提交的任务。
主要有以下四种取值:

  • ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。
  • ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。
  • ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)
  • ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务

2.2 线程池工作流程

  • 当有新任务提交给线程池时,首先检查一下当前线程数是否达到了corePoolSize,如果没达到则新创建线程,将当前任务作为该线程的第一个任务执行。
  • 如果已经到达了corePoolSize或超过了,则将任务放入缓存队列。
  • 如果缓存队列已满,则判断当前线程数是否已经达到了maximumPoolSize,如果没到达则创建新线程执行任务
  • 如果线程数达到了maximumPoolSize,则使用拒绝策略处理

2.3 ThreadPoolExecutor源码分析

2.3.1 线程池状态

线程池有running,shutdown,stop,tidying,termienated几种状态。在jdk1.8的实现中,复用了一个AtomicInteger对象来同时存储线程状态和当前线程数。具体代码不做展开,理解这种做法即可。

2.3.2 参数介绍
  • ReentrantLock mainLook 一个锁,添加工作线程时要先获取锁
  • HashSet<Worker> workers 存储工作线程
  • int largesetPoolSize 记录曾经达到的最大线程数
  • long completedTaskCount 记录已经完成的任务数量
    其他还有构造方法传入的7个参数也都有相应的属性进行保存
2.3.3 execute(Runnable command)方法分析

通过这个方法将任务提交给线程池执行。这个方法基本是2.2节线程池工作流程执行的。

    public void execute(Runnable command) {
        if (command == null) //空指针异常
            throw new NullPointerException();
        //获取clt,这个AtomicInteger对象中存储着当前线程数和线程运行状态
        int c = ctl.get(); 
        //如果当前线程数小于核心线程数则执行添加线程动作
        if (workerCountOf(c) < corePoolSize) {
            //addWorker的第二个参数表示添加的是否是核心线程,这里是true
            if (addWorker(command, true))
                return;
            //添加后重新获取状态值,因为线程数已经有变化了,线程池状态也可能变了
            c = ctl.get();
        }
        //执行到这里,说明添加核心线程不成功,可能是数量达到corePoolSize或线程池shutdown了
        //如果线程池还在运行尝试将任务加入缓冲队列
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //如果添加到队列后,线程池停止运行了,将任务从队列移除
            if (! isRunning(recheck) && remove(command))
                reject(command);  //移除成功后使用拒绝策略处理任务
            else if (workerCountOf(recheck) == 0)  //分析1:如果核心线程池为空,添加一个非核心线程,处理队列中可能的任务
                addWorker(null, false);
        }
        else if (!addWorker(command, false)) //队列满了,启动非核心线程执行任务
            reject(command);  //非核心线程启动失败,执行拒绝策略
    }
2.3.4 addWorker(Runnable,Boolean)分析

addWorker方法用来给线程池中添加线程。第二个参数表示添加的是否是核心线程。下面来看一下它是怎么工作的吧!

    private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            //分析1:注意这个if语句是和2.3.3中分析1相呼应的。
           //调用shutdown()将空闲线程interrupt,正在执行的线程继续执行,将状态设为shutdown
          //调用shutdownNow()将所有线程中断,不管有没有执行完。
          //如果shutdown()后,核心线程都关闭了,队列中却还有元素,2.3.3分析1就添加了新的非核心线程处理,就是这里的!(rs==SHUTDOWN&&……)这种情况
            if (rs >= SHUTDOWN &&
                ! (rs == SHUTDOWN &&
                   firstTask == null &&
                   ! workQueue.isEmpty()))
                return false;

            for (;;) {
                int wc = workerCountOf(c);
              //超过执行线程数,返回false
                if (wc >= CAPACITY ||
                    wc >= (core ? corePoolSize : maximumPoolSize))
                    return false;
              //cas将线程计数加1,失败后重试
                if (compareAndIncrementWorkerCount(c))
                    break retry;
                c = ctl.get();  // Re-read ctl
              //线程状态发生变化重新循环
                if (runStateOf(c) != rs) 
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }
        
        //下面是添加线程的过程
        boolean workerStarted = false; //标识线程是否启动
        boolean workerAdded = false; //标识线程是否被添加
        Worker w = null;
        try {
            w = new Worker(firstTask);   //新生成worker,线程池中的线程用它表达
            final Thread t = w.thread;      //获取实际的线程
            if (t != null) {  
               //添加线程要先获取锁
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    //获取当前状态
                    int rs = runStateOf(ctl.get());

                    //不是shutdown状态或者,处于shutdown状态但添加的tash是null,属于shutdown下添加线程处理队列中剩余任务的情况
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        workers.add(w);  //添加到workers,workers持有线程
                        int s = workers.size();  
                        if (s > largestPoolSize) //记录最大线程数记录
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    t.start();   //启动线程执行
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)  //没成功
                addWorkerFailed(w);
        }
        return workerStarted;
    }
2.3.5 worker的执行

添加线程后,worker就开始执行了,在它的执行方法run里会直接调用tast的run方法,执行要干的事情。
执行完成之后会去队列获取新的任务执行。
如果没有新任务执行呢?
Worker是ThreadPoolExecutor的一个内部类,它的实现了Runnable方法。这里有个问题是Runnable需要传递给Thread才能执行。Worker是如何做到的呢?
原来,Worker类持有了一个Thread类型的变量thread,并在初始化时使用Worker本身初始化了thread
Worker的构造方法:

 Worker(Runnable firstTask) {
    setState(-1); // inhibit interrupts until runWorker
    this.firstTask = firstTask;
    this.thread = getThreadFactory().newThread(this);
}

Worker的run方法调用了runWoker(this)。Worker还继承了AQS,这一块还有待学习。
第一次启动会执行初始化传进来的任务firstTask;然后会从workQueue中取任务执行,如果队列为空则等待keepAliveTime这么长时间

final void runWorker(Worker w) {
    Thread wt = Thread.currentThread();
    Runnable task = w.firstTask;
    w.firstTask = null;
    w.unlock(); // allow interrupts
    boolean completedAbruptly = true;
    try {
      //第一次时执行外部传过来的task,后面从getTask获取,getTask从队列获取任务执行,
      //如果队列为空则等待keepAliveTime这么长的时间
        while (task != null || (task = getTask()) != null) {
            w.lock();
            if ((runStateAtLeast(ctl.get(), STOP) ||
                 (Thread.interrupted() &&
                  runStateAtLeast(ctl.get(), STOP))) &&
                !wt.isInterrupted())
                wt.interrupt();
            try {
                beforeExecute(wt, task);
                Throwable thrown = null;
                try {
                    task.run();
                } catch (RuntimeException x) {
                    thrown = x; throw x;
                } catch (Error x) {
                    thrown = x; throw x;
                } catch (Throwable x) {
                    thrown = x; throw new Error(x);
                } finally {
                    afterExecute(task, thrown);
                }
            } finally {
                task = null;
                w.completedTasks++;
                w.unlock();
            }
        }
        completedAbruptly = false;
    } finally {
        processWorkerExit(w, completedAbruptly);
    }
}
2.3.5 其他内容

下一步应该分析getTask是如何从队列获取任务的了,这里不再展开讲了。
此外,还应该有队列的实现和选择问题,拒绝策略的具体等,目前先不做如此多的分析了,待未来时机成熟了再完善。

二.使用Executors创建具有默认配置的线程池

java.util.concurrent.Executors中提供了4个静态方法,可以用来创建具有指定特性的线程池。都是实例化了ThreadPoolExecutor对象。

2.1 newFixedThreadPool()方法

返回一个带缓存的线程池,该池在必要的时候创建线程,在线程空闲60s之后终止线程.
下面看一下这个方法的源码实现:

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

2.2 newFixedThreadPool

创建一个固定长度的线程池,每当提交一个任务时就创建一个线程,指导达到线程池的最大数量,这是线程池的规模将不再变化(如果某个线程由于发生了未预期的Exception而结束,那么线程池会补充一个新的线程)

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

2.3newScheduledThreadPool

创建一个固定长度的线程池,而且以延迟或定时的方式来执行任务,类似于Timer

    public static ScheduledExecutorService newScheduledThreadPool(
            int corePoolSize, ThreadFactory threadFactory) {
        return new ScheduledThreadPoolExecutor(corePoolSize, threadFactory);
    }

2.4newSingleThreadExecutor

是一个单线程的Executor,它创建单个工作者线程来执行任务,如果这个线程异常结束,会创建另一个线程来替代。

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

Java并发之基础知识
Java并发之volatile关键字
Java并发之synchronized关键字
Java并发之原子类
Java并发之线程池
Java并发之并发工具类
Java并发之AQS原理
Java并发之ThreadLocal使用和源码分析

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

推荐阅读更多精彩内容