线程池的基本用法

1.为什么要用线程池
在java中,开启线程的方式一般分为两种:
a.继承Thread,实现其run方法
b.实现Runnabler接口,通过Thread来实现线程
但无论哪种方式,当线程执行完成后,生命周期就结束了。在Linux系统中,线程的创建是一种很耗资源和时间的工作,因此,实现线程的复用便可以极大的减小资源的消耗,因此,有了线程池的出现

2.初始化线程池的参数问题

public ThreadPoolExecutor(int corePoolSize, // 1
                              int maximumPoolSize,  // 2
                              long keepAliveTime,  // 3
                              TimeUnit unit,  // 4
                              BlockingQueue<Runnable> workQueue, // 5
                              ThreadFactory threadFactory,  // 6
                              RejectedExecutionHandler handler ) { //7
        if (corePoolSize < 0 ||
            maximumPoolSize <= 0 ||
            maximumPoolSize < corePoolSize ||
            keepAliveTime < 0)
            throw new IllegalArgumentException();
        if (workQueue == null || threadFactory == null || handler == null)
            throw new NullPointerException();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

这是java提供的基本的线程池构造方法,在使用时,需要注意以上参数的意义。


线程池参数定义.png

3.参数间的关系
在上面七个参数中,我们重点要关注的是 参数1,2,5,7间的关系。
corePoolSize,核心线程池大小。当我们添加的任务小于该值时,每添加一个任务,但会开启一个线程;一旦任务量大于了corePoolSize,则新添加的任务就会进入workQueue中,这是一个阻塞队列,当队列填满时,如果再添加任务,此时,新添加的任务就会触发新的线程的初始化。此时持续添加任务,便会持续造成新的线程产生,但总共的线程不能超过maximumPoolSize。当总共开启的线程超过maximumPoolSize时,会便启动handler,对新任务进行拒绝。因此,workQueue在传入时,要设定一个大小,否则队列不满,则线程总数只会有corePoolSize个。
如果线程空闲时间超过了keepAliveTime后,线程就会自动销毁。注意,这里销毁的线程不包括核心线程。

4.如何实现线程复用
线程的生命周期在运行完run方法之后就结束了,因此,没办法将Thread拿过来重新用。想实现复用,只能让run方法无法结束,这时workQueue就起到了作用。
在线程池中,所用的队列为阻塞队列。当队列中无数据时,当前线程就会阻塞,直到有数据进入,线程才会运行。因此当线程运行完一个任务后,去队列中获取下一个,如果无法取到新任务,则会阻塞,进而完成一个线程中运行多个任务,即复用的功能。

5.代码验证

public class ThreadPoolTest {

    public static void main(String[] args) {
        int corePoolSize = 2;
        int maximumPoolSize = 5;
        int keepAliveTime = 10 * 1000;
        int workQueueSize = 10;

        int taskSize = 4; //输入不同的任务

        ExecutorService pool = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(workQueueSize));

        for (int i = 1; i <= taskSize; i++) {
            pool.execute(new MyThread(i));
        }
        pool.shutdown();
    }
}

class MyThread extends Thread {
    private int addNum;

    MyThread(int addNum) {
        this.addNum = addNum;
    }

    @Override
    public void run() {
        try {
            sleep(1000);
        } catch (Exception e) {

        }
        System.out.println(Thread.currentThread().getName() + "正在执行。。。" + addNum);
    }
}

corePoolSize,maximumPoolSize,blockQueueSize的值不变,我们测试taskSize不同时,输出的结果。

a. taskSize ==2

pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1

结果:只创建了两个核心线程

b. taskSize=12, workQueue有数据,但不满或刚满

pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-1正在执行。。。4
pool-1-thread-2正在执行。。。3
pool-1-thread-1正在执行。。。5
pool-1-thread-2正在执行。。。6
pool-1-thread-2正在执行。。。8
pool-1-thread-1正在执行。。。7
pool-1-thread-1正在执行。。。10
pool-1-thread-2正在执行。。。9
pool-1-thread-2正在执行。。。12
pool-1-thread-1正在执行。。。11

结果:只创建了两个核心线程,其他的任务均会进入队列中,当thread1和thread2运行完成后,进行复用执行其他任务。

c. taskSize =15, taskSize=(maximumPoolSize+workQueueSize)阻塞队列填满,且线程正好开启到最大值

