vlambda博客
学习文章列表

产品经理问我:手动创建线程不香吗,为什么非要用线程池呢?


每次写线程池的文章时,总会想起自己大三第一次面试就是挂在这上面,当时年少轻狂,连SpringBoot是什么都不知道就敢面阿里,真是初生牛犊不怕虎。

(一)什么是线程池

线程(thread)是操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位,我们的程序最终都是由线程进行运作。在Java中,创建和销毁线程的动作是很消耗资源的,因此就出现了所谓“池化资源”技术。

线程池是池化资源技术的一个应用,所谓线程池,顾名思义就是预先按某个规定创建若干个可执行线程放入一个容器中(线程池),需要使用的时候从线程池中去取,用完之后不销毁而是放回去,从而减少了线程创建和销毁的次数,达到节约资源的目的。

(二)为什么要使用线程池

2.1 降低资源消耗

前面已经讲到线程池的出现减少了线程创建和销毁的次数,每个线程都可以被重复利用,可执行多个任务。

2.2 提高系统的响应速度

每当有任务到来时,直接复用线程池中的线程,而不需要等待新线程的创建,这个动作可以带来响应速度的提升

2.3 防止过多的线程搞坏系统

可以根据系统的承受能力,调整线程池中的工作线程的数量,防止因为线程过多服务器变慢或死机。java一个线程默认占用空间为1M,可以想象一旦手动创建线程过多极有可能导致内存溢出。

(三)线程池主要参数

我们可以用Executors类来创建一些常用的线程池,但是像阿里是禁止直接通过Executors类直接创建线程池的,具体的原因稍后再谈。

在了解Executors类所提供的几个线程池前,我们首先来了解一下 ThreadPoolExecutor的主要参数,ThreadPoolExecutor是创建线程池的类,我们选取参数最多的构造方法来看一下:

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

名称 类型 含义
corePoolSize int 核心线程池的大小
maximumPoolSize int 最大线程池大小
keepAliveTime long 线程最大空闲时间
unit TimeUnit 时间单位
workQueue BlockingQueue 线程等待队列
threadFactory ThreadFactory 线程创建工程
handler RejectedExecutionHandler 拒绝策略

3.1 corePoolSize

当向线程池提交一个任务时,如果线程池中已创建的线程数量小于corePoolSIze,即便存在空闲线程,也会创建一个新线程来执行任务,直到创建的线程数大于或等于corePoolSIze。

3.2 maximumPoolSize

线程池所允许的最大线程个数,当队列满了且已经创建的线程数小于maximumPoolSize时,会创建新的线程执行任务。

3.3 keepAliveTime

当线程中的线程数大于corePoolSIze时,如果线程空闲时间大于keepAliveTime,该线程就会被销毁。

3.4 unit

keepAliveTime的时间单位

3.5 workQueue

用于保存等待执行任务的队列

3.6 threadFactory

用于创建新线程。threadFactory创建的线程也是采用new Thread()方式,threadFactory创建的线程名都具有统一的风格:pool-m-thread-n

3.7 handler

拒绝策略,当线程池和队列满了之后,再加入新线程后会执行此策略。下面是四种线程池的拒绝策略:

AbortPolicy:中断任务并抛出异常

DiscardPolicy:中段任务但是不抛出异常

DiscardOldestPolicy:丢弃队列中最老的任务,然后尝试提交新任务

CallerRunsPolicy:由调用线程处理该任务

(四)线程池执行流程

当我们了解了ThreadPoolExecutor的七个参数后,我们就可以很快的理解线程池的流程:

产品经理问我:手动创建线程不香吗,为什么非要用线程池呢?

当提交任务后,首先判断当前线程数是否超过核心线程数,如果没超过则创建新线程执行任务,否则判断工作队列是否已满,如果未满则将任务添加到队列中,否则判断线程数是否超过最大线程数,如果未超过则创建线程执行任务,否则执行拒绝策略。

(五)Executors提供的线程池

executors提供了许多种线程池供用户使用,虽然很多公司禁止使用executors创建线程池,但是对于刚开始解除线程池的人来说,Executors类所提供的线程池能很好的带你进入多线程的世界。

5.1 newSingleThreadExecutor

ExecutorService executorService = Executors.newSingleThreadExecutor();

听名字就可以知道这是一个单线程的线程池,在这个线程池中只有一个线程在工作,相当于单线程执行所有任务,此线程可以保证所有任务的执行顺序按照提交顺序执行,看构造方法也可以看出,corePoolSize和maximumPoolSize都是1。

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

5.2 newFixedThreadPool

ExecutorService executorService = Executors.newFixedThreadPool(2);

固定长度的线程池,线程池的长度在创建时通过变量传入。下面是newFixedThreadPool的构造方法,corePoolSize和maximumPoolSize都是传入的参数值

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

5.3 newCachedThreadPool

ExecutorService executorService = Executors.newCachedThreadPool();

可缓存线程池,这个线程池设定keepAliveTime为60秒,并且对最大线程数量几乎不做控制。

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

观察构造方法,corePoolSize = 0,maximumPoolSize = Integer.MAX_VALUE,即线程数量几乎无限制。设定keepAliveTime 为60秒,线程空闲60秒后自动结束,因为该线程池创建无限制,不会有队列等待,所以使用SynchronousQueue同步队列。

5.4 newScheduledThreadPool

创建一个定时的线程池。此线程池支持定时以及周期性执行任务的需求。下面是newScheduledThreadPool的用法:

Thread thread1=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread1");
    }
});
Thread thread2=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread2");
    }
});
Thread thread3=new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"thread3");
    }
});
ScheduledExecutorService scheduledExecutorService = Executors.newSingleThreadScheduledExecutor();
//在1000ms后执行thread1
scheduledExecutorService.schedule(thread1,1000,TimeUnit.MILLISECONDS);
//在1000ms后每隔1000ms执行一次thread2,如果任务执行时间比间隔时间长,则延迟执行
scheduledExecutorService.scheduleAtFixedRate(thread2,1000,1000,TimeUnit.MILLISECONDS);
//和第二种方式类似,但下一次任务开始的时间为:上一次任务结束时间(而不是开始时间) + delay时间
scheduledExecutorService.scheduleWithFixedDelay(thread3,1000,1000,TimeUnit.MILLISECONDS);

(六)为什么阿里巴巴禁止程序员用Exectors创建线程池

如果你的idea装了Alibaba Java Codeing Guidelines插件(推荐大家使用,有助于让你的代码更加规范),那么当你写了Exectors创建线程池后会看到提示:

产品经理问我:手动创建线程不香吗,为什么非要用线程池呢?

并且阿里将这个用法定义为Blocker,即不允许使用,而是让人们用ThreadPoolExecutor的方式创建线程池。原因是通过ThreadPoolExecutor的方式,这样的处理方式让写的人更加明确线程池的运行规则,规避资源耗尽的风险。

Executors返回的线程池对象的弊端如下:

1)FixedThreadPool和SingleThreadPool:

允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。   2)CachedThreadPool:

允许的创建线程数量为Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM。   下面是ThreadPoolExecutor创建线程池的简单例子

int corePoolSize=5;
int maximumPoolSize=10;
long keepAliveTime=30;
BlockingQueue<Runnable> blockingQueue=new ArrayBlockingQueue(2);
RejectedExecutionHandler handler=new ThreadPoolExecutor.AbortPolicy();
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, blockingQueue, handler);
threadPoolExecutor.execute(thread1);

(七)在SpringBoot项目中使用线程池

SpringBoot对线程池又做了一层封装,在SpringBoot中,可以通过ThreadPoolTaskExecutor类来创建线程池。需要注意两者的区别ThreadPoolExecutor时JUC包下的类,ThreadPoolTaskExecutor是springframework包下的类。但原理都是一样的。

7.1 项目搭建

首先搭建一个SpringBoot项目,只需要引入web依赖即可

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

7.2 配置文件配置

线程池的参数尽量写到配置文件里,这样就能根据需要进行修改,在application.properties文件中配置:

myExecutor.corePoolSize=5
myExecutor.maxPoolSize=10
myExecutor.keepAliveSeconds=30
myExecutor.allowCoreThreadTimeOut=false
myExecutor.queueCapacity=20
myExecutor.threadNamePrefix=myExecutor-

7.3 编写一个自己的线程池

新建一个包叫config,新建类ExecutorConfig ,首先参数的值从配置文件中获取,接着用ThreadPoolTaskExecutor 创建一个自己的线程池,注入到Bean容器中。

@Configuration
@EnableAsync
public class ExecutorConfig {

    @Value("${myExecutor.corePoolSize}")
    private int corePoolSize;
    @Value("${myExecutor.maxPoolSize}")
    private int maxPoolSize;
    @Value("${myExecutor.keepAliveSeconds}")
    private int keepAliveSeconds;
    @Value("${myExecutor.allowCoreThreadTimeOut}")
    private boolean allowCoreThreadTimeOut;
    @Value("${myExecutor.queueCapacity}")
    private int queueCapacity;
    @Value("${myExecutor.threadNamePrefix}")
    private String threadNamePrefix;

    @Bean("myExecutor")
    public Executor myExecutor(){
        ThreadPoolTaskExecutor executor=new ThreadPoolTaskExecutor();
        //核心线程数
        executor.setCorePoolSize(corePoolSize);
        //最大线程数
        executor.setMaxPoolSize(maxPoolSize);
        //线程空闲时间
        executor.setKeepAliveSeconds(keepAliveSeconds);
        //是否保留核心线程数
        executor.setAllowCoreThreadTimeOut(allowCoreThreadTimeOut);
        //队列长度
        executor.setQueueCapacity(queueCapacity);
        //拒绝策略
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.AbortPolicy());
        //设置线程名称前缀
        executor.setThreadNamePrefix(threadNamePrefix);
        executor.initialize();
        return executor;
    }
}

这里需要注意的是有一个方法setAllowCoreThreadTimeOut,当传入参数为true时,所有线程超时后都会被销毁,如果为false,只有超过核心线程数并且超时时才会被销毁。

7.4 编写service并使用

首先写一个service接口:

public interface DoSomeThing {
    /**
     * 通过线程池异步执行耗时的任务
     */

    public void doSomeThing();
}

再编写实现类:

@Service
public class DoSomeThingImpl implements DoSomeThing {

    @Override
    @Async("myExecutor")
    public void doSomeThing() {
        System.out.println(Thread.currentThread().getName()+"-in");
        try {
            Thread.sleep(5000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"-out");
    }
}

增加注解@Async("myExecutor")后,这段方法就会异步由myExecutor线程池执行。

7.5 编写一个controller

新建一个IndexController,每次请求执行一次doSomeThing()方法。

@RestController
public class IndexController {

    @Autowired
    private DoSomeThing doSomeThing;

    @RequestMapping(value = "/index",method = RequestMethod.GET)
    public String index(){
        doSomeThing.doSomeThing();
        return "success";
    }
}

7.6 测试

访问十次http://localhost:8080/index,由于设置的核心线程数是5,队列容量是30,因此最多只会用到5个线程资源,结果如下:

(八)总结

线程池不算什么很难的技术,但是一定要掌握,面试也会经常问,工作中也会用到。要趁别人不注意的时候悄悄拔尖,然后惊艳所有人,我们下期再见。