Java多线程系列--“JUC线程池”02之 线程池原理

Posted Hermioner

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java多线程系列--“JUC线程池”02之 线程池原理相关的知识,希望对你有一定的参考价值。

 

ThreadPoolExecutor简介

ThreadPoolExecutor是线程池类。对于线程池,可以通俗的将它理解为"存放一定数量线程的一个线程集合。

Executor框架最核心的类是ThreadPoolExecutor,它是线程池的实现类,主要由下列4个组件构成。

  • ·corePoolSize:核心线程池的大小。
  • ·maximumPool:最大线程池的大小。
  • keepAliveTime: 当线程数量大于核心线程池大小的时候,线程(超出核心线程池大小的这部分)可以空闲的最长时间,超过这个时间线程将自动销毁。
  • ·workQueue:用来暂时保存任务的工作队列。
  • ·RejectedExecutionHandler:当ThreadPoolExecutor已经关闭或ThreadPoolExecutor已经饱和时(达到了最大线程池大小且工作队列已满),execute()方法将要调用的Handler。

ThreadPoolExecutor数据结构

ThreadPoolExecutor的数据结构如下图所示:

各个数据在ThreadPoolExecutor.java中的定义如下:

复制代码
// 阻塞队列。
private final BlockingQueue<Runnable> workQueue;
// 互斥锁
private final ReentrantLock mainLock = new ReentrantLock();
// 线程集合。一个Worker对应一个线程。
private final HashSet<Worker> workers = new HashSet<Worker>();
// “终止条件”,与“mainLock”绑定。
private final Condition termination = mainLock.newCondition();
// 线程池中线程数量曾经达到过的最大值。
private int largestPoolSize;
// 已完成任务数量
private long completedTaskCount;
// ThreadFactory对象,用于创建线程。
private volatile ThreadFactory threadFactory;
// 拒绝策略的处理句柄。
private volatile RejectedExecutionHandler handler;
// 保持线程存活时间。
private volatile long keepAliveTime;

private volatile boolean allowCoreThreadTimeOut;
// 核心池大小
private volatile int corePoolSize;
// 最大池大小
private volatile int maximumPoolSize;
复制代码

 

1. workers
    workers是HashSet<Work>类型,即它是一个Worker集合。而一个Worker对应一个线程,也就是说线程池通过workers包含了"一个线程集合"。当Worker对应的线程池启动时,它会执行线程池中的任务;当执行完一个任务后,它会从线程池的阻塞队列中取出一个阻塞的任务来继续运行。
    wokers的作用是,线程池通过它实现了"允许多个线程同时运行"。

2. workQueue
    workQueue是BlockingQueue类型,即它是一个阻塞队列。当线程池中的线程数超过它的容量的时候,线程会进入阻塞队列进行阻塞等待。
    通过workQueue,线程池实现了阻塞功能。

3. mainLock
    mainLock是互斥锁,通过mainLock实现了对线程池的互斥访问。

4. corePoolSize和maximumPoolSize
    corePoolSize是"核心池大小",maximumPoolSize是"最大池大小"。它们的作用是调整"线程池中实际运行的线程的数量"。
    例如,当新任务提交给线程池时(通过execute方法)。
          -- 如果此时,线程池中运行的线程数量< corePoolSize,则创建新线程来处理请求。
          -- 如果此时,线程池中运行的线程数量> corePoolSize,但是却< maximumPoolSize;则仅当阻塞队列满时才创建新线程。
          如果设置的 corePoolSize 和 maximumPoolSize 相同,则创建了固定大小的线程池。如果将 maximumPoolSize 设置为基本的无界值(如 Integer.MAX_VALUE),则允许池适应任意数量的并发任务。在大多数情况下,核心池大小和最大池大小的值是在创建线程池设置的;但是,也可以使用 setCorePoolSize(int) 和 setMaximumPoolSize(int) 进行动态更改。

5. poolSize
    poolSize是当前线程池的实际大小,即线程池中任务的数量。

6. allowCoreThreadTimeOut和keepAliveTime
    allowCoreThreadTimeOut表示是否允许"线程在空闲状态时,仍然能够存活";而keepAliveTime是当线程池处于空闲状态的时候,超过keepAliveTime时间之后,空闲的线程会被终止。

7. threadFactory
    threadFactory是ThreadFactory对象。它是一个线程工厂类,"线程池通过ThreadFactory创建线程"

8. handler
    handler是RejectedExecutionHandler类型。它是"线程池拒绝策略"的句柄,也就是说"当某任务添加到线程池中,而线程池拒绝该任务时,线程池会通过handler进行相应的处理"。

newFixedThreadPool

 

对图10-4的说明如下。

1,如果当前运行的线程数少于corePoolSize, 会立刻创建新线程执行任务。
2,当线程数到达corePoolSize后,将任务加入到LinkedBlockingQueue中。
3,当线程执行完任务后,会循环从LinkedBlockingQueue中获取任务来执行。

