【Java】Java线程池ThredPoolExcutor分析及源码解析


一、为什么要使用线程池

  说到线程池那么一定是应用在多线程情况中,所谓的多线程系统,即多线程并发执行,实际上是通过共享CPU时间片段实现的。OS将CPU时间分成“CPU时间片段”,每一个片段CPU执行某一个线程;当时间片段到了,由OS中的线程调度程序来选择下一个时间片段内执行的线程。但不是立即切换,还要做一些事,分析其过程:线程A时间片段到后,线程调度进程需要先保护现场信息,即需要将当前正在执行的线程暂时中断,并将这个线程当前执行的状态保存起来,当然是为了以后这个线程恢复执行时,能够接着被中断的程序继续执行。所以多线程会导致线程信息Context(上下文)的频繁切换,CPU过度切换。

  再说我们的RMI框架。在RMI服务器端每侦听到一个客户端,需要产生个线程,完成客户端对特定方法的调用,并且返回相关结果。客户端连接服务器之后只是把方法名字和参数传递过去,服务器得到相关方法名称,能够发现他所对应的类,执行相应的方法,把参数注入进去,反射机制执行,执行后返回结果。这个过程本质上只是执行了一个方法,所以这个线程将很快结束。那么,在大量的客户端都存在向服务器端进行RMI请求的情况下,服务器端会存在海量的线程的创建和销毁,频繁的做这种事,会使得服务器效率很低。

  对于web开发中,服务器需要接受并处理请求,所以会为一个请求来分配一个线程来进行处理。如果每次请求都新创建一个线程的话实现起来非常简便,但是存在这些问题:

  1. 当用户访问量很大的情况下,对于服务器来说,每一个用户的请求都建立一个工作线程来处理,对于服务器来说压力很大,可能会造成服务器的崩溃,

  2. 另外创建线程要花费昂贵的资源和时间,有时创建一个线程在销毁的时间加在一起可能远远大于这个工作线程处理这个请求的时间。

  3. 还有就是如果任务来了才创建线程那么响应时间会变长,而且一个进程能创建的线程数有限。

  为了避免上述问题,在程序启动的时候就创建若干线程来响应处理,它们被称为线程池,里面的线程叫工作线程。线程池为线程生命周期开销问题和资源不足问题提供了解决方案。通过对多个任务重用线程,线程创建的开销被分摊到了多个任务上。其好处是,因为在请求到达时线程已经存在,所以无意中也消除了线程创建所带来的延迟。这样,就可以立即为请求服务,使应用程序响应更快。而且,通过适当地调整线程池中的线程数目,也就是当请求的数目超过某个阈值时,就强制其它任何新到的请求一直等待,直到获得一个线程来处理为止,从而可以防止资源不足。

使用线程池的好处

  • 降低资源消耗。通过重复利用已创建的线程降低线程创建和销毁造成的消耗。
  • 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  • 提高线程的可管理性。线程是稀缺资源,如果无限制的创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以进行统一的分配,调优和监控。

二、线程池的使用

  Java中的线程池是通过Executor框架实现的,该框架中用到了Executor,Executors,ExecutorService,ThredPoolExecutor这几个类。具体类图如下图所示(主要看红线)。

在这里插入图片描述
  Executor框架是一个根据一组执行策略调用,调度,执行和控制的异步任务的框架,目的是提供一种将”任务提交”与”任务如何运行”分离开来的机制。

(2.1)Executor接口

public interface Executor {
    void execute(Runnable command);
}

  Executor接口只有一个execute方法,可以是创建一个新线程并立即启动,也有可能是使用已有的工作线程来运行传入的任务,也可能是根据设置线程池的容量或者阻塞队列的容量来决定是否要将传入的线程放入阻塞队列中或者拒绝接收传入的线程。

(2.2)ExecutorService接口

在这里插入图片描述
ExecutorService主要具体定义了线程池的行为。

  1. void shutdown():在完成已提交的任务后封闭办事,不再接受新的任务。
  2. List<Runnable> shutdownNow():停止所有正在执行的任务,停止正在等待处理的任务,并返回等待执行但未执行的任务列表。
  3. Future<?> submit(Runnable task):可以用来提交Callable或Runnable任务,并返回代表此任务的Future对象。

(2.3)Executors类

  该类是我们创建线程池的辅助工具类,提供许多静态方法来创建几类线程池。
在这里插入图片描述

(2.3.1)FixedThreadPool定长线程池

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

创建固定大小的线程池,核心数和最大数是一样的,

特点可控制同时执行的线程数,超出的线程会在队列中等待。

(2.3.2)SingleThreadExecutor单线程化的线程池

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

创建一个单线程的线程池,这个线程池的核心数和最大数都是1,也就相当于单线程串行执行所有任务。

