java 线程——线程池

本文学习内容

线程池概述

ThreadPoolExecutor 的实现原理

继承关系

属性

阻塞队列

拒绝策略

构造方法

ThreadPoolExector 类的几个重要方法

任务的提交

关闭线程池

shutdown()

shutdownNow()


线程池概述

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

线程池实现了线程的复用,使得线程在一个任务执行完成之后并不销毁,而是去执行另一个任务。

线程池:Java中开辟出了一种管理线程的概念,这个概念叫做线程池,从概念以及应用场景中,我们可以看出,线程池的好处,就是可以方便的管理线程,也可以减少内存的消耗。

我们应该如何创建一个线程池? Java中已经提供了创建线程池的一个类:Executor,我们一般用它的子类ThreadPoolExecutor 来创建线程池

ThreadPoolExecutor 的实现原理

继承关系

ThreadPoolExecutor 继承AbstractExecutorService 抽象类,AbstractExecutorService 抽象类实现了ExecutorService 接口,ExecutorService 接口又继承了Executor。

Executor是一个顶层接口,在它里面只声明了一个方法execute(Runnable),返回值为void,参数为Runnable类型,从字面意思可以理解,就是用来执行传进去的任务的。

ExecutorService接口继承了Executor接口,并声明了一些方法:submit、invokeAll、invokeAny以及shutDown等。

抽象类AbstractExecutorService实现了ExecutorService接口,基本实现了ExecutorService中声明的所有方法;

然后ThreadPoolExecutor继承了类AbstractExecutorService。

属性

//线程状态
    volatile int runState;

	static final int RUNNING    = 0;
    static final int SHUTDOWN   = 1;
    static final int STOP       = 2;
    static final int TERMINATED = 3;

	//阻塞队列
    private final BlockingQueue<Runnable> workQueue;
	//互斥锁
    private final ReentrantLock mainLock = new ReentrantLock();
	//终止条件
    private final Condition termination = mainLock.newCondition();
	//线程集合,一个worker 对象为一个线程
    private final HashSet<Worker> workers = new HashSet<Worker>();
	//空闲状态
    private volatile long  keepAliveTime;
  	//表示是否允许“线程在空闲状态时,仍能够存活”
    private volatile boolean allowCoreThreadTimeOut;
	//核心池大小
    private volatile int   corePoolSize;
   	//最大池大小
    private volatile int   maximumPoolSize;
	//当前大小
    private volatile int   poolSize;
	//拒绝处理任务时的策略
    private volatile RejectedExecutionHandler handler;
	//线程工厂
    private volatile ThreadFactory threadFactory;
	//记录之前线程最大时的大小
    private int largestPoolSize;
    //完成线程的数量
    private long completedTaskCount;
	//默认处理器。抛出异常
    private static final RejectedExecutionHandler defaultHandler =
        new AbortPolicy();

在这里大部分属性我们都能一眼看出来它的作用,我们重点看一下他的则色队列和拒绝策略。

阻塞队列

用来存储待执行的任务,这个参数的选择也很重要,对线程池的运行过程产生重大的影响。

ArrayBlockingQueue

基于数组的先进先出队列,此队列创建时必须指定大小。

 public ArrayBlockingQueue(int capacity, boolean fair,
                              Collection<? extends E> c) {
        this(capacity, fair);
        if (capacity < c.size())
            throw new IllegalArgumentException();

        for (Iterator<? extends E> it = c.iterator(); it.hasNext();)
            add(it.next());
    }

LinkedBlockingQueue

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

public LinkedBlockingQueue(Collection<? extends E> c) {
        this(Integer.MAX_VALUE);
        final ReentrantLock putLock = this.putLock;
        putLock.lock(); // Never contended, but necessary for visibility
        try {
            int n = 0;
            for (E e : c) {
                if (e == null)
                    throw new NullPointerException();
                if (n == capacity)
                    throw new IllegalStateException("Queue full");
                enqueue(e);
                ++n;
            }
            count.set(n);
        } finally {
            putLock.unlock();
        }
    }

 SychronousQueue

这个队列比较特殊,它不会保存提交的任务 ,而是直接创建了一个线程去执行新来的任务.

拒绝策略

Handler:表示当拒绝处理任务时的策略。一共有四种策略。

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

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

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

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

可以自己实现RejectedExecutionHandler接口,并且实现其方法即可

构造方法

ThreadPoolExecutor 有四个构造方法,但其实前三个构造方法都调用的是第四个构造器,所以我们主要看一下最后一个构造器。