FixedThreadPool使用了LinkedBlockingQueue, 也就是无界队列(队列最大可容纳Integer.MAX_VALUE), 因此会造成以下影响:
a, 线程池线程数到达corePoolSize后,任务会被存放在LinkedBlockingQueue中
b, 因为无界队列,运行中(未调用shutdown()或者shutdownNow()方法)的不会拒绝任务(队列无界,可以放入"无限"任务)

 使用举例:

 1 public class FixedThreadPoolTest {
 2     public static void main(String[] args) {
 3         ExecutorService fixedThreadPool = Executors.newFixedThreadPool(4*20);
 4         for (int i = 0; i < 10; i++) {
 5             final int index = i;
 6             fixedThreadPool.execute(new Runnable() {
 7                 public void run() {
 8                     try {
 9                         System.out.println(Thread.currentThread().getName()+">>"+index);
10                         Thread.sleep(2000);
11                     } catch (InterruptedException e) {
12                         e.printStackTrace();
13                     }
14                 }
15             });
16         }
17         
18 //        // 此方法不阻塞, 调用后不再接收新的任务,所有线程池内任务执行完毕则关闭线程池
19 //        fixedThreadPool.shutdown();
20 //        System.out.println("..........分割线.............");
21 //        fixedThreadPool.execute(new Runnable() {
22 //            @Override
23 //            public void run() {
24 //                System.out.println("run after shutdown.......");
25 //            }
26 //        });
27         
28     }
29 }
View Code

运行结果为:(会阻塞,因为线程池还没关闭)

 

 

 如果将上面的注释代码取消注释,运行结果为:

 1 pool-1-thread-2>>1
 2 pool-1-thread-5>>4
 3 pool-1-thread-4>>3
 4 pool-1-thread-1>>0
 5 pool-1-thread-3>>2
 6 pool-1-thread-6>>5
 7 pool-1-thread-7>>6
 8 pool-1-thread-8>>7
 9 pool-1-thread-9>>8
10 pool-1-thread-10>>9
11 ..........分割线.............
12 Exception in thread "main" java.util.concurrent.RejectedExecutionException: Task com.test.lesson01.FixedThreadPoolTest$2@4b67cf4d rejected from java.util.concurrent.ThreadPoolExecutor@7ea987ac[Shutting down, pool size = 10, active threads = 10, queued tasks = 0, completed tasks = 0]
13     at java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(ThreadPoolExecutor.java:2063)
14     at java.util.concurrent.ThreadPoolExecutor.reject(ThreadPoolExecutor.java:830)
15     at java.util.concurrent.ThreadPoolExecutor.execute(ThreadPoolExecutor.java:1379)
16     at com.test.lesson01.FixedThreadPoolTest.main(FixedThreadPoolTest.java:26)
17 
18 Process finished with exit code 1
View Code

不会阻塞

newSingleThreadExecutor

SingleThreadExecutor是使用单个worker线程的Executor

SingleThreadExecutor的corePoolSize和maximumPoolSize被设置为1。其他参数与 FixedThreadPool相同。SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的工 作队列(队列的容量为Integer.MAX_VALUE)。SingleThreadExecutor使用无界队列作为工作队列 对线程池带来的影响与FixedThreadPool相同,这里就不赘述了。

对图10-5的说明如下。
1)如果当前运行的线程数少于corePoolSize(即线程池中无运行的线程),则创建一个新线
程来执行任务。
2)在线程池完成预热之后(当前线程池中有一个运行的线程),将任务加入LinkedBlockingQueue。
3)线程执行完1中的任务后,会在一个无限循环中反复从LinkedBlockingQueue获取任务来
执行。

举例:

 1 public class SingleThreadExecutorTest {
 2 
 3     public static void main(String[] args) {
 4         ExecutorService singleThreadExecutor = Executors.newSingleThreadExecutor();
 5         for (int i = 0; i < 10; i++) {
 6             final int index = i;
 7             Runnable task = new Runnable() {
 8                 public void run() {
 9                     try {
10                         System.out.println(Thread.currentThread().getName()+">>"+index);
11                         Thread.sleep(1000);
12                     } catch (InterruptedException e) {
13                         e.printStackTrace();
14                     }
15                 }
16             };
17             singleThreadExecutor.execute(task);
18         }
19         
20         singleThreadExecutor.shutdown();
21     }
22 
23 }
View Code

结果为:(每隔1秒就会打印一个输出)

 

 

 

newCachedThreadPool

CachedThreadPool是一个会根据需要创建新线程的线程池。CachedThreadPool的corePoolSize被设置为0,即corePool为空;maximumPoolSize被设置为 Integer.MAX_VALUE,即maximumPool是无界的。这里把keepAliveTime设置为60L,意味着 CachedThreadPool中的空闲线程等待新任务的最长时间为60秒,空闲线程超过60秒后将会被终止。

