【Java 多线程编程 | 从0到1】线程I/O模型

简介: 【Java 多线程编程 | 从0到1】线程I/O模型

线程I/O模型

2.1 线程与阻塞 I/O

IO 模型是指计算机在涉及 I/O 操作时使用到的模型。为了解决各种问题,人们提出了很多不同的 I/ 模型,与之相关的概念有线程、阻塞、非阻塞、同步以及异步等。I/O 可以分成阻塞IO 与非寨I/O 两大类型。阻塞 I/O 在进行 操作时会使当前线程进入阻塞状态,而非阻塞IO 则不进人阻寨状态。以服务器处理客户端连接为例,在单线程情况下由一个线程负责所有客户端连接的I/O操作,而在多线程情况下则由若干线程共同处理所有客户端连接的I/O操作。

此外,需要注意的是,计算机的I/O 其实包含了各种设备的 IO,比如网络 IO、磁盘IO键盘I/O 和鼠标IO等。我们以经典的网络 I/O 场景(见图 2.1)为例来讲解单线程阻塞IO模型和多线程阻塞 IO 模型。

程序在执行 I/O 时一般需要从内核空间复制数据,但内核空间的数据可能需要较长时间进行准备,由此导致用户空间产生阻塞。在图 2.2 中,应用程序处于用户空间,一个应用程序对应着一个进程,而进程则包含了缓冲区。当要进行I/O 操作时,需要通过内核来执行相应的操作,比如由内核负责与键盘、磁盘、网络等控制器进行通信。当内核得到不同设备的控制器发送过来的数据后,会将数据复制到用户空间供应用程序使用。

网络I/O产生阻塞的过程如图 2.3 所示。应用程序首先发起读取操作,然后进入阻塞状态接下来由操作系统内核完成IO 操作。内核刚开始时未准备好数据它需要不断读取网络数据一旦数据准备好,就将数据复制到用户空间供应用程序使用。应用程序在从发起读取操作到继续往下处理的这段时间就处于阻塞状态。

2.1.1 单线程阻塞 I/O 模型

单线程阻塞 I/O模型是最简单的一种服务器模型,几乎所有程序员在刚开始接触网络编程时都从这种模型开始。这种模型只能同时处理一个客户端访问,并且在 I/O 操作上是阻塞的线程会一直处于等待状态而不会做其他事情。对于多个客户端访问的情况,必须要等到前一个客户端访问结束后才能进行下一个访问的处理。也就是说,请求一个一个排队,且只提供一问一答服务。

图2.4所示为单线程阻塞服务器响应客户端访问的时间节点。首先,服务器必须初始化一个套接宇(socket)服务器,并绑定某个端口号使之监听客户端的访问。接着,客户端1 调用服务器的服务,服务器接收到请求后对其进行处理,处理完后写数据回客户端1。最后,处理客户端2的请求并写数据回客户端 2,期间即使客户端 2 在服务器处理完客户端1之前就进行请求,也要等服务器对客户端1 响应完后才会对客户端 2 进行响应处理。

这种模型的特点在于单线程和阻塞 I/O。单线程即服务器端只有一个线程处理客户端的所有请求,客户端连接与服务器端的处理线程比是 n:1,它无法同时处理多个连接,只能串行处理连接。而阻塞I/O 是指服务器在读写数据时是阻塞的,在读取客户端数据时要等待客户端发送数据并且把操作系统内核中的数据复制到用户进程中,这时才解除阻塞状态。将数据写回客户端时要等待用户进程将数据写入内核后才解除阻塞状态。

单线程阻塞IO 模型是最简单的一种服务器模型,整个运行过程都只有一个线程,只能同时处理一个客户端的请求(如果有多个客户端访问,就必须排队等待 )。服务器系统资源消耗较小,但并发能力低,容错能力差。


2.1.2 多线程阻塞 1/0 模型

