详解Java线程池和Executor原理的分析

详解Java线程池和Executor原理的分析

线程池作用与基本知识

在开始之前,我们先来讨论下“线程池”这个概念。“线程池”,顾名思义就是一个线程缓存。它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节。那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点:

减小线程创建和销毁带来的消耗

对于Java Thread的实现,我在前面的一篇blog中进行了分析。Java Thread与内核线程是1:1(Linux)的,再加上Thread在Java层与C++层都有不少成员数据,所以Java Thread其实是比较重的。创建和销毁一个Java Thread需要OS和JVM都做不少工作,因此如果将Java Thread缓存起来,可以实现一定的效率提升。

更加方便和透明的实现计算资源控制

讨论这一条,可能需要举一些例子。以非常闻名的web服务器Nginx为例,Nginx以强大的并发能力和低资源消耗而著称。Nginx为了实现这些严格的要求,它严格地限定了工作线程的数目(worker线程一般等于CPU数目)。这种设计的着眼点就是降低线程切换带来的性能损失,这条优化方式对Java同样适用。倘若,每来一个任务就新建一个Thread来运算,那最终的结果就是程序资源难以控制(某个功能把CPU跑满了),而且整体的执行速度也比较慢。 而Java线程池提供了FixedThreadPool,你可以使用它实现线程最大数目的控制。

上面说了这么多的“废话”,还是来结合Java线程池的实现来分析一下吧!Java的线程池有一下几种实现:

cached ThreadPool

缓存线程池的特点是它会缓存之前的线程,新提交的任务可以运行在缓存的线程中,即实现了前文所述的第一个优势。

fixed ThreadPool

cachedThreadPool的一个特点是——新提交的任务没有空闲线程可以执行了,就会创建一个新的线程。而fixedThreadPool不会这样,它会将任务保存起来,等到有空闲线程再执行。即实现了前文所述的第二个优势。

scheduled ThreadPool

scheduled ThreadPool的特点是可以实现任务的调度,比如任务的延迟执行和周期执行。

出了上面三种,Java还实现了newWorkStealingPool,这个是基于Fork/Join框架的。目前我还没研究这个,所以就先不管它了。Java的并发支持中,使用了Executor来包装各种线程池,“执行器”这个名称其实挺贴切的,线程池可不就是个执行器嘛!

1.cached ThreadPool、fixed ThreadPool的实现

从前文的描述就可以看出,这两种线程池非常类似。的确是这样,事实上它们是同时实现的,不行我们来看实际例子:

ThreadPoolExecutor executor1 = (ThreadPoolExecutor)Executors.newCachedThreadPool();
ThreadPoolExecutor executor2 = (ThreadPoolExecutor)Executors.newFixedThreadPool(4);

这是两种线程池的新建方法,看起来很像吧!如果你不这么认为,我只能让你看看真相了。

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

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

是的,它们调用了同一个构造函数,只是参数略有不同。那么我们来看看这些参数的含义,以及两组参数的区别。首先还是需要贴一下ThreadPoolExecutor的构造函数了。

public ThreadPoolExecutor(int corePoolSize,
             int maximumPoolSize,
             long keepAliveTime,
             TimeUnit unit,
             BlockingQueue<Runnable> workQueue) {
  this(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,
     Executors.defaultThreadFactory(), defaultHandler);
}

为了看起来清爽,再一层的构造函数我就不贴了,而且那个构造函数也只是简单的赋值而已。这里的函数原型已经能给我们很多很多信息了,不得不说JDK的代码命名确实好,简直就像注释一样。

maximumPoolSize就是线程池的最大线程数;对于cached ThreadPool来说,这个值是Integer.MAX_VALUE,基本相当于无穷大了,什么样的机器能跑几十亿线程!!对于fixed ThreadPool来讲,这个值就是用户设定的线程池的数目。
keepAliveTime和unit决定了线程的缓存过期时间;对于cached ThreadPool来讲,线程的缓存过期时间是一分钟,换言之,一个工作线程如果一分钟都无事可干,就把它撤销掉以节省资源。fixed ThreadPool传入的时间是0,这里的含义是fixed ThreadPool中的工作线程是永远不过期的。