pool-1-thread-5正在执行。。。15
pool-1-thread-3正在执行。。。13
pool-1-thread-4正在执行。。。14
pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-2正在执行。。。6
pool-1-thread-1正在执行。。。7
pool-1-thread-3正在执行。。。4
pool-1-thread-4正在执行。。。5
pool-1-thread-5正在执行。。。3
pool-1-thread-2正在执行。。。8
pool-1-thread-1正在执行。。。9
pool-1-thread-3正在执行。。。10
pool-1-thread-5正在执行。。。12
pool-1-thread-4正在执行。。。11

结论:可以看到任务1,2以及最后添加的13,14,15先运行了。这是因为,3到12之间的任务,会填入workQueue中,当workQueue填满时,还有任务进入,就会创建新的线程,运行后续加入的任务,直到所有线程数达到maximumPoolSize。我们这种情况正好wrokQueue填满,而线程开启到最大值maximumPoolSize,任务刚刚与两个值一样。

d. taskSize = 18 taskSize>(maximumPoolSize+workQueueSize), 任务超出最大线程数与队列等待数之和

pool-1-thread-2正在执行。。。2
pool-1-thread-1正在执行。。。1
pool-1-thread-3正在执行。。。13
pool-1-thread-4正在执行。。。14
pool-1-thread-5正在执行。。。15
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task Thread[Thread-15,5,main] rejected from java.util.concurrent.ThreadPoolExecutor@3d4eac69[Running, pool size = 5, active threads = 5, queued tasks = 10, completed tasks = 0]
    at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
    at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
    at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
    at com.game.thread.ThreadPoolTest.main(ThreadPoolTest.java:21)
pool-1-thread-1正在执行。。。4
pool-1-thread-2正在执行。。。3
pool-1-thread-4正在执行。。。6
pool-1-thread-5正在执行。。。7
pool-1-thread-3正在执行。。。5
pool-1-thread-1正在执行。。。8
pool-1-thread-2正在执行。。。9
pool-1-thread-4正在执行。。。10
pool-1-thread-5正在执行。。。11
pool-1-thread-3正在执行。。。12

结果:从上面可以看出,任务15之后的就看不到了且出现了异常,这说明超出的线程池的处理能力,如果我们传RejectedExecutionHandler handler,也就是拒绝策略,此时就会超到任务,

e. corePoolSize=0,maximumPoolSize=3,blockQueueSize=90,taskSize=10
这是一个特殊情况,就是如果我们把corePoolSize置为0,且所有的任务不超过等待对列的大小会如何?按上面理的理解,因为队列不满,所以除了核心线程外,不会创建新线程,但此时corePoolSize为0?难道任务就一直在队列里无法执行吗?

pool-1-thread-1正在执行。。。1
pool-1-thread-1正在执行。。。2
pool-1-thread-1正在执行。。。3
pool-1-thread-1正在执行。。。4
pool-1-thread-1正在执行。。。5
pool-1-thread-1正在执行。。。6
pool-1-thread-1正在执行。。。7
pool-1-thread-1正在执行。。。8
pool-1-thread-1正在执行。。。9
pool-1-thread-1正在执行。。。10

实际这种情况下,任务依然执行了,但线程只有一个。这个和我们设置 corePoolSize=1运行结果是一样的,原因呢?

 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)    //****注意这里  1
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

注意代码中标有注释的 1处,当corePoolSize==0时,会走到此处,引发创建线程的操作,所以当corePoolSize=0时,也会运行任务。关于 addWorker()代码的说明,可能参照 手撕ThreadPoolExecutor线程池源码
, 在Android OkHttp框架中,核心线程池就是0,且使用到的了个无容量的队列(相当于系统提供的newCachedThreadPool),有兴趣的可以去看一下。

6.参考值
在使用中,corePoolSize可根据业务来定,另一参数maximumPoolSize则比较重要了,其具体值可根据任务类型来定:

a.CPU密集型
此类的任务,特点为需要大量的使用cpu进行大量的计算,此时的最大线程数,最大值不能超过CPU核心数+1,之所以加1,考虑到cpu计算时,如果有数据在虚拟内存上,需要将其挪到内存上,此过程较为耗时,cpu在等待过程中,可能出现空闲,为了保证其不会空闲,所以+1。

b.IO密集型
当任务中存在大量的网络读取或磁盘文件读取时,maximumPoolSize最大值不要超过 cpu核心数2。因为IO密集型,在等待网络数据或文件读取时,是不需要cpu的,采用DMS机制,此时cpu会空闲下来,因此有了2的操作。

c.cpu+io混合型
如果任务中涉及到cpu计算以及IO操作,如果cpu计算与io操作所用的时间相差不大,则考虑将其拆分成两个任务;如果相差较大,一般是IO操作比较耗时,则可以忽略cpu任务,将其当成IO操作的任务即可。

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