【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 交替执行多个任务,每个任务都执行一小段时间,从宏观上看,就像是全部任务都在同时执行一样。

  • 提升执行效率
  • 提升用户体验
  • 让编码更难
  • 资源开销与上下文切换开销
目录
相关文章
|
2天前
|
安全 Java 开发者
【JAVA】封装多线程原理
Java 中的多线程封装旨在简化使用、提高安全性和增强可维护性。通过抽象和隐藏底层细节,提供简洁接口。常见封装方式包括基于 Runnable 和 Callable 接口的任务封装,以及线程池的封装。Runnable 适用于无返回值任务,Callable 支持有返回值任务。线程池(如 ExecutorService)则用于管理和复用线程,减少性能开销。示例代码展示了如何实现这些封装,使多线程编程更加高效和安全。
|
1月前
|
监控 Java
java异步判断线程池所有任务是否执行完
通过上述步骤,您可以在Java中实现异步判断线程池所有任务是否执行完毕。这种方法使用了 `CompletionService`来监控任务的完成情况,并通过一个独立线程异步检查所有任务的执行状态。这种设计不仅简洁高效,还能确保在大量任务处理时程序的稳定性和可维护性。希望本文能为您的开发工作提供实用的指导和帮助。
100 17
|
2月前
|
Java
Java—多线程实现生产消费者
本文介绍了多线程实现生产消费者模式的三个版本。Version1包含四个类:`Producer`(生产者)、`Consumer`(消费者)、`Resource`(公共资源)和`TestMain`(测试类)。通过`synchronized`和`wait/notify`机制控制线程同步,但存在多个生产者或消费者时可能出现多次生产和消费的问题。 Version2将`if`改为`while`,解决了多次生产和消费的问题,但仍可能因`notify()`随机唤醒线程而导致死锁。因此,引入了`notifyAll()`来唤醒所有等待线程,但这会带来性能问题。
Java—多线程实现生产消费者
|
1月前
|
缓存 安全 算法
Java 多线程 面试题
Java 多线程 相关基础面试题
|
2月前
|
安全 Java Kotlin
Java多线程——synchronized、volatile 保障可见性
Java多线程中,`synchronized` 和 `volatile` 关键字用于保障可见性。`synchronized` 保证原子性、可见性和有序性,通过锁机制确保线程安全;`volatile` 仅保证可见性和有序性,不保证原子性。代码示例展示了如何使用 `synchronized` 和 `volatile` 解决主线程无法感知子线程修改共享变量的问题。总结:`volatile` 确保不同线程对共享变量操作的可见性,使一个线程修改后,其他线程能立即看到最新值。
|
2月前
|
消息中间件 缓存 安全
Java多线程是什么
Java多线程简介:本文介绍了Java中常见的线程池类型,包括`newCachedThreadPool`(适用于短期异步任务)、`newFixedThreadPool`(适用于固定数量的长期任务)、`newScheduledThreadPool`(支持定时和周期性任务)以及`newSingleThreadExecutor`(保证任务顺序执行)。同时,文章还讲解了Java中的锁机制,如`synchronized`关键字、CAS操作及其实现方式,并详细描述了可重入锁`ReentrantLock`和读写锁`ReadWriteLock`的工作原理与应用场景。
|
2月前
|
安全 Java 编译器
深入理解Java中synchronized三种使用方式:助您写出线程安全的代码
`synchronized` 是 Java 中的关键字,用于实现线程同步,确保多个线程互斥访问共享资源。它通过内置的监视器锁机制,防止多个线程同时执行被 `synchronized` 修饰的方法或代码块。`synchronized` 可以修饰非静态方法、静态方法和代码块,分别锁定实例对象、类对象或指定的对象。其底层原理基于 JVM 的指令和对象的监视器,JDK 1.6 后引入了偏向锁、轻量级锁等优化措施,提高了性能。
69 3
|
2月前
|
存储 安全 Java
Java多线程编程秘籍:各种方案一网打尽,不要错过!
Java 中实现多线程的方式主要有四种:继承 Thread 类、实现 Runnable 接口、实现 Callable 接口和使用线程池。每种方式各有优缺点,适用于不同的场景。继承 Thread 类最简单,实现 Runnable 接口更灵活,Callable 接口支持返回结果,线程池则便于管理和复用线程。实际应用中可根据需求选择合适的方式。此外,还介绍了多线程相关的常见面试问题及答案,涵盖线程概念、线程安全、线程池等知识点。
218 2
|
2月前
|
安全 Java API
java如何请求接口然后终止某个线程
通过本文的介绍,您应该能够理解如何在Java中请求接口并根据返回结果终止某个线程。合理使用标志位或 `interrupt`方法可以确保线程的安全终止,而处理好网络请求中的各种异常情况,可以提高程序的稳定性和可靠性。
58 6
|
2月前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####