Java多线程-线程池ThreadPoolExecutor构造方法和规则

有时候,系统需要处理非常多的执行时间很短的请求,如果每一个请求都开启一个新线程的话,系统就要不断的进行线程的创建和销毁,有时花在创建和销毁线程上的时间会比线程真正执行的时间还长。而且当线程数量太多时,系统不一定能受得了。
使用线程池主要为了解决一下几个问题:

  • 通过重用线程池中的线程,来减少每个线程创建和销毁的性能开销。
  • 对线程进行一些维护和管理,比如定时开始,周期执行,并发数控制等等

Executor
Executor是一个接口,跟线程池有关的基本都要跟他打交道。下面是常用的ThreadPoolExecutor的关系。

Executor接口很简单,只有一个execute方法。
ExecutorService是Executor的子接口,增加了一些常用的对线程的控制方法,之后使用线程池主要也是使用这些方法。
AbstractExecutorService是一个抽象类。ThreadPoolExecutor就是实现了这个类。

ThreadPoolExecutor构造方法
ThreadPoolExecutor是线程池的真正实现,他通过构造方法的一系列参数,来构成不同配置的线程池。常用的构造方法有下面四个
构造方法参数说明
corePoolSize
核心线程数,默认情况下核心线程会一直存活,即使处于闲置状态也不会受存keepAliveTime限制。除非将allowCoreThreadTimeOut设置为true。
maximumPoolSize
线程池所能容纳的最大线程数。超过这个数的线程将被阻塞。当任务队列为没有设置大小的LinkedBlockingDeque时,这个值无效。
keepAliveTime
非核心线程的闲置超时时间,超过这个时间就会被回收。
unit
指定keepAliveTime的单位,如TimeUnit.SECONDS。当将allowCoreThreadTimeOut设置为true时对corePoolSize生效。
workQueue
线程池中的任务队列.
常用的有三种队列,SynchronousQueue,LinkedBlockingDeque,ArrayBlockingQueue。
threadFactory
线程工厂,提供创建新线程的功能。ThreadFactory是一个接口,只有一个方法

public interface ThreadFactory {

    /**
     * Constructs a new {@code Thread}.  Implementations may also initialize
     * priority, name, daemon status, {@code ThreadGroup}, etc.
     *
     * @param r a runnable to be executed by new thread instance
     * @return constructed thread, or {@code null} if the request to
     *         create a thread is rejected
     */
    Thread newThread(Runnable r);
}

通过线程工厂可以对线程的一些属性进行定制。
默认的工厂:

static class DefaultThreadFactory implements ThreadFactory {
    private static final AtomicInteger poolNumber = new AtomicInteger(1);
    private final ThreadGroup group;
    private final AtomicInteger threadNumber = new AtomicInteger(1);
    private final String namePrefix;

    DefaultThreadFactory() {
        SecurityManager s = System.getSecurityManager();
        group = (s != null) ? s.getThreadGroup() :
                              Thread.currentThread().getThreadGroup();
        namePrefix = "pool-" +
                      poolNumber.getAndIncrement() +
                     "-thread-";
    }

    public Thread newThread(Runnable r) {
        Thread t = new Thread(group, r,
                              namePrefix + threadNumber.getAndIncrement(),
                              0);
        if (t.isDaemon())
            t.setDaemon(false);
        if (t.getPriority() != Thread.NORM_PRIORITY)
            t.setPriority(Thread.NORM_PRIORITY);
        return t;
    }
}

RejectedExecutionHandler
RejectedExecutionHandler也是一个接口,只有一个方法

public interface RejectedExecutionHandler {

    /**
     * Method that may be invoked by a {@link ThreadPoolExecutor} when
     * {@link ThreadPoolExecutor#execute execute} cannot accept a
     * task.  This may occur when no more threads or queue slots are
     * available because their bounds would be exceeded, or upon
     * shutdown of the Executor.
     *
     * <p>In the absence of other alternatives, the method may throw
     * an unchecked {@link RejectedExecutionException}, which will be
     * propagated to the caller of {@code execute}.
     *
     * @param r the runnable task requested to be executed
     * @param executor the executor attempting to execute this task
     * @throws RejectedExecutionException if there is no remedy
     */
    void rejectedExecution(Runnable r, ThreadPoolExecutor executor);
}

当线程池中的资源已经全部使用,添加新线程被拒绝时,会调用RejectedExecutionHandler的rejectedExecution方法

线程池的运行状态

  1. RUNNING:接受新任务并处理排队任务
  2. SHUTDOWN:不接受新任务,但处理排队任务
  3. STOP:不接受新任务,不处理排队任务并中断正在进行的任务
  4. TIDYING:线程转换到状态TIDYING,将运行terminate()钩子方法,在调用shutdown()/shutdownNow()方法后都会尝试更新为这个状态
  5. TERMINATED :终止状态,terminated()方法后的状态

