并发编程 | 线程池的自动创建

自动创建线程池

前面介绍了通过ThreadPoolExecutor类可以创建出我们想要的线程池,但需要我们精准的控制各个参数,如果嫌麻烦,其实JDK给我们提供了一些常用的线程池,我们直接拿来用就可以了,这种通过Executors类的各种方法创建线程池的方式也叫自动创建线程池。

下面介绍常见的几种自动创建线程池的方式。

FixedThreadPool

FixedThreadPool底层也是通过ThreadPoolExecutor类创建的,通过源码可以看出FixedThreadPool特点:核心线程数和最大线程数相等;同时其任务队列又是一个无界的任务队列。

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

下面是一个使用FixedThreadPool线程池示例:

lass MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}class ThreadPoolExecutorTest{
    public void testNewFixedThreadPool(){
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0; i<6; i++){
            executorService.submit(new MyRunnable(i));
        }
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        threadPoolExecutorTest.testNewFixedThreadPool();
    }
}

//输出
22/06/25-11:18:47 线程pool-1-thread-1 执行任务0
22/06/25-11:18:47 线程pool-1-thread-3 执行任务2
22/06/25-11:18:47 线程pool-1-thread-2 执行任务1
22/06/25-11:18:49 线程pool-1-thread-2 执行任务4
22/06/25-11:18:49 线程pool-1-thread-1 执行任务3
22/06/25-11:18:49 线程pool-1-thread-3 执行任务5

在上面的程序中,创建了一个线程数为3的newFixedThreadPool类型线程池,然后提交了6个任务,任务的运行时间是2s。

通过程序的运行结果可以看出,11:18:47 时刻当六个任务提交时,只会马上创建三个线程(线程1,2,3)分别执行其中的三个任务(任务0,2,1),而另外三个任务(任务4,3,5)则会放在任务队列中,只有等到原来创建的线程执行完任务之后才会接着从任务队列取出任务继续执行,所以任务4,3,5的执行时间是11:18:49 。

SingleThreadExecutor

SingleThreadExecutor类型线程池可以看做是线程数为1的FixedThreadPool线程池。

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

下面是一个使用SingleThreadExecutor线程池示例:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testNewSingleThreadExecutor() throws InterruptedException {
        ExecutorService executorService = Executors.newSingleThreadExecutor();
        for (int i=0; i<6; i++){
            executorService.submit(new MyRunnable(i));
        }
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        threadPoolExecutorTest.testNewSingleThreadExecutor();
    }
}

//输出
22/06/25-15:09:46 线程pool-1-thread-1 执行任务0
22/06/25-15:09:48 线程pool-1-thread-1 执行任务1
22/06/25-15:09:50 线程pool-1-thread-1 执行任务2
22/06/25-15:09:52 线程pool-1-thread-1 执行任务3
22/06/25-15:09:54 线程pool-1-thread-1 执行任务4
22/06/25-15:09:56 线程pool-1-thread-1 执行任务5

在上面的程序中,创建了一个SingleThreadExecutor类型线程池,然后提交了6个任务,任务的运行时间是2s。

通过程序的运行结果可以看出,当六个任务提交时,只会创建1个线程(线程1)执行第一个提交的任务(任务0),而另外五个任务(任务1,2,3,4,5)则会放在任务队列中,只有等到执行完前面任务之后才会接着从任务队列取出1个任务继续执行,直到所有任务执行完毕,所以后面任务开始执行的时刻都比前面的任务执行时刻晚2s 。

CachedThreadPool

CachedThreadPool类型线程池通过构造函数可以看出其特点:corePoolSize设置为0,maxPoolSize设置为整型的最大值,任务队列用的是容量为0的SynchronousQueue队列。任务队列中并不实质存储任务,而是直接交给线程去执行,因此线程池需要不断创建线程来处理提交过来的任务。

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

下面是一个使用CachedThreadPool线程池示例:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testCachedThreadPool(){
        ExecutorService executorService = Executors.newCachedThreadPool();
        for (int i=0; i<6; i++){
            executorService.submit(new MyRunnable(i));
        }
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        threadPoolExecutorTest.testCachedThreadPool();
    }
}

