Java并发编程-Executor框架与线程池

 

线程简介

        并发编程是Java程序员最重要的技能之一,也是最难掌握的一种技能。它要求编程者对计算机最底层的运作原理有深刻的理解,同时要求编程者逻辑清晰、思维缜密,这样才能写出高效、安全、可靠的多线程并发程序。现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。线程是现代操作系统调度的最小单元,也叫轻量级进程,在一个进程里可以创建多个线程,这些线程都拥有各自的计算器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换,让使用者感觉到这些线程在同时执行。本文只是先对Executor进行浅析,对java并发编程基础做一个较为全面的解析。

基本概念

  程序:程序是由序列组成的,告诉计算机如何完成一个具体的任务

  进程:进程是一个程序的实例,是并发执行的程序在执行过程中分配和管理资源的基本单位,每一个进程都有它自己的地址空    间,一般情况下,包括文本区域(text region)、数据区(data region)和堆栈(stack region)。

  线程:线程自己不拥有系统资源,但它可与同属一个 进程的其它线程共享进程所拥有的全部资源。线程是处理器调度的基本单  位

  线程安全:当多个线程访问某个类时,不管运行时环境采用何种调度方式或者这些线程将如何交替执行,并且在主调代码中不  需要任何额外的同步或协同,这个类都能表现出正确的行为,那么这个类就是线程安全的。

同步和异步关注的是消息通信机制

 同步:是发出一个调用时,在没有得到结果之前,该调用就不返回,一旦调用返回,就得到返回值。

 异步:而异步则相反,调用在发出之后调用就直接返回,所以没有返回结果。

阻塞和非阻塞关注的是程序在等待调用结果(消息,返回值)时的状态.

 阻塞:阻塞调用是指调用结果返回之前,当前线程会被挂起,调用线程只有在得到结果之后才会返回。

 非阻塞:调用指在不能立刻得到结果之前,该调用不会阻塞当前线程。

线程分析

顺序执行

       应用程序是围绕执行任务进行管理的,所谓任务就是抽象,离散的工作单元。

应用程序内部的任务调度,最简单的方式就是单一的线程中顺序的执行任务,下图是在主线程中执行任务。

                                        

       图中的这个程序是顺序执行的,因为实际执行效率是非常糟糕的,主线程没有完成网络请求操作之前后面的执行数据库耗时操作也必须等待。

异步执行

       为了提供更好的响应性,处理更多的请求,可以为每个请求创建一个新的线程。

创建线程有三种方式:

第一种:继承Thread类创建线程

    首先定义Thread类的子类,并重写该类的run()方法,该对象的方法体就是现场需要完成的任务,run()方法也称为线程执行体,然后创建Thread子类的实例也就是创建线程对象,启动线程调用Thread的start()方法。

                                                            

第二种:通过实现Runnable接口创建并启动线程

       首先定义Runnable接口的实现类,一样要重写run()方法,这个run()方法和Thread中的run()方法一样是线程的执行体,然后创建Runnable实现类的实例,并用这个实例作为Thread的target来创建Thread对象,这个Thread对象才是真正的线程对象,最后依然是通过调用线程对象的start()方法来启动线程

                                                    

第三种:使用Callable和Futrue创建线程,这里我们先不做介绍,在后面介绍Callable和Futrue时再进行讲解

                                     

        主线程中为每个请求都创建了一个新的线程来处理耗时操作,而不是在主线程内部处理它,由此可以得出结论:

执行任务已经脱离了主线程,因此主线程可以迅速的开始处理下一个请求,这样就提高了响应性

并行处理任务,使得多个请求可以同时得到服务,即使某个线程被阻塞了,其他线程也可以正常工作,程序的吞吐量会得到提高

任务处理的代码必须是线程安全的,如果再多创建几个Thread处理相同网络请求和数据库操作,那么如果其中有多个线程公用的数据就会并发的调用它,很可能就会造成数据混乱

 