FixedThreadPool和SingleThreadExecutor使用无界队列LinkedBlockingQueue作为线程池的 工作队列。CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,但 CachedThreadPool的maximumPool是无界的。这意味着,如果主线程提交任务的速度高于 maximumPool中线程处理任务的速度时,CachedThreadPool会不断创建新线程。极端情况下, CachedThreadPool会因为创建过多线程而耗尽CPU和内存资源。

对图10-6的说明如下。
1)首先执行SynchronousQueue.offer(Runnable task)。如果当前maximumPool中有空闲线程 正在执行SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS),那么主线程执行 offer操作与空闲线程执行的poll操作配对成功,主线程把任务交给空闲线程执行,execute()方 法执行完成;否则执行下面的步骤2)。
2)当初始maximumPool为空,或者maximumPool中当前没有空闲线程时,将没有线程执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这种情况下,步骤1)将失 败。此时CachedThreadPool会创建一个新线程执行任务,execute()方法执行完成。
3)在步骤2)中新创建的线程将任务执行完后,会执行 SynchronousQueue.poll(keepAliveTime,TimeUnit.NANOSECONDS)。这个poll操作会让空闲线
程最多在SynchronousQueue中等待60秒钟。如果60秒钟内主线程提交了一个新任务(主线程执 行步骤1)),那么这个空闲线程将执行主线程提交的新任务;否则,这个空闲线程将终止。由于 空闲60秒的空闲线程会被终止,因此长时间保持空闲的CachedThreadPool不会使用任何资源。
前面提到过,SynchronousQueue是一个没有容量的阻塞队列。每个插入操作必须等待另一 个线程的对应移除操作,反之亦然。CachedThreadPool使用SynchronousQueue,把主线程提交的 任务传递给空闲线程执行。CachedThreadPool中任务传递的示意图如图10-7所示。

 

 

使用举例:

 1 package com.test.lesson01;
 2 
 3 import java.util.concurrent.ExecutorService;
 4 import java.util.concurrent.Executors;
 5 
 6 public class CachedThreadPoolTest {
 7 
 8     public static void main(String[] args) {
 9         ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
10         for (int i = 0; i < 10; i++) {
11             final int index = i;
12             /*try {
13                 Thread.sleep(1000);
14             } catch (InterruptedException e) {
15                 e.printStackTrace();
16             }*/
17             Runnable task = new Runnable() {
18                 public void run() {
19                     try {
20                         Thread.sleep(1000);
21                     } catch (InterruptedException e) {
22                         e.printStackTrace();
23                     }
24                     System.out.println(Thread.currentThread().getName()+">>"+index+","+Thread.currentThread().hashCode());
25                 }
26             };
27             cachedThreadPool.execute(task);
28         }
29     }
30 
31 }
View Code

结果为:

 1 pool-1-thread-9>>8,1119372326
 2 pool-1-thread-5>>4,2118164867
 3 pool-1-thread-8>>7,1955604205
 4 pool-1-thread-6>>5,1242746377
 5 pool-1-thread-1>>0,1296698618
 6 pool-1-thread-4>>3,188351009
 7 pool-1-thread-10>>9,1881928750
 8 pool-1-thread-7>>6,282966402
 9 pool-1-thread-3>>2,1447026184
10 pool-1-thread-2>>1,278180759
View Code

并且,上述运行在1分钟后就会终止,因为CachedThreadPool在没有任务继续提交的时候就会关闭线程池。上面的线程的名字都是一样的,是因为任务执行时间很短,任务已经执行完了,线程池都还没有关闭,下一个任务来了直接可以采用刚刚的缓存的线程来执行任务。

如果将上面的休眠时间取消注释,将会出现如下结果:

 1 pool-1-thread-1>>0,1986922183
 2 pool-1-thread-2>>1,903833002
 3 pool-1-thread-1>>2,1986922183
 4 pool-1-thread-2>>3,903833002
 5 pool-1-thread-1>>4,1986922183
 6 pool-1-thread-2>>5,903833002
 7 pool-1-thread-1>>6,1986922183
 8 pool-1-thread-2>>7,903833002
 9 pool-1-thread-1>>8,1986922183
10 pool-1-thread-2>>9,903833002
View Code

出现这种情况是因为当下次任务来的时候,上次的任务还没执行结束,线程池中只好再新建立一个线程来执行这个新到的任务。当没有任务需要执行的时候,程序运行将在1分钟后自动结束。

 

 

参考文献:

《Java并发编程艺术》

https://www.cnblogs.com/skywang12345/p/3509941.html

以上是关于Java多线程系列--“JUC线程池”02之 线程池原理的主要内容,如果未能解决你的问题,请参考以下文章

Java多线程系列--“JUC线程池”02之 线程池原理

Java多线程系列--“JUC线程池”03之 线程池原理

Java多线程系列--“JUC线程池”01之 线程池架构

java多线程系类:JUC线程池:03之线程池原理(转)

Java多线程系列--“JUC线程池”04之 线程池原理

Java多线程系列--“JUC线程池”03之 线程池原理