java 线程池梳理

一、什么是线程池:

线程池是 在一个多线程应用池的程序中创建一个线程集合,然后再执行新任务的时候可以重用这些线程而不是重新开启一个线程(提高线程复用,降低性能开销)。线程中线程的数量通常完全取决内存大小和应用程序的需求。线程池的每个线程都有分配任务,一旦任务完成了,就回到池子里等待下一次的分配任务。

二、为什么要使用线程池:

  • (线程复用) 重用线程池中的线程,减少因对象创建,销毁所带来的性能开销,加快了任务的响应时间;
  • (控制最大并发数)能有效的控制线程的最大并发数,提高系统资源利用率,同时避免过多的资源竞争,避免堵塞;
  • (管理线程)能够多线程进行简单的管理,使线程的使用简单、高效。

 本质上来讲,我们使用线程池主要就是为了减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务;节约应用内存(线程开的越多,消耗的内存也就越大,最后死机)

三、源码分析:

类结构

Executor (执行者)

public interface Executor {

    /**
     * Executes the given command at some time in the future.  The command
     * may execute in a new thread, in a pooled thread, or in the calling
     * thread, at the discretion of the {@code Executor} implementation.
     *
     * @param command the runnable task
     * @throws RejectedExecutionException if this task cannot be
     * accepted for execution
     * @throws NullPointerException if command is null
     */
    void execute(Runnable command);
}

 

Executor 是一个接口,其源码只有一个 execute()方法,用来执行线程的工具。

 

ExecutorService

 

线程池接口,包含(比如说执行 excute ( xxx ),比如关闭  isShutdown ( ))帮助我们去使用。ExecutorService接口的默认实现类为ThreadPoolExecutor

 

接下来重点分析ThreadPoolExecutor

a、首先看构造方法,它有4个构造方法,最终调用的还是7个参数的构造方法

 

各参数的含义

/**

* Core pool size is the minimum number of workers to keep alive

* (and not allow to time out etc) unless allowCoreThreadTimeOut

* is set, in which case the minimum is zero.

*/

1、corePoolSize : 核心线程数量的最大值

什么是核心线程:当新建线程池时,默认情况下线程池中本没有线程,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法 ,当任务来了,才会去创建线程,如果当前线程小于corePoolSize ,则新建的线程都是核心线程,否则新建的线程为 非核心线程。

默认情况下,核心线程会一直存活在线程池中,即使它处于闲置状态。

但是可以指定 allowCoreThreadTimeOut(true) 为true,那么超过 keepAliveTime 就会被销毁掉

 

/**

* Maximum pool size. Note that the actual maximum is internally

* bounded by CAPACITY.

*/

2、maximumPoolSize 线程中最大的线程数量

maximumPoolSize = 核心线程数 + 非核心线程数

 

/**

* Timeout in nanoseconds for idle threads waiting for work.

* Threads use this timeout when there are more than corePoolSize

* present or if allowCoreThreadTimeOut. Otherwise they wait

* forever for new work.

*/

3、keepAliveTime 默认 线程池非核心线程的超时时间

表示线程处于闲置状态,存活的时长,默认是当线程数大于corePoolSize 才会起作用,但如果设置了 allowCoreThreadTimeOut(true) ,那么也会作用于与核心线程,直到线程数为0;

 

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

TimeUnit.DAYS;               //天

TimeUnit.HOURS;             //小时

TimeUnit.MINUTES;           //分钟

TimeUnit.SECONDS;           //秒

TimeUnit.MILLISECONDS;      //毫秒

TimeUnit.MICROSECONDS;      //微妙

TimeUnit.NANOSECONDS;       //纳秒

 

5、BlockingQueue<Runnable> workQueue 阻塞队列

 

阻塞队列(BlockingQueue)是java.util.concurrent下的主要用来控制线程同步的工具。如果BlockQueue是空的,从BlockingQueue取东西的操作将会被阻断进入等待状态,直到BlockingQueue进了东西才会被唤醒。同样,如果BlockingQueue是满的,任何试图往里存东西的操作也会被阻断进入等待状态,直到BlockingQueue里有空间才会被唤醒继续操作。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。具体的实现类有LinkedBlockingQueue,ArrayBlockingQueued等。一般其内部的都是通过Lock和Condition(显示锁(Lock)及Condition的学习与使用)来实现阻塞和唤醒

 