无限制创建线程的缺点

        在实际的生产环境中,“每个任务每个线程”方式是存在一些缺陷的,尤其在需要创建大量的线程时会更加突出,例如在Android开发中。以下是这种方式的缺点:

  1. 线程生命周期的开销: 线程的创建与关闭不是“免费”的,创建线程需要时间,因此会带来处理请求的延迟,并且需要在JVM和操作系统之间进行相应的处理活动。
  2. 资源消耗量:活动的线程会消耗系统资源,尤其是内存,如果可运行的线程数多以可用的处理器数,线程就会闲置,并且大量线程会竞争CPU资源,产生其他开销。
  3. 稳定性:应该限制可创建线程的数目,如果超出了限制,很可能会收到一个OutOfMemoryError.

Executor框架

Executor介绍

       任务是逻辑上的工作单元,线程是使任务异步执行的机制。

所有任务在单一的线程中顺序执行,以及每个任务在自己的线程中执行,每一种策略都有严重的局限性,顺序执行会产生糟糕的响应性和吞吐量,“每任务每线程”会给资源管理带来麻烦。

      在Java类库中,任务执行的首要抽象不是Thread,而是Executor,查看源码中Executor的解释是它为任务提交和任务执行之间的解耦提供了标准的方法,为使用Runnable描述任务提供了通用的方式。

      Executor基于生产者-消费者模式,提交任务的执行者是生产者,执行任务的线程是消费者,在程序中实现一个生产者-消费者的设计,使用Executor通常是最简单的方式

                                       

        Executor接口定义了一个方法excute(Runnable command),在该方法接收一个Runnable实例,用来执行一个任务,任务即一个实现了Runnable接口的类。

ExecutorService介绍

     我们先来看一张类图结构。

                                          

       可以看到ExecutorService类继承了Executor,因为Executor是异步执行任务,所以在任何时间,所有值钱提交的任务的状态都不能立即可见,既然Executor是为应用程序执行任务提供服务的,那么这些任务理应可以被关闭,为了解决Executor的声明周期问题,ExecutorService接口扩展了Executor,并且添加了一些用于生命周期管理的方法,同时还有一些用于任务提交的便利方法。

      查看ExecutorService类结构如下图:

                                   

        从其内部方法中我们看到其实它暗示了ExecutorService的声明周期有三种状态,运行(running),关闭(shut down)和终止(terminate)。创建后便进入运行状态,当调用了shutdown()方法时,便进入关闭状态, 当所有已经提交了的任务执行完后,便到达终止状态。如果不调用shutdown()方法,ExecutorService会一直处在运行状态,不断接收新的任务,执行新的任务,服务器端一般不需要关闭它,保持一直运行即可。

      shutdown()方法会启动一个平缓的关闭过程:停止接受新的任务,同时等待已经提交的任务完成---包括尚未开始执行的任务。

      shutdownNow()方法会启动一个强制的关闭过程:尝试取消所有运行中的任务和排在队列中尚未开始的任务。一旦所有任务全部完成后, ExecutorService会转入终止状态。

      在ExecutorService中我们看到提交任务的submit()方法可以提交Runable任务对象,也可以提交Callable任务对象,并且submit()方法返回了Future这个类,我们来了解一下这两个类的具体作用。

      先看一下Callable和Runnable两个类具体区别,首先我们知道Runnable中的run()方法是没有返回值的,但是我们看一下Callable可以看到这个任务执行对象的call()方法是有返回值的。

                                                  

       可以看到call()方法有一个泛型的返回值,这个值在call()方法结束时可以return一个返回值,我们在使用Callable时需要实现这个方法。

       接下来我们看看Futrue这个类,Future是我们在使用java实现异步时最常用到的一个类,我们可以向线程池提交一个Callable,并通过future对象获取执行结果。我们从源码的角度看一下为什么它可以获取到执行状态以及执行结果,我们还是先看看这个类的结构。

                                                            

      看到这几个方法就可以大概判断出这几个方法的作用。我们根据源码中的注释翻译一下。

  • cancel():取消一个任务,并返回取消结果,参数表示是否中断线程。
  • isCanceled():判断任务是否被取消
  • isDone():判断当前任务是否执行完毕,包括正常执行完毕,执行异常或者任务取消
  • get():获取任务执行结果,任务结束之前会阻塞。
  • get():在指定时间内尝试获取执行结果,若超时则抛出超时异常

     这里几个方法可以看出Future提供了三种功能:

判断任务是否完成

能够中断任务

能够获取任务执行结果

    这个Future是一个接口,那么我们先看看它的实现类。

                                                        

    我们直接看最外面的实现类FutureTask,

   首先看到这个类中成员变量:

                                       

        后面判断执行状态时都是根据这个state来进行判断,并且state是赋值给下面的这些固定常量值。

       接下来我们看一下FutureTask中任务是怎么执行的,当任务被提交到线程池后,会执行FutureTask的run()方法。具体为什么ExecutorService提交任务之后会执行futureTask的run()方法,我们这里不做讲解,只是提供一下源码查看这个过程涉及到的类以及方法供自己去查看本文使用的是JDK1.8的源码。查看步骤首先是ExecutorService的实现类AbstractExecutorService中的submit(Callable<T> task)方法,然后进入看到newTaskFor()方法中new FutureTask(),然后执行的是execute(FutureTask)方法,跳到一个具体的线程池类中查看execute()方法,比如ThreadPoolExecutor类中的execute()方法,然后调用if(workerCountOf(c)<corePoolSize)判断当前线程总数是否小于核心线程数量,如果是true则进入addWorker()方法。

      这个方法中就可以看到使用Thread的start()来执行这个传递进来的FutureTask,也就是执行FutureTask的run()方法,run()方法中执行了当前Callable的call()方法,这个Callable可以是直接传递进来的也可以是通过Executors.callable(runnable)转换过来的。在FutureTask的run()方法中我们可以看到在调用call()方法结束后如果业务逻辑异常,则调用setException方法将异常对象赋给outcome,并且更新state值,如果业务正常,则调用set方法将执行结果赋给outcome,并且更新state值,并且是通过UNSAFE.compareAndSwapInt()方法来完成state状态值的更新,状态变更的原子性由unsafe对象提供的CAS操作保证,是通过底层库Native方法来实现的。

         通过以上代码的流程梳理我们可以知道为什么Future可以获取到执行状态以及执行结果,下面我们通过一个简单的例子看一下Callable以及Future如何使用。

                                      

线程池介绍

       ThreadPoolExecutor是一个灵活的,健壮的线程池的实现,允许用户进行各种各样的定制。

