• java并发编程(十七)----(线程池)java线程池架构和原理


    前面我们简单介绍了线程池的使用,但是对于其如何运行我们还不清楚,Executors为我们提供了简单的线程工厂类,但是我们知道ThreadPoolExecutor是线程池的具体实现类。我们先从他开始分析。

    1. ThreadPoolExecutor初探

    ThreadPoolExecutor一共有3个构造方法,我们来看一下其中看起来比较复杂的这个:

    public ThreadPoolExecutor(int corePoolSize,
                                  int maximumPoolSize,
                                  long keepAliveTime,
                                  TimeUnit unit,
                                  BlockingQueue<Runnable> workQueue,
                                  ThreadFactory threadFactory,
                                  RejectedExecutionHandler handler) {
            if (corePoolSize < 0 ||
                maximumPoolSize <= 0 ||
                maximumPoolSize < corePoolSize ||
                keepAliveTime < 0)
                throw new IllegalArgumentException();
            if (workQueue == null || threadFactory == null || handler == null)
                throw new NullPointerException();
            this.corePoolSize = corePoolSize;
            this.maximumPoolSize = maximumPoolSize;
            this.workQueue = workQueue;
            this.keepAliveTime = unit.toNanos(keepAliveTime);
            this.threadFactory = threadFactory;
            this.handler = handler;
        }

    看起来参数是挺多的,我们不妨耐心看看参数都是什么意思:

    1. corePoolSize:核心池的大小,默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中;

    2. maximumPoolSize:线程池最大线程数,它表示在线程池中最多能创建多少个线程;

    3. keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。

    4. unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性:

      TimeUnit.DAYS;               //天
      TimeUnit.HOURS;             //小时
      TimeUnit.MINUTES;           //分钟
      TimeUnit.SECONDS;           //秒
      TimeUnit.MILLISECONDS;      //毫秒
      TimeUnit.MICROSECONDS;      //微妙
      TimeUnit.NANOSECONDS;       //纳秒
      
    5. workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:

      ArrayBlockingQueue;
      LinkedBlockingQueue;
      SynchronousQueue;
      
    6. threadFactory:是构造Thread的方法,你可以自己去包装和传递,主要实现newThread方法即可;

    7. handler:表示当拒绝处理任务时的策略,也就是参数maximumPoolSize达到后丢弃处理的方法,java提供了4种丢弃处理的方法,当然你也可以自己根据实际情况去重写,主要是要实现接口:RejectedExecutionHandler中的方法: public void rejectedExecution(Runnabler, ThreadPoolExecutor e) java默认的是使用:AbortPolicy,他的作用是当出现这中情况的时候会抛出一个异常;有以下四种取值:

      ①ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。

      ②ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。

      ③ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程)

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

    上面说了ThreadPoolExecutor的构造方法,我们继续看他的类的关系:

    public class ThreadPoolExecutor extends AbstractExecutorService {
    }

    由源码我们看出ThreadPoolExecutor继承了AbstractExecutorService类,我们知道AbstractExecutorService是一个抽象类,它实现了ExecutorService接口。AbstractExecutorService存在的目的是为ExecutorService中的函数接口提供了默认实现。

    public abstract class AbstractExecutorService implements ExecutorService {
    }

    由上我们知道AbstractExecutorService又实现了ExecutorService接口,而ExecutorService是Executor实现类的最直接接口。

    public interface ExecutorService extends Executor {
    }

    由此我们似乎可以明白他们之间的关系:

    1. Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable);
    2. ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等;
    3. 抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;
    4. ThreadPoolExecutor继承了类AbstractExecutorService,成为线程池的具体实现类。

    2. 线程池的实现

    上面我们从ThreadPoolExecutor的构造方法出发提到了线程池的状态,执行,初始化,排队策略等等,下面我们就从这些方面入手,看看线程池的原理。

    线程池初始化

    默认情况下,创建线程池之后,线程池中是没有线程的,需要提交任务(execute或者submit)之后才会创建线程。在实际中如果需要线程池创建之后立即创建线程,可以通过以下两个方法办到:

    • prestartCoreThread():初始化一个核心线程
    • prestartAllCoreThreads():初始化所有核心线程

    下面是这两个方法的实现:

    public boolean prestartCoreThread() {
            return workerCountOf(ctl.get()) < corePoolSize && addWorker(null, true);
    }
    -----------------------------------------------------
    
    public int prestartAllCoreThreads() {
            int n = 0;
            while (addWorker(null, true))
                ++n;
            return n;
    }
    -----------------------------------------------------
    

    我们注意到上面两个方法都调用了addWorker方法,我们看一下实现:

    private boolean addWorker(Runnable firstTask, boolean core) {
           retry:
           for (;;) {
               int c = ctl.get();
               int rs = runStateOf(c);
    
               // Check if queue empty only if necessary.
               if (rs >= SHUTDOWN &&
                   ! (rs == SHUTDOWN &&
                      firstTask == null &&
                      ! workQueue.isEmpty()))
                   return false;
    
               for (;;) {
                   int wc = workerCountOf(c);
                   if (wc >= CAPACITY ||
                       wc >= (core ? corePoolSize : maximumPoolSize))
                       return false;
                   if (compareAndIncrementWorkerCount(c))
                       break retry;
                   c = ctl.get();  // Re-read ctl
                   if (runStateOf(c) != rs)
                       continue retry;
                   // else CAS failed due to workerCount change; retry inner loop
               }
           }
    
           boolean workerStarted = false;
           boolean workerAdded = false;
           Worker w = null;
           try {
               final ReentrantLock mainLock = this.mainLock;
               w = new Worker(firstTask);
               final Thread t = w.thread;
               if (t != null) {
                   mainLock.lock();
                   try {
                       // Recheck while holding lock.
                       // Back out on ThreadFactory failure or if
                       // shut down before lock acquired.
                       int c = ctl.get();
                       int rs = runStateOf(c);
    
                       if (rs < SHUTDOWN ||
                           (rs == SHUTDOWN && firstTask == null)) {
                           if (t.isAlive()) // precheck that t is startable
                               throw new IllegalThreadStateException();
                           workers.add(w);
                           int s = workers.size();
                           if (s > largestPoolSize)
                               largestPoolSize = s;
                           workerAdded = true;
                       }
                   } finally {
                       mainLock.unlock();
                   }
                   if (workerAdded) {
                       t.start();
                       workerStarted = true;
                   }
               }
           } finally {
               if (! workerStarted)
                   addWorkerFailed(w);
           }
           return workerStarted;
       }

    这个方法还是挺好理解:上面的retry是对当前线程池状态进行检查,如果当前线程池未初始化或者未分配则返回false;

    往下是初始化firstTask,我们看到在56行把初始化的firstTask加入workers集合,该集合定义为:

     private final HashSet<Worker> workers = new HashSet<Worker>();

    集合中包含当前所有的工作线程。
    看完addWorker的实现,那么上面的prestartCoreThread和prestartAllCoreThreads我们就很好理解,前一个是向当前工作线程池中加入一个工作线程,后一个是循环N次。

    线程池的执行

    通常你得到线程池后,会调用其中的:submit方法或execute方法去操作;其实你会发现,submit方法最终会调用execute方法来进行操作,只是他提供了一个Future来托管返回值的处理而已,当你调用需要有返回值的信息时,你用它来处理是比较好的;这个Future会包装对Callable信息,并定义一个Sync对象(),当你发生读取返回值的操作的时候,会通过Sync对象进入锁,直到有返回值的数据通知。

    我们先看一下submit方法的源码:

    public Future<?> submit(Runnable task) {
           if (task == null) throw new NullPointerException();
           RunnableFuture<Void> ftask = newTaskFor(task, null);
           execute(ftask);
           return ftask;
       }

    我们看到在源码的第4行实际上是调用了execute()方法来处理包装的RunnableFuture。下面是execute方法的源码:

    public void execute(Runnable command) {
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            if (! isRunning(recheck) && remove(command))
                reject(command);
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        else if (!addWorker(command, false))
            reject(command);
    }

    第一个if为非空判断;

    第二个if中的workerCountOf()方法拿到ctl中存储的当前线程总数,如果小于corePoolSize,那么就会走到addWorker()方法中,如果成功创建了Worker的话,那么返回true,直接return,否则重新通过cas拿一次c;

    第三个if中判断当前的线程池是否处于RUNNING状态,如果是,并且workQueue.offer加入队列成功话,那么就重新拿出来一次ctl,再判断如果加入队列之后,线程池如果不是处于RUNNING的状态,并且从队列中remove成功的话,那么就会执行reject操作;判断当前线程数是否为0,如果为0的话,那么就调用addWorker(null,false),否则如果非Running状态或者加入队列失败的话,那么就会调用addWorker(command,false)如果返回false,说明没有添加成功,就会执行reject操作。

    任务缓存队列

    我们还记得ThreadPoolExecutor的构造函数中有一个参数workQueue,它用来存放等待执行的任务。
    workQueue的类型为BlockingQueue,通常可以取下面三种类型:

    1)ArrayBlockingQueue:基于数组的先进先出队列,此队列创建时必须指定大小;

    2)LinkedBlockingQueue:基于链表的先进先出队列,如果创建时没有指定此队列大小,则默认为Integer.MAX_VALUE;

    3)synchronousQueue:这个队列比较特殊,它不会保存提交的任务,而是将直接新建一个线程来执行新来的任务。

    线程池的关闭

    ThreadPoolExecutor提供了两个方法,用于线程池的关闭,分别是shutdown()和shutdownNow(),其中:

    • shutdown():不会立即终止线程池,而是要等所有任务缓存队列中的任务都执行完后才终止,但再也不会接受新的任务
    • shutdownNow():立即终止线程池,并尝试打断正在执行的任务,并且清空任务缓存队列,返回尚未执行的任务
    线程池容量的动态调整

    ThreadPoolExecutor提供了动态调整线程池容量大小的方法:setCorePoolSize()和setMaximumPoolSize()

    setCorePoolSize:设置核心池大小

    setMaximumPoolSize:设置线程池最大能创建的线程数目大小

    当上述参数从小变大时,ThreadPoolExecutor进行线程赋值,还可能立即创建新的线程来执行任务。

    下面我们看一个小例子:

    public class ThreadPoolExecutorTest {
        private static int produceTaskSleepTime = 2;
    
        private static int produceTaskMaxNumber = 10;
    
        public static void main(String[] args) {
    
            // 构造一个线程池
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(2, 4, 3,
                    TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(3),
                    new ThreadPoolExecutor.DiscardOldestPolicy());
    
            for (int i = 1; i <= produceTaskMaxNumber; i++) {
                try {
                    String task = "task-- " + i;
                    System.out.println("创建任务并提交到线程池中:" + task);
                    threadPool.execute(new ThreadPoolTask(task));
                    System.out.println("线程池中线程数目:"+threadPool.getPoolSize()+",队列中等待执行的任务数目:"+
                           threadPool.getQueue().size()+",已执行完毕的任务数目:"+threadPool.getCompletedTaskCount());
    
                    Thread.sleep(produceTaskSleepTime);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    class ThreadPoolTask implements Runnable, Serializable {
    
        private Object attachData;
    
        ThreadPoolTask(Object tasks) {
            this.attachData = tasks;
        }
    
        public void run() {
    
            System.out.println("开始执行任务:" + attachData);
    
            attachData = null;
        }
    
        public Object getTask() {
            return this.attachData;
        }
    }

    结果为:

    创建任务并提交到线程池中:task-- 1
    开始执行任务:task-- 1
    线程池中线程数目:1,队列中等待执行的任务数目:0,已执行完毕的任务数目:0
    创建任务并提交到线程池中:task-- 2
    线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完毕的任务数目:1
    开始执行任务:task-- 2
    创建任务并提交到线程池中:task-- 3
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:2
    开始执行任务:task-- 3
    创建任务并提交到线程池中:task-- 4
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:3
    开始执行任务:task-- 4
    创建任务并提交到线程池中:task-- 5
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:4
    开始执行任务:task-- 5
    创建任务并提交到线程池中:task-- 6
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:5
    开始执行任务:task-- 6
    创建任务并提交到线程池中:task-- 7
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:6
    开始执行任务:task-- 7
    创建任务并提交到线程池中:task-- 8
    线程池中线程数目:2,队列中等待执行的任务数目:1,已执行完毕的任务数目:7
    开始执行任务:task-- 8
    创建任务并提交到线程池中:task-- 9
    开始执行任务:task-- 9
    线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完毕的任务数目:8
    创建任务并提交到线程池中:task-- 10
    开始执行任务:task-- 10
    线程池中线程数目:2,队列中等待执行的任务数目:0,已执行完毕的任务数目:9

    由结果我们可以看到当线程池中线程的数目大于2时,便将任务放入任务缓存队列里面,当任务缓存队列满了之后,便创建新的线程。

  • 相关阅读:
    【51nod1674】区间的价值 V2(算法效率--位运算合并优化+链表实现)
    【bzoj 2339】[HNOI2011]卡农(数论--排列组合+逆元+递推)
    关于中国剩余定理{附【转】中国剩余定理 }
    JavaScript操作BOM
    学员操作—统计考试平均成绩
    JavaScript基础
    JDBC
    进制
    事务
    复习
  • 原文地址:https://www.cnblogs.com/rickiyang/p/11074253.html
Copyright © 2020-2023  润新知