针对单线程阻塞 IO 模型的缺点,最简单的改进方式就是将其多线程化,使之能对多个客户端进行并发响应。多线程模型的核心就是利用多线程机制为每个客户端分配一个线程。如图 2.5所示,服务器端开始监听客户端的访问,假如有两个客户端发送请求过来,服务器端在接收到客户端请求后将创建两个线程分别对它们进行处理。每个线程负责一个客户端连接,直到响应完成。期间两个线程并发地为各自对应的客户端处理请求,包括读取客户端数据、处理客户端数据、将数据写回客户端等操作。

这种模型的I/O操作也是阻塞的,因为每个线程执行到读取或写入操作时都将进入阻塞状态,直到成功读取客户端的数据或数据成功写入内核后才解除阻塞状态。尽管此时的 IO 操作还是会阻塞,但这种模式比单线程模式的性能明显提高,它不用等到第一个请求处理完才处理第二个,而是并发地处理客户端请求,客户端连接与服务器端处理线程的关系是一对一的。多线程阻塞 IO 模型的特点如下所示:

  • 支持对多个客户端并发响应,处理能力得到大幅提高,有较强的并发能力;
  • 服务器系统资源消耗量较大,而且多线程之间会产生线程切换成本,同时拥有较复杂的结构。

2.2 线程与非阻塞 1/0 模型

前面讲到,多线程阳塞 I/O 模型通过引入多线程的方法来提升服务器端的并发处理能力确实能够达到一定的效果。但它还是存在一个严重的问题,那就是每个连接都需要一个线程负责I/O 操作。当连接数量较多时将导致机器线程数量太多,而这些线程在大多数时间内都处干等待状态,线程之间的切换成本非常高。对于多线程阻塞 I/O 模型的这个缺点,有没有可能只用一个线程就可以维护多个客户端连接并且不会阻塞在读写操作呢?这就是下面要介绍的单线程非阻塞I/O模型。

就单线程非阻塞 IO模型来说,与阻塞 IO型相同的地方是,程序在执行 I/O 时一般要从内核空间和用户空问复制数据,不同之处在于非阻塞 I/O 模型不会一直等到内核准备好需据、而是直接返回去做其他的事,也就是说并不产生阻塞。应用程序进程包含一个缓冲区,详个线得会不断循环滥用所有客户端,尝试对它们进行读写操作。如果内核已准备好数据,那么应用层线程就会将数据复制到用户空间供使用,如图 2.6 所示

非阻塞I/O 模型最重要的一个特点是在调用读或写接口后会立即返回,而不会进入阻塞状态。网络的非阻塞I/O 的过程如图 2.7 所示。应用程序首先发起读取操作,它会告知操作系统内核去执行IO 操作。刚开始时由于内核未准备好数据,所以会马上返回而不是阻塞。此时应用程序可以做其他事,过一段时间后再尝试读取操作。如果发现数据已经准备好,内核就将数据复制到用户空间供应用程序使用。

非阻塞 I/O模型可分为应用层 I/O 多路复用、内核 I/O 多路复用、内核回调事件驱动 I/O这3种

2.3 Java 多线程非阻塞I/O 模型

虽然现代计算机都是多 CPU 的,而且操作系统也提供了多线程机制,但并不是说单线程完全被抛弃,实际上单线程也有自己的优势。最大的优势就是一个 CPU 只负责一个线程,因此可以完全规避多线程中的所有疑难杂症,这样在编写代码时就简单多了,如图 2.12 所示。比如在以前的多线程环境中,共享变量的操作要考虑很多问题,而单线程则不必考虑这些。同时单线程也免去了线程上下文的切换,进一步提高了 CPU 的真正使用率。

在一个线程对应一个 CPU 的情况下,如果多核计算机中只执行一个线程,那么就只有一个 CPU 工作,这样也就无法充分利用 CPU 资源。为了解决这个问题,我们的程序可以根据CPU的数量来创建线程数,N个 CPU分别对应N个线程,如图 2.13 所示。这种模型充分利用了多个 CPU,同时也保持了单线程的优点,相当于多个线程并行执行而不是并发执行。