阻塞队列与我们平常接触的普通队列(LinkedList或ArrayList等)的最大不同点,在于阻塞队列支出阻塞添加和阻塞删除方法。

  • 阻塞添加 

所谓的阻塞添加是指当阻塞队列元素已满时,队列会阻塞加入元素的线程,直队列元素不满时才重新唤醒线程执行元素加入操作。

  • 阻塞删除 

阻塞删除是指在队列元素为空时,删除队列元素的线程将被阻塞,直到队列不为空再执行删除操作(一般都会返回被删除的元素)

 

用来存储等待执行的任务,维护着等待执行的Runnable对象。当所有的核心线程都在干活时,新添加的任务会被添加到这个队列中等待处理,如果队列满了,则新建非核心线程执行任务

ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueueSynchronous

 

 

 

其中,BlockingQueue中具体的API介绍:

offer(E e): 将给定的元素设置到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。

offer(E e, long timeout, TimeUnit unit): 将给定元素在给定的时间内设置到队列中,如果设置成功返回true, 否则返回false.

add(E e): 将给定元素设置到队列中,如果设置成功返回true, 否则抛出异常。如果是往限定了长度的队列中设置值,推荐使用offer()方法。

put(E e): 将元素设置到队列中,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。

take(): 从队列中获取值,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。

poll(long timeout, TimeUnit unit): 在给定的时间里,从队列中获取值,如果没有取到会抛出异常。

remainingCapacity():获取队列中剩余的空间。

remove(Object o): 从队列中移除指定的值。

contains(Object o): 判断队列中是否拥有该值。

drainTo(Collection c): 将队列中值,全部移除,并发设置到给定的集合中。

说完了BlockingQueue常用的API,在说说其常用的workQueue类型:

一般来说,workQueue有以下四种队列类型:

SynchronousQueue:(同步队列)这个队列接收到任务的时候,会直接提交给线程处理,而不保留它(名字定义为 同步队列)。但有一种情况,假设所有线程都在工作怎么办?

这种情况下,SynchronousQueue就会新建一个线程来处理这个任务。所以为了保证不出现(线程数达到了maximumPoolSize而不能新建线程)的错误,使用这个类型队列的时候,maximumPoolSize一般指定成Integer.MAX_VALUE,即无限大,去规避这个使用风险。

LinkedBlockingQueue(链表阻塞队列):这个队列接收到任务的时候,如果当前线程数小于核心线程数,则新建线程(核心线程)处理任务;如果当前线程数等于核心线程数,则进入队列等待。由于这个队列(默认)没有最大值限制,即所有超过核心线程数的任务都将被添加到队列中,这也就导致了maximumPoolSize的设定失效,因为总线程数永远不会超过corePoolSize

ArrayBlockingQueue(数组阻塞队列):可以限定队列的长度(既然是数组,那么就限定了大小),接收到任务的时候,如果没有达到corePoolSize的值,则新建线程(核心线程)执行任务,如果达到了,则入队等候,如果队列已满,则新建线程(非核心线程)执行任务,又如果总线程数到了maximumPoolSize,并且队列也满了,则发生错误

DelayQueue(延迟队列):队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口。这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务

PriorityBlockingQueue (优先级阻塞队列) : 实现了按序排列元素的功能。也就是说PriorityBlockingQueue是维护一个按序排列的队列,排序的方法可以通过指定Comparator来比较元素的大小,或者元素类型本身实现了Comparable接口。

 

 

 

6、threadFactory 创建线程的方式,这是一个接口,new它的时候需要实现他的Thread newThread(Runnable r)方法

 

7、RejectedExecutionHandler handler 拒绝处理任务的策略

 

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

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

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

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

当线程无法执行新任务时(一般是由于线程池中的线程数量已经达到最大数或者线程池关闭导致的),默认情况下,当线程池无法处理新线程时,会抛出一个RejectedExecutionException。

b、线程复用过程

 

首先了解线程的生命周期

新建(new)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead) 5种状态

 

Thread通过new来新建一个线程,这个过程是是初始化一些线程信息,如线程名,id,线程所属group等,可以认为只是个普通的对象。调用Thread的start()后Java虚拟机会为其创建方法调用栈和程序计数器,同时将hasBeenStarted为true,之后调用start方法就会有异常。