public ThreadPoolExecutor(int corePoolSize,//核心线程数量
                              int maximumPoolSize,//最大线程数量
                              long keepAliveTime,//超出核心线程以外线程的存活时间
                              TimeUnit unit,//参数keepAliveTime的时间单位
                              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;
    }

我们分析一下它的参数:

corePoolSize:核心线程数

maximumPoolSize:最大线程数

keepAliveTime

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

但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0;

unit:keepAliveTime 的单位,

TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒

workQueue:阻塞队列。

threadFactory:线程工厂,用来创建线程。

handler: 当任务被拒绝时的策略。

我们用一张图来看一下核心线程数、最大线程数和阻塞队列的关系。

ThreadPoolExector 类的几个重要方法

execute()方法实际上是Executor中声明的方法,在ThreadPoolExecutor进行了具体的实现,这个方法是ThreadPoolExecutor的核心方法,通过这个方法可以向线程池提交一个任务,交由线程池去执行。

submit()方法是在ExecutorService中声明的方法,在AbstractExecutorService就已经有了具体的实现,在ThreadPoolExecutor中并没有对其进行重写,这个方法也是用来向线程池提交任务的,但是它和execute()方法不同,它能够返回任务执行的结果,去看submit()方法的实现,会发现它实际上还是调用的execute()方法,只不过它利用了Future来获取任务执行结果。

executor()是ThreadPoolExector  的核心方法,我们来重点分析一下这个方法。

任务的提交

public void execute(Runnable command) {
		//线程为空,抛出异常
		if (command == null)
            throw new NullPointerException();
		//判断当前线程数是否大于核心线程数,若不大于,则执行第二个条件,将它加入核心线程里面。
		// 加入失败,进入下面的代码块
        if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command)) {
			//判断当前线程是否处于运行状态,并且将任务加入阻塞队列
            if (runState == RUNNING && workQueue.offer(command)) {
				//句判断是为了防止在将此任务添加进任务缓存队列的同时其他线程突然调用shutdown
				//或者shutdownNow方法关闭了线程池的一种应急措施。
                if (runState != RUNNING || poolSize == 0)
					//保证 添加到任务缓存队列中的任务得到处理
                    ensureQueuedTaskHandled(command);
            }
			
            else if (!addIfUnderMaximumPoolSize(command))
                reject(command); 
        }

   if (poolSize >= corePoolSize || !addIfUnderCorePoolSize(command))

判断当前线程数是否大于核心线程数,如果不大于就执行addIfUnderCorePoolSize(command)方法。我们来分析一下这个方法。

若这个方法执行成功,则任务提交成功。

private boolean addIfUnderCorePoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
			//当前线程数小于核心线程数,当前线程处于运行状态
            if (poolSize < corePoolSize && runState == RUNNING)
                t = addThread(firstTask);//创建线程去执行firstTask任务。
        } finally { 
            mainLock.unlock();
        }
        return t != null;
    }

这里有的人可能要问,在进入这个方法之前不是已经判断过poolSize < corePoolSize了嘛,但事实是在前面我们判断的时候没有加锁,也许在我们运行完上一个语句后就有其他任务提交进去了,导致核心线程满了,所以这里需要再判断一下。让我们看一下是怎么创建线程的。

private Thread addThread(Runnable firstTask) { 
        Worker w = new Worker(firstTask);
		//创建一个线程,执行任务  
		Thread t = threadFactory.newThread(w);
		
        boolean workerStarted = false;
        if (t != null) {
            if (t.isAlive()) // precheck that t is startable
                throw new IllegalThreadStateException();
            w.thread = t;//将创建的线程的引用赋值为w的成员变量
            workers.add(w);
            int nt = ++poolSize; //当前线程数加1 
            if (nt > largestPoolSize)
                largestPoolSize = nt;
            try {
                t.start();
                workerStarted = true;
            }
            finally {
                if (!workerStarted)
                    workers.remove(w);
            }
        }
        return t;
    }

如果当前线程大于核心线程数就执行括号里的内容,也就是下面这句。

  if (runState == RUNNING && workQueue.offer(command))

判断当前线程池是否处于运行状态,如果处于运行状态就执行workQueue.offer(command) 方法,这个方法是向阻塞队列里面提交任务。加入阻塞队列成功后就执行下面的if语句。

 if (runState != RUNNING || poolSize == 0)      
         ensureQueuedTaskHandled(command);
  }

