3. Executors


  线程池可以说是面试热门问题了,所以是必须要搞懂的东西,但是理顺比较难啊。我勉强试试。

固定线程池

  固定数量线程池,就是可以做到每次执行固定个数的线程。比如线程池大小为4.那么每次都有四个线程在运行。
用时间戳记录,会更加直观。

package com.youngthing.interpolation.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 9/4/2022 10:17 PM 创建
 *
 * @author 花书粉丝
 */
public class ExecutorDemo {
    private static final AtomicInteger I = new AtomicInteger();

    public static void main(String[] args) {
        final ExecutorService executorService = Executors.newFixedThreadPool(4);
        final Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
                System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int j = 0; j < 12; j++) {
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}


打印结果:

i = 0:1662303030
i = 3:1662303030
i = 2:1662303030
i = 1:1662303030
i = 6:1662303031
i = 5:1662303031
i = 4:1662303031
i = 7:1662303031
i = 8:1662303032
i = 9:1662303032
i = 11:1662303032
i = 10:1662303032

  可见线程池大小为4的时候,每次就只有四个线程在运行。
  newSingleThreadExecutor,这个不过是将大小固定为1的固定线程池而已,不多赘述。

缓存线程池

  缓存线程池不能指定线程数量,所以更加灵活。它在任务增加时会多创建一些线程,在任务减少时,会回收一些线程,可以说是一种“弹性计算”。这个回收时间默认是60秒。把上面代码改一改,会发现缓存的线程池是几乎同时执行所有线程的,代码如下:

package com.youngthing.interpolation.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 9/4/2022 10:17 PM 创建
 *
 * @author 花书粉丝
 */
public class CachedExecutorDemo {
    private static final AtomicInteger I = new AtomicInteger();

    public static void main(String[] args) {
        final ExecutorService executorService = Executors.newCachedThreadPool();
        final Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
                System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int j = 0; j < 12; j++) {
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}


  执行结果:

i = 4:1662302891
i = 0:1662302891
i = 8:1662302891
i = 7:1662302891
i = 2:1662302891
i = 11:1662302891
i = 1:1662302891
i = 5:1662302891
i = 9:1662302891
i = 6:1662302891
i = 3:1662302891
i = 10:1662302891

定时线程池

  四大线程池介绍了三个,现在这是第四个。这个有点特殊,上述三个都是返回ExecutorService,而newScheduledThreadPool返回的是ScheduledExecutorService,是ExecutorService的子类,提供了一些定时的方法比如延迟方法。同时也支持固定线程池的功能,把上面例子改写,记录下启动时间就知道了:

package com.youngthing.interpolation.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 9/4/2022 10:17 PM 创建
 *
 * @author 花书粉丝
 */
public class ScheduleExecutorDemo {
    private static final AtomicInteger I = new AtomicInteger();