处于这个状态中的线程并没有开始运行,只是表示该线程可以运行了。至于该线程何时开始运行,取决于JVM里线程调度器的调度。当线程获取cpu后,run()方法会被调用。不要自己去调用Thread的run()方法。之后根据CPU的调度在就绪——运行——阻塞间切换,直到run()方法结束或其他方式停止线程,进入dead状态。

 

所以实现线程的复用的原理应该是保持线程处于存活状态(就绪、运行、阻塞)。接下来通过源码分析ThreadPoolExecutor是怎么实现线程复用的。

 

在ThreadPoolExecutor是通过Worker类来控制线程复用的

private final class Worker extends AbstractQueuedSynchronizer implements Runnable
{
    final Thread thread;
    Runnable firstTask;

    Worker(Runnable firstTask) {
        setState(-1); // inhibit interrupts until runWorker
        this.firstTask = firstTask;
        this.thread = getThreadFactory().newThread(this);
    }

    public void run() {
        runWorker(this);
    }

void runWorker(Worker w) {
    Runnable task = w.firstTask;
    w.firstTask = null;
    while (task != null || (task = getTask()) != null) {
              task.run();
   }
}

Worker是一个Runnable,拥有一个Thread,这个Thread就是要开启的线程,当这个thread执行start()的时候,实际运行的runWorker()方法,接着进入一个无限循环
来看一下 getTask()的源码

private Runnable getTask() {
        if(;;){
         if(特殊情况){
          return null;
         }
          Runnable r = timed ? 
          workQueue.poll(keepAliveTime, TimeUnit.NANOSECONDS) : workQueue.take();
           f (r != null)
             return r;
               ...
    }
}

 


 

 

可以看出getTask() 是从workQueue(阻塞队列) 取出一个runnable(任务),由于队列是阻塞的,所以 workQueue.take(); 如果为空,会一直进入等待状态,直到有新的任务被加入时。才能唤醒线程。所以Thread的run()方法永远不会结束,而是不断的从workQueue里取出Runnable任务,这样就达到了线程复用。

 

c、控制最大并发数,Runnable是如何被添加到阻塞队列中的

 

可能你也有疑问,Runnable是什么时候添加到workQueue里的,Worker又是什么时候创建的。

Worker的thread又是什么时候start()的,Worker的run()方法什么时候被调用的?

从上面的源码看runWorker()执行任务是一个接一个的,串行进行的,那并发是如何体现的呢。

来看一下 execute(Runnable runnable) 方法

public void execute(Runnable command) {
    if (command == null)
        throw new NullPointerException();
    
    int c = ctl.get();
    //当线程数 < corePoolSize
    if (workerCountOf(c) < corePoolSize) {
       // 直接启动线程
        if (addWorker(command, true))
            return;
        c = ctl.get();
    }
    // 活动线程数 >= corePoolSize   	
    //workQueue.offer 将任务插入队列,返回true,表示插入成功
    // runState为RUNNING && 队列未满
    if (isRunning(c) && workQueue.offer(command)) {
        int recheck = ctl.get();
    // 再次检验是否为RUNNING状态       
    // 非RUNNING状态 则从workQueue中移除任务并拒绝
        if (! isRunning(recheck) && remove(command))
            reject(command);// 采用线程池指定的策略拒绝任务
        else if (workerCountOf(recheck) == 0) //Worker的的数量为0时,创建worker
            addWorker(null, false);
    }
    else if (!addWorker(command, false))
// reject有两种情况:        
// 1.非RUNNING状态拒绝新的任务        
// 2.队列满了启动新的线程池(workCount> maximumPoolSize)
        reject(command);
}

 

addWorker:

简化后的代码

private boolean addWorker(Runnable firstTask, boolean core) {
            int wc = workerCountOf(c);
            if (wc >= CAPACITY || wc >= (core ? corePoolSize : maximumPoolSize))
                return false;

        Worker w = new Worker(firstTask);
        final Thread t = w.thread; // 执行Worker的线程
 t.start(); 
}

根据代码再来看上面提到的线程池工作过程中的添加任务的情况:

* 如果正在运行的线程数量小于 corePoolSize,那么马上创建线程运行这个任务;

* 如果正在运行的线程数量大于或等于 corePoolSize,那么将这个任务放入队列;

* 如果这时候队列满了,而且正在运行的线程数量小于 maximumPoolSize,那么还是要创建非核心线程立刻运行这个任务;

* 如果队列满了,而且正在运行的线程数量大于或等于 maximumPoolSize,那么线程池会抛出异常RejectExecutionException。

 

如何做到并发?

通过addWorker() 如果创建新的线程成功,调用start() 开启线程,firstTask是要执行的第一个任务,

虽然每个Worker的任务是串行处理的,但如果创建了多个Worker,共用一个workQueue, 所以就会并行处理了.

 

 

 

d、管理线程

 

在ThreadPoolExecutor有个ctl的AtomicInteger变量。通过这一个变量保存了两个内容:

