写点什么

如何正确理解 Java 领域中的多线程模型,主要用来解决什么问题?

作者:PivotalCloud
  • 2022 年 8 月 13 日
    广东
  • 本文字数:17752 字

    阅读完需:约 58 分钟

如何正确理解Java领域中的多线程模型,主要用来解决什么问题?

苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》


写在开头


我国宋代禅宗大师青原行思在《三重境界》中有这样一句话:“ 参禅之初,看山是山,看水是水;禅有悟时,看山不是山,看水不是水;禅中彻悟,看山仍然山,看水仍然是水。”


作为一名 Java Developer,在面对 Java 并发编程的时候,有过哪些的疑惑与不解 ?对于 Java 领域中的线程机制与多线程,你都做了哪些功课?是否和我一样,在看完《Java 编程思想》和《Java 并发编程实战》之后,依旧一头雾水,不知其迹?那么,希望你看完此篇文章之后,对你有所帮助。


从一定程度上说,Java 并发编程之路,实则是一条“看山是山,看山不是山,看山还是山”的修行之路。大多数情况下,当我们觉得有迹可循到有迹可寻时,何尝不是陷入了另外一个“怪圈”之中?


从搭载 Linux 系统上的服务器程序来说,使用 Java 编写的是”单进程-多线程"程序,而用 C++语言编写的,可能是“单进程-多线程”程序,“多进程-单线程”程序或者是“多进程-多线程”程序。其中,“多进程-多线程”程序是”单进程-多线程"程序和“多进程-单线程”程序的组合体。


相对于操作系统内核来说,Java 程序属于应用程序,只能在这一个进程里面,一般我们都是直接利用 JDK 提供的 API 开发多个线程实现并发。


而 C++直接运行在 Linux 系统上,可以直接利用 Linux 系统提供的强大的进程间通信(Inter-Process Communication,IPC),很容易创建多个进程实现并发程序,并实现进程间通信。


但是,多线程的开发难度远远高于单线程的开发,主要是需要处理线程间的通信,需要对线程并发做控制,需要做好线程间的协调工作。


对于固定负载情况下,在描述和研究计算并发系统处理能力,以及描述并行处理效果的加速比,一直有一个比较著名的计算公式:



就是我们熟知的阿姆达尔定律(Amdahl"s Law),在这个公式中,


[1]. P:指的是程序中可并行部分的程序在单核上执行的时间占比。一般用作表示可改进性能的部件原先运行占用的时间与系统整体运行需要的时间的比值,取值范围是 0 ≤ P ≤ 1。<br>[2]. S:指的是处理器的个数(总核心数)。一般用作表示升级加速比,可改进部件原先运行速度与改进后的部件速度的比值,取值范围是 S ≥ 1。<br>[3]. Slatency(s):指的是程序在 S 个处理器相对在单个处理器(单核)中速度提升比率。一般用作表示整个任务的提速比。<br>


根据这个公式,我们可以依据可确定程序中可并行代码的比例,来决定我们实际工作中增加处理器(总核心数)所能带来的速度提升的上限。


无论是 C++开发者在 Linux 系统中使用的 pthread,还是 Java 开发者使用的 java.util.concurrent(JUC)库,这些线程机制的都需要一定的线程 I/O 模型来做理论支撑。


所以,接下来,我们就让我们一起探讨和揭开 Java 领域中的线程 I/O 模型的神秘面纱,针对那些盘根错落的枝末细节,才能让我们更好地了解和正确认识 ava 领域中的线程机制。

关健术语


本文用到的一些关键词语以及常用术语,主要如下:


  • 阿姆达尔定律(Amdahl 定律): 用于确定并发系统中性能瓶颈部件在采用措施提示性能后,此部件对系统性能提示的改进程度,即系统加速比。

  • 任务(Task): 表示一个程序需要被完成工作内容,与线程非一对一对应的关系,是一个相对概念。

  • 并发(Concurrent): 表示至少一个任务或者若干 个任务同一个时间段内被执行,但是不是顺序执行,大多数都是以交替的方式被执行。

  • 并行(Parallel): 表示至少一个任务或者若干 个任务同一个时刻被执行。主要是指一个并行连接通过多个通道在同一时间内传播多个数据流。

  • 串行(Serial): 表示至多一个任务或者只有一个 个任务同一个时刻被执行。主要是指在同一时间内只连接传输一个数据流。

  • 内核线程(Kernel Thread): 表示由内核管理的线程,处于操作系统内核空间。用户应用程序通过 API 和系统调用(system call)来访问线程工具。

  • 应用线程(Application Thread): 表示不需要内核支持而在用户应用程序中实现的线程,处于应用程序空间,也称作用户线程。主要是由 JVM 管理的线程和 JVM 自己携带的 JVM 线程。

  • 上下文切换(Context Switch): 一般是指任务切换, 或者 CPU 寄存器切换。当多任务内核决定运行另外的任务时, 它保存正在运行任务的当前状态, 也就是 CPU 寄存器中的全部内容。这些内容被保存在任务自己的堆栈中, 入栈工作完成后就把下一个将要运行的任务的当前状况从该任务的栈中重新装入 CPU 寄存器, 并开始下一个任务的运行过程。在 Java 领域中,线程有生命周期,其上下文信息的保存和恢复的过程。

  • 线程安全(Thread Safe): 一段操作共享数据的代码能够保证同一个时间内被多个线程执行而依然保证其数据的正确性的考量。

基本概述

Java 领域中的线程主要分为 Java 层线程(Java Thread) ,JVM 层线程(JVM Thread),操作系统层线程(Kernel Thread)。



对于 Java 领域中,从一定程度上来说,由于 Java 程序并不直接运行在 Linux 系统上,而是运行在 JVM(Java 虚拟机)上,而一个 JVM 实例是一个 Linux 进程,每一个 JVM 都是一个独立的“沙盒”,JVM 之间相互独立,互不通信。


按照操作系统和应用程序两个层次来说,线程主要可以分为内核线程(Kernel Thread) 和应用线程(Application Thread)。


其中,在 Java 领域中的线程主要分为 Java 层线程(Java Thread) ,JVM 层线程(JVM Thread),操作系统层线程(Kernel Thread)。


一般来说,我们把应用线程看作更高层面的线程,而内核线程需要向应用线程提供支持。由此可见,内核线程和应用线程之间存在一定的映射关系。


因此,从线程映射关系来看,不同的操作系统可能采用不同的映射方式,我们把这些映射关系称为线程的映射,或者可以说作线程映射理论模型(Thread Mappered Theory Model )。


在 Java 领域中,对于文件的 I/O 操作,提供了一系列的 I/O 功能 API,主要基于基于流模型实现。我们把这些流模型的设计,称作为 I/O 流模型(I/O Stream Model )。


其中,Java 对照操作系统内核以及网络通信 I/O 中的传统 BIO 来说,提供并支持了 NIO 和 AIO 的功能 API 设计,我们把这些设计,称作为线程 I/O 参考模型(Thread I/O Reference Model )。


另外,对于 NIO 和 AIO 还参考了一定的设计模式来实现,我们把这些基于设计模式的设计,称作为线程设计模式模型(Thread I/O Design Pattern Model )。


综上所述,在 Java 领域中,我们在学习和掌握 Java 并发编程的时候,可以按照:线程映射理论模型->I/O 流模型->线程 I/O 参考模型->线程设计模式模型->线程价值模型等脉络来一一进行对比分析。

一. Java 领域中的线程映射理论模型

Java 领域中的线程映射模型主要有内核级线程模型(Kernel-Level Thread ,KLT)、应用级线程模型(Application-Level Thread ,ALT)、混合两级线程模型(Mixture-Level Thread ,MLT)等 3 种模型。



从 Java 线程映射类型来看,主要有线程一对一(1:1)映射,线程多对多(M:1)映射,线程多对多(M:N)映射等关系。


对应到线程模型来说,线程一对一(1:1)映射对应着内核线程(Kernel-Level Thread ,KLT),线程多对多(M:1)映射对应着应用级线程(Application-Level Thread,ALT),线程多对多(M:N)映射对应着混合两级线程(Mixture-Level Thread ,MLT)。


因此,Java 领域中实现多线程主要有 3 种模型:内核级线程模型、应用级线程模型、混合两级线程模型。它们之间最大的差异就在于线程与内核调度实体( Kernel Scheduling Entity,简称 KSE)之间的对应关系上。


顾名思义,内核调度实体就是可以被内核的调度器调度的对象,因此称为内核级线程,是操作系统内核的最小调度单元。


综上所述,接下来,我们来详细讨论 Java 领域中的线程映射理论模型。

1. 应用级线程模型

应用级线程模型主要是指(Application-Level Thread ,ALT),就是多个用户线程映射到同一个内核线程上,用户线程的创建、调度、同步的所有操作全部都是由用户空间的线程来完成的。



在 Java 领域中,应用级线程主要是指 Java 语言编写应用程序的 Java 线程(Java Thread)和 JVM 虚拟机中 JVM 线程(JVM Thread)。


在应用级线程模型下,完全建立在用户空间的线程库上,不依赖于系统内核,用户线程的创建、同步、切换和销毁等操作完全在用户态执行,不需要切换到内核态。


其中,用户进程使用系统内核提供的接口——轻量级进程(Light Weight Process,LWP)来使用系统内核线程。


在此种线程模型下,由于一个用户线程对应一个 LWP,因此某个 LWP 在调用过程中阻塞了不会影响整个进程的执行。


但是各种线程的操作都需要在用户态和内核态之间频繁切换,消耗太大,速度相对用户线程模型来说要慢。

2. 内核级线程模型

内核级线程模型主要是指(Kernel-Level Thread ,KLT),用户线程与内核线程建立了一对一的关系,即一个用户线程对应一个内核线程,内核负责每个线程的调度。



在 Linux 中,对于内核级线程,操作系统会为其创建一套栈:用户栈+内核栈,其中用户栈工作在用户态,内核栈工作在内核态,在发生系统调用时,线程的执行会从用户栈切换到内核栈。


在内核级线程模型下,完全依赖操作系统内核提供的内核线程来实现多线程。线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个 CPU 中去执行。


其中,glibc 中的 pthread_create 方法主要是创建一个 OS 内核级线程,我们不深入细节,主要是为该线程分配了栈资源;需要注意的是这个栈资源对于 JVM 而言是堆外内存,因此堆外内存的大小会影响 JVM 可以创建的线程数。


在 JVM 概念中,JVM 栈用来执行 Java 方法,而本地方法栈用来执行 native 方法;但需要注意的是 JVM 只是在概念上区分了这两种栈,而并没有规定如何实现。


在 HotSpot 中,则是将 JVM 栈与本地方法栈二合一,使用核心线程的用户栈来实现(因为 JVM 栈和本地方法栈都是属于用户态的栈),即 Java 方法与 native 方法都在同一个用户栈中调用,而当发生系统调用时,再切换到核心栈运行。


这种设计的好处是线程的各种操作以及切换消耗很低;


但是线程的所有操作都需要在用户态实现,线程的调度实现起来异常复杂,并且系统内核对 ULT 无感知,如果线程阻塞则会引起整个进程的阻塞。

3. 混合两级线程模型

混合两级线程模型主要是指(Mixture-Level Thread ,MLT),是应用级线程模型和内核级线程模型等两种模型的混合版本,用户线程仍然是在用户态中创建,用户线程的创建、切换和销毁的消耗很低,用户线程的数量不受限制。



对于混合两级线程模型,是应用级线程模型和内核级线程模型等两种模型的混合版本,主要是充分吸收前面两种线程模型的优点且尽量规避它们的缺点。


在此模型下用户线程与内核线程是多对多(M : N,通常 M >= N)的映射模型。主要是维护一个轻量级进程(Light Weight Process,LWP),在用户线程和内核线程之间充当桥梁,就可以使用操作系统提供的线程调度和处理器映射功能。


一般来说,Java 虚拟机使用的线程模型是基于操作系统提供的原生线程模型来实现的,Windows 系统和 Linux 系统都是使用的内核线程模型,而 Solaris 系统支持混合线程模型和内核线程模型两种实现。


还有,Java 线程内存模型中,可以将虚拟机内存划分为两部分内存:主内存和线程工作内存,主内存是多个线程共享的内存,线程工作内存是每个线程独享的内存。方法区和堆内存就是主内存区域,而虚拟机栈、本地方法栈以及程序计数器则属于每个线程独享的工作内存。


Java 内存模型规定所有成员变量都需要存储在主内存中,线程会在其工作内存中保存需要使用的成员变量的拷贝,线程对成员变量的操作(读取和赋值等)都是对其工作内存中的拷贝进行操作。各个线程之间不能互相访问工作内存,线程间变量的传递需要通过主内存来完成。

二. Java 领域中的 I/O 流模型

Java 领域中的 I/O 模型主要指 Java 领域中的 I/O 模型大致可以分为字符流 I/O 模型,字节流 I/O 模型以及网络通信 I/O 模型。



在编程语言的 I/O 类库中常使用流(Stream)这个概念,代表了任何有能力产出数据的数据源对象或者是有能力接收数据的接收端对象。


流是个抽象的概念,是对输入输出设备的高度抽象,一般来说,编程语言都会涉及输入流和输出流两部分。


一定意义上来说,输入流可以看作一个输入通道,输出流可以看作一个输出通道,其中:


  • 输入流是相对程序而言的,外部传入数据给程序需要借助输入流。

  • 输出流是相对程序而言的,程序把数据传输到外部需要借助输出流。


由于,“流”模型屏蔽了实际的 I/O 设备中处理数据的细节,这就意味着我们只需要根据相关的基础 API 的功能和设计,便可实现数据处理和交互。


Java IO 方式有很多种,基于不同的 IO 抽象模型和交互方式,可以进行简单区分:第一,传统的 java.io 包,它基于流模型实现,提供了我们最熟知的一些 IO 功能,比如 File 抽象、输入输出流等。交互方式是同步、阻塞的方式,也就是说,在读取输入流或者写入输出流时,在读、写动作完成之前,线程会一直阻塞在那里,它们之间的调用是可靠的线性顺序。java.io 包的好处是代码比较简单、直观,缺点则是 IO 效率和扩展性存在局限性,容易成为应用性能的瓶颈。


很多时候,人们也把 java.net 下面提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 也归类到同步阻塞 IO 类库,因为网络通信同样是 IO 行为。


第二,在 Java 1.4 中引入了 NIO 框架(java.nio 包),提供了 Channel、Selector、Buffer 等新的抽象,可以构建多路复用的、同步非阻塞 IO 程序,同时提供了更接近操作系统底层的高性能数据操作方式。


第三,在 Java 7 中,NIO 有了进一步的改进,也就是 NIO 2,引入了异步非阻塞 IO 方式,也有很多人叫它 AIO(Asynchronous IO)。异步 IO 操作基于事件和回调机制,可以简单理解为,应用操作直接返回,而不会阻塞在那里,当后台处理完成,操作系统会通知相应线程进行后续工作。


其中,Java 类库中的 I/O 类分成输入和输出两部分,主要是对应着实现我们与计算机操作交互时的一种规范和约束,但是对于不同的数据有着不同的实现。


综上所述,Java 领域中的 I/O 模型大致可以分为字符流 I/O 模型,字节流 I/O 模型以及网络通信 I/O 模型等 3 类。

1. 字节流 I/O 模型

字节流 I/O 模型是指在 I/O 操作,数据传输过程中,传输数据的最基本单位是字节的流,按照 8 位传输字节为单位输入/输出数据。



在 Java 领域中,对字节流的类通常以 stream 结尾,对于字节数据的操作,提供了输入流(InputStream)、输出流(OutputStream)这样式的设计,是用于读取或写入字节的基础 API,一般常用于操作类似文本或者图片文件。

2. 字符流 I/O 模型

字符流 I/O 模型是指在 I/O 操作,数据传输过程中,传输数据的最基本单位是字符的流,按照 16 位传输字符为单位输入/输出数据。



在 Java 领域中,对字符流的类通常以 reader 和 writer 结尾,对于字节数据的操作,提供了输入流(Reader)、输出流(Writer)这样式的设计,是用于读取或写入字节的基础 API,一般常用于类似从文件中读取或者写入文本信息。

3. 网络通信 I/O 模型

网络通信 I/O 模型是指 java.net 下,提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 等 IO 类库,实现网络通信同样是 IO 行为。



在 Java 领域中,NIO 提供了与传统 BIO 模型中的 Socket 和 ServerSocket 相对应的 SocketChannel 和 ServerSocketChannel 两种不同的套接字通道实现。SocketChannel 可以看作是 socket 的一个完善类,除了提供 Socket 的相关功能外,还提供了许多其他特性,如后面要讲到的向选择器注册的功能。


其中,新增的 SocketChannel 和 ServerSocketChannel 两种通道都支持阻塞和非阻塞两种模式。

三. Java 领域中的线程 I/O 参考模型

在 Java 领域中,我们对照线程概念(单线程和多线程)来说,可以分为 Java 线程-阻塞 I/O 模型和 Java 线程-非阻塞 I/O 模型两种。



由于阻塞与非阻塞主要是针对于应用程序对于系统函数调用角度来限定的,从阻塞与非阻塞的意义上来说,I/O 可以分为阻塞 I/O 和非阻塞 I/O 两种大类。其中:


  • 阻 塞 I/O : 进行 I/O 操作时,使当前线程进入阻塞状态,从具体应用程序来看,如果当一次 I/O 操作(Read/Write)没有就绪或者没有完成,则函数调用则会一直处于等待状态。

  • 非阻塞 I/O:进行 I/O 操作时,使当前线程不进入阻塞状态,从具体应用程序来看,如果当一次 I/O 操作(Read/Write)即使没有就绪或者没有完成,则函数调用立即返回结果,然后由应用程序轮询处理。


而同步与异步主要正针对应用程序对于系统函数调用后,其 I/O 操作中读/写(Read/Write)是由谁完成来限定的,I/O 可以分为同步 I/O 和异步 I/O 两种大类。其中:


  • 同步 I/O: 进行 I/O 操作时,可以使当前线程进入进入阻塞或或非阻塞状态,从具体应用程序来看,如果当一次 I/O 操作(Read/Write)都是托管给应用程序来完成。

  • 异步 I/O: 进行 I/O 操作时,可以使当前线程进入进入非阻塞状态,从具体应用程序来看,如果当一次 I/O 操作(Read/Write)都是托管给操作系统来完成,完成后回调或者事件通知应用程序。


由此可见,按照这些个定义可以知道:


  • 当程序在执行 I/O 操作时,经典的网络 I/O 操作(Read/Write)场景,主要可以分为阻塞 I/O,非阻塞 I/O,单线程以及多线程等场景。

  • 异步 I/O 一定是非阻塞 I/O,不存在是异步还阻塞的情况;同步 I/O 可能存在阻塞或或非阻塞的情况,还有可能是 I/O 线程多路复用的情况。


因此,我们可以对其线程 I/O 模型来说,I/O 可以分为同步-阻塞 I/O 和同步-非阻塞 I/O,以及异步 I/O 等 3 种,其中 I/O 多路复用属于同步-阻塞 I/O。


综上所所述,,在 Java 领域中,我们对照线程概念(单线程和多线程)来说,可以分为 Java 线程-阻塞 I/O 模型和 Java 线程-非阻塞 I/O 模型两种。接下来,我们就详细地来探讨一下。

(一). Java 线程阻塞 I/O 模型

Java 线程-阻塞 I/O 模型主要可以分为单线程阻塞 I/O 模型和多线程阻塞 I/O 模型。



从一个服务器处理客户端连接来说,单线程情况下,一般都是以一个线程负责处理所有客户端连接的 I/O 操作(Read/Write)操作。


程序在执行 I/O 操作,一般都是从内核空间复制数据,但内核空间的数据可能需要很长的时间去准备数据,由此很有可能导致用户空间产生阻塞。


其产生阻塞的过程,主要如下:


  • 应用程序发起 I/O 操作(Read/Write)之后,进入阻塞状态,然后提交给操作系统内核完成 I/O 操作。

  • 当内核没有准备数据,需要不断从网络中读取数据,一旦准备就绪,则将数据复制到用户空间供应用程序使用。

  • 应用程序从发起读取数据操作到继续执行后续处理的这段时间,便是我们说的阻塞状态。


由此可见,引入 Java 线程的概念,我们可以把 Java 线程-阻塞 I/O 模型主要可以分为单线程阻塞 I/O 模型和多线程阻塞 I/O 模型。

1. 单线程阻塞 I/O 模型

单线程阻塞 I/O 模型主要是指对于多个客户端访问时,只能同时处理一个客户端的访问,并且在 I/O 操作上是阻塞的,线程会一直处于等待状态,直到当前线程中前一个客户端访问结束后,才继续开始下一个客户端的访问。



单线程阻塞 I/O 模型是最简单的服务器模型,是 Java Developer 面对网络编程最基础的模型。


由于对于多个客户端访问时,只能同时处理一个客户端的访问,并且在 I/O 操作上是阻塞的,线程会一直处于等待状态,直到当前线程中前一个客户端访问结束后,才继续开始下一个客户端的访问。


也就意味着,客户端的访问请求需要一个一个排队等待,只提供一问一答的服务机制。


这种模型的特点,主要在于单线程和阻塞 I/O。其中:


  • 单线程 :指的是服务器端只有一个线程处理客户端的请求,客户端连接与服务器端的处理线程比例关系为 N:1,无法同时处理多个连接,只能串行方式连接处理。

  • 阻塞 I/O:服务器在 I/O 操作(Read/Write)操作时是阻塞的,主要表现在读取客户端数据时,需要等待客户端发送数据并且把操作系统内核中的数据复制到用户空间中的用户线程中,完成后才解除阻塞状态;同时,数据回写客户端要等待用户进程把数据写入到操作系统系统内核后才解除阻塞状态。


综上所述,单线程阻塞 I/O 模型最明显的特点就是服务机制简单,服务器的系统资源开销小,但是并发能力低,容错能力也低。

2. 多线程阻塞 I/O 模型

多线程阻塞 I/O 模型主要是指对于多个客户端访问时,利用多线程机制为每一个客户端的访问分配独立线程,实现同时处理,并且在 I/O 操作上是阻塞的,线程不会一直处于等待状态,而是并发处理客户端的请求访问。



多线程阻塞 I/O 模型是针对于单线程阻塞 I/O 模型的缺点,对其进行多线程化改进,使之能对于多个客户端的请求访问实现并发响应处理。


也就意味着,客户端的访问请求不需要一个一个排队等待,利用多线程机制为每一个客户端的访问分配独立线程。


这种模型的特点,主要在于多线程和阻塞 I/O。其中:


  • 多线程 :指的是服务器端至少有一个线程或者若干个线程处理客户端的请求,客户端连接与服务器端的处理线程比例关系为 M:N,并发同时处理多个连接,可以并行方式连接处理。但客户端连接与服务器处理线程的关系是一对一的。

  • 阻塞 I/O:服务器在 I/O 操作(Read/Write)操作时是阻塞的,主要表现在读取客户端数据时,需要等待客户端发送数据并且把操作系统内核中的数据复制到用户空间中的用户线程中,完成后才解除阻塞状态;同时,数据回写客户端要等待用户进程把数据写入到操作系统系统内核后才解除阻塞状态。


综上所述,多线程阻塞 I/O 模型最明显的特点就是支持多个客户端并发响应,处理能力得到极大提高,有一定的并发能力和容错能力,但是服务器资源消耗较大,且多线程之间会产生线程切换成本,结构也比较复杂。

(二). Java 线程非阻塞 I/O 模型

Java 线程-非阻塞 I/O 模型主要可以分为应用层 I/O 多路复用模型和内核层 I/O 多路复用模型,以及内核回调事件驱动 I/O 模型。



从一个服务器处理客户端连接来说,多线程情况下,一般都是至少一个线程或者若干个线程负责处理所有客户端连接的 I/O 操作(Read/Write)操作。


非阻塞 I/O 模型与阻塞 I/O 模型,相同的地方在于是程序在执行 I/O 操作,一般都是从内核空间和应用空间复制数据。


与之不同的是,非阻塞 I/O 模型不会一直等到内核空间准备好数据,而是立即返回去做其他的事,因此不会产生阻塞。其中:


  • 应用程序中的用户线程包含一个缓冲区,单个线程会不断轮询客户端,以及不断尝试进行 I/O(Read/Write)操作。

  • 一旦内核准好数据,应用程序中的用户线程就会把数据复制到用户空间使用。


由此可见,我们可以把 Java 线程-非阻塞 I/O 模型主要可以分为应用层 I/O 多路复用模型和内核层 I/O 多路复用模型,以及内核回调事件驱动 I/O 模型。

1. 应用层 I/O 多路复用模型

应用层 I/O 多路复用模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个 socket 列表中,应用程序中的用户线程会不断轮询 sockst 列表中的客户端连接请求访问,并尝试进行读写。



应用层 I/O 多路复用模型最大的特点就是,不论有多少个 socket 连接,都可以使用应用程序中的用户线程的一个线程来管理。


这个线程负责轮询 socket 列表,不断进行尝试进行 I/O(Read/Write)操作,其中:


  • I/O(Read)操作:如果成功读取数据,则对数据进行处理。反之,如果失败,则下一个循环再继续尝试。

  • I/O(Write)操作:需要先尝试把数据写入指定的 socket,直到调用成功结束。反之,如果失败,则下一个循环再继续尝试。


这种模型,虽然很好地利用了阻塞的时间,使得批处理能提升。但是由于不断轮询 sockst 列表,同时也需要处理数据的拼接。

2. 内核层 I/O 多路复用模型

内核层 I/O 多路复用模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个 socket 列表中,操作系统内核不断轮询 sockst 列表,并把遍历结果组织罗列成一系列的事件,并驱动事件返回到应用层处理,最后托管给应用程序中的用户线程按照需要处理对应的事件对象。



内核层 I/O 多路复用模型与应用层 I/O 多路复用模型,最大的不同就是,轮询 sockst 列表是操作系统内核来完成的,有助于检测效率。


操作系统内核负责轮询 socket 列表的过程,其中:


  • 首先,最主要的就是将所有连接的标记为可读事件和可写事件列表,最后传入到应用程序的用户空间处理。

  • 然后,操作系统内核复制数据到应用层的用户空间的用户线程,会随着 socket 数量的增加,也会形成不小的开销。

  • 另外,当活跃连接数比较少时,内核空间和用户空间会存在很多无效的数据副本,并且不管是否活跃,都会复制到用户空间的应用层。

3. 内核回调事件驱动 I/O 模型

内核回调事件驱动 I/O 模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个 socket 列表中,操作系统内核不断轮询 sockst 列表,利用回调函数来检测 socket 列表是否可读可写的一种事件驱动 I/O 机制。



不论是内核层的轮询 sockst 列表,还是应用层的轮询 sockst 列表,通过循环遍历的方式来检测 socket 列表是否可读可写的操作方式,其效率都比较低效。


为了寻求一种高效的机制来优化循环遍历方式,因此,提出了会回调函数事件驱动机制。其中,主要是:


  • 内核空间:当客户端往 socket 发送数据时,内核中 socket 都对应着一个回调函数,内核就可以直接从网卡中接收数据后,直接调用回调函数。

  • 应用空间:回调函数会维护一个事件列表,应用层则获取事件即可以得到感兴趣的事件,然后进行后续操作。


一般来说,内核回调事件驱动的方式主要有 2 种:


  • 第一种:利用可读列表(ReadList)和可写列表(WriteList)来标记读事件(Read-Event)/写事件(Write-Event)来进行 I/O(Read/Write)操作。

  • 第二种:利用在应用层中直接指定 socket 感兴趣的事件,通过维护事件列表(EventList)再来进行 I/O(Read/Write)操作。


综上所述,这两种方式都是有操作系统内核维护客户端中的所有连接,再通过回调函数不断更新事件列表,应用空间中的应用层的用户线程只需要根据轮询遍历事件列表即可知道是否进行 I/O(Read/Write)操作。


由此可见,这种方式极大地提高了检测效率,也增强了数据处理能力。


特别指出,在 Java 领域中,非阻塞 I/O 的实现完全是基于操作系统内核的非阻塞 I/O,Java 把操作系统中的非阻塞 I/O 的差异最大限度的屏蔽并提供了统一的 API,JDK 自己会帮助我们选择非阻塞 I/O 的实现方式。


一般来说,在 Linux 系统中,只要支持 epoll,JDK 会优先选择 epoll 来实现 Java 的非阻塞 I/O。

(三). Java 线程异步 I/O 模型

Java 线程异步 I/O 模型主要是指异步非阻塞模型(AIO 模型), 需要操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作。



对于非阻塞 I/O 模型(NIO)来说,异步 I/O 模型的工作机制来说,与之不同的是采用“订阅(Subscribe)-通知(Notification)”模式,主要如下:



  • 订阅(Subscribe): 用户线程通过操作系统调用,向内核注册某个 IO 操作后,即应用程序向操作系统注册 IO 监听,然后继续做自己的事情。

  • 通知(Notification):当操作系统发生 IO 事件,并且准备好数据后,即内核在整个 IO 操作(包括数据准备、数据复制)完成后,再主动通知应用程序,触发相应的函数,执行后续的业务操作。


在异步 IO 模型中,整个内核的数据处理过程中,包括内核将数据从网络物理设备(网卡)读取到内核缓存区、将内核缓冲区的数据复制到用户缓冲区,用户程序都不需要阻塞。


由此可见,异步 I/O 模型(AIO 模型)需要依赖操作系统的支持,CPU 资源开销比较大,最大的特性是异步能力,对 socket 和 I/O 起作用,适合连接数目比较多以及连接时间长的系统架构。


一般来说,在操作系统里,异步 IO 是指 Windows 系统的 IOCP(Input/Output Completion Port),或者 C++的网络库 asio。


在 Linux 系统中,aio 虽然是异步 IO 模型的具体实现,但是由于不成熟,现在大部分还是依据是否支持 epoll 等,来模拟和封装 epoll 实现的。


在 Java 领域中,支持异步 I/O 模型(AIO 模型)是 Jdk 1.7 版本开始的,基于 CompletionHandler 接口来实现操作完成回调,其中分别有三个新的异步通道,AsynchronousFileChannel,AsynchronousSocketChannel 和 AsynchronousServerSocketChannel。


但是,对于支持异步编程模式是在 Jdk 1.5 版本就已经存在,最典型的就是基于 Future 模型实现的 Executor 和 FutureTask。


由于 Future 模型存在一定的局限性,在 JDK 1.8 之后,对 Future 的扩展和增强实现又新增了一个 CompletableFuture。


由此可见,在 Java 领域中,对于异步 I/O 模型提供了异步文件通道(AsynchronousFileChannel)和异步套接字通道(AsynchronousSocketChannel 和 AsynchronousServerSocketChannel)的实现。 其中:


  • 首先,对于异步文件通道的实现,提供两种方式获取操作结果:

  • 通过 java.util.concurrent.Future 类來表示异步操作的结果:

  • 在执行异步操作的时候传入一个 java.nio.channels.CompletionHandler 接口的实现类作为操作完成的回调。

  • 其次,对于异步套接字通道的是实现:

  • 异步 Socket Channel 是被动执行对象,不需要像 NIO 编程那样创建一个独立的 I/O 线程来处理读写操作。

  • 对于 AsynchronousServerSocketChannel 和 AsynchronousSocketChannel 都由 JDK 底层的线程池负责回调并驱动读写操作。

  • 异步套接字通道是真正的异步非阻塞 I/O,它对应 UNIX 网络编程中的事件驱动 I/O (AIO),它不需要通过多路复用器(Selector)对注册的通道进行轮询操作即可实现异步读写, 从而简化了 NIO 的编程模型。


综上所述,对于在 Java 领域中的异步 IO 模型,我们在使用的时候,需要依据实际业务场景需要而进行选择和考量。


⚠️[特别注意]:


[1].IOCP: 输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步 I/O 操作的应用程序编程接口。<br>[2].epoll: Linux 系统中 I/O 多路复用实现方式的一种,主要是(select,poll,epoll)。都是同步 I/O,同时也是阻塞 I/O。<br>[3].Future: 属于 Java JDK 1.5 版本支持的编程异步模型,在包 java.util.concurrent.下面。<br>[4].CompletionHandler: 属于 Java JDK 1.7 版本支持的编程异步 I/O 模型,在包 java.nio.channels.下面。<br>[5].CompletableFuture: 属于 Java JDK 1.8 版本对 Future 的扩展和增强实现编程异步 I/O 模型,在 java.util.concurrent.下面。<br>

四. Java 领域中的线程设计模型

Java 领域中的线程设计模型最典型就是基于 Reactor 模式设计的非阻塞 I/O 模型和 基于 Proactor 模式设计的异步 I/O 模型和基于 Promise 模式的 Promise 模型。



在 Java 领域中,对于并发编程的支持,不仅提供了线程机制,也引入了多线程机制,还有许多同步和异步的实现。


单从设计原则和实现来说,都采用了许多设计模式,其中多线程机制最常见的就是线程池模式。


对于非阻塞 I/O 模型,主要采用基于 Reactor 模式设计,而异步 I/O 模型,主要采用基于 Proactor 模式设计。


当然,还有基于 Promise 模式的异步编程模型,不过这算是一个特例。


综上所述,Java 领域中的线程设计模型最典型就是基于 Reactor 模式设计的非阻塞 I/O 模型和 基于 Proactor 模式设计的异步 I/O 模型和基于 Promise 模式的 Promise 模型。

1. 多线程非阻塞 I/O 模型

多线程非阻塞 I/O 模型是针对于多线程机制而设计的,根据 CPU 的数量来创建线程数,并且能够让多个线程并行执行的非阻塞 I/O 模型。


现在的计算机大多数都是多核 CPU 的,而且操作系统都提供了多线程机制,但是我们也没有办法抹掉单线程的优势。


单线程最大的优势就是一个 CPU 只负责一个线程,对于多线程中出现的疑难杂症,它都可以避免,而且编码简单。


在一个线程对应一个 CPU 的情况下,如果多核计算机中 只执行一个线程,那么就只有一个 CPU 工作,无法充分发挥 CPU 和优势,且资源也无法充分利用。


因此,我们的程序则可以根据 CPU 的数量来创建线程数,N 个 CPU 对应多个 N 个线程,便可以充分利用多个 CPU。同时也保持了单线程的特点,相当于多个线程并行执行而不是并发执行。


在多核计算机时代,多线程和非阻塞都是提升服务器处理性能的利器。一般我们都是将客户端连接按照分组分配给至少一个线程或者若干线程,每个线程负责处理对应组的连接。


在 Java 领域中,最常见的多线程阻塞 I/O 模型就是基于 Reactor 模式的 Reactor 模型。


2. 基于 Reactor 模式的 Reactor 模型

Reactor 模型是指在事件驱动的思想上,基于 Reactor 的工作模式而设计的非阻塞 I/O 模型(NIO 模型)。一定程度上来说,可以说是主动模式 I/O 模型。



对于 Reactor 模式,我特意在网上查询了一下资料,查询的结果都是无疾而终,解释更是五花八门的。最后,参考一些资料整理得出结论。


引用一下 Doug Lea 大师在文章“Scalable IO in Java”中对 Reactor 模式的定义:


Reactor 模式由 Reactor 线程、Handlers 处理器两大角色组成,两大角色的职责分别如下:

  • Reactor 线程的职责:负责响应 IO 事件,并且分发到 Handlers 处理器。<br>

  • Handlers 处理器的职责:非阻塞的执行业务处理逻辑。


个人理解,Reactor 模式是指在事件驱动的思想上,通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。其中,基本思想有两个:


  • 基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理

  • 基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。


总体来说,Reactor 模式有点类似事件驱动模式。在事件驱动模式中,当有事件触发时,事件源会将事件分发到 Handler(处理器),由 Handler 负责事件处理。Reactor 模式中的反应器角色类似于事件驱动模式中的事件分发器(Dispatcher)角色。


具体来说,在 Reactor 模式中有 Reactor 和 Handler 两个重要的组件:


  • Reactor:负责查询 IO 事件,当检测到一个 IO 事件时将其发送给相应的 Handler 处理器去处理。其中,IO 事件就是 NIO 中选择器查询出来的通道 IO 事件。

  • Handler:与 IO 事件(或者选择键)绑定,负责 IO 事件的处理,完成真正的连接建立、通道的读取、处理业务逻辑、负责将结果写到通道等。


从 Reactor 的代码实现上来看,实现 Reactor 模式需要实现以下几个类:


  • EventHandler:事件处理器,可以根据事件的不同状态创建处理不同状态的处理器。

  • Handler:可以理解为事件,在网络编程中就是一个 Socket,在数据库操作中就是一个 DBConnection。

  • InitiationDispatcher:用于管理 EventHandler,分发 event 的容器,也是一个事件处理调度器,Tomcat 的 Dispatcher 就是一个很好的实现,用于接收到网络请求后进行第一步的任务分发,分发给相应的处理器去异步处理,来保证吞吐量。

  • Demultiplexer:阻塞等待一系列的 Handle 中的事件到来,如果阻塞等待返回,即表示在返回的 Handler 中可以不阻塞的执行返回的事件类型。这个模块一般使用操作系统的 select 来实现。在 Java NIO 中用 Selector 来封装,当 Selector.select()返回时,可以调用 Selector 的 selectedKeys()方法获取 Set,一个 SelectionKey 表达一个有事件发生的 Channel 以及该 Channel 上的事件类型。


接下来,我们便从具体的常见来一一探讨一下 Reactor 模式下的各种线程模型。


从一定意义上来说, 基于 Reactor 模式的 Reactor 模型是非阻塞 I/O 模型。

2.0. 单 Reactor 单线程模型

单 Reactor 单线程模型主要是指将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件 Handler 分发给不同的处理器去处理。简单来说,Reactor 和 Handle 都放入一个线程中执行。



在实际工作中,若干个客户端连接访问服务端,假如会有接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等,其中,:


  • Reactor 模型则把这些事件都分发到各自的处理器。

  • 整个过程,只要有等待处理的事件存在,Reactor 线程模型不断往后续执行,而且不会阻塞,所以效率很高。


由此可见,单 Reactor 单线程模型具有简单,没有多线程,没有进程通信。但是从性能上来说,无法发挥多核的极致,一个 Handler 卡死,导致当前进程无法使用,IO 和 CPU 不匹配。


在 Java 领域中,对于一个单 Reactor 单线程模型的实现,主要需用到 SelectionKey(选择键)的几个重要的成员方法:


  • void attach(Object o):将对象附加到选择键。可以将任何 Java POJO 对象作为附件添加到 SelectionKey 实例。

  • Object attachment():从选择键获取附加对象。与 attach(Object o)是配套使用的,其作用是取出之前通过 attach(Object o)方法添加到 SelectionKey 实例的附加对象。这个方法同样非常重要,当 IO 事件发生时,选择键将被 select 方法查询出来,可以直接将选择键的附件对象取出。


因此,在 Reactor 模式实现中,通过 attachment()方法所取出的是之前通过 attach(Object o)方法绑定的 Handler 实例,然后通过该 Handler 实例完成相应的传输处理。


综上所述,在 Reactor 模式中,需要将 attach 和 attachment 结合使用:


  • 在选择键注册完成之后调用 attach()方法,将 Handler 实例绑定到选择键。

  • 当 IO 事件发生时调用 attachment()方法,可以从选择键取出 Handler 实例,将事件分发到 Handler 处理器中完成业务处理。


从一定意义上来说,单 Reactor 单线程模型是基于单线程的 Reactor 模式。

2.1. 单 Reactor 多线程模型

单 Reactor 多线程模型是指采用多线程机制,将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件 Handler 分发给不同的处理器去处理。



单 Reactor 多线程模型是基于单线程的 Reactor 模式的结构,将其利用线程池机制改进多线程模式。


相当于,Reactor 对于接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等分发到各自的处理器时:


  • 首先,对于耗时的任务引入线程池机制,事件处理器自己不执行任务,而是交给线程池来托管,避免了耗时的操作。

  • 其次,虽然 Reactor 只有一个线程,但是也保证了 Reactor 的高效。


在 Java 领域中,对于一个单 Reactor 多线程模型的实现,主要可以从升级 Handler 和升级 Reactor 来改进:


  • 升级 Handler:既要使用多线程,又要尽可能高效率,则可以考虑使用线程池。

  • 升级 Reactor:可以考虑引入多个 Selector(选择器),提升选择大量通道的能力。


总体来说,多线程版本的 Reactor 模式大致如下:


  • 将负责数据传输处理的 IOHandler 处理器的执行放入独立的线程池中。这样,业务处理线程与负责新连接监听的反应器线程就能相互隔离,避免服务器的连接监听受到阻塞。

  • 如果服务器为多核的 CPU,可以将反应器线程拆分为多个子反应器(SubReactor)线程;同时,引入多个选择器,并且为每一个 SubReactor 引入一个线程,一个线程负责一个选择器的事件轮询。这样充分释放了系统资源的能力,也大大提升了反应器管理大量连接或者监听大量传输通道的能力。


由此可见,单 Reactor 单线程模型具有充分利用的 CPU 的特点,但是进程通信,复杂,Reactor 承放了太多业务,高并发下可能成为性能瓶颈。


从一定意义上来说,单 Reactor 多线程模型是基于多线程的 Reactor 模式。

2.2. 主从 Reactor 多线程模型

主从 Reactor 多线程模型采用多个 Reactor 的机制,将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件 Handler 分发给不同的处理器去处理。每一个 Reactor 对应着一个线程。



采用多个 Reactor 实例的机制:-主 Reactor:负责建立连接,建立连接后的句柄丢给从 Reactor。-从 Reactor: 负责监听所有事件进行处理。


相当于,Reactor 对于接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等分发到各自的处理器时:


  • 由于接收事件是针对于服务器端而言的,连接接收的工作统一由连接处理器完成,则连接处理器把接收到的客户端连接均匀分配到所有的实例中去。

  • 每一个 Reactor 实例负责处理分配到该 Reactor 实例的客户端连接,完成连接时的读写操作和其他逻辑操作。


由此可见,主从 Reactor 多线程模型中 Reactor 实例职责分工明确,具有一定分摊压力的效能,我们常见 Nginx/Netty/Memcached 等就是采用这中模型。


从一定意义上来说,主从 Reactor 多线程模型是基于多实例的 Reactor 模式。

2. 基于 Proactor 模式的 Proactor 模型

Proactor 模型是指在事件驱动的思想上,基于 Proactor 的工作模式而设计的异步 I/O 模型(AIO 模型),一定程度上来说,可以说是被动模式 I/O 模型。



无论是 Reactor,还是 Proactor,都是一种基于事件分发的网络编程模式,区别在于 Reactor 模式是基于「待完成」的 I/O 事件,而 Proactor 模式则是基于「已完成」的 I/O 事件。


相对于 Reactor 来说,Proactor 模型处理读取操作的主要流程:


  • 应用程序初始化一个异步读取操作,然后注册相应的事件处理器,此时事件处理器不关注读取就绪事件,而是关注读取完成事件。

  • 事件分离器等待读取操作完成事件。

  • 在事件分离器等待读取操作完成的时候,操作系统调用内核线程完成读取操作,并将读取的内容放入用户传递过来的缓存区中。

  • 事件分离器捕获到读取完成事件后,激活应用程序注册的事件处理器,事件处理器直接从缓存区读取数据,而不需要进行实际的读取操作。


由此可见,Proactor 中写入操作和读取操作基本一致,只不过监听的事件是写入完成事件而已。


在 Java 领域中,异步 IO(AIO)是在 Java JDK 7 之后引入的,都是操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作。


其中,从对于 Proactor 模式的设计来看,Proactor 模式的工作流程:



  • Proactor Initiator: 负责创建 Proactor 和 Handler 对象,并将 Proactor 和 Handler 都通过 Asynchronous Operation Processor 注册到内核。

  • Asynchronous Operation Processor :负责处理注册请求,并处理 I/O 操作。

  • Asynchronous Operation Processor :完成 I/O 操作后通知 Proactor。

  • Proactor :根据不同的事件类型回调不同的 Handler 进行业务处理。

  • Handler: 完成业务处理,其中是通过 CompletionHandler 表示完成后处理器。


从一定意义上来说, 基于 Proactor 模式的 Proactor 模型是异步 IO。

3. 基于 Promise 模式的 Promise 模型

Promise 模型是基于 Promise 异步编程模式,客户端代码调用某个异步方法所得到的返回值仅是一个凭据对象,凭借该对象,客户端代码可以获取异步方法相应的真正任务的执行结果的一种模型。



Promise 模式是开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。


从 Promise 模式的工作机制来看,主要如下:


  • 当我们开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。

  • 等到我们需要该任务的执行结果时,再调用凭据对象的相关方法来获取。


由此可以确定的是,Promise 模式既发挥了异步编程的优势——增加系统的并发性,减少不必要的等待,又保持了同步编程的简单性。


从 Promise 模式技术实现来说,主要职责角色如下:


  • Promisor:负责对外暴露可以返回 Promise 对象的异步方法,并启动异步任务的执行,主要利用 compute 方法启动异步任务的执行,并返回用于获取异步任务执行结果的凭据对象。

  • Promise :负责包装异步任务处理结果的凭据对象。负责检测异步任务是否处理完毕、返回和存储异步任务处理结果。

  • Result :负责表示异步任务处理结果。具体类型由应用决定。

  • TaskExecutor:负责真正执行异步任务所代表的计算,并将其计算结果设置到相应的 Promise 实例对象。


在 Java 领域中,最典型的就是基于 Future 模型实现的 Executor 和 FutureTask。


由于 Future 模型存在一定的局限性,在 JDK 1.8 之后,对 Future 的扩展和增强实现又新增了一个 CompletableFuture。


当然,Promise 模式在前端技术 JavaScript 中 Promise 有具体的体现,而且随着前端技术的发展日趋成熟,对于这种模式的运用早已日臻化境。

写在最后


在 Java 领域中,Java 领域中的线程主要分为 Java 层线程(Java Thread) ,JVM 层线程(JVM Thread),操作系统层线程(Kernel Thread)。


从 Java 线程映射类型来看,主要有线程一对一(1:1)映射,线程多对多(M:1)映射,线程多对多(M:N)映射等关系。


因此,Java 领域中的线程映射模型主要有内核级线程模型(Kernel-Level Thread ,KLT)、应用级线程模型(Application-Level Thread ,ALT)、混合两级线程模型(Mixture-Level Thread ,MLT)等 3 种模型。


在 Java 领域中,我们对照线程概念(单线程和多线程)来说,可以分为 Java 线程-阻塞 I/O 模型和 Java 线程-非阻塞 I/O 模型两种。其中,


  • Java 线程-阻塞 I/O 模型: 主要可以分为单线程阻塞 I/O 模型和多线程阻塞 I/O 模型。

  • Java 线程-非阻塞 I/O 模型:主要可以分为应用层 I/O 多路复用模型和内核层 I/O 多路复用模型,以及内核回调事件驱动 I/O 模型。


特别指出,在 Java 领域中,非阻塞 I/O 的实现完全是基于操作系统内核的非阻塞 I/O,JDK 会依据操作系统内核支持的非阻塞 I/O 方式来帮助我们选择实现方式。


综上所述,在 Java 领域中,并发编程中的线程机制以及多线程的控制,在实际开发过程中,需要依据实际业务场景来考虑和衡量,这需要我们对其有更深的研究,才可以得心应手。


在讨论编程模型的时候,我们提到了像基于 Promise 模式和基于 Thread Pool 模式的这样的设计模式的概念,这也是一个我们比较容易忽略的概念,如果有兴趣的话,可以自行进行查询相关资料进行了解。


最后,祝福大家在 Java 并发编程的“看山是山,看山不是山,看山还是山”的修行之路上,“拨开云雾见天日,守得云开见月明”,早日达到有迹可循到有迹可寻的目标!

发布于: 刚刚阅读数: 5
用户头像

PivotalCloud

关注

⌨️ 90后程序员,后端码客一枚 2019.03.15 加入

阡城有子,幻影成年;北方八度,今出岭南。珠海之边,浮生流年!

评论

发布
暂无评论
如何正确理解Java领域中的多线程模型,主要用来解决什么问题?_PivotalCloud_InfoQ写作社区