在多核的机器时代,多线程和非阻塞都是提升服务器处理性能的利器。那么如何将它们结合起来呢?最常规的做法就是将客户端连接按组分配给若干线程,每个线程负责处理对应组内的连接。如图 2.14 所示,有 4 个客户端访问服务器,服务器将 socketl 和 socket2 交由线程管理,而线程 2则管理 socket3 和 socket4,通过事件检测及非阻塞读写就可以让每个线程都能高效运行。

在实际的工程中,最经典的多线程非阻塞 I/O 模式是 Reactor 模式。首先看单线程下的Reactor。Reactor 将服务器端的整个处理过程分成若干个事件,例如分为接收事件、读事件、写事件、执行事件等。Reactor 通过事件检测机制将这些事件分发给不同的处理器去处理。如图2.15 所示,若干客户端连接访问服务器端,Reactor 负责检测各种事件并分发到处理器,这些处理器包括接收连接的 accept 处理器、读数据的 read 处理器、写数据的 write 处理器以及执行逻辑的 process 处理器。在整个过程中只要有待处理的事件存在,就可以让 Reactor 线程不断往下执行,而不会阻塞在某处,所以处理效率很高。

基于单线程的 Reactor 模式在实际中很少使用,我们更多地是将它改进为多线程模式。常见的有下面两种:

  • 多线程 Reactor 模式,即在耗时的 process 处理器中引入多线程,例如使用线程池;
  • 多Reactor 实例模式,即直接使用多个 Reactor 实例,每个 Reactor 实例对应一个线程

Reactor 模式的第一种改进方式是多线程 Reactor 模式,如图 2.16 所示。多线程 Reactor 模式的整体结构基本与单线程的 Reactor 类似,只是额外引人了一个线程池。由于对连接的接收对数据的读取和对数据的写人等操作基本上都耗时较少,因此把它们都放到 Reactor 线程中处理。然而,对于可能比较耗时的逻辑处理工作,则在 process 处理器中引入线程池。process 处理器自己不执行任务,而是交给线程池,从而在 Reactor 线程中避免了耗时的操作。将耗时的操作转移到线程池中后,尽管 Reactor 只有一个线程,也能保证 Reactor 的高效性。

Reactor模式的第二种改进方式是多 Reactor 实例模式,如图2.17 所示。其中有多个 Reactor实例,每个 Reactor 实例对应一个线程。因为接收事件是相对于服务器端而言的,所以客户端的连接接收工作统一由一个accept 处理器负责,accept 处理器会将接收的客户端连接均匀分配给所有 Reactor 实例。每个 Reactor 实例负责处理分配到该 Reactor 上的客户端连接,包括连接的读数据、写数据和逻辑处理。这就是多 Reactor 实例的原理。

2.4 多线程带来了什么

并发和并行都是相对于进程或线程来说的。并发是指一个或若干个 CPU 对多个进程或线程进行多路复用,用简单的语言来说就是 CPU 交替执行多个任务,每个任务都执行一小段时间,从宏观上看,就像是全部任务都在同时执行一样。

  • 提升执行效率
  • 提升用户体验
  • 让编码更难
  • 资源开销与上下文切换开销