//输出
22/06/25-15:20:48 线程pool-1-thread-2 执行任务1
22/06/25-15:20:48 线程pool-1-thread-3 执行任务2
22/06/25-15:20:48 线程pool-1-thread-5 执行任务4
22/06/25-15:20:48 线程pool-1-thread-1 执行任务0
22/06/25-15:20:48 线程pool-1-thread-6 执行任务5
22/06/25-15:20:48 线程pool-1-thread-4 执行任务3

在上面的程序中,创建了一个CachedThreadPool类型线程池,然后提交了6个任务,任务的运行时间是2s。

通过程序的运行结果可以看出,当六个任务提交时,是在15:20:48时刻六个任务同时执行的,说明这种类型的线程池每次来了任务就新建线程。

ScheduledThreadPool

ScheduledThreadPool类型线程池支持定时或周期性执行任务。比如每隔 10 秒钟执行一次任务,而实现这种功能的方法主要有 3 种,如代码所示:

//延迟指定时间后执行一次指定的任务
schedule(new MyRunnable(), 4, TimeUnit.SECONDS)

//第一次延时4s执行一次指定任务,然后每隔3s执行一次指定任务
scheduleAtFixedRate(new MyRunnable(), 4, 3, TimeUnit.SECONDS);

//第一次延时4s执行一次指定任务,然后每隔3s执行一次指定任务
scheduleWithFixedDelay(new MyRunnable(), 4, 3, TimeUnit.SECONDS);

下面是一个使用ScheduledThreadPool线程池示例:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }
    
   MyRunnable(){
        this.i = 8;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testScheduledThreadPool1(){
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
        executorService.schedule(new MyRunnable(), 4, TimeUnit.SECONDS);
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " main 开始运行");
        
        threadPoolExecutorTest.testScheduledThreadPool1();
    }
}

//输出
22/06/25-15:58:13 main 开始运行
22/06/25-15:58:17 线程pool-1-thread-1 执行任务8

在上面的程序中,创建了一个ScheduledThreadPool类型线程池,然后调用schedule方法,通过程序的运行结果可以看出,main运行后的4s执行了一次指定的任务。注意这个方法只会执行一次任务,后续就不会再执行该任务了。

要想周期性的执行指定的任务,则需要用到上面介绍的第2种或第3种方法,先看看第二种方法scheduleAtFixedRate,如下示例:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }
    
   MyRunnable(){
        this.i = 8;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testScheduledThreadPool2(){
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
        executorService.scheduleAtFixedRate(new MyRunnable(), 4, 3, TimeUnit.SECONDS);
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " main 开始运行");
        
        threadPoolExecutorTest.testScheduledThreadPool2();
    }
}

//输出
22/06/25-16:04:50 main 开始运行
22/06/25-16:04:54 线程pool-1-thread-1 执行任务8
22/06/25-16:04:57 线程pool-1-thread-1 执行任务8
22/06/25-16:05:00 线程pool-1-thread-1 执行任务8
22/06/25-16:05:03 线程pool-1-thread-1 执行任务8
22/06/25-16:05:06 线程pool-1-thread-1 执行任务8

通过程序的运行结果可以看出,调用scheduleAtFixedRate执行周期任务,这个周期是以任务开始的时间为时间起点开始计时,时间到就开始执行第二次任务,不管任务的执行时间,如本例我设置的周期是3s,那么第一次执行任务的时间是16:04:54 , 那么过3s之后也就是16:04:57时刻第二次执行任务,以此类推周期执行。

我们再看看使用第三种方法scheduleWithFixedDelay执行周期任务,如下示例:

class MyRunnable implements Runnable{
    private int i;
    MyRunnable(int i){
        this.i = i;
    }
    
   MyRunnable(){
        this.i = 8;
    }

    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务" + i);
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolExecutorTest{
    public void testScheduledThreadPool3(){
        ScheduledExecutorService executorService = Executors.newScheduledThreadPool(2);
        executorService.scheduleWithFixedDelay(new MyRunnable(), 4, 3, TimeUnit.SECONDS);
    }
}

public class ThreadPoolExecutorExample {
    public static void main(String[] args) throws InterruptedException {
        ThreadPoolExecutorTest threadPoolExecutorTest = new ThreadPoolExecutorTest();
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " main 开始运行");
        
        threadPoolExecutorTest.testScheduledThreadPool3();
    }
}

