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

线程池的基本用法

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

    相关文章

      网友评论

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

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