特点有且仅有一个工作线程执行任务,所有任务按照指定顺序执行,即遵循队列的入队出队规则。

(2.3.3)CachedThreadPool可缓存线程池

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

创建一个可缓存的线程池。核心数是0,最大数是Integer.MAX_VALUE,60秒不执行任务就回收。

特点线程数无限制,有空闲线程则复用空闲线程,若无空闲线程则新建线程,一定程序减少频繁创建/销毁线程,减少系统开销。

(2.3.4)ScheduledThreadPool预先安排线程池

   public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }

创建一个线程池,该线程池可以在给定的延迟之后执行任务,或周期性地执行。

特点支持定时及周期性任务执行。

(2.3.5)WorkStealingPool任务窃取线程池

    public static ExecutorService newWorkStealingPool() {
        return new ForkJoinPool
            (Runtime.getRuntime().availableProcessors(),
             ForkJoinPool.defaultForkJoinWorkerThreadFactory,
             null, true);
    }

1.8之后新加的线程池,ForkJoinPool可以根据CPU的核数并行的执行,适合在很耗时的操作,可以充分的利用CPU执行任务。

线程池中有多个线程队列,有的队列中有大量的比较耗时的任务堆积,而有的线程队列却是空的,就存在有的线程存在“饥饿”状态,当一个线程处于“饥饿”状态时,它就会去其他的线程队列窃取任务,解决饥饿导致的效率问题。

特点适合使用在很耗时的操作,可以充分利用CPU执行任务,但不能保证任务的顺序执行。

这么多种线程池,我们工程种需要哪种最好呢?答案都不在这里面,我们可以看《阿里巴巴Java开发手册》。

【强制】线程池不允许使用 Executors 去创建,而是通过 ThreadPoolExecutor 的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明: Executors 返回的线程池对象的弊端如下:
1) FixedThreadPool 和 SingleThreadPool :
允许的请求队列长度为 Integer.MAX_VALUE ,可能会堆积大量的请求,从而导致 OOM 。
2) CachedThreadPool 和 ScheduledThreadPool :
允许的创建线程数量为 Integer.MAX_VALUE ,可能会创建大量的线程,从而导致 OOM 。

是的,如果我们只是使用Executors提供的工厂方法创建线程池,那我们只能算是一个API的使用者,忽略了线程池内部的实现。因此我们需要深挖线程池(ThreadPoolExecutor)内部。

三、线程池几个重要的参数介绍

  我们直接来看ThreadPoolExecutor的最多参数的构造。

    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.acc = (System.getSecurityManager() == null)
            ? null
            : AccessController.getContext();
        this.corePoolSize = corePoolSize;
        this.maximumPoolSize = maximumPoolSize;
        this.workQueue = workQueue;
        this.keepAliveTime = unit.toNanos(keepAliveTime);
        this.threadFactory = threadFactory;
        this.handler = handler;
    }

(3.1)corePoolSize

  核心池的大小。在创建了线程池后,默认情况下,线程池种并没有任何线程,而是等待有任务提交时才创建线程去执行任务,除非调用了prestartCoreThread()prestartAllCoreThreads方法,从这两个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建一个线程或者corePoolSize个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来的时候,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,再来新的任务就会把到达的任务放到workQueue中,如果线程池中的线程数量大于等于 corePoolSize 且小于 maximumPoolSize,则只有当workQueue满时才创建新的线程去处理任务;如果设置的corePoolSize 和 maximumPoolSize相同,则创建的线程池的大小是固定的,这时如果有新任务提交,若workQueue未满,则将请求放入workQueue中,等待有空闲的线程去从workQueue中取任务并处理;如果运行的线程数量大于等于maximumPoolSize,这时如果workQueue已经满了,则通过handler所指定的策略来处理任务;所以,任务提交时,判断的顺序依次为 corePoolSize –> workQueue –> maximumPoolSize。

(3.2)maximumPoolSize

  表示线程池能够容纳同时执行的最大线程数。线程总数= 核心线程数+ 非核心线程数。如果当阻塞队列已满时,并且当前线程池线程个数没有超过 maximumPoolSize 的话,就会创建新的线程(非核心线程)来执行任务。注意 maximumPoolSize >= 1 必须大于等于 1。核心线程是不会被回收的、回收条件是线程池中的线程数量是否大于核心线程数。corePoolSize 大小和 maximumPoolSize 大小一致的话 线程池中的线程将不会空闲、 keepAliveTime 和 timeUnit 就不会再起作用。

(3.3)keepAliveTime

  线程空闲时间。当空闲时间达到 keepAliveTime值时,线程会被销毁,直到只剩下 corePoolSize 个线程为止,避免浪费内存和句柄资源。默认情况,当线程池的线程数大于 corePoolSize 时,keepAliveTime 才会起作用。但当 ThreadPoolExecutor 的 allowCoreThreadTimeOut 变量设置为 true 时,核心线程超时后也会被回收。

