Java 并发系列十三 : Thread-Per-Message
前言
感谢王宝令老师极客时间的并发系列课程
我们曾经把并发编程领域的问题总结为三个核心问题:分工、同步和互斥。其中同步和互斥的问题更多源自微观,而分工问题则源自宏观,我们解决问题,往往都是从宏观入手,在编程领域,软件的设计也是从概要设计开始,而后才进行详细设计,同样,解决并发问题,也要首先解决宏观的分工问题。
在并发编程领域,解决分工问题也有一系列的设计模式,比较常用的有 Thread-Per-Message 模式、 Worker Thread 模式、 生产者 - 消费者模式等等 。
今天重点介绍:Thread-Per-Message 模式。
如何理解 Thread-Per-Message 模式
现实世界,很多事情都需要委托其他人代为办理。例如:天天忙着写BUG 哪有时间教育小孩?只能委托小学老师了。委托他人代办一个最大的好处就是,可以专心做自己的事情了。
在编程领域有许多类似的需求,比如写一个HTTP Server, 很显然只能在主线程中接受请求,而不能处理请求,如果在主线程中处理的话,那同一时间只能处理一个请求,太慢了?怎么办。。。可以利用代办的思路,创建一个子线程,委托子线程去处理请求。
这种委托他人办理的模式,在并发编程领域总结为一种设计模式。叫做Thread-Per-Message 模式, 简而言之就是为每个任务分配一个独立的线程,这是最简单的一种分工方法。
用 Thread 实现 Thread-Per-Message 模式
Thread-Per-Message 模式一个经典的应用就是 网络编程服务端的实现,服务端为每个客户端请求创建一个独立的线程,当线程处理完请求后,自动销毁,这是一种最简单的并发处理网络请求的方法。
Java 语言里,Java 线程是和操作系统线程一一对应的,这种做法本质上是将Java 线程的调度权完全委托给操作系统,而操作系统再者方面非常成熟,所以这种做法是稳定可靠的,但是也继承了操作系统的缺点:创建成本高,为了解决这个问题,Java 并发包提供了线程池等工具类。这个 思路再很长一段时间都是很稳妥的方案,但并不是唯一的方案。
业界还有一种方案叫做轻量级线程。这个方案再Java 领域知名度并不高, 但是再其他语言叫的很火。
例如 Go 语言、Lua 语言里的协程, 本质上就是一种轻量级的线程。轻量级线程创建成本很低。创建的速度和内存占用相比操作系统内存至少有一个数量级的提升。所以基于轻量级线程实现Thread-Per-Message 模式就完全没有问题了。
Java 语言目前也已经意识到轻量级线程的重要性了, OpenJDK 有个 Loom 项目,就是要解决 Java 语言的轻量级线程问题,在这个项目中,轻量级线程被叫做 Fiber。 Loom 项目在设计轻量级线程时,充分考量了当前 Java 线程的使用方式,采取的是尽量兼容的态度,所以使用上还是挺简单的。
总结
并发编程领域的分工问题,指的是如何高效地拆解任务并分配给线程。前面我们在并发工具类模块中已经介绍了不少解决分工问题的工具类,例如 Future、CompletableFuture 、CompletionService、Fork/Join 计算框架等,这些工具类都能很好地解决特定应用场景的问题,所以,这些工具类曾经是 Java 语言引以为傲的。不过这些工具类都继承了 Java 语言的老毛病:太复杂。
Thread-Per-Message 模式在 Java 领域并不是那么知名,根本原因在于 Java 语言里的线程是一个重量级的对象,为每一个任务创建一个线程成本太高,尤其是在高并发领域,基本就不具备可行性。不过这个背景条件目前正在发生巨变,Java 语言未来一定会提供轻量级线程,这样基于轻量级线程实现 Thread-Per-Message 模式就是一个非常靠谱的选择。
当然,对于一些并发度没那么高的异步场景,例如定时任务,采用 Thread-Per-Message 模式是完全没有问题的。实际工作中,我就见过完全基于 Thread-Per-Message 模式实现的分布式调度框架,这个框架为每个定时任务都分配了一个独立的线程。