这里要先判断线程池是不是处于运行状态,和上面一样,也是因为前面的操作没有加锁,为了防止在运行过程中其它线程关闭了线程池。ensureQueuedTaskHandled(command)是为了确保添加到任务缓存队列中的任务得到处理。

我们再回到f (runState == RUNNING && workQueue.offer(command)) 语句,当加入阻塞队列失败,我们就运行下面的else 语句的内容,也就是运行addIfUnderMaximumPoolSize(command)。

else if (!addIfUnderMaximumPoolSize(command))
                reject(command);

private boolean addIfUnderMaximumPoolSize(Runnable firstTask) {
        Thread t = null;
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
			//判断当前线程是否小于最大线程数,如果小于则判断线程是否处于运行状态
            if (poolSize < maximumPoolSize && runState == RUNNING)
				//创建线程去执行firstTask任务。
				t = addThread(firstTask);
        } finally {
            mainLock.unlock();
        }
        return t != null;
    }

这个方法和addIfUnderCorePoolSize 方法大同小异,可以参考一下,此时若这个方法执行成功,则任务提交成功,否则采取拒绝策略。

 从executor方法中,我们大概能梳理出这个提交流程:

  • 判断核心线程有没有满,没满就创建新线程执行任务
  • 核心线程满了就判断阻塞队列有没有满,没满就放入阻塞队列。
  • 阻塞队列满了就判断当前县车行有没有大于最大线程数,不大于就创建新线程执行任务。
  • 大于就按照拒绝策略。

关闭线程池

shutdown()和shutdownNow()是用来关闭线程池的。

shutdown()

 shutdown并不是直接关闭线程池,而是不再接受新的任务...如果线程池内有任务,那么把这些任务执行完毕后,关闭线程池

//关闭线程池:不管其他线程有没有结束,都关闭
    public void shutdown() {
    //安全管理器
	SecurityManager security = System.getSecurityManager();
	///判断调用者是否有权限shutdown线程池
	if (security != null)
            security.checkPermission(shutdownPerm);

		//互斥锁
        final ReentrantLock mainLock = this.mainLock;
		//加锁
		mainLock.lock();
        try {
            if (security != null) {
                for (Worker w : workers)
                    security.checkAccess(w.thread);
            }
			//运行状态
            int state = runState;
			//修改状态
			if (state < SHUTDOWN)
                runState = SHUTDOWN;
			//遍历线程并停止
            try {
                for (Worker w : workers) {
                    w.interruptIfIdle();
                }
            } catch (SecurityException se) { 
                runState = state;
                throw se;
            }
			//尝试终止线程池
            tryTerminate(); 
        } finally {
            mainLock.unlock();
        }
    }
  • 线程池的状态变成SHUTDOWN状态,此时不能再往线程池中添加新的任务,否则会抛出RejectedExecutionException异常。
  • 线程池不会立刻退出,直到添加到线程池中的任务都已经处理完成,才会退出。 

shutdownNow()


    public List<Runnable> shutdownNow() {
       
	SecurityManager security = System.getSecurityManager();
	if (security != null)
            security.checkPermission(shutdownPerm);
		
        final ReentrantLock mainLock = this.mainLock;
        mainLock.lock();
        try {
            if (security != null) { // Check if caller can modify our threads
                for (Worker w : workers)
                    security.checkAccess(w.thread);
            }
			//线程状态
            int state = runState;
			//修改状态
            if (state < STOP)
                runState = STOP;
			//遍历线程并中断
            try {
                for (Worker w : workers) {
                    w.interruptNow();
                }
            } catch (SecurityException se) { 
                runState = state;   
                throw se;
            }

            List<Runnable> tasks = drainQueue();
			//尝试终止线程池
            tryTerminate();
            return tasks;
        } finally {
            mainLock.unlock();
        }
    }

  这个方法表示不再接受新的任务,并把任务队列中的任务直接移出掉,如果有正在执行的,尝试进行停止...

  • 线程池的状态立刻变成STOP状态,此时不能再往线程池中添加新的任务。
    • 终止等待执行的线程,并返回它们的列表;
    • 试图停止所有正在执行的线程,试图终止的方法是调用Thread.interrupt(),但是大家知道,如果线程中没有sleep 、wait、Condition、定时锁等应用, interrupt()方法是无法中断当前的线程的。所以,ShutdownNow()并不代表线程池就一定立即就能退出,它可能必须要等待所有正在执行的任务都执行完成了才能退出。

 

 

 

猜你喜欢

转载自blog.csdn.net/Alyson_jm/article/details/82865954