(3.4)unit

  配合keepAliveTime的时间单位。

在这里插入图片描述

(3.5)workQueue

  保存等待执行的任务的阻塞队列。其类型是BlockingQueue,它是基于ReentrantLock。

  阻塞队列:在任意时刻,不管并发有多高,永远只有一个元素入队或者出队的操作。线程安全的队列。

有界:队列有大小。队列满,只能进行出队操作,所有入队操作被阻塞。队列空,只能进行入队操作,所有出队操作被阻塞。

无界:理论上是无界的,实际上受物理主机内存大小限制。

在Java中,BlockingQueue是一个接口,它的实现类有ArrayBlockingQueue、DelayQueue、 LinkedBlockingQueue、PriorityBlockingQueue、SynchronousQueue等,它们的区别主要体现在存储结构上或对元素操作上的不同,但是对于take与put操作的原理,却是类似的。
转自:https://www.jianshu.com/p/7b2f1fa616c6

(3.5.1)LinkedBlockingQueue

  LinkedBlockingQueue是一个基于链表实现的可选容量的阻塞队列。队头的元素是插入时间最长的,队尾的元素是最新插入的。新的元素将会被插入到队列的尾部。

  LinkedBlockingQueue的容量限制是可选的,如果在初始化时没有指定容量,那么默认使用int的最大值作为队列容量。

  LinkedBlockingQueue中维持两把锁,一把锁用于入队,一把锁用于出队,这也就意味着,同一时刻,只能有一个线程执行入队,其余执行入队的线程将会被阻塞;同时,可以有另一个线程执行出队,其余执行出队的线程将会被阻塞。换句话说,虽然入队和出队两个操作同时均只能有一个线程操作,但是可以一个入队线程和一个出队线程共同执行,也就意味着可能同时有两个线程在操作队列,那么为了维持线程安全,LinkedBlockingQueue使用一个AtomicInterger类型的变量表示当前队列中含有的元素个数,所以可以确保两个线程之间操作底层队列是线程安全的。

特点:采用了链表,最大容量为整数最大值,可看做容量无限(无界)

(3.5.2)ArrayBlockingQueue

  ArrayBlockingQueue底层是使用一个数组实现队列的,并且在构造ArrayBlockingQueue时需要指定容量,也就意味着底层数组一旦创建了,容量就不能改变了,因此ArrayBlockingQueue是一个容量限制的阻塞队列。因此,在队列全满时执行入队将会阻塞,在队列为空时出队同样将会阻塞。其内部只有一把锁,意味着同一时刻只有一个线程能进行入队或者出队的操作。

特点:采用了数组,必须指定大小,即容量有限(有界)

(3.5.3)PriorityBlockingQueue

  PriorityBlockingQueue是一个支持优先级的无界阻塞队列。默认情况下元素采用自然顺序升序排序,当然我们也可以通过构造函数来指定Comparator来对元素进行排序。需要注意的是PriorityBlockingQueue不能保证同优先级元素的顺序。

特点:可设置优先级,无界

(3.5.4)SynchronousQueue

  SynchronousQueue没有容量。与其他BlockingQueue不同,SynchronousQueue是一个不存储元素的BlockingQueue。每一个put操作必须要等待一个take操作,否则不能继续添加元素,

特点:SynchronousQueue非常适合做交换工作,生产者的线程和消费者的线程同步以传递某些信息、事件或者任务。

(3.6)threadFactory

  创建线程的工程类。可以通过指定线程工厂为每个创建出来的线程设置更有意义的名字,如果出现并发问题,也方便查找问题原因。

(3.7)RejectedExecutionHandler

  拒绝策略:如果阻塞队列满了并且没有空闲的线程,这时如果继续提交任务,就需要采取一种策略处理该任务。线程池提供四种。

(3.7.1)AbortPolicy(默认)

    public static class AbortPolicy implements RejectedExecutionHandler {
        /**
         * Creates an {@code AbortPolicy}.
         */
        public AbortPolicy() { }

        /**
         * Always throws RejectedExecutionException.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         * @throws RejectedExecutionException always
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            throw new RejectedExecutionException("Task " + r.toString() +
                                                 " rejected from " +
                                                 e.toString());
        }
    }

拒绝所提交的任务,并抛出 RejectedExecutionException 异常;

(3.7.2)DiscardPolicy

    public static class DiscardPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardPolicy}.
         */
        public DiscardPolicy() { }

        /**
         * Does nothing, which has the effect of discarding task r.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
        }
    }

不处理并且直接丢弃掉任务,不抛异常;

(3.7.3)DiscardOldestPolicy

    public static class DiscardOldestPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code DiscardOldestPolicy} for the given executor.
         */
        public DiscardOldestPolicy() { }

        /**
         * Obtains and ignores the next task that the executor
         * would otherwise execute, if one is immediately available,
         * and then retries execution of task r, unless the executor
         * is shut down, in which case task r is instead discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                e.getQueue().poll();
                e.execute(r);
            }
        }
    }

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

(3.7.4)CallerRunsPolicy

    public static class CallerRunsPolicy implements RejectedExecutionHandler {
        /**
         * Creates a {@code CallerRunsPolicy}.
         */
        public CallerRunsPolicy() { }

        /**
         * Executes task r in the caller's thread, unless the executor
         * has been shut down, in which case the task is discarded.
         *
         * @param r the runnable task requested to be executed
         * @param e the executor attempting to execute this task
         */
        public void rejectedExecution(Runnable r, ThreadPoolExecutor e) {
            if (!e.isShutdown()) {
                r.run();	//从这里看出来让r去跑,而不是e.execute();
            }
        }
    }

