Java 阻塞
对于用ServerSocket 及 Socket 编写的服务器程序和客户程序, 他们在运行过程中常常会阻塞. 例如, 当一个线程执行 ServerSocket 的accept() 方法时, 假如没有客户连接, 该线程就会一直等到有客户连接才从 accept() 方法返回. 再例如, 当线程执行 Socket 的 read() 方法时, 如果输入流中没有数据, 该线程就会一直等到读入足够的数据才从 read() 方法返回.
假如服务器程序需要同时与多个客户通信, 就必须分配多个工作线程, 让他们分别负责与一个客户通信, 当然每个工作线程都有可能经常处于长时间的阻塞状态.
从 JDK1.4 版本开始, 引入了非阻塞的通信机制. 服务器程序接收客户连接, 客户程序建立与服务器的连接, 以及服务器程序和客户程序收发数据的操作都可以按非阻塞的方式进行. 服务器程序只需要创建一个线程, 就能完成同时与多个客户通信的任务.
非阻塞的通信机制主要由 java.nio 包(新I/O包) 中的类实现, 主要的类包括 ServerSocketChannel, SocketChannel, Selector, SelectionKey 和 ByteBuffer 等.
本章介绍如何用 java.nio 包中的类来创建服务器程序和客户程序, 并且 分别采用阻塞模式和非阻塞模式来实现它们. 通过比较不同的实现方式, 可以帮助读者理解它们的区别和适用范围.
一. 线程阻塞的概念
在生活中, 最常见的阻塞现象是公路上汽车的堵塞. 汽车在公路上快速行驶, 如果前方交通受阻, 就只好停下来等待, 等到交通畅顺, 才能恢复行驶.
线程在运行中也会因为某些原因而阻塞. 所有处于阻塞状态的线程的共同特征是: 放弃CPU, 暂停运行, 只有等到导致阻塞的原因消除, 才能恢复运行; 或者被其他线程中断, 该线程会退出阻塞状态, 并且抛出 InterruptedException.
1.1 线程阻塞的原因
导致线程阻塞的原因主要有以下几方面.
线程执行了 Thread.sleep(int n) 方法, 线程放弃 CPU, 睡眠 n 毫秒, 然后恢复运行.
线程要执行一段同步代码, 由于无法获得相关的同步锁, 只好进入阻塞状态, 等到获得了同步锁, 才能恢复运行.
线程执行了一个对象的 wait() 方法, 进入阻塞状态, 只有等到其他线程执行了该对象的 notify() 和 notifyAll() 方法, 才可能将其呼醒.
线程执行 I/O 操作或进行远程通信时, 会因为等待相关的资源而进入阻塞状态. 例如, 当线程执行 System.in.read() 方法时, 如果用户没有向控制台输入数据, 则该线程会一直等读到了用户的输入数据才从 read() 方法返回.
进行远程通信时, 在客户程序中, 线程在以下情况可能进入阻塞状态.
请求与服务器建立连接时, 即当线程执行 Socket 的带参数构造方法, 或执行 Socket 的 connect() 方法时, 会进入阻塞状态, 直到连接成功, 此线程才从 Socket 的构造方法或 connect() 方法返回.
线程从 Socket 的输入流读入数据时, 如果没有足够的数据, 就会进入阻塞状态, 直到读到了足够的数据, 或者到达输入流的末尾, 或者出现了异常, 才从输入流的 read() 方法返回或异常中断. 输入流中有多少数据才算足够呢? 这要看线程执行的 read() 方法的类型.
> int read(): 只要输入流中有一个字节, 就算足够.
> int read( byte[] buff): 只要输入流中的字节数目与参数buff 数组的长度相同, 就算足够.
> String readLine(): 只要输入流中有一行字符串, 就算足够. 值得注意的是, InputStream 类并没有 readLine() 方法, 在过滤流 BufferedReader 类中才有此方法.
线程向 Socket 的输出流写一批数据时, 可能会进入阻塞状态, 等到输出了所有的数据, 或者出现异常, 才从输出流 的 write() 方法返回或异常中断.
调用 SOcket 的setSoLinger() 方法设置了关闭 Socket 的延迟时间, 那么当线程执行 Socket 的 close() 方法时, 会进入阻塞状态, 直到底层 Socket 发送完所有剩余数据, 或者超过了 setSoLinger() 方法设置的延迟时间, 才从 close() 方法返回.
在服务器程序中, 线程在以下情况下可能会进入阻塞状态.
线程执行 ServerSocket 的 accept() 方法, 等待客户的连接, 直到接收到了客户连接, 才从 accept() 方法返回.
线程从 Socket 的输入流读入数据时, 如果输入流没有足够的数据, 就会进入阻塞状态.
线程向 Socket 的输出流写一批数据时, 可能会进入阻塞状态, 等到输出了所有的数据, 或者出现异常, 才从输出流的 write() 方法返回或异常中断.
由此可见, 无论在服务器程序还是客户程序中, 当通过 Socket 的输入流和输出流来读写数据时, 都可能进入阻塞状态. 这种可能出现阻塞的输入和输出操作被称为阻塞 I/O. 与此对照, 如果执行输入和输出操作时, 不会发生阻塞, 则称为非阻塞 I/O.
1.2 服务器程序用多线程处理阻塞通信的局限
本书第三章的第六节(创建多线程的服务器) 已经介绍了服务器程序用多线程来同时处理多个客户连接的方式. 服务器程序的处理流程如图 4-1 所示. 主线程负责接收客户的连接. 在线程池中有若干工作线程, 他们负责处理具体的客户连接. 每当主线程接收到一个客户连接, 就会把与这个客户交互的任务交给一个空闲的工作线程去完成, 主线程继续负责接收下一个客户连接.
图4-1 服务器程序用多线程处理阻塞通信
在图4-1 总, 用粗体框标识的步骤为可能引起阻塞的步骤. 从图中可以看出, 当主线程接收客户连接, 以及工作线程执行 I/O 操作时, 都有可能进入阻塞状态.
服务器程序用多线程来处理阻塞 I/O, 尽管能满足同时响应多个客户请求的需求, 但是有以下局限:
⑴ Java 虚拟机会为每个线程分配独立的堆栈空间, 工作线程数目越多, 系统开销就越大, 而且增加了 Java虚拟机调度线程的负担, 增加了线程之间同步的复杂性, 提高了线程死锁的可能性;
⑵ 工作线程的许多时间都浪费在阻塞 I/O 操作上, Java 虚拟机需要频繁地转让 CPU 的使用权, 使进入阻塞状态的线程放弃CPU, 再把CPU 分配给处于可运行状态的线程.
由此可见, 工作线程并不是越多越好. 如图 4-2 所示, 保持适量的工作线程, 会提高服务器的并发性能, 但是当工作线程的数目达到某个极限, 超出了系统的负荷时, 反而会减低并发性能, 使得多数客户无法快速得到服务器的响应.
图4-2 线程数目与并发性能的更新
1.3 非阻塞通信的基本思想
假如要同时做两件事: 烧开水和烧粥. 烧开水的步骤如下:
锅里放水, 打开煤气炉;
等待水烧开; //阻塞
关闭煤气炉, 把开水灌到水壶里;
烧粥的步骤如下:
锅里放水和米, 打开煤气炉;
等待粥烧开; //阻塞
调整煤气炉, 改为小火;
等待粥烧熟; //阻塞
关闭煤气炉;
为了同时完成两件事, 一个方案是同时请两个人分别做其中的一件事, 这相当于采用多线程来同时完成多个任务. 还有一种方案是让一个人同时完成两件事, 这个人应该善于利用一件事的空闲时间去做另一件事, 一刻也不应该闲着:
锅子里放水, 打开煤气炉; //开始烧水
锅子力放水和米, 打开煤气炉; //开始烧粥
while(一直等待, 直到有水烧开, 粥烧开或粥烧熟事件发生){ //阻塞
if(水烧开)
关闭煤气炉, 把开水灌到水壶里;
if(粥烧开)
调整煤气炉, 改为小火;
if(粥烧熟)
关闭煤气炉;
if(水已经烧开并且粥已经烧熟)
退出循环;
} //这里的煤气炉我可以理解为每件事就有一个煤气炉配给吧, 这也是一部分的开销呢
//并且if里面的动作必须要能快速完成的才行, 不然后面的就要排队了
//如是太累的工作还是不要用这个好
这个人不断监控烧水及烧粥的状态, 如果发生了 "水烧开", "粥烧开" 或 "粥烧熟" 事件, 就去处理这些事件, 处理完一件事后进行监控烧水及烧粥的状态, 直到所有的任务都完成.
以上工作方式也可以运用到服务器程序中, 服务器程序只需要一个线程就能同时负责接收客户的连接, 接收各个客户发送的数据, 以及向各个客户发送响应数据. 服务器程序的处理流程如下:
while(一直等待, 直到有接收连接就绪事件, 读就绪事件或写就绪事件发生){ //阻塞
if(有客户连接)
接收客户的连接; //非阻塞
if(某个 Socket 的输入流中有可读数据)
从输入流中读数据; //非阻塞
if(某个 Socket 的输出流可以写数据)
向输出流写数据; //非阻塞
}
以上处理流程采用了轮询的工作方式, 当某一种操作就绪时, 就执行该操作, 否则就查看是否还有其他就绪的操作可以执行. 线程不会因为某一个操作还没有就绪, 就进入阻塞状态, 一直傻傻地在那里等待这个操作就绪.
为了使轮询的工作方式顺利进行, 接收客户的连接, 从输入流读数据, 以及向输出流写数据的操作都应该以非阻塞的方式运行. 所谓非阻塞, 就是指当线程执行这些方法时, 如果操作还没有就绪, 就立即返回, 而不会一直等到操作就绪. 例如, 当线程接收客户连接时, 如果没有客户连接, 就立即返回; 再例如, 当线程从输入流中读数据时, 如果输入流中还没有数据, 就立即返回, 或者如果输入流还没有足够的数据, 那么就读取现有的数据, 然后返回. 值得注意的是, 以上 while 学校条件中的操作还是按照阻塞方式进行的, 如果未发生任何事件, 就会进入阻塞状态, 直到接收连接就绪事件, 读就绪事件或写就绪事件中至少有一个事件发生时, 才会执行 while 循环体中的操作. 在while 循环体中, 一般会包含在特定条件下退出循环的操作.
二. java.nio 包中的主要类
java.nio 包提供了支持非阻塞通信的类.
ServerSocketChannel: ServerSocket 的替代类, 支持阻塞通信与非阻塞通信.
SocketChannel: Socket 的替代类, 支持阻塞通信与非阻塞通信.
Selector: 为ServerSocketChannel 监控接收连接就绪事件, 为 SocketChannel 监控连接就绪, 读就绪和写就绪事件.
SelectionKey: 代表 ServerSocketChannel 及 SocketChannel 向 Selector 注册事件的句柄. 当一个 SelectionKey 对象位于Selector 对象的 selected-keys 集合中时, 就表示与这个 SelectionKey 对象相关的事件发生了.
ServerSocketChannel 及 SocketChannel 都是 SelectableChannel 的子类, 如图 4-3 所示. SelectableChannel 类及其子类都能委托 Selector 来监控他们可能发生的一些事件, 这种委托过程也称为注册事件过程.
Java 阻塞的更多相关文章
- lesson2:java阻塞队列的demo及源码分析
本文向大家展示了java阻塞队列的使用场景.源码分析及特定场景下的使用方式.java的阻塞队列是jdk1.5之后在并发包中提供的一组队列,主要的使用场景是在需要使用生产者消费者模式时,用户不必再通过多 ...
- 细说并发5:Java 阻塞队列源码分析(下)
上一篇 细说并发4:Java 阻塞队列源码分析(上) 我们了解了 ArrayBlockingQueue, LinkedBlockingQueue 和 PriorityBlockingQueue,这篇文 ...
- Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例
Java阻塞队列(BlockingQueue)实现 生产者/消费者 示例 本文由 TonySpark 翻译自 Javarevisited.转载请参见文章末尾的要求. Java.util.concurr ...
- JAVA阻塞(IO)和非阻塞(NIO)
查看这篇文章,了解更多关于Java的阻塞和非阻塞替代创建套接字的信息. 套接字使用TCP / IP传输协议,是两台主机之间的最后一块网络通信. 您通常不必处理它们,因为它们之上构建了协议,如HTTP或 ...
- Java 并发系列之七:java 阻塞队列(7个)
1. 基本概念 2. 实现原理 3. ArrayBlockingQueue 4. LinkedBlockingQueue 5. LinkedBlockingDeque 6. PriorityBlock ...
- Java阻塞队列四组API介绍
Java阻塞队列四组API介绍 通过前面几篇文章的学习,我们已经知道了Java中的队列分为阻塞队列和非阻塞队列以及常用的七个阻塞队列.如下图: 本文来源:凯哥Java(kaigejava)讲解Java ...
- java阻塞队列
对消息的处理有些麻烦,要保证各种确认.为了确保消息的100%发送成功,笔者在之前的基础上做了一些改进.其中要用到多线程,用于重复发送信息. 所以查了很多关于线程安全的东西,也看到了阻塞队列,发现这个模 ...
- Java阻塞队列的实现
阻塞队列与普通队列的区别在于,当队列是空的时,从队列中获取元素的操作将会被阻塞,或者当队列是满时,往队列里添加元素的操作会被阻塞.试图从空的阻塞队列中获取元素的线程将会被阻塞,直到其他的线程往空的队列 ...
- java阻塞队列与非阻塞队列
在并发编程中,有时候需要使用线程安全的队列.如果要实现一个线程安全的队列有两种方式:一种是使用阻塞算法,另一种是使用非阻塞算法. //使用阻塞算法的队列可以用一个锁(入队和出队用同一把锁)或两个锁(入 ...
- (原创)JAVA阻塞队列LinkedBlockingQueue 以及非阻塞队列ConcurrentLinkedQueue 的区别
阻塞队列:线程安全 按 FIFO(先进先出)排序元素.队列的头部 是在队列中时间最长的元素.队列的尾部 是在队列中时间最短的元素.新元素插入到队列的尾部,并且队列检索操作会获得位于队列头部的元素.链接 ...
随机推荐
- vue组件值传递之父组件向子组件传递(props)
<template> <div class="hello"> <h1>{{ msg }}</h1> <ul> <l ...
- navicat 在写存储过程的时候总是说语法错误
这个时候我们在sql的头部加上 DELIMITER $$ 尾部加上 $$DELIMITER;
- git使用(一)----git安装
windows安装git msysgit是windows版本的Git 下载地址:https://git-for-windows.github.io/ 安装步骤 linux安装git https://g ...
- linux工具大全
Linux Performance hi-res: observability + static + perf-tools/bcc (svg)slides: observabilityslides: ...
- MySQL慢查询优化、索引优化、以及表等优化总结
MySQL优化概述 MySQL数据库常见的两个瓶颈是:CPU和I/O的瓶颈. CPU在饱和的时候一般发生在数据装入内存或从磁盘上读取数据时候. 磁盘I/O瓶颈发生在装入数据远大于内存容量的时候,如果应 ...
- LoadRunner Controller 常见用法
Controller 工作原理:通过场景设计来模拟用户的真实操作并调用vugen中的脚本,再通过设置的压力机产生压力 Scenario-convert scenario to the percenta ...
- ny2 括号配对问题
括号配对问题 时间限制:3000 ms | 内存限制:65535 KB 难度:3 描述 现在,有一行括号序列,请你检查这行括号是否配对. 输入 第一行输入一个数N(0<N<=1 ...
- iOS-ARC-环境下如何查看引用计数的变化
iOS-ARC-环境下如何查看引用计数的变化 一,新建立一个工程,用于测试引用计数的变化. 二,找到如下路径Build Phases---->Compile Sources---->App ...
- vue实现复制粘贴的两种形式
方式一: 1.安装clipboard:npm install clipboard 2.src/utils/clipboard.js import Vue from 'vue' import Clipb ...
- Linux shell 执行修改配置文件中的内容
在开发的过程中可能Linux环境不一致需要适应本地环境的HOME目录,可以通过脚本来修改配置文件内容,写一个test.sh的脚本 在脚本里写入以下命令 sed -i “s#ftfts_com_serv ...