Java之线程池原理

线程池原理

为什么使用线程池?

在进行线程池原理叙述之前,首先我们需要知道为什么使用线程池?

1、首先我们知道创建或者销毁线程都需要消耗系统资源,使用线程池我们就可以复用已创建的线程。

2、其次我们知道线程数量过多会导致资源浪费,从而导致服务器崩溃,所以线程池可以帮助我们控制并发的数量。

3、线程池可以对线程做统一的管理。

线程池参数

从源码中我们可以知道线程池的类是ThreadPoolExecutor,它实现了 Executor接口。其中构造函数有4个,必须参数是5个。首先我们先对必须参数进行描述。

public ThreadPoolExecutor(int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue) {
        this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
             Executors.defaultThreadFactory(), defaultHandler);
    }
corePoolSize(最大核心线程数)

线程池中分为两类线程,一类是核心线程,默认情况下一直存在于线程池中,尽管什么也不干。而非核心线程如果长时间不工作就会被销毁。

maximumPoolSize(线程总数最大值)

核心线程数+非核心线程数

keepAliveTime(非核心线程超时时长)

如果非核心线程限制时间超过该时长就会被销毁。如果设置属性:**private volatile boolean allowCoreThreadTimeOut; **也会作用于核心线程。

unit(keepAliveTime的单位)

keepAliveTime的单位是一个枚举,最小支持到NANOSECONDS(纳秒),最大支持到DAYS天。

BlockingQueue workQueue

阻塞队列,常见的三种阻塞队列

1、LinkedBlockingQueue 链式阻塞队列,数据结构为链表。默认大小为Integer.MAX_VALUE,可以指定大小。

2、ArrayBlockingQueue 数组阻塞队列,数据结构为数组需要指定大小。

3、SynchronousQueue 同步队列,每个put操作都需要等待一个take操作,反之亦然。

4、DelayQueue 延迟队列,该队列中的元素只有当前指定的延迟时间到了,才能够从队列中获取到该元素。

注:阻塞队列将在下篇文章中着重讲解。

当然还有两个不必须的参数

ThreadFactory

创建线程的工厂,用于批量创建线程,统一在创建线程的时候设置一些参数,如果不指定,会使用默认的线程工厂。

static class DefaultThreadFactory implements ThreadFactory {
		//线程池数量
        private static final AtomicInteger poolNumber = new AtomicInteger(1);
        //线程组
        private final ThreadGroup group;
        //线程数量
        private final AtomicInteger threadNumber = new AtomicInteger(1);
        //前缀名
        private final String namePrefix;
		
        DefaultThreadFactory() {
            SecurityManager s = System.getSecurityManager();
            group = (s != null) ? s.getThreadGroup() :
                                  Thread.currentThread().getThreadGroup();
            namePrefix = "pool-" +
                          poolNumber.getAndIncrement() +
                         "-thread-";
        }
RejectedExecutionHandler handler(拒绝策略)

当线程数大于最大线程数的时候就会采取拒绝策略。

四种拒绝策略:

1、AbortPolicy
默认的拒绝策略,丢弃任务并抛出RejectedExecutionException异常

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());
        }
    }

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、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);
            }
        }
    }

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();
            }
        }
    }
ThreadPoolExecutor的状态流转

线程池中会存在一个调度线程,这个线程用于管理整个线程池中的各种任务和事务,线程的创建和销毁,任务队列管理等功能。
所以线程池也有自己的状态。

线程池创建后处于RUNNING 状态。调用shutdown()方法之后处于SHUTDOWM 状态,这个状态下线程池不再接受新任务,清空一些空闲的Worker,然后等待阻塞队列的任务完成。调用shutdownNow方法的时候处于STOP 状态,这个状态下不再接受新任务,阻塞队列中没有完成的任务全部丢弃。线程池中有个控制状态的属性为ctl 这个一个AtomicInteger的变量。当ctl记录的任务数量为0的时候会变成TIDYING 状态,紧接着会调用terminated()方法,然后进入TERMINATED 终止态。