“调用者运行”策略是一种调节机制,该策略既不会丢弃任务,也不会抛出异常,而是将任务回退到调用者(由调用者处理该任务,调用execute方法的线程),从而降低新任务的流量。

(3.7.5)自定义拒绝策略

import java.util.concurrent.RejectedExecutionHandler;
import java.util.concurrent.ThreadPoolExecutor;

public class MyRejectPolicy implements RejectedExecutionHandler {

	@Override
	public void rejectedExecution(Runnable r, ThreadPoolExecutor executor) {

		System.out.println("未被执行的任务" + r);
		System.out.println("因为饱和拒绝任务的线程池" + executor);
		System.out.println("可以做想做的事,例如记录相关未执行的任务");
	}

}

自定义拒绝策略只需要实现RejectedExecutionHandler接口即可。自定义策略可以记录日志或持久化存储不能处理的任务。

四、一个例子解释线程池工作过程

  可能你觉得上述纯语言描述太晦涩难懂线程池处理的实际过程。下来我举一个例子和图来说明。为了深刻理解corePoolSize –> workQueue –> maximumPoolSize的执行顺序!

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class RejectDemo {

	public static void main(String[] args) {

		/*创建一个核心线程数为2,最大线程数为3,等待阻塞队列为5,默认拒绝策略的线程池*/
		ThreadPoolExecutor thredPool = new ThreadPoolExecutor(2, 3, 60, TimeUnit.SECONDS, 
						new ArrayBlockingQueue<Runnable>(5), 
						Executors.defaultThreadFactory(), new ThreadPoolExecutor.AbortPolicy());
		
		try {
			/*执行9个Task任务*/
			for (int i =0 ; i < 9; i++) {
				thredPool.execute(new Task(i));
			}
		} catch (Exception e) {
			e.printStackTrace();
		} finally {
			thredPool.shutdown();
		}
		
	}

	
	public static class Task implements Runnable{

		private int num;
		
		public Task(int num) {
			this.num = num;
		}
		
		@Override
		public void run() {
			System.out.println("执行当前任务的线程是:" + Thread.currentThread().getName());
			
			try {
				/*需要延迟2s来模拟线程执行的复杂过程*/
				Thread.sleep(2000);
			} catch (InterruptedException e) {
				e.printStackTrace();
			}
			System.out.println("任务" + num + "执行完毕");
		}
		
	}
}

从上述例子我们可知创建的线程池最多可以3(maxPoolSize)个任务并行,线程池总共可容纳3(maxPoolSize) + 5(workQueue长度)= 8个线程。但我们例子运行了9个线程,这会触发拒绝策略。我们看输出。