概念介绍

       我们看一下它的构造函数

                                           

     我们分析一下这几个参数。

  • corePoolSize:核心池大小就是目标的大小,线程池的实现试图维护池的大小,即使没有任务执行,池的代销也等于核心池的大小,并且直到工作队列充满前,池都不会创建更多的线程,当ThreadPoolExecutor被初始化创建后,所有的核心线程并非立即开始,需要等到有任务提交的时刻,除非我们调用prestartAllCoreThreads方法,核心线程在allowCoreThreadTimeout被设置为true时会超时退出,默认情况下不会退出。
  • maximunPoolSize:最大线程池大小,是同时可以活动的线程数的上限。
  • keepAliveTime:如果线程数多于corePoolSize,则这些多于的线程的空闲时间超过keepAliveTime时将被终止。
  • TimeUnit:线程活动保持时间的单位,TimeUnit.DAYS 天 TimeUnit.HOURS 小时 TimeUnit.MINUTES 分钟 TimeUnit.SECONDS 秒 TimeUnit.MILLISECONDS 毫秒 TimeUnit.MICROSECONDS 微妙 TimeUnit.NANOSECONDS 纳秒
  • workQueue:工作队列任务队列这里需要仔细讲解一下,不然后面分析各种线程池时很容易弄混淆。首先一个任务来了需要排队,这个时候涉及到排队的策略,任务排队有三种基本方法:无限队列,有限队列,同步移交。
  1. 无限队列:LinkedBlockingQueue队列,这是一个链表结构所以可以无限延长,如果所有的工作线程都处于忙碌状态,任务将会在队列中等候,如果任务持续的快速到达超过了他们被执行的速度,队列将会无限制的增加,可能会造成资源耗尽的情况。
  2. 有界队列:ArrayBlockingQueue有界队列这是一个数组,所以是一个定长的队列,有界队列能避免资源耗尽的情况发生,但是当队列满了后新的任务怎么办。
  3. 同步移交:SynchronousQueue同步移交队列,这个队列中不保存任务任务,相当于一个管道的作用,队列接收到任务的时候,会直接提交给线程处理,而不保留它
  4. 延时队列:DelayQueue队列内元素必须实现Delayed接口,这就意味着你传进去的任务必须先实现Delayed接口,这个队列接收到任务时,首先先入队,只有达到了指定的延时时间,才会执行任务。
  1. RejectedExecutionHandler:饱和策略,当一个有限队列充满后,饱和策略就开始起作用了,以及当任务提交到一个已经关闭的Executor时,也会用到饱和策略。

      AbortPolicy:中止策略 会Execute抛出未检查的RejectExecutorException异常,调用者可以捕获这个异常,然后按自己的需求处理;

     DiscardPolic:遗弃策略 默认放弃这个任务

     DiscardOldestPolicy:遗弃最旧的策略会选择丢弃本应该下来就执行的任务,该策略还会尝试去重新提交新任务。

     CallerRunsPolicy:调用者运行策略 既不会丢弃哪个任务,也不会抛出任何异常,它会把一些任务退回到调用者那里,以减缓新任务流。

ThreadFactory:线程工厂,线程池总是通过线程工厂来创建线程,我们可以通过自定义线程工厂来实现我们自己的操作。

我们先来看一下创建线程池的时候系统默认使用的线程工厂是什么样的

                                 

可以看到其实默认的线程工厂实现很简单,它做的事就是统一给线程池中的线程设置线程group来分组,以及同意线程前缀名,以及设置线程优先级。

步骤介绍

        我们先看一下线程池时怎么处理线程任务提交的,下面是步骤图

 

https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1527672157637&di=dcc0d826aa3c321102ec673b64ea7be0&imgtype=jpg&src=http%3A%2F%2Fimg3.imgtn.bdimg.com%2Fit%2Fu%3D3369160353%2C2107528450%26fm%3D214%26gp%3D0.jpg

    从图上可以看出来,线程池执行所提交的任务过程主要有这样几个阶段“

  1. 先判断线程池中核心线程池所有的线程是否都在执行任务,如果不是,则新建一个线程执行刚提交的任务,否则核心线程池中所有的线程都在执行任务,则进入第二步;
  2. 判断阻塞队列是否已满,如果未满,则将提交的任务放置在阻塞队列中,否则进入第三步,这里需要指出如果是有界队列才会出现队列已满的情况。
  3. 判断线程池中所有的线程是否都在执行任务,如果没有,则创建一个新的线程来执行任务,否则交给饱和策略进行处理

                                  

  Execute()方法执行逻辑有这样几种情况:

  1. 如果当前运行的线程少于corePoolSize,则会创建新的线程来执行新的任务;
  2. 如果运行的线程个数等于或者大于corePoolSize,则会将提交的任务存放到阻塞队列workQueue中;
  3. 如果当前workQueue队列已满的话,则会创建新的线程来执行任务
  4. 如果线程个数已经超过了maximumPoolSize,则会使用饱和策略RejectedExecutionHandler来进行处理