corePoolSize是线程池的最小线程数;对于cached ThreadPool,这个值为0,因为在完全没有任务的情况下,cached ThreadPool的确会成为“光杆司令”。至于fixed ThreadPool,这个fixed已经表明corePoolSize是等于线程总数的。
接下来,我们根据一个简单的使用例子,来看看一下cached ThreadPool的流程。

public class Task implements Callable<String> {

private String name;
public Task(String name) {
  this.name = name;
}
@Override
public String call() throws Exception {
  System.out.printf("%s: Starting at : %s\n", this.name, new Date());
  return "hello, world";
}
public static void main(String[] args) {
  ThreadPoolExecutor executor = (ThreadPoolExecutor)Executors.newCachedThreadPool();
  Task task = new Task("test");
  Future<String> result = executor.submit(task);
  try {
    System.out.printf("%s\n", result.get());
  } catch (InterruptedException | ExecutionException e) {
    e.printStackTrace();
  }
  executor.shutdown();
  System.out.printf("Main ends at : %s\n", new Date());
}
}

首先,来看看executor.submit(task),这其实调用了ThreadPoolExecutor.execute(Runnable command)方法,这个方法的代码如下,整段代码的逻辑是这样的。首先检查线程池的线程数是否不够corePoolSize,如果不够就直接新建线程并把command添加进去;如果线程数已经够了或者添加失败(多个线程增加添加的情况),就尝试把command添加到队列中(workQueue.offer(command)),如果添加失败了,就reject掉cmd。大体的逻辑是这样的,这段代码有很多基于线程安全的设计,这里为了不跑题,就先忽略细节了。

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

到这里,看起来线程池实现的整体思路其实也没多么复杂。但是还有一个问题——一个普通的Thread在执行完自己的run方法后会自动退出。那么线程池是如何实现Worker线程不断的干活,甚至在没有任务的时候。其实答案很简单,就是Worker其实在跑大循环,Worker实际运行方法如下:

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) {
      w.lock();
  /***/
      try {
        beforeExecute(wt, task);
        Throwable thrown = null;
        try {
          task.run();
        /***/
        } finally {
          afterExecute(task, thrown);
        }
      } finally {
        task = null;
        w.completedTasks++;
        w.unlock();
      }
    }
    completedAbruptly = false;
  } finally {
    processWorkerExit(w, completedAbruptly);
  }
}

关键就在这个while的判断条件,对于需要cached线程的情况下,getTask()会阻塞起来,如果缓存的时间过期,就会返回一个null,然后Worker就退出了,也就结束了它的服役周期。而在有任务的情况下,Woker会把task拿出来,然后调用task.run()执行任务,并通过Future通知客户线程(即future.get()返回)。这样一个简单的线程池使用过程就完了。。。

当然,线程池的很多精髓知识——基于线程安全的设计,我都没有分析。有兴趣可以自己分析一下,也可以和我讨论。此外Scheduled ThreadPool这里也没有分析,它的要点其实是调度,主要是根据时间最小堆来驱动的。

感谢阅读,希望能帮助到大家,谢谢大家对本站的支持,如有疑问请留言,或者到本站社区交流,大家共同进步!

(0)