执行当前任务的线程是:pool-1-thread-2
执行当前任务的线程是:pool-1-thread-1
执行当前任务的线程是:pool-1-thread-3
java.util.concurrent.RejectedExecutionException: Task com.mec.thredPool.core.RejectDemo$Task@5679c6c6 rejected from java.util.concurrent.ThreadPoolExecutor@27ddd392[Running, pool size = 3, active threads = 3, queued tasks = 5, completed tasks = 0]
	at java.base/java.util.concurrent.ThreadPoolExecutor$AbortPolicy.rejectedExecution(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor.reject(Unknown Source)
	at java.base/java.util.concurrent.ThreadPoolExecutor.execute(Unknown Source)
	at com.mec.thredPool.core.RejectDemo.main(RejectDemo.java:20)
任务0执行完毕
任务7执行完毕
任务1执行完毕
执行当前任务的线程是:pool-1-thread-3
执行当前任务的线程是:pool-1-thread-1
执行当前任务的线程是:pool-1-thread-2
任务3执行完毕
执行当前任务的线程是:pool-1-thread-3
任务2执行完毕
任务4执行完毕
执行当前任务的线程是:pool-1-thread-1
任务5执行完毕
任务6执行完毕

确实我们第9次执行任务完毕没有输出,由于默认拒绝策略(AbortPolicy),有个异常说明了任务8被拒绝了。结合任务输出顺序我们可以总结如下线程池内部运行过程。

  1. 当N多个任务提交过来,线程池只接受固定的任务,必须是实现Runnable接口或者Callable接口的任务。

  2. 任务通过execute()方法放到线程池里,然后由线程池自行决定怎么去处理。

  3. 以上面例子为例。
    (a)首先来了两个任务(task0和task1),线程池会创建两个核心线程去执行这两个任务。
    在这里插入图片描述

    (b)当来第三个任务(task2)的时候,核心池已经满了
    (c)第三个任务(task2)会被放到阻塞队列里面去,直至把队列放满(task3,4,5,6)
    在这里插入图片描述

    (d)当阻塞队列放满了之后,新来任务就会创建非核心线程去执行任务
    在这里插入图片描述

    (e)当核心线程,阻塞队列,非核心线程都满了之后,再来任务就会触发拒绝策略。
    在这里插入图片描述
    (f)所以我们看到的输出顺序任务7为什么会在2,3,4,5,6之前就是它直接在非核心线程开始运行了,其他任务都还在排队着。

希望通过这个例子能让你对线程池执行相关过程和线程池几个重要的参数有深刻的理解!

五、ThredPoolExecutor源码解析

  我们知道,根据面对对象思想,一个类最为核心的不是其方法,而是其成员,方法都是围绕其成员来进行操作的,所以我们看源码也是,直接先来重点关注其成员。

(5.1)成员

    //用来标记线程池生命状态(高3位)和当前活动线程数(低29位)
    //默认初始化是RUNNING状态,线程个数是0个
    private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));

    //线程个数位数。整性最大位数-3(32-3)
    private static final int COUNT_BITS = Integer.SIZE - 3;
    //线程个数掩码。1左移29位-1。
    //0001 1111 1111 1111 1111 1111 1111 1111
    private static final int COUNT_MASK = (1 << COUNT_BITS) - 1;

    //线程池生命状态
    //1110 0000 0000 0000 0000 0000 0000 0000
    private static final int RUNNING    = -1 << COUNT_BITS;

    //0000 0000 0000 0000 0000 0000 0000 0000
    private static final int SHUTDOWN   =  0 << COUNT_BITS;

    //0010 0000 0000 0000 0000 0000 0000 0000
    private static final int STOP       =  1 << COUNT_BITS;

    //0100 0000 0000 0000 0000 0000 0000 0000
    private static final int TIDYING    =  2 << COUNT_BITS;

    //0110 0000 0000 0000 0000 0000 0000 0000
    private static final int TERMINATED =  3 << COUNT_BITS;

    //获取高三位 线程池生命状态
    private static int runStateOf(int c)     { return c & ~COUNT_MASK; }
    //获取低29位 线程个数
    private static int workerCountOf(int c)  { return c & COUNT_MASK; }
    //计算ctl值 状态 或 线程个数
    private static int ctlOf(int rs, int wc) { return rs | wc; }

  其中最重要的就是ctl这个值。在线程池运行状态改变的时候,会涉及到线程池生命状态活动线程数的改变,为了保证绝对安全,这两个状态巧妙利用 bit 操作联系在一起。高3位记录线程池生命状态,低29位记录当前活动线程数。ctl值在下面操作经常要获取,就是为了保证操作安全。

  然后再介绍下线程池的生命状态. 线程池一共有五种状态, 分别是:

  1. RUNNING:能接受新的任务,也可以处理已经添加在阻塞队列的任务。
  2. SHUTDOWN:不能接受新任务,但是可以处理已经添加在阻塞队列的任务。
  3. STOP:不接受新任务,不处理已经添加的任务,并且中断正在处理的任务。
  4. TIDYING:所有任务都执行完(包含阻塞队列里面任务)当前线程池活动线程为 0,将要调用terminated方法
  5. TERMINATED:线程池彻底终止,terminated方法调用完成以后的状态。

其状态转换过程。
在这里插入图片描述

    public void shutdown() {
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
       		// 安全策略判断
            checkShutdownAccess();
            // 切换状态为SHUTDOWN
            advanceRunState(SHUTDOWN);
            // 中断空闲线程
            interruptIdleWorkers();
            onShutdown(); // hook for ScheduledThreadPoolExecutor
        } finally {
            mainLock.unlock();
        }
        // 尝试结束线程池
        tryTerminate();
    }

shutdown():不再接受新任务,但是正在执行的,以及队列中的任务都要执行完

    public List<Runnable> shutdownNow() {
        List<Runnable> tasks;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            checkShutdownAccess();
            advanceRunState(STOP);
            // 中断所有工作线程,无论是否空闲
            interruptWorkers();
            // 取出队列中没有被执行的任务
            tasks = drainQueue();
        } finally {
            mainLock.unlock();
        }
        tryTerminate();
        return tasks;
    }