//输出
22/06/25-16:07:03 main 开始运行
22/06/25-16:07:07 线程pool-1-thread-1 执行任务8
22/06/25-16:07:12 线程pool-1-thread-1 执行任务8
22/06/25-16:07:17 线程pool-1-thread-1 执行任务8
22/06/25-16:07:22 线程pool-1-thread-1 执行任务8

通过程序的运行结果可以看出,调用scheduleWithFixedDelay执行周期任务,这个周期是以上次任务结束的时间为起点开始计时,因此定时的周期不但和设置的参数有关,还和任务的执行时间有关,如本例我设置的周期是3s,任务的执行时间是2s,那么第一次执行任务的时间是16:07:07, 任务执行了2s在16:07:09结束第一次任务,那么在16:07:09的基础上再过3s之后也就是16:07:12 时刻第二次执行任务,以此类推周期执行。

SingleThreadScheduledExecutor

线程池SingleThreadScheduledExecutor只是 ScheduledThreadPool 的一个特例,内部只有一个线程。

public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
    return new DelegatedScheduledExecutorService
        (new ScheduledThreadPoolExecutor(1));

}

不应该自动创建线程池

在实际开发中,其实是不建议自动创建线程池的,因为自动创建线程池是有一定风险的,下面针对每一种自动创建线程池的方式来分析其可能带来哪些问题。

FixedThreadPool

FixedThreadPool是线程数量固定的线程池,使用的队列是容量没有上限的LinkedBlockingQueue,设想一种场景:如果任务的处理速度比较慢,而提交的任务很多,那么任务队列中堆积的任务也会越来越多,最终大量堆积的任务会占用大量内存,并发生OOM,会对整个程序造成很严重的影响。SingleThreadExecutor线程池其任务队列也是是无界的,所以存在的问题和FixedThreadPool线程池一样。

CachedThreadPool

CachedThreadPool线程池的最大线程数被设置成了Integer.MAX_VALUE,所以当任务数量特别多的时候,就可能会导致创建非常多的线程,最终超过了操作系统的上限而无法创建新线程,或者导致内存不足。

ScheduledThreadPool

ScheduledThreadPool和SingleThreadScheduledExecutor线程池采用的任务队列是 DelayedWorkQueue,这是一个延迟队列,同时也是一个无界队列,所以和 LinkedBlockingQueue 一样,如果队列中存放过多的任务,也可能会导致OOM。纵上所述,这几种自动创建的线程池都存在风险,相比较而言,我们自己手动创建会更好,因为我们可以更加明确线程池的运行规则,不仅可以选择适合自己的线程数量,更可以在必要的时候拒绝新任务的提交,避免资源耗尽的风险。

一个OOM例子

下面程序模拟出通过newFixedThreadPool创建线程池造成OOM的场景:

class MyRunnables implements Runnable{
    private long[] arr = new long[10000];
    @Override
    public void run() {
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yy/MM/dd-HH:mm:ss");
        String time = simpleDateFormat.format(new Date());
        System.out.println(time + " 线程"  + Thread.currentThread().getName() + " 执行任务");
        try {
            Thread.sleep(20000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

class ThreadPoolOOMtest{
    public void testNewFixedThreadPoolOOM(){
        ExecutorService executorService = Executors.newFixedThreadPool(3);
        for (int i=0; i<6000000; i++){
            executorService.submit(new MyRunnables());
        }
    }
}

public class ThreadPoolOOMExample {
    public static void main(String[] args) {
        ThreadPoolOOMtest threadPoolOOMtest = new ThreadPoolOOMtest();
        threadPoolOOMtest.testNewFixedThreadPoolOOM();
    }
}

//输出
22/06/25-17:14:05 线程pool-1-thread-1 执行任务
22/06/25-17:14:05 线程pool-1-thread-3 执行任务
22/06/25-17:14:05 线程pool-1-thread-2 执行任务

Exception: java.lang.OutOfMemoryError thrown from the UncaughtExceptionHandler in thread "main"

该程序中,任务执行时间20s,提交了6000000个任务,而线程池线程数量为3,那么20s内,只有3个任务在执行,其他的任务被提交到任务队列中,最后导致占用大量内存,发生 OOM现象。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值