苍穹之边,浩瀚之挚,眰恦之美; 悟心悟性,善始善终,惟善惟道! —— 朝槿《朝槿兮年说》
我国宋代禅宗大师青原行思在《三重境界》中有这样一句话:“ 参禅之初,看山是山,看水是水;禅有悟时,看山不是山,看水不是水;禅中彻悟,看山仍然山,看水仍然是水。”
作为一名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。
[2]. S:指的是处理器的个数(总核心数)。一般用作表示升级加速比,可改进部件原先运行速度与改进后的部件速度的比值,取值范围是S ≥ 1。
[3]. Slatency(s):指的是程序在S个处理器相对在单个处理器(单核)中速度提升比率。一般用作表示整个任务的提速比。
根据这个公式,我们可以依据可确定程序中可并行代码的比例,来决定我们实际工作中增加处理器(总核心数)所能带来的速度提升的上限。
无论是C++开发者在Linux系统中使用的pthread,还是Java开发者使用的java.util.concurrent(JUC)库,这些线程机制的都需要一定的线程I/O模型来做理论支撑。
所以,接下来,我们就让我们一起探讨和揭开Java领域中的线程I/O模型的神秘面纱,针对那些盘根错落的枝末细节,才能让我们更好地了解和正确认识ava领域中的线程机制。
本文用到的一些关键词语以及常用术语,主要如下:
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 领域中的线程映射模型主要有内核级线程模型(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 领域中的线程映射理论模型。
应用级线程模型主要是指(Application-Level Thread ,ALT),就是多个用户线程映射到同一个内核线程上,用户线程的创建、调度、同步的所有操作全部都是由用户空间的线程来完成的。
在Java领域中,应用级线程主要是指Java语言编写应用程序的Java 线程(Java Thread)和JVM虚拟机中JVM线程(JVM Thread)。
在应用级线程模型下,完全建立在用户空间的线程库上,不依赖于系统内核,用户线程的创建、同步、切换和销毁等操作完全在用户态执行,不需要切换到内核态。
其中,用户进程使用系统内核提供的接口——轻量级进程(Light Weight Process,LWP)来使用系统内核线程。
在此种线程模型下,由于一个用户线程对应一个LWP,因此某个LWP在调用过程中阻塞了不会影响整个进程的执行。
但是各种线程的操作都需要在用户态和内核态之间频繁切换,消耗太大,速度相对用户线程模型来说要慢。
内核级线程模型主要是指(Kernel-Level Thread ,KLT),用户线程与内核线程建立了一对一的关系,即一个用户线程对应一个内核线程,内核负责每个线程的调度。
在Linux中,对于内核级线程,操作系统会为其创建一套栈:用户栈+内核栈,其中用户栈工作在用户态,内核栈工作在内核态,在发生系统调用时,线程的执行会从用户栈切换到内核栈。
在内核级线程模型下,完全依赖操作系统内核提供的内核线程来实现多线程。线程的切换调度由系统内核完成,系统内核负责将多个线程执行的任务映射到各个CPU中去执行。
其中,glibc中的pthread_create方法主要是创建一个OS内核级线程,我们不深入细节,主要是为该线程分配了栈资源;需要注意的是这个栈资源对于JVM而言是堆外内存,因此堆外内存的大小会影响JVM可以创建的线程数。
在JVM概念中,JVM栈用来执行Java方法,而本地方法栈用来执行native方法;但需要注意的是JVM只是在概念上区分了这两种栈,而并没有规定如何实现。
在HotSpot中,则是将JVM栈与本地方法栈二合一,使用核心线程的用户栈来实现(因为JVM栈和本地方法栈都是属于用户态的栈),即Java方法与native方法都在同一个用户栈中调用,而当发生系统调用时,再切换到核心栈运行。
这种设计的好处是线程的各种操作以及切换消耗很低;
但是线程的所有操作都需要在用户态实现,线程的调度实现起来异常复杂,并且系统内核对ULT无感知,如果线程阻塞则会引起整个进程的阻塞。
混合两级线程模型主要是指(Mixture-Level Thread ,MLT),是应用级线程模型和内核级线程模型等两种模型的混合版本,用户线程仍然是在用户态中创建,用户线程的创建、切换和销毁的消耗很低,用户线程的数量不受限制。
对于混合两级线程模型,是应用级线程模型和内核级线程模型等两种模型的混合版本,主要是充分吸收前面两种线程模型的优点且尽量规避它们的缺点。
在此模型下用户线程与内核线程是多对多(M : N,通常M >= N)的映射模型。主要是维护一个轻量级进程(Light Weight Process,LWP),在用户线程和内核线程之间充当桥梁,就可以使用操作系统提供的线程调度和处理器映射功能。
一般来说,Java虚拟机使用的线程模型是基于操作系统提供的原生线程模型来实现的,Windows系统和Linux系统都是使用的内核线程模型,而Solaris系统支持混合线程模型和内核线程模型两种实现。
还有,Java线程内存模型中,可以将虚拟机内存划分为两部分内存:主内存和线程工作内存,主内存是多个线程共享的内存,线程工作内存是每个线程独享的内存。方法区和堆内存就是主内存区域,而虚拟机栈、本地方法栈以及程序计数器则属于每个线程独享的工作内存。
Java内存模型规定所有成员变量都需要存储在主内存中,线程会在其工作内存中保存需要使用的成员变量的拷贝,线程对成员变量的操作(读取和赋值等)都是对其工作内存中的拷贝进行操作。各个线程之间不能互相访问工作内存,线程间变量的传递需要通过主内存来完成。
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类。
字节流I/O模型是指在I/O操作,数据传输过程中,传输数据的最基本单位是字节的流,按照8位传输字节为单位输入/输出数据。
在Java 领域中,对字节流的类通常以stream结尾,对于字节数据的操作,提供了输入流(InputStream)、输出流(OutputStream)这样式的设计,是用于读取或写入字节的基础API,一般常用于操作类似文本或者图片文件。
字符流I/O模型是指在I/O操作,数据传输过程中,传输数据的最基本单位是字符的流,按照16位传输字符为单位输入/输出数据。
在Java 领域中,对字符流的类通常以reader和writer结尾,对于字节数据的操作,提供了输入流(Reader)、输出流(Writer)这样式的设计,是用于读取或写入字节的基础API,一般常用于类似从文件中读取或者写入文本信息。
网络通信I/O模型是指java.net 下,提供的部分网络 API,比如 Socket、ServerSocket、HttpURLConnection 等IO 类库,实现网络通信同样是 IO 行为。
在Java领域中,NIO提供了与传统BIO模型中的Socket和ServerSocket相对应的SocketChannel和ServerSocketChannel两种不同的套接字通道实现。SocketChannel可以看作是 socket 的一个完善类,除了提供 Socket 的相关功能外,还提供了许多其他特性,如后面要讲到的向选择器注册的功能。
其中,新增的SocketChannel和ServerSocketChannel两种通道都支持阻塞和非阻塞两种模式。
在Java领域中,我们对照线程概念(单线程和多线程)来说,可以分为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可以分为同步-阻塞I/O和同步-非阻塞I/O,以及异步I/O等3种,其中I/O多路复用属于同步-阻塞I/O。
综上所所述,,在Java领域中,我们对照线程概念(单线程和多线程)来说,可以分为Java 线程-阻塞I/O模型和Java 线程-非阻塞I/O模型两种。接下来,我们就详细地来探讨一下。
Java 线程-阻塞I/O模型主要可以分为单线程阻塞I/O模型和多线程阻塞I/O模型。
从一个服务器处理客户端连接来说,单线程情况下,一般都是以一个线程负责处理所有客户端连接的I/O操作(Read/Write)操作。
程序在执行I/O操作,一般都是从内核空间复制数据,但内核空间的数据可能需要很长的时间去准备数据,由此很有可能导致用户空间产生阻塞。
其产生阻塞的过程,主要如下:
由此可见,引入Java线程的概念,我们可以把Java 线程-阻塞I/O模型主要可以分为单线程阻塞I/O模型和多线程阻塞I/O模型。
单线程阻塞I/O模型主要是指对于多个客户端访问时,只能同时处理一个客户端的访问,并且在I/O操作上是阻塞的,线程会一直处于等待状态,直到当前线程中前一个客户端访问结束后,才继续开始下一个客户端的访问。
单线程阻塞I/O模型是最简单的服务器模型,是Java Developer面对网络编程最基础的模型。
由于对于多个客户端访问时,只能同时处理一个客户端的访问,并且在I/O操作上是阻塞的,线程会一直处于等待状态,直到当前线程中前一个客户端访问结束后,才继续开始下一个客户端的访问。
也就意味着,客户端的访问请求需要一个一个排队等待,只提供一问一答的服务机制。
这种模型的特点,主要在于单线程和阻塞I/O。其中:
综上所述,单线程阻塞I/O模型最明显的特点就是服务机制简单,服务器的系统资源开销小,但是并发能力低,容错能力也低。
多线程阻塞I/O模型主要是指对于多个客户端访问时,利用多线程机制为每一个客户端的访问分配独立线程,实现同时处理,并且在I/O操作上是阻塞的,线程不会一直处于等待状态,而是并发处理客户端的请求访问。
多线程阻塞I/O模型是针对于单线程阻塞I/O模型的缺点,对其进行多线程化改进,使之能对于多个客户端的请求访问实现并发响应处理。
也就意味着,客户端的访问请求不需要一个一个排队等待,利用多线程机制为每一个客户端的访问分配独立线程。
这种模型的特点,主要在于多线程和阻塞I/O。其中:
综上所述,多线程阻塞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模型。
应用层I/O多路复用模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个socket列表中,应用程序中的用户线程会不断轮询sockst列表中的客户端连接请求访问,并尝试进行读写。
应用层I/O多路复用模型最大的特点就是,不论有多少个socket连接,都可以使用应用程序中的用户线程的一个线程来管理。
这个线程负责轮询socket列表,不断进行尝试进行I/O(Read/Write)操作,其中:
这种模型,虽然很好地利用了阻塞的时间,使得批处理能提升。但是由于不断轮询sockst列表,同时也需要处理数据的拼接。
内核层I/O多路复用模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个socket列表中,操作系统内核不断轮询sockst列表,并把遍历结果组织罗列成一系列的事件,并驱动事件返回到应用层处理,最后托管给应用程序中的用户线程按照需要处理对应的事件对象。
内核层I/O多路复用模型与应用层I/O多路复用模型,最大的不同就是,轮询sockst列表是操作系统内核来完成的,有助于检测效率。
操作系统内核负责轮询socket列表的过程,其中:
内核回调事件驱动I/O模型主要是指当多个客户端向服务器发出请求时,服务器会将每一个客户端连接维护到一个socket列表中,操作系统内核不断轮询sockst列表,利用回调函数来检测socket列表是否可读可写的一种事件驱动I/O机制。
不论是内核层的轮询sockst列表,还是应用层的轮询sockst列表,通过循环遍历的方式来检测socket列表是否可读可写的操作方式,其效率都比较低效。
为了寻求一种高效的机制来优化循环遍历方式,因此,提出了会回调函数事件驱动机制。其中,主要是:
一般来说,内核回调事件驱动的方式主要有2种:
综上所述,这两种方式都是有操作系统内核维护客户端中的所有连接,再通过回调函数不断更新事件列表,应用空间中的应用层的用户线程只需要根据轮询遍历事件列表即可知道是否进行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模型主要是指异步非阻塞模型(AIO模型), 需要操作系统负责将数据读写到应用传递进来的缓冲区供应用程序操作。
对于非阻塞I/O模型(NIO)来说,异步I/O模型的工作机制来说,与之不同的是采用“订阅(Subscribe)-通知(Notification)”模式,主要如下:
在异步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领域中的异步IO模型,我们在使用的时候,需要依据实际业务场景需要而进行选择和考量。
⚠️[特别注意]:
[1].IOCP: 输入输出完成端口(Input/Output Completion Port,IOCP), 是支持多个同时发生的异步I/O操作的应用程序编程接口。
[2].epoll: Linux系统中I/O多路复用实现方式的一种,主要是(select,poll,epoll)。都是同步I/O,同时也是阻塞I/O。
[3].Future: 属于Java JDK 1.5 版本支持的编程异步模型,在包java.util.concurrent.下面。
[4].CompletionHandler: 属于Java JDK 1.7 版本支持的编程异步I/O模型,在包java.nio.channels.下面。
[5].CompletableFuture: 属于Java JDK 1.8 版本对Future的扩展和增强实现编程异步I/O模型,在java.util.concurrent.下面。
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模型。
多线程非阻塞I/O模型是针对于多线程机制而设计的,根据CPU的数量来创建线程数,并且能够让多个线程并行执行的非阻塞I/O模型。
现在的计算机大多数都是多核CPU的,而且操作系统都提供了多线程机制,但是我们也没有办法抹掉单线程的优势。
单线程最大的优势就是一个CPU只负责一个线程,对于多线程中出现的疑难杂症,它都可以避免,而且编码简单。
在一个线程对应一个CPU的情况下,如果多核计算机中 只执行一个线程,那么就只有一个CPU工作,无法充分发挥CPU和优势,且资源也无法充分利用。
因此,我们的程序则可以根据CPU的数量来创建线程数,N个CPU对应多个N个线程,便可以充分利用多个CPU。同时也保持了单线程的特点,相当于多个线程并行执行而不是并发执行。
在多核计算机时代,多线程和非阻塞都是提升服务器处理性能的利器。一般我们都是将客户端连接按照分组分配给至少一个线程或者若干线程,每个线程负责处理对应组的连接。
在Java领域中,最常见的多线程阻塞I/O模型就是基于Reactor模式的Reactor模型。
Reactor模型是指在事件驱动的思想上,基于Reactor的工作模式而设计的非阻塞I/O模型(NIO 模型)。一定程度上来说,可以说是主动模式I/O模型。
对于Reactor模式,我特意在网上查询了一下资料,查询的结果都是无疾而终,解释更是五花八门的。最后,参考一些资料整理得出结论。
引用一下Doug Lea大师在文章“Scalable IO in Java”中对Reactor模式的定义:
Reactor模式由Reactor线程、Handlers处理器两大角色组成,两大角色的职责分别如下:
- Reactor线程的职责:负责响应IO事件,并且分发到Handlers处理器。
- Handlers处理器的职责:非阻塞的执行业务处理逻辑。
个人理解,Reactor模式是指在事件驱动的思想上,通过一个或多个输入同时传递给服务处理器的服务请求的事件驱动处理模式。其中,基本思想有两个:
基于 I/O 复用模型:多个连接共用一个阻塞对象,应用程序只需要在一个阻塞对象等待,无需阻塞等待所有连接。当某个连接有新的数据可以处理时,操作系统通知应用程序,线程从阻塞状态返回,开始进行业务处理
基于线程池复用线程资源:不必再为每个连接创建线程,将连接完成后的业务处理任务分配给线程进行处理,一个线程可以处理多个连接的业务。
总体来说,Reactor模式有点类似事件驱动模式。在事件驱动模式中,当有事件触发时,事件源会将事件分发到Handler(处理器),由Handler负责事件处理。Reactor模式中的反应器角色类似于事件驱动模式中的事件分发器(Dispatcher)角色。
具体来说,在Reactor模式中有Reactor和Handler两个重要的组件:
从Reactor的代码实现上来看,实现Reactor模式需要实现以下几个类:
接下来,我们便从具体的常见来一一探讨一下Reactor模式下的各种线程模型。
从一定意义上来说, 基于Reactor模式的Reactor模型是非阻塞I/O模型。
单Reactor单线程模型主要是指将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件Handler分发给不同的处理器去处理。简单来说,Reactor和Handle都放入一个线程中执行。
在实际工作中,若干个客户端连接访问服务端,假如会有接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等,其中,:
由此可见,单Reactor单线程模型具有简单,没有多线程,没有进程通信。但是从性能上来说,无法发挥多核的极致,一个Handler卡死,导致当前进程无法使用,IO和CPU不匹配。
在Java领域中,对于一个单Reactor单线程模型的实现,主要需用到SelectionKey(选择键)的几个重要的成员方法:
因此,在Reactor模式实现中,通过attachment()方法所取出的是之前通过attach(Object o)方法绑定的Handler实例,然后通过该Handler实例完成相应的传输处理。
综上所述,在Reactor模式中,需要将attach和attachment结合使用:
从一定意义上来说,单Reactor单线程模型是基于单线程的Reactor模式。
单Reactor多线程模型是指采用多线程机制,将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件Handler分发给不同的处理器去处理。
单Reactor多线程模型是基于单线程的Reactor模式的结构,将其利用线程池机制改进多线程模式。
相当于,Reactor对于接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等分发到各自的处理器时:
在Java领域中,对于一个单Reactor多线程模型的实现,主要可以从升级Handler和升级Reactor来改进:
总体来说,多线程版本的Reactor模式大致如下:
由此可见,单Reactor单线程模型具有充分利用的CPU的特点,但是进程通信,复杂,Reactor承放了太多业务,高并发下可能成为性能瓶颈。
从一定意义上来说,单Reactor多线程模型是基于多线程的Reactor模式。
主从Reactor多线程模型采用多个Reactor 的机制,将服务端的整个处理事件分为若干个事件,Reactor 通过事件检测机制把若干个事件Handler分发给不同的处理器去处理。每一个Reactor对应着一个线程。
采用多个Reactor实例的机制:
-主Reactor:负责建立连接,建立连接后的句柄丢给从Reactor。
-从Reactor: 负责监听所有事件进行处理。
相当于,Reactor对于接收事件(Accept Event),读事件(Read Event),写事件(Write Event),以及执行事件(Process Event)等分发到各自的处理器时:
由此可见,主从Reactor多线程模型中Reactor实例职责分工明确,具有一定分摊压力的效能,我们常见Nginx/Netty/Memcached等就是采用这中模型。
从一定意义上来说,主从Reactor多线程模型是基于多实例的Reactor模式。
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模式的Proactor模型是异步IO。
Promise模型是基于Promise异步编程模式,客户端代码调用某个异步方法所得到的返回值仅是一个凭据对象,凭借该对象,客户端代码可以获取异步方法相应的真正任务的执行结果的一种模型。
Promise 模式是开始一个任务的执行,并得到一个用于获取该任务执行结果的凭据对象,而不必等待该任务执行完毕就可以继续执行其他操作。
从Promise 模式的工作机制来看,主要如下:
由此可以确定的是,Promise 模式既发挥了异步编程的优势——增加系统的并发性,减少不必要的等待,又保持了同步编程的简单性。
从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,JDK会依据操作系统内核支持的非阻塞I/O方式来帮助我们选择实现方式。
综上所述,在Java领域中,并发编程中的线程机制以及多线程的控制,在实际开发过程中,需要依据实际业务场景来考虑和衡量,这需要我们对其有更深的研究,才可以得心应手。
在讨论编程模型的时候,我们提到了像基于Promise模式和基于Thread Pool 模式的这样的设计模式的概念,这也是一个我们比较容易忽略的概念,如果有兴趣的话,可以自行进行查询相关资料进行了解。
最后,祝福大家在Java并发编程的“看山是山,看山不是山,看山还是山”的修行之路上,“拨开云雾见天日,守得云开见月明”,早日达到有迹可循到有迹可寻的目标!
版权声明:本文为博主原创文章,遵循相关版权协议,如若转载或者分享请附上原文出处链接和链接来源。