shutdownNow():如果现在正在执行任务,会发出一个中断信号(安全退出机制),赶紧运行到一个安全点,然后退出;正在执行的以及队列中的任务都不再执行。可以从源码发现返回值是排空队列中没执行的任务。

    /**
     * Method invoked when the Executor has terminated.  Default
     * implementation does nothing. Note: To properly nest multiple
     * overridings, subclasses should generally invoke
     * {@code super.terminated} within this method.
     */
    protected void terminated() { }

从TIDYING状态切换到TERMINATED状态之前,会调用一个钩子函数terminated(),这个钩子函数里面什么都没有实现,需要你自己根据实际情况去实现,可以在这个函数里定义在线程池结束之前要做一些什么业务逻辑处理。

(5.2)execute方法

  我们创建好一个线程池之后,直接把要执行的任务由execute()方法,来执行了,内部到底有什么逻辑关系,我们要仔细观察和研究。

       public void execute(Runnable command) {
        if (command == null)
            
            /*若要执行的任务为null,则抛出异常*/
            throw new NullPointerException();

        /*获取当前ctl值,里面保存着线程池生命状态和当前活动线程个数*/
        int c = ctl.get();
        
        /*如果当前活动线程数小心corePoolSize,则新建一个线程往核心线程池加*/
        /*addWorker(任务,true/flase) true以corePoolSize进行判断,false以maxPoolSize进行判断*/
        if (workerCountOf(c) < corePoolSize) {
            if (addWorker(command, true))
                return;
        
        /*添加失败,重新获取当前ctl值*/
            c = ctl.get();
        }

        /*当前活动线程数大于等于corePoolSize,如果当前线程池是运行状态并且任务添加到队列成功*/
        if (isRunning(c) && workQueue.offer(command)) {

            /*重新获取当前ctl值*/
            int recheck = ctl.get();

        /*再次判断线程池的运行状态,如果不是运行状态,由于之前已经把command添加到workQueue中了,
        这时需要移除该command
        执行过后通过handler使用拒绝策略对该任务进行处理,整个方法返回*/
            if (! isRunning(recheck) && remove(command))
                reject(command);

         /*
         * 获取线程池中的有效线程数,如果数量是0,则执行addWorker方法
         * 这里传入的参数表示:
         * 1. 第一个参数为null,表示在线程池中创建一个线程,但不去启动;
         * 2. 第二个参数为false,将线程池的有限线程数量的上限设置为maximumPoolSize,添加线程时根据maximumPoolSize来判断;
         * 如果判断workerCount大于0,则直接返回,在workQueue中新增的command会在将来的某个时刻被执行。
         */
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }


        /*
        * 如果执行到这里,有两种情况:
        * 1. 线程池已经不是RUNNING状态;
        * 2. 线程池是RUNNING状态,但workerCount >= corePoolSize并且workQueue已满。
        * 这时,再次调用addWorker方法,但第二个参数传入为false,将线程池的有限线程数量的上限设置为maximumPoolSize;
         * 如果失败则拒绝该任务
        */
        else if (!addWorker(command, false))
            reject(command);
    }

简单来说,在执行execute()方法时如果状态一直是RUNNING时,执行过程如下:

  1. 如果workerCount < corePoolSize,则创建并启动一个核心线程来执行新提交的任务;
  2. 如果workCount >= corePoolSize,且线程池内的阻塞队列未满,则将任务添加到该阻塞队列中;
  3. 如果workCount >= corePoolSize && workCount < maximumPoolSize,且内部阻塞队列已经满了,则创建一个非核心线程来执行新提交的任务。
  4. 如果workCount >= maximumPoolSize,并且线程池内的阻塞队列已经满了,则根据拒绝策略来处理该任务。

  这里要注意的是addWorker(null, false);也就是创建一个线程,但并没传入任务,因为任务已经被添加到workQueue中了,所以work在执行的时候,会直接从workQueue中获取任务。所以,在workerCountOf(recheck) == 0时执行addWorker(null, false);也是为了保证线程池在RUNNING状态下必要有一个线程来执行任务。

执行流程基本如下

在这里插入图片描述

如果你对这里还是不懂,可以结合给的例子图文结合的方式仔细想一想。

(5.3)addWorker方法