ThreadPoolExecutor的任务执行流程

我们知道ThreadPoolExecutor这个类是实现了executor接口,这个接口中定义了execute()方法,也就是执行方法,现在我们看一下ThreadPoolExecutor中execute()方法是执行了什么逻辑。

execute()源码分析
public void execute(Runnable command) {
		//如果线程为null,就抛出NullPointerException
        if (command == null)
            throw new NullPointerException();
        int c = ctl.get();
        //如果当前线程数小于最大核心线程数
        if (workerCountOf(c) < corePoolSize) {
        	//就调用addWorker创建核心线程执行任务
            if (addWorker(command, true))
                return;
            c = ctl.get();
        }
        //如果不小于那就加到工作队列中
        if (isRunning(c) && workQueue.offer(command)) {
            int recheck = ctl.get();
            //二次检查,如果状态不是RUNNING,就移除掉当前任务,然后执行拒绝策略
            if (! isRunning(recheck) && remove(command))
                reject(command);
            // 如果是RUNNING,但是没有线程,那么就创建核心线程执行任务
            else if (workerCountOf(recheck) == 0)
                addWorker(null, false);
        }
        //如果放入工作队列失败,那么就创建非核心线程执行任务。
        //如果创建非核心线程失败(线程数不小于设置的最大线程数),那么就执行拒绝策略
        else if (!addWorker(command, false))
            reject(command);
    }

从源码中我们可以看出,在执行策略中需要进行线程池状态的二次检查 ,那么为什么要进行二次检查呢?

在多线程的情况下,线程池状态是随时变化的,可能刚获得的线程池状态,随后就发生了变化,所以我们可以看到每次判断之前都会执行ctl.get()方法获取最新的线程池状态。如果不进行二次检查很可能会发生线程加入到工作队列中之后线程池状态发生了变化,导致工作队列中的线程永远不会被执行。

这里我们先简单叙述一下整个执行过程。
1、如果当前线程数小于最大核心线程数,无论线程是否空闲都会新建一个核心线程执行任务,为的就是让核心线程数快速饱和,提高可利用线程的数量。这个过程会加全局锁,后面会详细讲解。
2、如果不小于说明核心线程数饱和了,那么就先加入到工作队列中,然后让空闲的线程去队列中取任务执行,这就体现了线程复用
3、如果任务队列满了,说明任务已经爆满了,就需要创建非核心线程去执行任务。这一步也需要加全局锁
4、如果非核心线程都失败了(也就是现有线程数不小于最大设置线程数),就执行拒绝策略。
过程如图所示:
图片来源《深入浅出Java多线程》
从上面的源码中我们已经知道了,里面让创建核心线程或者非核心线程执行任务都是调用了一个方法,addWorker(),下面我们仔细的看看这个方法中执行了什么逻辑。