    public static void main(String[] args) {
        final ScheduledExecutorService executorService = Executors.newScheduledThreadPool(4);
        final Runnable runnable = () -> {

            try {
                Thread.sleep(1000);
                System.out.println("i = " + I.getAndIncrement() + ":"+ System.currentTimeMillis()/1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

        };
        System.out.println("current:" + System.currentTimeMillis() / 1000);
        for (int j = 0; j < 12; j++) {
            executorService.schedule(runnable, 2l, TimeUnit.SECONDS);
        }
        executorService.shutdown();
    }
}

  因为有点误差,所以我这里记录的延迟了三秒启动,结果如下:

current:1662302723
i = 1:1662302726
i = 3:1662302726
i = 0:1662302726
i = 2:1662302726
i = 4:1662302727
i = 6:1662302727
i = 7:1662302727
i = 5:1662302727
i = 8:1662302728
i = 11:1662302728
i = 9:1662302728
i = 10:1662302728

  所以他是固定线程池和定时线程池的混合版本。类似这样的混合版本还有newSingleThreadScheduledExecutor,这个是单线程池和定时线程池的混合版本。

工厂参数

  在很多公司的开发规范中,线程池必须自己取名字,这样日志里比较好追踪,其实这个比较简单,因为上述四大线程池,都有个工厂参数,在工厂参数里自己定一个线程名字规则就行了。如以下例子:

package com.youngthing.interpolation.test;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * 9/4/2022 10:17 PM 创建
 *
 * @author 花书粉丝
 */
public class ExecutorFactoryDemo {
    private static final AtomicInteger I = new AtomicInteger();

    public static void main(String[] args) {
        final ExecutorService executorService = Executors.newFixedThreadPool(4, new ThreadFactory() {
            @Override
            public Thread newThread(Runnable r) {
                final Thread thread = new Thread(Thread.currentThread().getThreadGroup(), r, "OK-" + I.getAndIncrement());
                return thread;
            }
        });
        final Runnable runnable = () -> {
            try {
                Thread.sleep(1000);
                System.out.println(Thread.currentThread().getName() + ":" + System.currentTimeMillis() / 1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        };

        for (int j = 0; j < 12; j++) {
            executorService.execute(runnable);
        }
        executorService.shutdown();
    }
}

  打印结果如下:

OK-2:1662303969
OK-0:1662303969
OK-1:1662303969
OK-3:1662303969
OK-0:1662303970
OK-2:1662303970
OK-1:1662303970
OK-3:1662303970
OK-1:1662303971
OK-3:1662303971
OK-0:1662303971
OK-2:1662303971

  从名字上看得一清二楚,就只创建了四个线程,而且线程名字也是我们自定义的名字。

ThreadPoolExecutor

  ExecutorService有两个非常重要的实现类,一个是ThreadPoolExecutor,另一个是ForkJoinPool。ForkJoinPool我不会在这篇博客里讲,因为我准备写另外一篇博客去介绍ForkJoinPool。所以我这里只讲ThreadPoolExecutor,这个类在面试时是热门问题,经常被问起,主要问的是参数细节。那我们来看看总共多少个参数吧!ThreadPoolExecutor参数最多的构造器是以下方法:

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

  总共7个参数,我将一一介绍。
  前两个重要参数是corePoolSize与maximumPoolSize,corePoolSize和maximumPoolSize控制线程池的大小。当线程数量小于corePoolSize时,会创建新线程去执行任务。当队列满的时候,线程数量大于corePoolSize且小于maximumPoolSize时,也会创建新线程去执行任务。当设置的corePoolSize和maximumPoolSize相等时,这时候就是一个固定尺寸的线程池了。当然,这两个参数可以用set方法修改,并不是传入构造器就不能改了。
  接下来的两个参数keepAliveTime和unit是活跃时间。在线程数量超过corePoolSize时,溢出的线程,如果剩余的空闲时间超过keepAliveTime,那么将会中止,然后资源得到释放,保持一个较小的线程池。
  再看看workQueue这个参数,这里需要注意的是线程拒绝策略。

  1. 当线程数小于corePoolSize时,创建新线程。
  2. 当线程数等于corePoolSize时,加入队列。
  3. 当线程数等于corePoolSize时,且队列满了时,创建新线程。
  4. 当线程数等于maximumPoolSize时,拒绝新任务。

  在实践中有三种队列策略:

  1. 接力策略,使用SynchronousQueue,这个队列可以理解为长度只能为1的队列,相当于一个小缓冲。
  2. 无边界策略,使用LinkedBlockingQueue,因为这个阻塞队列不限制大小,但是这个时候maximumPoolSize参数就没有效果了。
  3. 有边界策略。使用 ArrayBlockingQueue,这是实际生产中最常用的策略。

  而threadFactory我就不说了,前一段落说过了。直接说下一个重点,handler。拒绝任务的场景上面说过,在拒绝时会执行拒绝策略,如果时间充裕,可以自己实现一个拒绝策略,但如果时间不充裕,可以有JDK自带的四种拒绝策略:

  1. 中断策略,实现类为ThreadPoolExecutor.AbortPolicy,在拒绝时抛出异常,这也是默认实现
  2. 调用者执行策略,实现类为ThreadPoolExecutor.CallerRunsPolicy,由调用者执行溢出的任务。
  3. 丢弃策略,实现类为ThreadPoolExecutor.DiscardPolicy,直接放弃新加的任务。
  4. 丢弃最旧任务策略,实现类为ThreadPoolExecutor.DiscardOldestPolicy,直接放弃队列头部的任务,也就是等待最久的任务。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

醒过来摸鱼

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值