相关推荐

  • java线程池:获取运行线程数并控制线程启动速度的方法

    在java里, 我们可以使用Executors.newFixedThreadPool 来创建线程池, 然后就可以不停的创建新任务,并用线程池来执行了. 在提交任务时,如果线程池已经被占满,任务会进到一个队列里等待执行. 这种机制在一些特定情况下会有些问题.今天我就遇到一种情况:创建线程比线程执行的速度要快的多,而且单个线程占用的内存又多,所以很快内存就爆了. 想了一个办法,就是在提交任务之前,先检查目前正在执行的线程数目,只有没把线程池占满的时候在去提交任务. 代码很简单: int thread

  • Java线程安全问题小结_动力节点Java学院整理

    浅谈java内存模型 不同的平台,内存模型是不一样的,但是jvm的内存模型规范是统一的.其实java的多线程并发问题最终都会反映在java的内存模型上,所谓线程安全无非是要控制多个线程对某个资源的有序访问或修改.总结java的内存模型,要解决两个主要的问题:可见性和有序性.我们都知道计算机有高速缓存的存在,处理器并不是每次处理数据都是取内存的.JVM定义了自己的内存模型,屏蔽了底层平台内存管理细节,对于java开发人员,要清楚在jvm内存模型的基础上,如果解决多线程的可见性和有序性. 那么,何谓

  • Java探索之Thread+IO文件的加密解密代码实例

    这篇文章向大家分享了几段代码,主要是关于Thread+IO文件的加密解密,下面看看具体代码: 加密启动线程 package com.hz.subsection; import java.io.File; public class enCodeFileThread extends Thread { public Files files; public File file; public File dst; public enCodeFileThread(String name,Files file

  • 浅谈Java线程间通信之wait/notify

    Java中的wait/notify/notifyAll可用来实现线程间通信,是Object类的方法,这三个方法都是native方法,是平台相关的,常用来实现生产者/消费者模式.先来我们来看下相关定义: wait() :调用该方法的线程进入WATTING状态,只有等待另外线程的通知或中断才会返回,调用wait()方法后,会释放对象的锁. wait(long):超时等待最多long毫秒,如果没有通知就超时返回. notify() :通知一个在对象上等待的线程,使其从wait()方法返回,而返回的前提

  • JAVA线程sleep()和wait()详解及实例

    JAVA线程sleep()和wait()详解及实例 sleep 1.sleep是Thread的一个静态(static)方法.使得Runnable实现的线程也可以使用sleep方法.而且避免了线程之前相互调用sleep()方法,引发死锁. 2.sleep()执行时需要赋予一个沉睡时间.在沉睡期间(阻塞线程期间),CPU会放弃这个线程,执行其他任务.当沉睡时间到了之后,该线程会自动苏醒,不过此时线程不会立刻被执行,而是要等CPU分配资源,和其他线程进行竞争. 3.此外如果这个线程之前获取了一个机锁,

  • 详解Java线程池和Executor原理的分析

    详解Java线程池和Executor原理的分析 线程池作用与基本知识 在开始之前,我们先来讨论下"线程池"这个概念."线程池",顾名思义就是一个线程缓存.它是一个或者多个线程的集合,用户可以把需要执行的任务简单地扔给线程池,而不用过多的纠结与执行的细节.那么线程池有哪些作用?或者说与直接用Thread相比,有什么优势?我简单总结了以下几点: 减小线程创建和销毁带来的消耗 对于Java Thread的实现,我在前面的一篇blog中进行了分析.Java Thread与内

  • 详解Java线程池的使用及工作原理

    一.什么是线程池? 线程池是一种用于实现计算机程序并发执行的软件设计模式.线程池维护多个线程,等待由调度程序分配任务以并发执行,该模型提高了性能,并避免了由于为短期任务频繁创建和销毁线程而导致的执行延迟. 二.线程池要解决什么问题? 说到线程池就一定要从线程的生命周期讲起. 从图中可以了解无论任务执行多久,每个线程都要经历从生到死的状态.而使用线程池就是为了避免线程的重复创建,从而节省了线程的New至Runnable, Running至Terminated的时间:同时也会复用线程,最小化的节省系

  • 详解Java线程池是如何重复利用空闲线程的

    在Java开发中,经常需要创建线程去执行一些任务,实现起来也非常方便,但如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间.此时,我们很自然会想到使用线程池来解决这个问题. 使用线程池的好处: 降低资源消耗.java中所有的池化技术都有一个好处,就是通过复用池中的对象,降低系统资源消耗.设想一下如果我们有n多个子任务需要执行,如果我们为每个子任务都创建一个执行线程,而创建线程的过程是需要一定的系统消耗

  • 详解Java线程池如何实现优雅退出

    目录 shutdown()方法 shutdownNow()方法 awaitTermination(long, TimeUnit)方法 在[高并发专题]中,我们从源码角度深度分析了线程池中那些重要的接口和抽象类.深度解析了线程池是如何创建的,ThreadPoolExecutor类有哪些属性和内部类,以及它们对线程池的重要作用.深度分析了线程池的整体核心流程,以及如何拆解Worker线程的执行代码,深度解析Worker线程的执行流程. 本文,我们就来从源码角度深度解析线程池是如何优雅的退出程序的.首

  • 详解Java线程池队列中的延迟队列DelayQueue

    目录 DelayQueue延迟队列 DelayQueue使用场景 DelayQueue属性 DelayQueue构造方法 实现Delayed接口使用示例 DelayQueue总结 在阻塞队里中,除了对元素进行增加和删除外,我们可以把元素的删除做一个延迟的处理,即使用DelayQueue的方法.本文就来和大家聊聊Java线程池队列中的DelayQueue—延迟队列 public enum QueueTypeEnum { ARRAY_BLOCKING_QUEUE(1, "ArrayBlockingQ

  • 详解Java线程池如何统计线程空闲时间

    背景介绍 你刚从学校毕业后,到新公司实习,试用期又被毕业,然后你又不得不出来面试,好在面试的时候碰到个美女面试官! 面试官: 小伙子,我看你简历上写的项目中用到了线程池,你知道线程池是怎样实现复用线程的? 这面试官是不是想坑我?是不是摆明了不让我通过? 难道你不应该问线程池有哪些核心参数?每个参数具体作用是什么? 往线程池中不断提交任务,线程池的处理流程是什么? 这些才是你应该问的,这些八股文我已经背熟了,你不问,瞎问什么复用线程? 幸亏我看了一灯的八股文,听我给你背一遍! 我: 线程池复用线程

  • 详解Java线程池的增长过程

    通过ThreadPoolExecutor的方式创建线程池 ThreadPoolExecutor 构造方法: public ThreadPoolExecutor(int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handle

  • Java线程池FutureTask实现原理详解

    前言 线程池可以并发执行多个任务,有些时候,我们可能想要跟踪任务的执行结果,甚至在一定时间内,如果任务没有执行完成,我们可能还想要取消任务的执行,为了支持这一特性,ThreadPoolExecutor提供了 FutureTask 用于追踪任务的执行和取消.本篇介绍FutureTask的实现原理. 类视图 为了更好的理解FutureTask的实现原理,这里先提供几个重要接口和类的结构,如下图所示: RunnableAdapter ThreadPoolExecutor提供了submit接口用于提交任

  • Java详解使用线程池处理任务方法

    什么是线程池? 线程池就是一个可以复用线程的技术. 不使用线程池的问题: 如果用户每发起一个请求,后台就创建一个新线程来处理,下次新任务来了又要创建新线程,而创建新线程的开销是很大的,这样会严重影响系统的性能. 线程池常见面试题: 1.临时线程什么时候创建? 新任务提交时发现核心线程都在忙,任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程. 2.什么时候会开始拒绝任务? 核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候才会开始任务拒绝. 1.线程池处理Runnable任务

  • 详解Java数据库连接池

    一.什么是数据库连接池 就是一个容器持有多个数据库连接,当程序需要操作数据库的时候直接从池中取出连接,使用完之后再还回去,和线程池一个道理. 二.为什么需要连接池,好处是什么? 1.节省资源,如果每次访问数据库都创建新的连接,创建和销毁都浪费系统资源 2.响应性更好,省去了创建的时间,响应性更好. 3.统一管理数据库连接,避免因为业务的膨胀导致数据库连接的无限增多. 4.便于监控. 三.都有哪些连接池方案 数据库连接池的方案有不少,我接触过的连接池方案有: 1.C3p0 这个连接池我很久之前看到

随机推荐