  • 所有线程的数量
  • 每个线程所处的状态

其中低29位存线程数,高3位存runState,通过位运算来得到不同的值。

 */
private final AtomicInteger ctl = new AtomicInteger(ctlOf(RUNNING, 0));


// 线程的状态

//线程池正常运行,可以接受新的任务并处理队列中的任务;
private static final int RUNNING    = -1 << COUNT_BITS;
//不再接受新的任务,但是会执行队列中的任务;
private static final int SHUTDOWN   =  0 << COUNT_BITS;
//不再接受新任务,不处理队列中的任务
private static final int STOP       =  1 << COUNT_BITS;

//得到线程的状态
private static int runStateOf(int c)     { return c & ~CAPACITY; }
//得到Worker的的数量
private static int workerCountOf(int c)  { return c & CAPACITY; }
// 判断线程是否在运行
private static int ctlOf(int rs, int wc) { return rs | wc; }

四、java提供的四种线程池

生成线程池采用了工具类Executors的静态方法,以下是几种常见的线程池。

 

SingleThreadExecutor:单个后台线程 (其缓冲队列是无界的)

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

创建一个单线程的线程池。这个线程池只有一个核心线程在工作,也就是相当于单线程串行执行所有任务。如果这个唯一的线程因为异常结束,那么会有一个新的线程来替代它。此线程池保证所有任务的执行顺序按照任务的提交顺序执行。

 

FixedThreadPool:只有核心线程的线程池,大小固定 (其缓冲队列是无界的) 。

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

创建固定大小的线程池。每次提交一个任务就创建一个线程,直到线程达到线程池的最大大小。线程池的大小一旦达到最大值就会保持不变,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程。

CachedThreadPool:无界线程池,可以进行自动线程回收。

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

如果线程池的大小超过了处理任务所需要的线程,那么就会回收部分空闲(60秒不执行任务)的线程,当任务数增加时,此线程池又可以智能的添加新线程来处理任务。此线程池不会对线程池大小做限制,线程池大小完全依赖于操作系统(或者说JVM)能够创建的最大线程大小。SynchronousQueue是一个没有数据缓冲的BlockingQueue,生产者线程对其的插入操作put必须等待消费者的移除操作take,反过来也一样。

ScheduledThreadPool:核心线程池固定,大小无限的线程池。此线程池支持定时以及周期性执行任务的需求。

 

  public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
        return new ScheduledThreadPoolExecutor(corePoolSize);
    }
    public ScheduledThreadPoolExecutor(int corePoolSize) {
        super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
              new DelayedWorkQueue());
    }

execute:

ExecutorService.execute(Runnable runable);

submit:

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

submit(Callable callable)的实现,submit(Runnable runnable)同理。

    public <T> Future<T> submit(Callable<T> task) {
        if (task == null) throw new NullPointerException();
        RunnableFuture<T> ftask = newTaskFor(task);
        execute(ftask);
        return ftask;
    }

可以看出submit开启的是有返回结果的任务,会返回一个FutureTask对象,这样就能通过get()方法得到结果。submit最终调用的也是execute(Runnable runable),submit只是将Callable对象或Runnable封装成一个FutureTask对象,因为FutureTask是个Runnable,所以可以在execute中执行。关于Callable对象和Runnable怎么封装成FutureTask对象,见Callable和Future、FutureTask的使用

 

下面是楼主项目封装的一个线程池工具类,用的PriorityBlockingQueue队列,可以自定义优先级,

有需要的可以下载参考一下 点击下载

 

 

参考 

https://www.jianshu.com/p/50fffbf21b39

http://m.blog.csdn.net/u012516166/article/details/76328104

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值