干货分享 | 深入学习 Java 线程池

译文链接:http://www.importnew.com/29212.html


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

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

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

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

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

java线程池

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

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

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

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

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

除了这些接口,这个包中也提供了 Executors 类直接获取实现了这些接口的 executor 实例。一般来说,一个 Java 线程池包含以下部分:

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

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

►等待执行的任务队列

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

Executors 类和 Executor 接口

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

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

1Executorexecutor=Executors.newSingleThreadExecutor();

2executor.execute(()->System.out.println("Single thread pool test"));

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

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

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

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

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

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

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

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

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

ExecutorService

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

1ExecutorServiceexecutor=Executors.newFixedThreadPool(10);

Besides the execute() method, this interface also defines a similar submit() method that can return a Future object:

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

CallablecallableTask=()->{returnemployeeService.calculateBonus(employee);

};

Futurefuture=executor.submit(callableTask);

// execute other operations

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

Futurefuture=executor.schedule(callableTask,2,TimeUnit.MILLISECONDS);

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

xecutor.scheduleAtFixedRate(()->System.out.println("Fixed Rate Scheduled"),2,2000,TimeUnit.MILLISECONDS);executor.scheduleWithFixedDelay(()->System.out.println("Fixed Delay Scheduled"),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 秒。

1ThreadPoolExecutorcachedPoolExecutor

2=(ThreadPoolExecutor)Executors.newCachedThreadPool();

The parameters can also be set through a constructor or through setter methods:

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

1ThreadPoolExecutorexecutor=newThreadPoolExecutor(

24,6,60,TimeUnit.SECONDS,newLinkedBlockingQueue()

3);

4executor.setMaximumPoolSize(8);

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

1ScheduledThreadPoolExecutorexecutor

2=(ScheduledThreadPoolExecutor)Executors.newScheduledThreadPool(5);

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

ForkJoinPool

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

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

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

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

►创建 ForkJoinTask 子类

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

►调用执行任务

►将任务结果合并

►实例化对象并添加到池中

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

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

publicclassFactorialTaskextendsRecursiveTask{privateintstart=1;privateintn;privatestaticfinalintTHRESHOLD=20;// standard constructors@OverrideprotectedBigIntegercompute(){if((n-start)>=THRESHOLD){returnForkJoinTask.invokeAll(createSubtasks()).stream().map(ForkJoinTask::join).reduce(BigInteger.ONE,BigInteger::multiply);}else{returncalculate(start,n);}}

}

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

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

privateCollectioncreateSubtasks(){ListdividedTasks=newArrayList<>();intmid=(start+n)/2;dividedTasks.add(newFactorialTask(start,mid));dividedTasks.add(newFactorialTask(mid+1,n));returndividedTasks;}

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

privateBigIntegercalculate(intstart,intn){returnIntStream.rangeClosed(start,n).mapToObj(BigInteger::valueOf).reduce(BigInteger.ONE,BigInteger::multiply);}

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

1ForkJoinPoolpool=ForkJoinPool.commonPool();

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

ThreadPoolExecutor 与 ForkJoinPool 对比

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

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

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

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

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

►包含所有任务的主要队列

►每个线程的任务队列

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

来自The H Developer的图很好的表现出了这一过程:

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

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

总结

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

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


转载自:https://mp.weixin.qq.com/s/68cD7YM52xJRORxUboN41w

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,657评论 6 505
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,889评论 3 394
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,057评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,509评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,562评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,443评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,251评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,129评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,561评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,779评论 3 335
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,902评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,621评论 5 345
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,220评论 3 328
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,838评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,971评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,025评论 2 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,843评论 2 354