完美避开线程池血坑

江湖草根测试小A经过三年蛰伏,声名鹊起,终于鼓起勇气,去参与了自己向往已久的霸主阿里的选拔。经过一番精心准备,雄心万丈的小A来到阿里参加了入门考察,结果遭遇当头一板砖(FixedThreadPool在实战中是如何运用的),直接被淘汰。无奈之余,小A只能灰溜溜的回到门派,并虚心向师傅资深测试大C请教。大C酝酿了一下,完整的解释了一下线程池,并重点介绍了一下FixedThreadPool及其使用场景。听完了大C的介绍,小A只能感叹个人还需提高。让我们追随大C的讲解,一起提升一下自己吧。

一、线程池的定义

一种线程使用模式。线程过多会带来调度开销,进而影响缓存局部性和整体性能。而线程池维护着多个线程,等待着监督管理者分配可并发执行的任务。这避免了在处理短时间任务时创建与销毁线程的代价。线程池不仅能够保证内核的充分利用,还能防止过分调度。可用线程数量应该取决于可用的并发处理器、处理器内核、内存、网络sockets等的数量。例如,线程数一般取cpu数量+2比较合适,线程数过多会导致额外的线程切换开销。

二、线程池的创建类型

线程池4种默认类型如下:

1、newCachedThreadPool

创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。用法如下:

    ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(index);
            }
        });
    }
    cachedThreadPool.shutdown();

线程池为无限大,当执行第二个任务时第一个任务已经完成,会复用执行第一个任务的线程,而不用每次新建线程。

缺点:一般不用,是因为newCachedThreadPool 可以无限的新建线程,容易造成堆外内存溢出,因为它的最大值是在初始化的时候设置为 Integer.MAX_VALUE,一般来说机器都没那么大内存给它不断使用。当然知道可能出问题的点,就可以去重写一个方法限制一下这个最大值。

2、newFixedThreadPool

Executors.newFixedThreadPool(n)创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

下面代码创建了5个线程,但是定长线程池的大小最好根据系统资源进行设置,如Runtime.getRuntime().availableProcessors(),可参考PreloadDataCache。其实newFixedThreadPool()在严格上说并不会复用线程,每运行一个Runnable都会通过ThreadFactory创建一个线程。

ExecutorService cachedThreadPool = Executors.newFixedThreadPool(5);
    for (int i = 0; i < 10; i++) {
        final int index = i;
        try {
            Thread.sleep(index * 1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        cachedThreadPool.execute(new Runnable() {
            @Override
            public void run() {
                System.out.println(index);
            }
        });
    }
    cachedThreadPool.shutdown();
3、newScheduledThreadPool

创建一个定长线程池,支持定时及周期性任务执行。下面定义线程池,最大线程数是5。

ScheduledExecutorService scheduledThreadPool = Executors.newScheduledThreadPool(5);

延迟执行:

 scheduledThreadPool.schedule(new Runnable() {@Override
        public void run()
 {
            System.out.println("delay");
        }
    }, 3, TimeUnit.SECONDS);

延迟1秒,并每隔3秒定期执行:

scheduledThreadPool.scheduleAtFixedRate(new Runnable() {@Override
        public void run()
 {
            System.out.println("delay 1 seconds, and excute every 3 seconds");
        }
    }, 1, 3, TimeUnit.SECONDS);

关于延迟执行和周期性执行我们还会想到Timer

Timer timer = new Timer();
    TimerTask timerTask = new TimerTask() {
        @Override
        public void run() {
            
        }
    };
    timer.schedule(timerTask, 1000, 3000);

Timer 的优点在于简单易用,但由于所有任务都是由同一个线程来调度,因此所有任务都是串行执行的,同一时间只能有一个任务在执行,前一个任务的延迟或异常都将会影响到之后的任务(比如:一个任务出错,以后的任务都无法继续)。

ScheduledThreadPoolExecutor的设计思想是,每一个被调度的任务都会由线程池中一个线程去执行,因此任务是并发执行的,相互之间不会受到干扰。需要注意的是,只有当任务的执行时间到来时,ScheduedExecutor 才会真正启动一个线程,其余时间 ScheduledExecutor 都是在轮询任务的状态。

通过对比可以发现ScheduledExecutorService比Timer更安全,功能更强大,在以后的开发中尽可能使用ScheduledExecutorService(JDK1.5以后)替代Timer。

4、newSingleThreadExecutor

Executors.newSingleThreadExecutor(创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

 ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
    for (int i = 0; i < 10; i++) {
        final int index = i;
        singleThreadExecutor.execute(new Runnable() {
            @Override
            public void run() {
                try {
                    System.out.println(index);
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });
    }

三、线程的启动方式

Java可以用以下三种方式来创建线程

  1. 继承Thread类创建线程
  2. 实现Runnable接口创建线程
  3. 使用Callable和Future创建线程

四、FixedThreadPool原理

FixedTreadPool在实际开发过程中是不建议使用的,同时阿里巴巴Java开发手册中也明确指出,而且用的词是『不允许』使用Executors创建定长线程池。因为可能会导致OOM(OutOfMemory ,内存溢出),但是并没有说明为什么,那么接下来就从源码来看一下到底为什么不允许?

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

在定义newFixedThreadPool时,会创建一个LinkedBlockingQueue,但是未指定容量。此时,LinkedBlockingQueue就是一个无边界队列,对于一个无边界队列来说,是可以不断的向队列中加入任务的,这种情况下就有可能因为任务过多而导致内存溢出问题。(Java中的LinkedBlockingQueue是一个用链表实现的有界阻塞队列,容量可以选择进行设置,不设置的话,将是一个无边界的阻塞队列,最大长度为Integer.MAX_VALUE)。既然存在内存溢出的问题,显然在实际项目开发中,newFixedThreadPool应该是实际摒弃的,那正确的使用姿势是什么呢?

private static ExecutorService executor = new ThreadPoolExecutor(10, 10,
  60L, TimeUnit.SECONDS,
new ArrayBlockingQueue(10));

主要是避免使用其中的默认实现,直接调用ThreadPoolExecutor的构造函数来自己创建线程池。在创建的同时,给BlockQueue指定容量就可以了。

以上介绍了线程池的定义及其4种默认实现,并从源码角度上分析了FixedTreadPool可能存在的问题,显然小A在这里是被阿里大侠给挖了一个坑埋了,失败自然就不可避免。

部分测试同学可能会质疑,我们需要了解这些吗?答案显而易见,首先这个是进入BAT大厂的敲门砖,其次如果知道了其中的优劣及适用场景,在白盒测试过程中可以快速发现系统实现可能存在的问题,并且在平台工具开发过程中,高并发是一个绕不开的门槛,选择合适的框架可以使我们自己开发的平台工具对我们的实际业务测试切实提效,传统的点点点测试方式终将会被逐步被淘汰。让我们一起练好内功,迎接时代的变化吧。

其他文章可以关注微信公众号测试架构师养成记,还有资料可以领哦~
在这里插入图片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章