状态间的转换如下图所示:

 

execute方法执行

1.获取当前线程池的状态。
2.当前线程数量小于 coreSize 时创建一个新的线程运行。
3.如果当前线程处于运行状态,并且写入阻塞队列成功。
4.双重检查,再次获取线程状态;如果线程状态变了(非运行状态)就需要从阻塞队列移除任务,并尝试判断线程是否全部执行完毕。同时执行拒绝策略。
5.如果当前线程池为空就新创建一个线程并执行。
6.如果在第三步的判断为非运行状态,尝试新建线程,如果失败则执行拒绝策略。

线程池规则
线程池的线程执行规则跟任务队列有很大的关系。
下面都假设任务队列没有大小限制:
a.如果线程数量<=核心线程数量,那么直接启动一个核心线程来执行任务,不会放入队列中。
b.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是LinkedBlockingDeque的时候,超过核心线程数量的任务会放在任务队列中排队。
c.如果线程数量>核心线程数,但<=最大线程数,并且任务队列是SynchronousQueue的时候,线程池会创建新线程执行任务,这些任务也不会被放在任务队列中。这些线程属于非核心线程,在任务完成后,闲置时间达到了超时时间就会被清除。
d.如果线程数量>核心线程数,并且>最大线程数,当任务队列是LinkedBlockingDeque,会将超过核心线程的任务放在任务队列中排队。也就是当任务队列是LinkedBlockingDeque并且没有大小限制时,线程池的最大线程数设置是无效的,他的线程数最多不会超过核心线程数。
e.如果线程数量>核心线程数,并且>最大线程数,当任务队列是SynchronousQueue的时候,会因为线程池拒绝添加任务而抛出异常。
任务队列大小有限时:
a.当LinkedBlockingDeque塞满时,新增的任务会直接创建新线程来执行,当创建的线程数量超过最大线程数量时会抛异常。
b.SynchronousQueue没有数量限制。因为他根本不保持这些任务,而是直接交给线程池去执行。当任务数量超过最大线程数时会直接抛异常。

规则验证

前提
所有的任务都是下面这样的,睡眠两秒后打印一行日志:

Runnable myRunnable = () -> {
    try {
        Thread.sleep(2000);
        System.out.println(Thread.currentThread().getName() + "run");
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
};

所有验证过程都是下面这样,先执行三个,再执行三个,8秒后,各看一次信息

executor.execute(myRunnable);
executor.execute(myRunnable);
executor.execute(myRunnable);
System.out.println("---先开启三个线程---");
System.out.println("核心线程数" + executor.getCorePoolSize());
System.out.println("线程池线程数" + executor.getPoolSize());
System.out.println("队列任务数" + executor.getQueue().size());
executor.execute(myRunnable);
executor.execute(myRunnable);
executor.execute(myRunnable);
System.out.println("---再开启三个线程---");
System.out.println("核心线程数" + executor.getCorePoolSize());
System.out.println("线程池线程程数" + executor.getPoolSize());
System.out.println("队列任务数" + executor.getQueue().size());
Thread.sleep(8000);
System.out.println("----8秒之后----");
System.out.println("核心线程数" + executor.getCorePoolSize());
System.out.println("线程池线程线程池数" + executor.getPoolSize());
System.out.println("队列任务数" + executor.getQueue().size());
executor.shutdown();

验证1:核心线程数为6,最大线程数为10。超时时间为5秒

ThreadPoolExecutor executor = new ThreadPoolExecutor(6, 10, 5, TimeUnit.SECONDS, new SynchronousQueue<>());
---先开启三个线程---
核心线程数6
线程池线程数3
队列任务数0
---再开启三个线程---
核心线程数6
线程池线程程数6
队列任务数0
pool-1-thread-2run
pool-1-thread-1run
pool-1-thread-3run
pool-1-thread-6run
pool-1-thread-5run
pool-1-thread-4run
----8秒之后----
核心线程数6
线程池线程线程池数6
队列任务数0

结论:可以看到每个任务都是是直接启动一个核心线程来执行任务,一共创建了6个线程,不会放入队列中。8秒后线程池还是6个线程,核心线程默认情况下不会被回收,不收超时时间限制。

验证2:核心线程数为3,最大线程数为6。超时时间为5秒,队列是LinkedBlockingDeque

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 10, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
---先开启三个线程---
核心线程数3
线程池线程数3
队列任务数0
---再开启三个线程---
核心线程数3
线程池线程程数3
队列任务数3
pool-1-thread-3run
pool-1-thread-1run
pool-1-thread-2run
pool-1-thread-1run
pool-1-thread-2run
pool-1-thread-3run
----8秒之后----
核心线程数3
线程池线程线程池数3
队列任务数0

结论:当任务数超过核心线程数时,会将超出的任务放在队列中,只会创建3个线程重复利用。

验证3:核心线程数为3,最大线程数为6。超时时间为5秒,队列是SynchronousQueue

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 6, 5, TimeUnit.SECONDS, new SynchronousQueue<>());
---先开启三个线程---
核心线程数3
线程池线程数3
队列任务数0
---再开启三个线程---
核心线程数3
线程池线程程数6
队列任务数0
pool-1-thread-2run
pool-1-thread-3run
pool-1-thread-6run
pool-1-thread-4run
pool-1-thread-5run
pool-1-thread-1run
----8秒之后----
核心线程数3
线程池线程线程池数3
队列任务数0

