美文网首页
线程池的基本用法

线程池的基本用法

作者: baifanger | 来源:发表于2020-11-07 18:39 被阅读0次

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操作的任务即可。

相关文章

  • python3 线程池和异常处理

    引用 线程池的基本使用as_completedwaitmap 线程池的异常处理 进程池用法 引用 Python中已...

  • 线程池的基本用法

    1.为什么要用线程池在java中,开启线程的方式一般分为两种:a.继承Thread,实现其run方法b.实现Run...

  • JAVA线程池常见用法及其原理

    JAVA线程池常见用法及其原理 JAVA线程池常见用法: 1.代码实现 import lombok.extern....

  • 2020-02-01-Java线程池

    Java线程池基本用法 Java提供了一些通用接口来创建线程池: 但是通常不推荐使用这些简易接口,因为这些接口可能...

  • Java中线程池,你真的了解会用吗

    在《深入源码分析Java线程池的实现原理》这篇文章中,我们介绍过了Java中线程池的常见用法以及基本原理。 在文中...

  • JAVA线程池常见用法

    JAVA线程池常见用法及其原理 1.JAVA线程池常见用法: 2.核心参数讲解 corePoolSize: 核心线...

  • Java并发 之 线程池系列 (1) 让多线程不再坑爹的线程池

    背景线程池的来由什么是线程池背景总结 用法通过Executors创建线程池Executors及其服务的类Execu...

  • 1203-AsyncTask详解一:线程池的基本设置

    AsyncTask的内部使用线程池处理并发,要了解它是怎样使用线程池的,那要先了解线程池的基本设置 线程池的基本参...

  • 综合问题总结

    线程池用过哪些?线程池有哪些参数?这几个常用线程池的用法和实际场景?线程池是为了解决大量的请求造成的服务器大量创建...

  • Java线程池源码分析

    前言 在上一篇文章【Java线程池的使用】中,我们分析了线程池的用法。但那仅仅是用法,关于线程池内部是如何实现的,...

网友评论

      本文标题:线程池的基本用法

      本文链接:https://www.haomeiwen.com/subject/ivkqbktx.html