addWorker方法的主要工作是在线程池中创建一个新的线程并执行,firstTask参数 用于指定新增的线程执行的第一个任务,core参数为true表示在新增线程时会判断当前活动线程数是否少于corePoolSize,false表示新增线程前需要判断当前活动线程数是否少于maximumPoolSize。

     private boolean addWorker(Runnable firstTask, boolean core) {
        retry:
        for (int c = ctl.get();;) {
            
            /*仅在必要时检查队列是否为空。
             * 这个if判断
             * 如果rs(运行状态) >= SHUTDOWN,则表示此时不再接收新任务;
             * 接着判断以下3个条件,只要有1个不满足,则返回false:
             * 1. rs == SHUTDOWN,这时表示关闭状态,不再接受新提交的任务,但却可以继续处理阻塞队列中已保存的任务
             * 2. firsTask为空
             * 3. 阻塞队列不为空
             * 
             * 首先考虑rs == SHUTDOWN的情况
             * 这种情况下不会接受新提交的任务,所以在firstTask不为空的时候会返回false;
             * 然后,如果firstTask为空,并且workQueue也为空,则返回false,
             * 因为队列中已经没有任务了,不需要再添加线程了
             */
            if (runStateAtLeast(c, SHUTDOWN)
                && (runStateAtLeast(c, STOP)
                    || firstTask != null
                    || workQueue.isEmpty()))
                return false;

            for (;;) {
                
                /* 如果wc线程数超过CAPACITY(COUNT_MASK),也就是ctl的低29位的最大值(二进制是29个1),返回false;
                // 这里的core是addWorker方法的第二个参数,如果为true表示根据corePoolSize来比较,
                // 如果为false则根据maximumPoolSize来比较。
                */ 
                if (workerCountOf(c)
                    >= ((core ? corePoolSize : maximumPoolSize) & COUNT_MASK))
                    return false;

                // 尝试增加workerCount,如果成功,则跳出第一个for循环
                if (compareAndIncrementWorkerCount(c))
                    break retry;


                // 如果增加workerCount失败,则重新获取ctl的值
                c = ctl.get();  // Re-read ctl

                // 如果当前的运行状态不等于rs,说明状态已被改变,返回第一个for循环继续执行
                if (runStateAtLeast(c, SHUTDOWN))
                    continue retry;
                // else CAS failed due to workerCount change; retry inner loop
            }
        }

        boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
            
            // 根据firstTask来创建Worker对象
            w = new Worker(firstTask);
            
            // 每一个Worker对象都会创建一个线程
            final Thread t = w.thread;
            if (t != null) {
                final ReentrantLock mainLock = this.mainLock;
                mainLock.lock();
                try {
                    // Recheck while holding lock.
                    // Back out on ThreadFactory failure or if
                    // shut down before lock acquired.
                    int c = ctl.get();

                    // rs < SHUTDOWN表示是RUNNING状态;
                    // 如果rs是RUNNING状态或者rs是SHUTDOWN状态并且firstTask为null,向线程池中添加线程。
                    // 因为在SHUTDOWN时不会在添加新的任务,但还是会执行workQueue中的任务
                    if (isRunning(c) ||
                        (runStateLessThan(c, STOP) && firstTask == null)) {
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        
                        // workers是一个HashSet
                        workers.add(w);
                        int s = workers.size();

                        // largestPoolSize记录着线程池中出现过的最大线程数量
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        workerAdded = true;
                    }
                } finally {
                    mainLock.unlock();
                }
                if (workerAdded) {
                    //启动线程
                    t.start();
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

这里需要注意以下几点

  1. 在获取锁后重新检查线程池的状态,这是因为其他线程可可能在本方法获取锁前改变了线程池的状态,比如调用了shutdown方法。添加成功则启动任务执行。
  2. t.start()会调用 Worker 类中的 run 方法,Worker 本身实现了 Runnable 接口。原因在创建线程得时候,将 Worker 实例传入了 t 当中,可参见 Worker 类的构造函数。
  3. wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize)) 每次调用 addWorker 来添加线程会先判断当前线程数是否超过了CAPACITY,然后再去判断是否超 corePoolSize 或 maximumPoolSize,说明线程数实际上是由 CAPACITY 来控制的。

(5.4)Worker类

该类是ThredPoolExcutor类中的一个内部类。线程池中的每一个线程被封装成一个Worker对象,ThreadPool维护的其实就是一组Worker对象,看一下Worker的定义:

       private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        /**
         * This class will never be serialized, but we provide a
         * serialVersionUID to suppress a javac warning.
         */
        private static final long serialVersionUID = 6138294804551838833L;

        /** Thread this worker is running in.  Null if factory fails. */
        final Thread thread;
        /** Initial task to run.  Possibly null. */
        Runnable firstTask;
        /** Per-thread task counter */
        volatile long completedTasks;

        // TODO: switch to AbstractQueuedLongSynchronizer and move
        // completedTasks into the lock word.

        /**
         * Creates with given first task and thread from ThreadFactory.
         * @param firstTask the first task (null if none)
         */
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            // 注意此处传入的是this
            this.thread = getThreadFactory().newThread(this);
        }

        /** Delegates main run loop to outer runWorker. */
        // 这里其实会调用外部的 runWorker 方法来执行自己。
        public void run() {
            runWorker(this);
        }

        // Lock methods
        //
        // The value 0 represents the unlocked state.
        // The value 1 represents the locked state.

        protected boolean isHeldExclusively() {
            return getState() != 0;
        }

        protected boolean tryAcquire(int unused) {
            // 如果已经设置过1了,这时候在设置1就会返回false,也就是不可重入
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

        protected boolean tryRelease(int unused) {
            setExclusiveOwnerThread(null);
            setState(0);
            return true;
        }

        public void lock()        { acquire(1); }
        public boolean tryLock()  { return tryAcquire(1); }
        public void unlock()      { release(1); }
        public boolean isLocked() { return isHeldExclusively(); }

        // 提供安全中断线程得方法
        void interruptIfStarted() {
            Thread t;
            // 一开始 setstate(-1) 避免了还没开始运行就被中断可能
            if (getState() >= 0 && (t = thread) != null && !t.isInterrupted()) {
                try {
                    t.interrupt();
                } catch (SecurityException ignore) {
                }
            }
        }
    }

