专注Java教育14年 全国咨询/投诉热线:444-1124-454
赢咖4LOGO图
始于2009,口口相传的Java黄埔军校
首页 学习攻略 Java学习 深入学习Java线程池:Java线程池学习教程

深入学习Java线程池:Java线程池学习教程

更新时间:2019-09-24 09:39:35 来源:赢咖4 浏览1728次



image.png

  线程池是多线程编程中的核心概念,简单来说就是一组可以执行任务的空闲线程。


  首先,我们了解一下多线程框架模型,明白为什么需要线程池。


  线程是在一个进程中可以执行一系列指令的执行环境,或称运行程序。多线程编程指的是用多个线程并行执行多个任务。当然,JVM对多线程有良好的支持。


  尽管这带来了诸多优势,首当其冲的就是程序性能提高,但多线程编程也有缺点——增加了代码复杂度、同步问题、非预期结果和增加创建线程的开销。


  在这篇文章中,我们来了解一下如何使用Java线程池来缓解这些问题。


  为什么使用线程池?


  创建并开启一个线程开销很大。如果我们每次需要执行任务时重复这个步骤,那将会是一笔巨大的性能开销,这也是我们希望通过多线程解决的问题。


  为了更好理解创建和开启一个线程的开销,让我们来看一看JVM在后台做了哪些事:


  为线程栈分配内存,保存每个线程方法调用的栈帧。


  每个栈帧包括本地变量数组、返回值、操作栈和常量池


  一些JVM支持本地方法,也将分配本地方法栈


  每个线程获得一个程序计数器,标识处理器正在执行哪条指令


  系统创建本地线程,与Java线程对应


  和线程相关的描述符被添加到JVM内部数据结构


  线程共享堆和方法区


  当然,这些步骤的具体细节取决于JVM和操作系统。


  另外,更多的线程意味着更多工作量,系统需要调度和决定哪个线程接下来可以访问资源。


  线程池通过减少需要的线程数量并管理线程生命周期,来帮助我们缓解性能问题。


  本质上,线程在我们使用前一直保存在线程池中,在执行完任务之后,线程会返回线程池等待下次使用。这种机制在执行很多小任务的系统中十分有用。


  Java线程池


  Java通过executor对象来实现自己的线程池模型。可以使用executor接口或其他线程池的实现,它们都允许细粒度的控制。


  java.util.concurrent包中有以下接口:


  Executor——执行任务的简单接口


  ExecutorService——一个较复杂的接口,包含额外方法来管理任务和executor本身


  ScheduledExecutorService——扩展自ExecutorService,增加了执行任务的调度方法


  除了这些接口,这个包中也提供了Executors类直接获取实现了这些接口的executor实例


  一般来说,一个Java线程池包含以下部分:


  工作线程的池子,负责管理线程


  线程工厂,负责创建新线程


  等待执行的任务队列


  在下面的章节,让我们仔细看一看Java类和接口如何为线程池提供支持。


  Executors类和Executor接口


  Executors类包含工厂方法创建不同类型的线程池,Executor是个简单的线程池接口,只有一个execute()方法。


  我们通过一个例子来结合使用这两个类(接口),首先创建一个单线程的线程池,然后用它执行一个简单的语句:


  Executorexecutor=Executors.newSingleThreadExecutor();

  executor.execute(()->System.out.println("Singlethreadpooltest"));


  注意语句写成了lambda表达式,会被自动推断成Runnable类型。


  如果有工作线程可用,execute()方法将执行语句,否则就把Runnable任务放进队列,等待线程可用。


  基本上,executor代替了显式创建和管理线程。


  Executors类里的工厂方法可以创建很多类型的线程池:


  newSingleThreadExecutor():包含单个线程和无界队列的线程池,同一时间只能执行一个任务


  newFixedThreadPool():包含固定数量线程并共享无界队列的线程池;当所有线程处于工作状态,有新任务提交时,任务在队列中等待,直到一个线程变为可用状态


  newCachedThreadPool():只有需要时创建新线程的线程池


  newWorkStealingThreadPool():基于工作窃取(work-stealing)算法的线程池,后面章节详细说明


  接下来,让我们看一下ExecutorService接口提供了哪些新功能


  ExecutorService


  创建ExecutorService方式之一便是通过Excutors类的工厂方法。


  ExecutorServiceexecutor=Executors.newFixedThreadPool(10);


  Besidestheexecute()method,thisinterfacealsodefinesasimilarsubmit()methodthatcanreturnaFutureobject:


  除了execute()方法,接口也定义了相似的submit()方法,这个方法可以返回一个Future对象。


  Callable<Double>callableTask=()->{

  returnemployeeService.calculateBonus(employee);

  };

  Future<Double>future=executor.submit(callableTask);

  //executeotheroperations

  try{

  if(future.isDone()){

  doubleresult=future.get();

  }

  }catch(InterruptedException|ExecutionExceptione){

  e.printStackTrace();

  }


  从上面的例子可以看到,Future接口可以返回Callable类型任务的结果,而且能显示任务的执行状态。


  当没有任务等待执行时,ExecutorService并不会自动销毁,所以你可以使用shutdown()或shutdownNow()来显式关闭它。


  executor.shutdown();


  ScheduledExecutorService


  这是ExecutorService的一个子接口,增加了调度任务的方法。


  ScheduledExecutorServiceexecutor=Executors.newScheduledThreadPool(10);

  schedule()方法的参数指定执行的方法、延时和TimeUnit


  Future<Double>future=executor.schedule(callableTask,2,TimeUnit.MILLISECONDS);


  另外,这个接口定义了其他两个方法:


  executor.scheduleAtFixedRate(

  ()->System.out.println("FixedRateScheduled"),2,2000,TimeUnit.MILLISECONDS);

  executor.scheduleWithFixedDelay(

  ()->System.out.println("FixedDelayScheduled"),2,2000,TimeUnit.MILLISECONDS);


  scheduleAtFixedRate()方法延时2毫秒执行任务,然后每2秒重复一次。相似的,scheduleWithFixedDelay()方法延时2毫秒后执行第一次,然后在上一次执行完成2秒后再次重复执行。


  在下面的章节,我们来看一下ExecutorService接口的两个实现:ThreadPoolExecutor和ForkJoinPool。


  ThreadPoolExecutor


  这个线程池的实现增加了配置参数的能力。创建ThreadPoolExecutor对象最方便的方式就是通过Executors工厂方法:


  ThreadPoolExecutorexecutor=(ThreadPoolExecutor)Executors.newFixedThreadPool(10);


  这种情况下,线程池按照默认值预配置了参数。线程数量由以下参数控制:


  corePoolSize和maximumPoolSize:表示线程数量的范围


  keepAliveTime:决定了额外线程存活时间


  我们深入了解一下这些参数如何使用。


  当一个任务被提交时,如果执行中的线程数量小于corePoolSize,一个新的线程被创建。如果运行的线程数量大于corePoolSize,但小于maximumPoolSize,并且任务队列已满时,依然会创建新的线程。如果多于corePoolSize的线程空闲时间超过keepAliveTime,它们会被终止。


  上面那个例子中,newFixedThreadPool()方法创建的线程池,corePoolSize=maximumPoolSize=10并且keepAliveTime为0秒。


  如果你使用newCachedThreadPool()方法,创建的线程池maximumPoolSize为Integer.MAX_VALUE,并且keepAliveTime为60秒。


  ThreadPoolExecutorcachedPoolExecutor

  =(ThreadPoolExecutor)Executors.newCachedThreadPool();

  Theparameterscanalsobesetthroughaconstructororthroughsettermethods:


  这些参数也可以通过构造函数或setter方法设置:

  ThreadPoolExecutorexecutor=newThreadPoolExecutor(

  4,6,60,TimeUnit.SECONDS,newLinkedBlockingQueue<Runnable>()

  );

  executor.setMaximumPoolSize(8);


  ThreadPoolExecutor的一个子类便是ScheduledThreadPoolExecutor,它实现了ScheduledExecutorService接口。你可以通过newScheduledThreadPool()工厂方法来创建这种类型的线程池。


  ScheduledThreadPoolExecutorexecutor


  =(ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(5);


  上面语句创建了一个线程池,corePoolSize为5,maximumPoolSize无限制,keepAliveTime为0秒。


  ForkJoinPool


  另一个线程池的实现是ForkJoinPool类。它实现了ExecutorService接口,并且是Java7中fork/join框架的重要组件。


  fork/join框架基于“工作窃取算法”。简而言之,意思就是执行完任务的线程可以从其他运行中的线程“窃取”工作。


  ForkJoinPool适用于任务创建子任务的情况,或者外部客户端创建大量小任务到线程池。


  这种线程池的工作流程如下:


  创建ForkJoinTask子类


  根据某种条件将任务切分成子任务


  调用执行任务


  将任务结果合并


  实例化对象并添加到池中


  创建一个ForkJoinTask,你可以选择RecursiveAction或RecursiveTask这两个子类,后者有返回值。


  我们来实现一个继承RecursiveTask的类,计算阶乘,并把任务根据阈值划分成子任务。

      image.png

  这个类需要实现的主要方法就是重写compute()方法,用于合并每个子任务的结果。


  具体划分任务逻辑在createSubtasks()方法中:

      image.png

  最后,calculate()方法包含一定范围内的乘数。

     image.png

  接下来,任务可以添加到线程池:


  ForkJoinPoolpool=ForkJoinPool.commonPool();

  BigIntegerresult=pool.invoke(newFactorialTask(100));


  ThreadPoolExecutor与ForkJoinPool对比


  初看上去,似乎fork/join框架带来性能提升。但是这取决于你所解决问题的类型。


  当选择线程池时,非常重要的一点是牢记创建、管理线程以及线程间切换执行会带来的开销。


  ThreadPoolExecutor可以控制线程数量和每个线程执行的任务。这很适合你需要在不同的线程上执行少量巨大的任务。


  相比较而言,ForkJoinPool基于线程从其他线程“窃取”任务。正因如此,当任务可以分割成小任务时可以提高效率。


  为了实现工作窃取算法,fork/join框架使用两种队列:


  包含所有任务的主要队列


  每个线程的任务队列


  当线程执行完自己任务队列中的任务,它们试图从其他队列获取任务。为了使这一过程更加高效,线程任务队列使用双端队列(doubleendedqueue)数据结构,一端与线程交互,另一端用于“窃取”任务。


  来自TheHDeveloper的图很好的表现出了这一过程:

image.png

  和这种模型相比,ThreadPoolExecutor只使用一个主要队列。


  最后要注意的一点ForkJoinPool只适用于任务可以创建子任务。否则它和ThreadPoolExecutor没区别,甚至开销更大。


  跟踪线程池的执行


  现在我们对Java线程池生态系统有了基本的了解,让我们通过一个使用了线程池的应用,来看一看执行中到底发生了什么。


  通过在FactorialTask的构造函数和calculate()方法中加入日志语句,你可以看到下面调用序列:

      image.png

  你可以看到创建了很多任务,但只有3个工作线程——所以任务通过线程池被可用线程处理。


  也可以看到在放到执行池之前,主线程中对象如何被创建。


  使用Prefix这一类可视化的日志工具是一个很棒的方式来探索和理解运行时的线程池。


  记录线程池日志的核心便是保证在日志信息中方便辨识线程名字。Log4J2通过使用布局能够很好完成这种工作。


  使用线程池的潜在风险


  尽管线程池有巨大优势,你在使用中仍会遇到一些问题,比如:


  用的线程池过大或过小:如果线程池包含太多线程,会明显的影响应用的性能;另一方面,线程池太小并不能带来所期待的性能提升。


  正如其他多线程情形一样,死锁也会发生。举个例子,一个任务可能等待另一个任务完成,而后者并没有可用线程处理执行。所以说避免任务之间的依赖是个好习惯。


  等待执行时间很长的任务:为了避免长时间阻塞线程,你可以指定最大等待时间,并决定过期任务是拒绝处理还是重新加入队列。


  为了降低风险,你必须根据要处理的任务,来谨慎选择线程池的类型和参数。对你的系统进行压力测试也是值得的,它可以帮你获取真实环境下的系统行为数据。


  结论


  线程池有很大优势,简单来说就是可以将任务的执行从线程的创建和管理中分离。另外,如果使用得当,它们可以极大提高应用的性能。


  如果你学会充分利用线程池,Java生态系统好处便是其中有很多成熟稳定的线程池实现。


  以上就是赢咖4java培训机构小编介绍的“深入学习Java线程池:Java线程池学习教程”的内容,希望对大家有帮助,更多java最新资讯请继续关注赢咖4java培训机构官网,每天会有精彩内容分享与你。

提交申请后,顾问老师会电话与您沟通安排学习

免费课程推荐 >>
技术文档推荐 >>