为什么为什么?
Java 8流背后的驱动程序之一是并发编程。 在流管道中,指定要完成的工作,然后任务将自动分发到可用处理器上:
var result = myData.parallelStream().map(someBusyOperation).reduce(someAssociativeBinOp).orElse(someDefault);
当数据结构便宜且可拆分为多个部分且操作使处理器繁忙时,并行流将发挥出色的作用。 这就是它的设计目的。
但是,如果您的工作负载包含大部分阻塞的任务,那么这对您没有帮助。 那是您的典型Web应用程序,可以处理许多请求,每个请求都花费大量时间等待REST服务,数据库查询等结果。
1998年,令人惊奇的是,Sun Java Web Server(Tomcat的前身)在单独的线程而不是OS进程中运行了每个请求。 这样就可以满足数千个并发请求! 如今,这并不令人惊讶。 每个线程占用大量内存,典型服务器上不能拥有数百万个线程。
这就是为什么服务器端编程的现代口号是:“永不阻塞!” 相反,您指定一旦数据可用就应该发生什么。
这种异步编程风格非常适合服务器,使它们可以轻松支持数百万个并发请求。 对于程序员来说不是那么好。
这是使用HttpClient
API的异步请求:
HttpClient.newBuilder().build().sendAsync(request, HttpResponse.BodyHandlers.ofString()).thenAccept(response -> . . .);.thenApply(. . .);.exceptionally(. . .);
我们通常用语句实现的功能现在被编码为方法调用。 如果我们喜欢这种编程风格,就不会在Lisp中使用我们的编程语言来编写语句和编写快乐的代码。
诸如JavaScript和Kotlin之类的语言为我们提供了“异步”方法,在这些方法中,我们编写语句,然后将这些语句转换为您刚刚看到的方法调用。 很好,只不过它意味着现在有两种方法-常规方法和转换方法。 而且您不能混合使用它们(“红色药丸/蓝色药丸”的分界)。
Project Loom从Erlang和Go等语言中获得指导,在这些语言中,阻塞并不是什么大问题。 您可以在“光纤”或“轻型线程”或“虚拟线程”中运行任务。 该名称尚待讨论,但我更喜欢“光纤”,因为它很好地表示了多个光纤在一个载波线程中执行的事实。 当发生阻塞操作(例如等待锁定或I / O)时,光纤将停放。 停车比较便宜。 如果很多时候都停放了一根承载线,则可以支撑一千根光纤。
请记住,Project Loom不能解决所有并发问题。 如果您有大量计算任务并且想让所有处理器内核都忙,它对您无济于事。 它对于使用单个线程的用户界面没有帮助(用于序列化对不是线程安全的数据结构的访问)。 在该用例中继续使用AsyncTask
/ SwingWorker
/ JavaFX Task
。 当您有很多任务花费大量时间阻塞时,Project Loom很有用。
注意 如果您已经存在很长时间了,您可能还记得早期的Java版本具有映射到OS线程的“绿色线程”。 但是,有一个关键的区别。 当绿色线程被阻塞时,其承载线程也被阻塞,从而阻止了同一承载线程上的所有其他绿色线程取得进展。
踢轮胎
在这一点上,Project Loom仍处于探索阶段。 API会不断变化,因此在假期过后尝试使用该代码时,请准备好适应最新的API版本。
您可以从http://jdk.java.net/loom/下载Project Loom的二进制文件,但是它们很少更新。 但是,在Linux机器或VM上,自己构建最新版本很容易:
git clone https://github.com/openjdk/loom
cd loom
git checkout fibers
sh configure
make images
根据您已经安装的内容, configure
可能会失败一些,但是消息会告诉您需要安装哪些软件包才能继续进行。
在API的当前版本中,光纤或现在称为虚拟线程的虚拟线程表示为Thread
类的对象。 这是三种生产纤维的方法。 首先,有一个新的工厂方法可以构造OS线程或虚拟线程:
Thread thread = Thread.newThread(taskname, Thread.VIRTUAL, runnable);
如果您需要更多自定义,则有一个构建器API:
Thread thread = Thread.builder().name(taskname).virtual().priority(Thread.MAX_PRIORITY).task(runnable).build();
但是,一段时间以来,手动创建线程一直被认为是较差的做法,因此您可能不应该执行任何一种操作。 而是将执行程序与线程工厂一起使用:
ThreadFactory factory = Thread.builder().virtual().factory();
ExecutorService exec = Executors.newFixedThreadPool(NTASKS, factory);
现在,熟悉的固定线程池将以与以往相同的方式从工厂调度虚拟线程。 当然,还将有OS级别的载体线程来运行这些虚拟线程,但这是虚拟线程实现的内部。
固定线程池将限制并发虚拟线程的总数。 默认情况下,从虚拟线程到载体线程的映射是通过使用系统属性jdk.defaultScheduler.parallelism
或默认情况下Runtime.getRuntime().availableProcessors()
所给定数量的内核的jdk.defaultScheduler.parallelism
池完成的。 您可以在线程工厂中提供自己的调度程序:
factory = Thread.builder().virtual().scheduler(myExecutor).factory();
我不知道这是否是人们想要做的。 为什么载具线程多于核心?
返回我们的执行人服务。 您可以在虚拟线程上执行任务,就像在OS级线程上执行任务时一样:
for (int i = 1; i <= NTASKS; i++) {String taskname = "task-" + i;exec.submit(() -> run(taskname));
}
exec.shutdown();
exec.awaitTermination(delay, TimeUnit.MILLISECONDS);
作为一个简单的测试,我们可以在每个任务中入睡。
public static int DELAY = 10_000;public static void run(Object obj) {try {Thread.sleep((int) (DELAY * Math.random()));} catch (InterruptedException ex) {ex.printStackTrace();}System.out.println(obj);}
如果现在将NTASKS
设置为1_000_000
并在工厂生成器中.virtual()
,则该程序将失败,并显示内存不足错误。 一百万个OS级线程占用大量内存。 但是使用虚拟线程,它可以工作。
至少,它应该可以工作,并且对我之前的Loom版本确实有效。 不幸的是,在12月5日下载的构建中,我得到了一个核心转储。 当我尝试使用Loom时,这时有发生。 希望它会在您尝试时解决。
现在,您可以尝试更复杂的事情了。 亨氏·卡布兹(Heinz Kabutz)最近为益智游戏提供了一个程序,该程序可加载数千个Dilbert卡通图像。 对于每个日历日,都有一个页面,例如https://dilbert.com/strip/2011-06-05 。 程序读取这些页面,在每个页面中找到卡通图像的URL,然后加载每个图像。 这是一堆乱七八糟的期货 ,有点像:
CompletableFuture.completedFuture(getUrlForDate(date)).thenComposeAsync(this::readPage, executor).thenApply(this::getImageUrl).thenComposeAsync(this::readPage).thenAccept(this::process);
使用光纤,代码更加清晰:
exec.submit(() -> { String page = new String(readPage(getUrlForDate(date)));byte[] image = readPage(getImageUrl(page));process(image);
});
当然,每个对readPage
的调用readPage
块,但是对于纤维,我们不在乎。
尝试一下您关心的事情。 阅读大量网页,进行处理,进行更多的阻塞读取,并享受光纤阻塞便宜的事实。
结构化的一致性
Project Loom的最初动机是实现光纤,但今年早些时候,该项目开始了针对结构化并发的实验性API。 在这篇强烈推荐的文章 (从中拍摄以下图像)中,Nathaniel Smith提出了结构化的并发形式。 这是他的中心论点。 在新线程中启动任务实际上并不比使用GOTO编程好,即有害:
new Thread(runnable).start();
当多个线程在没有协调的情况下运行时,这将是意大利面条代码。 在1960年代,结构化编程将goto
替换为分支,循环和函数:
现在,结构化并发的时机已经到来。 启动并发任务时,通过阅读程序文本,我们应该知道它们何时全部完成。
这样,我们可以控制任务使用的资源。
到2019年夏季,Project Loom有了一个用于表达结构化并发的API。 不幸的是,由于最近进行了统一线程和光纤API的实验,该API目前处于混乱状态,但是您可以通过http://jdk.java.net/loom/上的原型进行尝试。
在这里,我们安排了许多任务:
FiberScope scope = FiberScope.open();
for (int i = 0; i < NTASKS; i++) {scope.schedule(() -> run(i));
}
scope.close();
调用scope.close()
阻塞,直到所有光纤完成。 请记住,光纤阻塞不是问题。 一旦关闭示波器,您就可以确定光纤已经完成。
FiberScope
是可FiberScope
的,因此您可以使用try
-with-resources语句:
try (var scope = FiberScope.open()) {...
}
但是,如果其中一项任务永远无法完成怎么办?
您可以使用截止日期( Instant
)或超时( Duration
)创建范围:
try (var scope = FiberScope.open(Instant.now().plusSeconds(30))) {for (...)scope.schedule(...);
}
截止期限/超时之前尚未完成的所有光纤都将被取消。 怎么样? 继续阅读。
消除
取消一直是Java的痛苦。 按照惯例,您可以通过中断线程来取消线程。 如果线程正在阻塞,则阻塞操作以InterruptedException
终止。 否则,设置中断状态标志。 正确地进行检查是乏味的。 可以重置中断状态,或者InterruptedException
是已检查的异常,这没有帮助。
java.util.concurrent
中取消的处理一直不一致。 考虑ExecutorService.invokeAny
。 如果有任务产生结果,则其他任务将被取消。 但是CompletableFuture.anyOf
允许所有任务运行完成,即使其结果将被忽略。
2019年夏季的Project Loom API解决了取消问题。 在该版本中,光纤具有cancel
操作,类似于interrupt
,但是取消是不可撤销的。 如果当前光纤已被取消,则静态Fiber.cancelled
方法将返回true
。
当示波器超时时,其光纤将被取消。
取消可以由FiberScope
构造函数中的以下选项控制。
-
CANCEL_AT_CLOSE
:关闭范围取消所有计划的光纤而不是阻塞 -
PROPAGATE_CANCEL
:如果取消拥有光纤,则任何新调度的光纤都会自动取消 -
IGNORE_CANCEL
:无法取消预定的光纤
所有这些选项都未在顶层设置。 PROPAGATE_CANCEL
和IGNORE_CANCEL
选项是从父范围继承的。
如您所见,有相当多的可调整性。 我们必须看看重新考虑此问题后会发生什么。 对于结构化并发,当示波器超时或被强制关闭时,必须自动取消示波器中的所有光纤。
螺纹局部
让我感到惊讶的是,Project Loom实现者的痛苦之一是ThreadLocal
变量,以及更深奥的东西-上下文类加载器AccessControlContext
。 我不知道有那么多东西骑在线程上。
如果您的数据结构不适合并发访问,则有时可以在每个线程中使用一个实例。 经典示例是SimpleDateFormat
。 当然,您可以继续构造新的格式化程序对象,但这并不高效。 所以你想分享一个。 但是全球
public static final SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
将无法正常工作。 如果两个线程同时访问它,则格式可能会混乱。
因此,每个线程中有一个是有意义的:
public static final ThreadLocal<SimpleDateFormat> dateFormat= ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));
要访问实际的格式化程序,请致电
String dateStamp = dateFormat.get().format(new Date());
首次调用get
时,将调用构造函数中的lambda。 从那时起,get方法返回属于当前线程的实例。
对于线程,这是公认的做法。 但是,如果真的有一百万个光纤,您是否真的想拥有一百万个实例?
这对我来说不是问题,因为使用线程安全的东西(如java.time
格式化程序)似乎更容易。 但是Project Loom一直在考虑“范围本地”对象-那些FiberScope
被重新激活了。
在线程与处理器数量一样多的情况下,线程局部变量也已被用作处理器局部性的近似值。 可以实际模拟用户意图的API可以支持此功能。
项目状况
想要使用Project Loom的开发人员自然会沉迷于API,如您所见,该API尚未解决。 但是,许多实施工作都处于幕后。
一个关键部分是在操作阻塞时使光纤停放。 已经完成了网络连接,因此您可以在光纤内连接到网站,数据库等。 当前不支持本地文件操作块时的停车。
实际上,在JDK 11、12和13中已经重新实现了这些库,这是对频繁发布实用程序的致敬。
目前尚不支持在监视器上进行阻塞( synchronized
块和方法),但最终需要这样做。 ReentrantLock
现在可以了。
如果光纤以本机方法阻塞,则将“固定”线程,并且所有光纤都不会前进。 Project Loom对此无能为力。
Method.invoke
需要更多工作才能得到支持。
有关调试和监视支持的工作正在进行中。
如前所述,稳定性仍然是一个问题。
最重要的是,性能还有一段路要走。 停放光纤不是免费的午餐。 每次都需要替换运行时堆栈的一部分。
在所有这些方面都取得了很大的进展,所以让我们回顾一下开发人员关心的API。 现在是查看Project Loom并考虑如何使用它的好时机。
同一类代表线和纤维对您有价值吗? 还是您希望将某些Thread
行李丢掉? 您是否认同结构化并发的承诺?
试一下Project Loom,看看它如何与您的应用程序和框架一起工作,并为无畏的开发团队提供反馈!
翻译自: https://www.javacodegeeks.com/2019/12/project-loom.html