线程池应用

       Executors可以创建四种类型的ThreadPoolExecutor线程池,因为cheduledThreadPoolExecutor比较特殊,WorkStealingPool内部不是使用ThreadPoolExecutor所以这两个我们就不做分析

FixedThreadPool

       创建固定长度的线程池,每次提交任务创建一个线程,直到达到线程池的最大数量,线程池的大小不再变化。

这个线程池可以创建固定线程数的线程池,特点就是可以重用固定数量线程的线程池,它的构造源码如下;

  1. FixedThreadPoolExecutor的corePoolSize和maxiumPoolSize都被设置为创建FixedThreadPool时指定的参数nThreads.
  2. 0L则表示当前线程池中的线程数量操作核心线程的数量时,多余线程被立即停止
  3. 最后一个参数表示FixedThreadPool使用了无界队列LinkedBlockingQueue作为线程池的做工队列,由于是无界的,当线程池的线程数达到corePoolSize后,新任务将在无界队列中等待,因此线程池线程数量不会超过corePoolSize,同时maxiumPoolSize也就变成了一个无效的参数,并且运行中的线程池并不会拒绝任务

SingleThreadExecutor

        SingleThreadExecutor是使用单个工作线程的Executor,特点是使用单个工作线程执行任务,它的构造源码如下。

       SingleThreadExecutor的corePoolSize和maxiumPoolSize都设置为1,

     执行过程如下;

  1. 如果当前工作中的线程数量少于corePoolSize,就创建一个新的线程来执行任务,这里corePoolSize为1,
  2. 当线程池的工作中的线程数量达到了corePoolSize,则将任务加入LinkedBlockingQueue中,这是一个无界队列
  3. 线程池中的唯一的线程执行完任务后再去队列中取任务,由于在线程池中只有一个工作线程,所以任务可以按照添加顺序执行

CachedThreadPool

       CachedThreadPool是一个“无限容量的线程池,它会根据需要创建新线程,特点是可以根据需要来创建新的线程执行任务,没有特定的corePool即核心线程为0,下面是它的构造函数:

        可以看见CachedThreadPool的corePoolSize是0,maxiumPoolSize设置为Integer.MAX_VALUE,即maximum是无限大的,这里的keepAliveTime设置为60秒,意味着空闲的线程最多可以等待任务60秒,否则将被回收。

       CachedThreadPool使用没有容量的SynchronousQueue作为线程池的工作队列,它是一个没有容量的阻塞队列,每个插入操作必须等待另一个线程的对应移除操作,这意味着,如果主线程提交任务的速度高于线程池中处理任务的速度时,CachedThreadPool会不断的创建新线程,极端情况下,CachedThreadPool会因为创建过多的线程而耗尽CPU资源

      执行过程:

  1. 首先执行SynchronousQueue.offer(Runnable task) 如果在当前的线程池中有空闲的线程正在执行SynchronousQueue.poll(),那么当前执行SynchronousQueue.offer()的线程与空闲线程执行poll操作配对成功,主线程把任务交给空闲线程执行,execute()方法执行成功,否则执行步骤b
  2. 当前线程池为空(初始化maximumPool为空)或者没有空闲线程,那么配对失败,将没有线程执行SynchronousQueue.poll()操作,这种情况下,线程池会创建一个新的线程执行任务。进入步骤c
  3. 在创建完新的线程后,将会执行poll操作,进行配对,然后执行任务,当任务执行完成之后包活时间60秒以内,如果这个时间内又有新的任务提交进来,如果其他线程还是在工作状态那么这个空闲线程将执行新任务,因此长时间不提交任务的CachedThreadPool不会占用系统资源。这些线程会在等待包活时间之后被回收。

     SynchronousQueue是一个不存储元素的阻塞队列,每次进行offer操作时必须等待poll操作,否则不能继续添加元素。

 

    介绍了上面这些参数的具体意思之后我们就可以利用ThreadPoolExecutor的构造函数来自定义线程池,根据我们的实际使用场景来输入各种参数自定义自己的线程池。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章