addWorker()源码分析
private boolean addWorker(Runnable firstTask, boolean core) {
	//循环标志,有点类似于goto语句,多伴随循环出现
    retry:
    //s死循环
    for (;;) {
    	//拿到当前线程池状态
        int c = ctl.get();
        int rs = runStateOf(c);
		
        // 如果状态处于非运行态,并且不等于SHUTDOWN,并且任务不为空,并且工作队列为空,直接return false(这里就表示的是线程池已经不接受任务了)
        if (rs >= SHUTDOWN &&
            ! (rs == SHUTDOWN &&
               firstTask == null &&
               ! workQueue.isEmpty()))
            return false;
		//下面是cas的过程了
        for (;;) {
        	//先拿到工作线程数
            int wc = workerCountOf(c);
            //如果大于容量或者大于设置数(核心线程就和核心线程数比,非核心线程就和最大线程数比)
            if (wc >= CAPACITY ||
                wc >= (core ? corePoolSize : maximumPoolSize))
                return false;
            //线程数允许就通过CAS进行工作线程+1,然后跳出循环
            if (compareAndIncrementWorkerCount(c))
                break retry;
            //如果失败了就继续CAS    
            c = ctl.get();  // Re-read ctl
            //如果取得的状态不一致,那就继续retry
            if (runStateOf(c) != rs)
                continue retry;
        }
    }

上面的这一部分代码主要就是要判断线程是否到达阈值,如果到了就拒绝。
下面看下一部分代码,就是真正的执行逻辑。

		boolean workerStarted = false;
        boolean workerAdded = false;
        Worker w = null;
        try {
        	//创建一个worker对象(后面会讲述这个Worker对象的作用)
            w = new Worker(firstTask);
            //拿到worker对象中的线程
            final Thread t = w.thread;
            if (t != null) {
            	//一个线程池的全局锁
                final ReentrantLock mainLock = this.mainLock;
                //开启锁
                mainLock.lock();
                try {
                    
                    int rs = runStateOf(ctl.get());
					//如果状态为运行态,或者状态为SHUTDOWN且任务为空的话进入
                    if (rs < SHUTDOWN ||
                        (rs == SHUTDOWN && firstTask == null)) {
                        //这里就是还没start,状态就是alive了,这里肯定是异常
                        if (t.isAlive()) // precheck that t is startable
                            throw new IllegalThreadStateException();
                        //将入到workers中
                        workers.add(w);
                        int s = workers.size();
                        //当前最大线程数替换
                        if (s > largestPoolSize)
                            largestPoolSize = s;
                        //标识已经加入到workers中了
                        workerAdded = true;
                    }
                } finally {
                	//锁释放
                    mainLock.unlock();
                }
                if (workerAdded) {
                	//任务执行
                    t.start();
                    //更新任务开启标识
                    workerStarted = true;
                }
            }
        } finally {
            if (! workerStarted)
            //如果任务开启失败,就减少工作线程数(因为之前增加了)
                addWorkerFailed(w);
        }
        return workerStarted;
    }

接着我们看一下worker对象里面是什么?