目录
相关文章
|
20天前
|
Java 开发者
Java多线程编程中的常见误区与最佳实践####
本文深入剖析了Java多线程编程中开发者常遇到的几个典型误区,如对`start()`与`run()`方法的混淆使用、忽视线程安全问题、错误处理未同步的共享变量等,并针对这些问题提出了具体的解决方案和最佳实践。通过实例代码对比,直观展示了正确与错误的实现方式,旨在帮助读者构建更加健壮、高效的多线程应用程序。 ####
|
11天前
|
缓存 Java 开发者
Java多线程编程的陷阱与最佳实践####
本文深入探讨了Java多线程编程中常见的陷阱,如竞态条件、死锁和内存一致性错误,并提供了实用的避免策略。通过分析典型错误案例,本文旨在帮助开发者更好地理解和掌握多线程环境下的编程技巧,从而提升并发程序的稳定性和性能。 ####
|
6天前
|
监控 Java 开发者
深入理解Java中的线程池实现原理及其性能优化####
本文旨在揭示Java中线程池的核心工作机制,通过剖析其背后的设计思想与实现细节,为读者提供一份详尽的线程池性能优化指南。不同于传统的技术教程,本文将采用一种互动式探索的方式,带领大家从理论到实践,逐步揭开线程池高效管理线程资源的奥秘。无论你是Java并发编程的初学者,还是寻求性能调优技巧的资深开发者,都能在本文中找到有价值的内容。 ####
|
11天前
|
安全 Java 开发者
Java中的多线程编程:从基础到实践
本文深入探讨了Java多线程编程的核心概念和实践技巧,旨在帮助读者理解多线程的工作原理,掌握线程的创建、管理和同步机制。通过具体示例和最佳实践,本文展示了如何在Java应用中有效地利用多线程技术,提高程序性能和响应速度。
42 1
|
19天前
|
安全 Java 开发者
Java 多线程并发控制:深入理解与实战应用
《Java多线程并发控制:深入理解与实战应用》一书详细解析了Java多线程编程的核心概念、并发控制技术及其实战技巧,适合Java开发者深入学习和实践参考。
40 6
|
19天前
|
Java 开发者
Java多线程编程的艺术与实践####
本文深入探讨了Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的技术文档,本文以实战为导向,通过生动的实例和详尽的代码解析,引领读者领略多线程编程的魅力,掌握其在提升应用性能、优化资源利用方面的关键作用。无论你是Java初学者还是有一定经验的开发者,本文都将为你打开多线程编程的新视角。 ####
|
18天前
|
存储 安全 Java
Java多线程编程中的并发容器:深入解析与实战应用####
在本文中,我们将探讨Java多线程编程中的一个核心话题——并发容器。不同于传统单一线程环境下的数据结构,并发容器专为多线程场景设计,确保数据访问的线程安全性和高效性。我们将从基础概念出发,逐步深入到`java.util.concurrent`包下的核心并发容器实现,如`ConcurrentHashMap`、`CopyOnWriteArrayList`以及`BlockingQueue`等,通过实例代码演示其使用方法,并分析它们背后的设计原理与适用场景。无论你是Java并发编程的初学者还是希望深化理解的开发者,本文都将为你提供有价值的见解与实践指导。 --- ####
|
21天前
|
安全 Java 开发者
Java多线程编程中的常见问题与解决方案
本文深入探讨了Java多线程编程中常见的问题,包括线程安全问题、死锁、竞态条件等,并提供了相应的解决策略。文章首先介绍了多线程的基础知识,随后详细分析了每个问题的产生原因和典型场景,最后提出了实用的解决方案,旨在帮助开发者提高多线程程序的稳定性和性能。
|
24天前
|
监控 安全 Java
Java中的多线程编程:从入门到实践####
本文将深入浅出地探讨Java多线程编程的核心概念、应用场景及实践技巧。不同于传统的摘要形式,本文将以一个简短的代码示例作为开篇,直接展示多线程的魅力,随后再详细解析其背后的原理与实现方式,旨在帮助读者快速理解并掌握Java多线程编程的基本技能。 ```java // 简单的多线程示例:创建两个线程,分别打印不同的消息 public class SimpleMultithreading { public static void main(String[] args) { Thread thread1 = new Thread(() -> System.out.prin
|
7月前
|
存储 安全 Java
深入理解Java并发编程:线程安全与锁机制
【5月更文挑战第31天】在Java并发编程中,线程安全和锁机制是两个核心概念。本文将深入探讨这两个概念,包括它们的定义、实现方式以及在实际开发中的应用。通过对线程安全和锁机制的深入理解,可以帮助我们更好地解决并发编程中的问题,提高程序的性能和稳定性。