结论:当队列是SynchronousQueue时,超出核心线程的任务会创建新的线程来执行,看到一共有6个线程。但是这些线程是费核心线程,收超时时间限制,在任务完成后限制超过5秒就会被回收。所以最后看到线程池还是只有三个线程。

验证4:核心线程数是3,最大线程数是4,队列是LinkedBlockingDeque

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>());
---先开启三个线程---
核心线程数3
线程池线程数3
队列任务数0
---再开启三个线程---
核心线程数3
线程池线程程数3
队列任务数3
pool-1-thread-1run
pool-1-thread-3run
pool-1-thread-2run
pool-1-thread-3run
pool-1-thread-2run
pool-1-thread-1run
----8秒之后----
核心线程数3
线程池线程线程池数3
队列任务数0

LinkedBlockingDeque根本不受最大线程数影响
但是当LinkedBlockingDeque有大小限制时就会受最大线程数影响了
如下面,将队列大小设置为2.
ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(2));

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(2));
---先开启三个线程---
核心线程数3
线程池线程数3
队列任务数0
---再开启三个线程---
核心线程数3
线程池线程程数4
队列任务数2
pool-1-thread-1run
pool-1-thread-3run
pool-1-thread-2run
pool-1-thread-4run
pool-1-thread-1run
pool-1-thread-3run
----8秒之后----
核心线程数3
线程池线程线程池数3
队列任务数0

首先为三个任务开启了三个核心线程1,2,3,然后第四个任务和第五个任务加入到队列中,第六个任务因为队列满了,就直接创建一个新线程4,这是一共有四个线程,没有超过最大线程数。8秒后,非核心线程收超时时间影响回收了,因此线程池只剩3个线程了。
将队列大小设置为1.

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new LinkedBlockingDeque<>(1));
---先开启三个线程---
Exception in thread "main" 核心线程数3
线程池线程数3
队列任务数0
java.util.concurrent.RejectedExecutionException: Task com.gysoft.utils.test.threadpool.ThreadPoolExecutorTest$$Lambda$1/1586270964@694f9431 rejected from java.util.concurrent.ThreadPoolExecutor@f2a0b8e[Running, pool size = 4, active threads = 4, queued tasks = 1, 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.gysoft.utils.test.threadpool.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:80)
pool-1-thread-2run
pool-1-thread-3run
pool-1-thread-1run
pool-1-thread-4run
pool-1-thread-2run

直接出错在第6个execute方法上。因为核心线程是3个,当加入第四个任务的时候,就把第四个放在队列中。加入第五个任务时,因为队列满了,就创建新线程执行,创建了线程4。当加入第六个线程时,也会尝试创建线程,但是因为已经达到了线程池最大线程数,所以直接抛异常了。

验证5:核心线程数是3 ,最大线程数是4,队列是SynchronousQueue

ThreadPoolExecutor executor = new ThreadPoolExecutor(3, 4, 5, TimeUnit.SECONDS, new SynchronousQueue<>());
---先开启三个线程---
核心线程数3
线程池线程数3
队列任务数0
Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.gysoft.utils.test.threadpool.ThreadPoolExecutorTest$$Lambda$1/981661423@c038203 rejected from java.util.concurrent.ThreadPoolExecutor@8bd1b6a[Running, pool size = 4, active threads = 4, queued tasks = 0, 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.gysoft.utils.test.threadpool.ThreadPoolExecutorTest.main(ThreadPoolExecutorTest.java:79)
pool-1-thread-1run
pool-1-thread-2run
pool-1-thread-3run
pool-1-thread-4run

这次在添加第五个任务时就报错了,因为SynchronousQueue根本不保存任务,收到一个任务就去创建新线程。所以第五个就会抛异常了。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值