worker对象源码分析
//继承了AQS,同时实现了Runnable,说明本身也是一个线程任务
private final class Worker
        extends AbstractQueuedSynchronizer
        implements Runnable
    {
        private static final long serialVersionUID = 6138294804551838833L;
		//线程
        final Thread thread;
        //工作任务
        Runnable firstTask;
       //完成的任务数
        volatile long completedTasks;

		//构造方法
        Worker(Runnable firstTask) {
            setState(-1); // inhibit interrupts until runWorker
            this.firstTask = firstTask;
            //这里就是 自身线程
            this.thread = getThreadFactory().newThread(this);
        }

        
        public void run() {
            runWorker(this);
        }
        
        //这个方法说明这是一个独占锁。
        protected boolean tryAcquire(int unused) {
            if (compareAndSetState(0, 1)) {
                setExclusiveOwnerThread(Thread.currentThread());
                return true;
            }
            return false;
        }

从上面我们可以看到它将工作任务进行了封装,最后执行任务的线程就是自身worker,执行的start()方法。接着再看一下runworker方法

runworker()源码分析
final void runWorker(Worker w) {
        Thread wt = Thread.currentThread();
        //将之前封装的任务取出来
        Runnable task = w.firstTask;
        w.firstTask = null;
        //先解锁,允许中断,因为还没有正式执行
        w.unlock(); // allow interrupts
        boolean completedAbruptly = true;
        try {
        	//当任务不为空的时候开始循环了
            while (task != null || (task = getTask()) != null) {
            	//加锁,下面开始执行任务了,所以不允许中断了,这也就是实现AQS并且让其不可重入了。
                w.lock();
                //检查线程池状态,如果线程池中断,那么强行让线程中断
                if ((runStateAtLeast(ctl.get(), STOP) ||
                     (Thread.interrupted() &&
                      runStateAtLeast(ctl.get(), STOP))) &&
                    !wt.isInterrupted())
                    wt.interrupt();
                try {
                	//前处理逻辑
                    beforeExecute(wt, task);
                    Throwable thrown = null;
                    try {
                    	//实际执行任务
                        task.run();
                    } catch (RuntimeException x) {
                        thrown = x; throw x;
                    } catch (Error x) {
                        thrown = x; throw x;
                    } catch (Throwable x) {
                        thrown = x; throw new Error(x);
                    } finally {
                    //后处理逻辑
                        afterExecute(task, thrown);
                    }
                } finally {
                //清空task
                    task = null;
                    //完成任务数+1,提供给线程池统计
                    w.completedTasks++;
                    //释放锁
                    w.unlock();
                }
            }
            completedAbruptly = false;
        } finally {
        	//流程执行结束之后的逻辑,如清除work,累积完成任务数等等。
            processWorkerExit(w, completedAbruptly);
        }
    }

我们可以看到真正执行的时候并不是只取当前任务,还有一个就是getTask()方法,这个方法里面就是从阻塞队列中取任务。这就体现了线程复用的思想,所以一个worker的生命周期不只是当前一个任务,而且还要取阻塞队列任务。
最后看一下getTask()方法的源码。

getTask()源码分析
private Runnable getTask() {
        boolean timedOut = false; // Did the last poll() time out?

        for (;;) {
            int c = ctl.get();
            int rs = runStateOf(c);

            // 满足括号中条件就减worker,返回null,外面的worker自然就停止了
            if (rs >= SHUTDOWN && (rs >= STOP || workQueue.isEmpty())) {
                decrementWorkerCount();
                return null;
            }

            int wc = workerCountOf(c);

            // timed取值:如果设置了核心线程超时也回收就是true或者当前线程数大于核心线程数
            //主要为了线程回收
            boolean timed = allowCoreThreadTimeOut || wc > corePoolSize;
			//如果线程数超了并且缓存队列为空,就减少worker数,然后返回null
            if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
			
            try {
            //如果没有超过核心线程数,就用poll方法弹出队列头部任务。
            //如果核心线程超时不回收,并且当前线程数小于核心线程数的时候
            //调用take方法,阻塞任务,挂起当前线程,这时不会占用CPU资源,直到拿到Runnable的时候返回。
                Runnable r = timed ?
                    workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) :
                    workQueue.take();
                if (r != null)
                    return r;
                timedOut = true;
            } catch (InterruptedException retry) {
                timedOut = false;
            }
        }
    }

下面这段代码就是比较关键的,就是决定了非核心线程的销毁!当前线程数大于核心线程数,并且队列为空,那么就销毁非核心线程。

if ((wc > maximumPoolSize || (timed && timedOut))
                && (wc > 1 || workQueue.isEmpty())) {
                if (compareAndDecrementWorkerCount(c))
                    return null;
                continue;
            }
四种常见的线程池(不推荐使用,容易造成资源耗尽的情况)
newCachedThreadPool
public static ExecutorService newCachedThreadPool() {
        return new ThreadPoolExecutor(0, Integer.MAX_VALUE,
                                      60L, TimeUnit.SECONDS,
                                      new SynchronousQueue<Runnable>());
    }

核心线程数为0,最大线程数为Integer的最大值,超时时间为60s,阻塞队列用的是同步队列。
需要执行很多短时间的任务的时候,线程的复用率很高,而且不创建核心线程,超时时间为60s,不会占用过多资源。如果队列中已有任务在等待,入列操作会被阻塞。

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

核心线程数和最大线程数相等,所以不会创建非核心线程数。因为线程不会回收,会一直阻塞,所以没有任务的情况下newFixedThreadPool占用资源更多。

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

只有一个核心线程工作,任务都堆积在LinkedBlockingQueue中。

newScheduledThreadPool

创建⼀个定⻓线程池,⽀持定时及周期性任务执⾏。

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

参考资料《深入浅出Java多线程》

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值