Worker类继承了AQS,并实现了Runnable接口,注意其中的firstTask和thread属性:firstTask用它来保存传入的任务;thread是在调用构造方法时通过ThreadFactory来创建的线程,是用来处理任务的线程。

在调用构造方法时,需要把任务传入,这里通过getThreadFactory().newThread(this);来新建一个线程,newThread方法传入的参数是this,因为Worker本身继承了Runnable接口,也就是一个线程,所以一个Worker对象在启动的时候会调用Worker类中的run方法。

Worker继承了AQS,使用AQS来实现独占锁的功能。为什么不使用ReentrantLock来实现呢?可以看到tryAcquire方法,它是不允许重入的,而ReentrantLock是允许重入的:

  1. lock方法一旦获取了独占锁,表示当前线程正在执行任务中;
  2. 如果正在执行任务,则不应该中断线程;
  3. 如果该线程现在不是独占锁的状态,也就是空闲的状态,说明它没有在处理任务,这时可以对该线程进行中断;
  4. 线程池在执行shutdown方法或tryTerminate方法时会调用interruptIdleWorkers方法来中断空闲的线程,interruptIdleWorkers方法会使用tryLock方法来判断线程池中的线程是否是空闲状态;
  5. 之所以设置为不可重入,是因为我们不希望任务在调用像setCorePoolSize这样的线程池控制方法时重新获取锁。如果使用ReentrantLock,它是可重入的,这样如果在任务中调用了如setCorePoolSize这类线程池控制的方法,会中断正在运行的线程。

所以,Worker继承自AQS,用于判断线程是否空闲以及是否可以被中断。

此外,在构造方法中执行了setState(-1);,把state变量设置为-1,为什么这么做呢?是因为AQS中默认的state是0,如果刚创建了一个Worker对象,还没有执行任务时,这时就不应该被中断,看一下tryAquire方法:

 protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

tryAcquire方法是根据state是否是0来判断的,所以,setState(-1);将state设置为-1是为了禁止在执行任务前对线程进行中断。

正因为如此,在runWorker方法中会先调用Worker对象的unlock方法将state设置为0。

更深层次的源码分析请看https://www.cnblogs.com/huansky/p/12467720.html这篇文章,所有重要方法分析完了。

六、如何合理配置线程池的大小

  我们工作中遇到的问题不尽相同,因此要设置一个绝对精准的线程数是不可能的,但我们可以通过一些实际操作因素来计算出一个合理的线程数,避免由于线程池设置的不合理而导致的性能问题。下面就来看具体的计算方法。

一般多线程执行的任务类型可以分为CPU密集型和I/O密集型,根据不同的任务类型,我们计算线程数的方法也不一样。

  CPU密集型任务:我们知道CPU有个ALU(算逻)部件,因此CPU主要就是做算数和逻辑的运算。这种任务消耗的主要是CPU资源,就需要尽量压榨CPU,可以将线程数设置为N(CPU核心数)+ 1,比CPU核心数多出来一个是为了防止线程偶发缺页中断,或者其他原因导致的任务暂停而带来的影响。一旦任务暂停,CPU就会处于空闲状态,而在这种情况下多出来的一个线程就可以充分利用CPU的空闲时间。

  I/O密集型任务:这种任务很好理解,网络传输,数据库的访问都涉及到IO。这种任务应用起来,系统会大部分的时间来处理I/O交互,而线程在处理I/O的时间段不会占用CPU来处理,这时就可以将CPU使用权交出给其他线程使用。因此在I/O密集型任务中,我们可以多配置一些线程,具体计算方法是2N

  当然,这只是一个参考值,具体的设置还需要根据实际情况进行调整,比如可以先将线程池大小设置为参考值,再观察任务运行情况和系统负载、资源利用率来进行适当调整。

  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值