这里我们用固定线程池来测试,传入核心线程数为 5,最大数量自然就也是 5,
public static void main(String[] args) { ExecutorService threadPool = Executors.newFixedThreadPool(5); try { //模拟10个顾客办理业务 for (int i = 0; i < 10; i++){ //execute 执行方法,传入参数为实现了 Runnable 接口的类 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+"号线程办理业务"); }); } } catch (Exception e){ e.printStackTrace(); } finally { threadPool.shutdown(); } }其中,execute 方法就是将任务提交的方法,我们用 lambda 表达式给 execute 方法传入了参数,实际上相当于一个完整的实现了 Runnable 接口的类。
执行结果:
可以看到,我们循环了 10 次,执行任务,但是线程只用到了 1-5 ,其中有多次复用。
再比如,我们按照各种类型的线程池,自己定义一个线程池,核心线程数 2, 最大线程数 5,阻塞队列长度为 3:
public static void main(String[] args) { ExecutorService threadPool = new ThreadPoolExecutor( 2, 5, 2L, TimeUnit.SECONDS, new LinkedBlockingDeque<>(3), Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy() ); try { //模拟10个顾客办理业务 for (int i = 0; i < 10; i++){ //execute 执行方法,传入参数为实现了 Runnable 接口的类 threadPool.execute(()->{ System.out.println(Thread.currentThread().getName()+"号线程办理业务"); }); } } catch (Exception e){ e.printStackTrace(); } finally { threadPool.shutdown(); } }同样 10 个线程,执行起来:
可以看到,执行了 8 个任务后,就抛出了异常,说明执行了拒绝策略。
上面两个示例,我们的任务本身都是没有返回值的,如果创建的任务本身需要有返回值就需要实现 Callable 接口,然后搭配FutureTask 来传入任务,那么线程池就应该调用 submit 方法而不是 execute。
二、线程池底层原理
2.1 线程池执行逻辑
处理的流程核心就 execute() 方法,他接收一个实现了 Runnable 接口的任务,决定对这个任务的处理策略。
下图是一个比较形象的策略流程:
可能的情况有四种,也就是图中的1234:
如果线程池中的线程数量少于corePoolSize,就创建新的核心线程来执行新添加的任务
如果线程池中的线程数量大于等于corePoolSize,但队列workQueue未满,则将新添加的任务放到队列workQueue中
如果线程池中的线程数量大于等于corePoolSize,且队列workQueue已满,但线程池中的线程数量小于maximumPoolSize,则会创建新的非核心线程来处理被添加的任务
如果线程池中的线程数量等于了maximumPoolSize,就用RejectedExecutionHandler来执行拒绝策略。会抛出异常,一般的拒绝策略是RejectedExecutionException
注意,执行的顺序,在 java 里有一个不合理的地方:
在池里安排任务的时候,我们的核心线程,队列,非核心线程里面排的任务顺序应该是 1 2 3;
但是真正实现上,如果三个都满了,开始执行的时候,依次执行的顺序却是 核心线程,非核心线程,队列。也就是执行顺序会变成 1 3 2
2.2 拒绝策略有些时候,我们并不希望拒绝策略是直接抛出异常,那么 jdk 里面提供的默认拒绝策略有 4 种,他们体现在代码中就是 ThreadPoolExecutor 的四个静态内部类:
2.2.1 CallerRunsPolicy:调用者运行策略。这种策略不会抛弃任务,也不抛出异常,而是将某些任务回退给调用者,从而降低新任务的流量。