Java总结2

枚举

enum关键字在java5中引入表示一种特殊类型的类,其总是继承java.lang.Enum
类,以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录
可接受值的列表,并避免由于传入无效值而引起的意外行为

枚举类

当定义一个枚举类型时,每一个枚举类型成员都可以看作是Enum 类的实例,这些
枚举成员默认都被 final、public、static 修饰,当使用枚举类型成员时,直接
使用枚举名称调用成员即可

1
2
3
4
values()	以数组形式返回枚举类型的所有成员
valueOf() 将普通字符串转换为枚举实例
compareTo() 比较两个枚举成员在定义时的顺序
ordinal() 获取枚举成员的索引位置

Java 注解是什么?

Java 注解用于为Java 代码提供元数据。作为元数据,注解不直接影响你的
代码执行,但也有一些类型的注解实际上可以用于这一目的,注解可以放在
类或者方法上,在类、方法、成员变量、形参位置

  1. 自定义注解 自定义注解就是我们自己写的注解
  2. JDK内置注解 @Override @Deprecated
  3. 还有第三方框架提供的注解 SpringMVC的 @Controller
1
2
3
public @interface MyTestAnnotation {

}
  • @Override,表示当前的方法定义将覆盖超类中的方法
  • @Deprecated,如果程序员使用了注解为它的元素,那么编译器会发出警告信息
  • @SuppressWarnings,关闭不当的编译器警告信息。在java SE5之前的
    版本中,也可以使用该注解,不过会被忽略不起作用。

Java 注解有什么作用?

  1. 生成帮助文档
  2. 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并
    不是覆盖了超类方法,则编译时就能检查出,@Deprecated标识方法过期
  3. 提供信息给编译器 编译器可以利用注解来检测出错误或者警告信息,
    打印出日志

元注解是什么?

注解的注解,它是作用在注解中,方便我们使用注解实现想要的功能

  1. @Retention 表示注解存在阶段是保留在源码(编译期),字节码(类
    加载)或者运行期(JVM中运行),使用枚举来表示注解保留时期
  2. @Target 表示注解作用的范围可以是类,方法,方法参数变量
  3. @Documented 将注解中的元素包含到 Javadoc 中去
  4. @Inherited 该注解了的注解修饰了一个父类,如果他的子类没有被其
    他注解修饰,则它的子类也继承了父类的注解
  5. @Repeatable 被这个元注解修饰的注解可以同时作用一个对象多次,但
    是每次作用注解又可以代表不同的含义

Java IO

I/O(Input/Outpu)即输入/输出。指应用程序和外部设备之间的数据传递,
常见的外部设备包括文件、管道、网络连接。用户空间的程序不能直接访问内核
空间必须通过系统调用来间接访问内核空间,主要是磁盘IO(读写文件)和网
络IO(网络请求和响应)

什么是流

流是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式
发送信息的通道。流的特性有三点

  1. 先进先出:最先写入输出流的数据最先被输入流读取到
  2. 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序
    读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外)
  3. 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能
    ,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,
    如果既要写入数据,又要读取数据,则要分别提供两个流

Java IO流的分类

IO流主要的分类方式有以下3种:

  1. 按数据流的方向:输入流、输出流
  2. 按处理数据单位:字节流、字符流
  3. 按功能:节点流、处理流

输入流与输出流

输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文
件是输出流,这点很容易搞反

字节流与字符流

字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据
单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据
单元为16位的字符

为什么要有字符流

Java中字符是采用Unicode标准,Unicode 编码中,一个英文为一个字节,一
个中文为两个字节。而在UTF-8编码中,一个中文字符是3个字节。如果使用字节
流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个
字符对应的字节分裂开来,就会出现乱码了。并且如果我们不知道编码类型就
很容易出现乱码问题。为了更方便地处理中文这些字符,Java 就推出了字符
流。字节流和字符流的其他区别如下

  1. 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一
    般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本
    文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本
    文件
  2. 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字
    符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了

节点流和处理流

  1. 节点流:直接操作数据读写的流类,比如FileInputStream
  2. 处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供
    功能强大、灵活的读写功能,例如BufferedInputStream(缓冲字节流)

程序与磁盘的交互相对于内存运算是很慢的,容易成为程序的性能瓶颈。减少程
序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普
通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足
够的待操作数据后,再与内存或磁盘进行交互。这样在总数据量不变的情况下
,通过提高每次交互的数据量,减少了交互次数

字节流

InputStream与OutputStream是两个抽象类,是字节流的基类,所有具体的字节
流实现类都是分别继承了这两个类

字符流

与字节流类似,字符流也有两个抽象基类,分别是Reader和Writer。其他的字符
流实现类都是继承了这两个类

Java IO使用的设计模式

使用了适配器模式和装饰器模式

  1. 适配器模式 把一个类的接口变换成客户端所期待的另一种接口,从而
    使原本因接口不匹配而无法在一起工作的两个类能够在一起工作
    1
    2
    3
    4
    //实现从字节流解码成字符流;
    Reader reader = new InputStreamReader(inputStream);
    //字节输出流转字符输出流通过
    Writer writer = new OutputStreamWriter(outputStream);
  2. 装饰器模式 一种动态地往一个类中添加新的行为的设计模式。就功能而
    言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个
    类添加一些功能,BufferedInputStream 为FileInputStream 提供缓
    存的功能
    1
    new BufferedInputStream(new FileInputStream(inputStream));

获取用键盘输入常用的两种方法

  1. 通过 Scanner
    1
    2
    3
    Scanner input = new Scanner(System.in);
    String s = input.nextLine();
    input.close();
  2. 通过 BufferedReader
    1
    2
    BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
    String s = input.readLine();

阻塞和非阻塞IO

IO操作包括:对硬盘的读写、对socket的读写以及外围设备的读写。
当用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪,对于阻
塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪,对于非阻塞
IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数
据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完
整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:

  1. 查看数据是否就绪
  2. 进行数据拷贝(内核将数据拷贝到用户线程)

那么阻塞和非阻塞的区别就在于第一个阶段,如果数据没有就绪,在查看数据
是否就绪的过程中是一直等待,还是直接返回一个标志信息。Java中传统的IO
都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没
有就绪,当前线程就会一直阻塞在read 方法调用那里,直到有数据才返回,
而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信
息,告知当前线程数据没有就绪,而不是一直在那里等待

同步和异步IO

  1. 同步IO:当用户发出IO请求操作后,内核会去查看要读取的数据是否就
    绪,如果没有,就一直等待。期间用户线程或内存会不断地轮询数据是否就
    绪。当数据就绪时,再把数据从内核拷贝到用户空间
  2. 异步IO:用户线程只需发出IO请求和接收IO操作完成通知,期间的IO操
    作由内核自动完成,并发送通知告知用户线程IO操作已经完成。也就是说,
    在异步IO中,并不会对用户线程产生任何阻塞

Java 常见IO 模型

  1. BIO(Blocking I/O) BIO属于同步阻塞IO 模型。同步阻塞IO 模型中,应用
    程序发起 read 调用后会一直阻塞,直到在内核把数据拷贝到用户空间。在客户
    端连接数量不高的情况下是没问题的。但是当面对十万甚至百万级连接的时候,
    传统的 BIO 模型是无能为力的。因此我们需要一种更高效的I/O 处理模型来应
    对更高的并发量
  2. NIO(Non-blocking/New I/O) 对应java.nio包,提供了Channel, Selector
    ,Buffer 等抽象。NIO 中的N 可以理解为Non-blocking,不单纯是New。它支持面
    向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使
    用NIO。NIO 可以看作是I/O 多路复用模型
  3. AIO (Asynchronous I/O) 异步IO 模型。异步IO 是基于事件和回调机制实
    现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操
    作系统会通知相应的线程进行后续的操作

BIO,NIO,AIO 有什么区别

  1. BIO:Block IO 同步阻塞式IO,就是我们平常使用的传统IO,它的特点
    是模式简单使用方便,并发处理能力低。在服务器中实现的模式为一个连接
    一个线程。也就是说客户端有连接请求的时候,服务器就需要启动一个线程
    进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这
    也可以通过线程池机制改善。BIO 一般适用于连接数目小且固定的架构,
    这种方式对于服务器资源要求比较高,而且并发局限于应用中,是
    JDK1.4之前的唯一选择,但好在程序直观简单,易理解
  2. NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端
    通过Channel(通道)通讯,实现了多路复用。同步并非阻塞,在服务器中
    实现的模式为一个请求一个线程,也就是说客户端发送的连接请求都会注
    册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程
    进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并
    发局限于应用中,编程比较复杂,从JDK1.4开始支持
  3. AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非
    阻塞IO ,异步 IO 的操作基于事件和回调机制。异步并非阻塞,在服务器
    中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是
    通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO
    一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作
    系统参与并发操作,编程比较复杂,从JDK1.7开始支持

有了解过Java中的NIO吗?原理是什么?

  1. 基于直接内存实现
  2. NIO底层原理,同步非阻塞的IO模型,它是面向缓冲区的

NIO

NIO实现高性能处理的原理是使用较少的线程处理更多的任务

NIO和IO的区别

  1. IO是面向流的,NIO是面向缓冲区的。 Java IO 面向流意味着每次从流中
    读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。NIO 的
    缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,一次处理一个
    数据块
  2. IO的各种流是阻塞的。这意味着当一个线程调用read()或write()时,该
    线程被阻塞,直到有一些数据被读取或写入。该线程在此期间不能再干任何事
    情了。 NIO 的非阻塞模式使一个线程从某通道发送请求读取数据,但是它仅
    能得到目前可用的数据,如果目前没有数据可用时就什么都不会获取。而不
    是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他
    的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需
    要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO
    的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管
    理多个输入和输出通道
  3. 常规io使用的byte[]、char[]进行封装,而NIO采用ByteBuffer类来操
    作数据,再结合针对File或socket技术的channel,采用同步非阻塞技术来
    实现高性能处理,而Netty正是采用ByteBuffer(缓冲区)、Channel(通
    道)、Selector(选择器)进行封装的
  4. 传统IO基于字节流和字符流进行操作,NIO基于Channel和Buffer(缓冲区)
    进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
    Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到
    达)。因此单个线程可以监听多个数据通道。

为什么使用NIO

  1. 传统的服务器端同步阻塞 I/O 处理使用多线程,主要原因在于socket.
    accept()、socket.read()、socket.write() 三个主要函数都是同步阻塞
    的,当一个连接在处理I/O 的时候,系统是阻塞的,如果是单线程的话必然
    就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更
    多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O
    阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。
  2. 现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低
    。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不
    错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多
    考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲
    一些系统处理不了的连接或请求。
  3. 这个模型最本质的问题在于严重依赖于线程。但线程是很”贵”的资源,主
    要表现在: 1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,
    线程本质上就是一个进程。创建和销毁都是重量级的系统函数。 2. 线程本
    身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果
    系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。 3. 线程的切
    换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,
    然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于
    线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率
    特别高(超过20%以上),导致系统几乎陷入不可用的状态。 4. 容易造成
    锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程
    数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返
    回,激活大量阻塞线程从而使系统负载压力过大。
  4. NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待
    就绪阶段都是非阻塞的,真正的I/O 操作是同步阻塞的(消耗CPU但性能非
    常高)。回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,
    一是没有办法知道到底能不能写、能不能读,只能”傻等”,即使通过各种估
    算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket
    .write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程
    另起炉灶,没有好的办法利用CPU
  5. NIO的读写函数可以立刻返回,这就给了我们不开线程利用CPU的最好机
    会:如果一个连接不能读写(socket.read()返回0或者socket.write()
    返回0),我们可以把这件事记下来,记录的方式通常是在Selector上注
    册标记位,然后切换到其它就绪的连接(channel)继续进行读写。

NIO同步非阻塞特性

NIO的主要事件有几个:读就绪、写就绪、有新连接到来。
我们首先需要注册当这几个事件到来的时候所对应的处理器。然后在合适的时
机告诉事件选择器:我对这个事件感兴趣。对于写操作,就是写不出去的时候
对写事件感兴趣;对于读操作,就是完成连接和系统没有办法承载新读入的
数据的时;对于accept,一般是服务器刚启动的时候;而对于connect,一
般是connect失败需要重连或者直接异步调用connect的时候。
其次,用一个死循环选择就绪的事件,会执行系统调用(Linux 2.6 之前是
select、poll,2.6之后是epoll,Windows是IOCP),还会阻塞的等待新事
件的到来。新事件到来的时候,会在selector上注册标记位,标示可读、可写
或者有连接到来。
注意,select是阻塞的,无论是通过操作系统的通知(epoll)还是不停的
轮询(select,poll ),这个函数是阻塞的。所以你可以放心大胆地在一个
while(true)里面调用这个函数而不用担心CPU空转。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
//IO线程主循环:
class IoThread extends Thread{
public void run(){
Channel channel;
while(channel=Selector.select()){//选择就绪的事件和对应的连接
if(channel.event==accept){
registerNewChannelHandler(channel);//如果是新连接,则注册一个新的读写处理器
}
if(channel.event==write){
getChannelHandler(channel).channelWritable(channel);
//如果可以写,则执行写事件
}
if(channel.event==read){
getChannelHandler(channel).channelReadable(channel);
//如果可以读,则执行读事件
}
}
}
Map<Channel,ChannelHandler> handlerMap;//所有channel的对应事件处理器
}

优化线程模型

NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读
写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要
阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。

Proactor与Reactor

一般情况下,I/O 复用机制需要事件分发器。事件分发器的作用,即将那些读写
事件源分发给各读写事件的处理者,开发人员在开始的时候需要在分发器那里
注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函
数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调
函数。涉及到事件分发器的两种模式称为:Reactor和Proactor。

  1. Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在
    Reactor 模式中,事件分发器等待某个事件或者可应用或可操作的状态发生
    (比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事
    件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。
  2. 而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起
    一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发
    起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于
    存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得
    知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事
    件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO
    操作(称为overlapped技术),事件分发器等IO Complete事件完成。这
    种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之
    为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系
    统代劳的。
  3. Reactor模式:主动模式,所谓主动,是指应用程序不断轮询,询问操作系
    统或者网络框架,IO是否就绪。其中java的NIO就属于这种模式。在这种模式下
    ,实际的IO操作还是应用程序执行的。
  4. Procator模式:被动模式,应用程序的读写函数操作交给操作系统或者
    网络框架,实际IO操作由操作系统或者网络框架完成,之后再回调应用程序
    。微软的asio库就是这种模式。

事件驱动模型

  1. 采用轮询的方式:线程不断轮询询问相关事件发生源有没有发生事件,有发
    生事件就调用事件处理逻辑
  2. 事件驱动方式:发生事件,主线程把事件加入到事件队列,在另外线程不断
    循环消费事件列表的事件,调用事件对应的处理逻辑事件。事件驱动方式也被称
    为消息通知方式,其实是设计模式中观察者模式的思路。基于事件驱动的优点:
    可扩展性好,高性能。

在Reactor中实现读

  1. 注册读就绪事件和相应的事件处理器。
  2. 事件分发器等待事件。
  3. 事件到来,激活分发器,分发器调用事件对应的处理器。
  4. 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返
    还控制权。

Reactor线程模型中2个关键组成:

  1. Reactor:Reactor在一个单独的线程运行,负责监听和分发事件,分发给适
    当的处理程序来对IO事件做出反应。
  2. Handlers:处理程序执行I/O事件要完成的实际事件。

Reactor的三种模型:单Reactor单线程、单Reactor多线程、主从Reactor多线程。

  1. 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服
  2. 单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
  3. 主从 Reactor 多线程,多个前台接待员,多个服务生

Netty主要基于主从Reactor多线程模型,主从Reactor主从模型有多个Reactor:

1
2
3
4
5
6
7
8
// MainReactor负责客户端的连接请求,并将请求转交给SubReactor
// SubReactor负责相应通道的IO读写请求
// 非IO请求(具体业务逻辑处理)的任务则会直接写入队列,等待worker线程进行处理。

EventLoopGroup bossGroup = new NioEventLoopGroup();
EvenLoopGroup workerGroup = new NioEventLoopGroup();
ServerBootstrap server = new ServerBootstrap();
server.group(bossGroup,workerGroup).channel(NioServerSocketChannel.class);

bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor
,专门处理端口的Accept事件,每个端口对应一个Boss线程,workerGroup线程
池会被各个SubReactor和Worker线程充分利用。

  1. NioEventLoop:维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用
  2. NioEventLoop的run方法,执行I/O任务和非I/O任务:

任务分为两种

  1. I/O任务:即selectionKey中的ready的事件,如accept、connect、read、
    write等,由processSelectKeys方法触发。
  2. 非I/O任务:添加到taskQueue中的任务,如register0、bind0等任务,由
    runAllTasks方法触发。

在Proactor中实现读

  1. 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下
    ,处理器无视IO就绪事件,它关注的是完成事件。
  2. 事件分发器等待操作完成事件。
  3. 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并
    将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。
  4. 事件分发器呼唤处理器。
  5. 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,
    并将控制权返回事件分发器。

Channel

Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的
,譬如:InputStream, OutputStream。而Channel是双向的,既可以用来
进行读操作,又可以用来进行写操作。
在Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel
space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间
(user space)中的用户缓冲区(user buffer)

  1. FileChannel 对应文件IO
  2. DatagramChannel 对应UDP
  3. SocketChannel 对应TCP Client
  4. ServerSocketChannel 对应TCP Server

SocketChannel

NIO的channel抽象的一个重要特征就是可以通过配置它的阻塞行为,以实现
非阻塞式的信道。在非阻塞式信道上调用一个方法总是会立即返回。这种调用
的返回值指示了所请求的操作完成的程度。
例如在一个非阻塞式ServerSocketChannel上调用accept()方法,如果有连
接请求来了,则返回客户端SocketChannel,否则返回null。

TCP服务端的NIO写法

Selector类可以用于避免使用阻塞式客户端中很浪费资源的“忙等”方法。一个
Selector实例可以同时检查一组信道的I/O状态。用专业术语来说,选择器就
是一个多路开关选择器,因为一个选择器能够管理多个信道上的I/O操作。
当一个信道有I/O操作的时候,他会通知Selector,Selector就是记住这个信
道有I/O操作,并且知道是何种I/O操作,是读呢?是写呢?还是接受新的连接
;所以如果使用Selector ,它返回的结果只有两种结果,一种是0,即在你调
用的时刻没有任何客户端需要I/O操作,另一种结果是一组需要I/O操作的客户
端。要使用选择器(Selector),需要创建一个Selector实例(使用静态工厂
方法open())并将其注册(register)到想要监控的信道上(注意,这要通过
channel的方法实现,而不是使用selector的方法)。最后调用选择器select()
方法。该方法会阻塞等待,直到有一个或更多的信道准备好了I/O操作或等待超时。
select()方法将返回可进行I/O操作的信道数量。现在,在一个单独的线程中,
通过调用select()方法就能检查多个信道是否准备好进行I/O操作。如果经过一
段时间后仍然没有信道准备好,select()方法就会返回0,并允许程序继续执行
其他任务。

1
Set selectedKeys = selector.selectedKeys();

一旦调用了select()方法,并且返回值表明有一个或更多个通道就绪了,然后可
以通过调用selector的selectedKeys() 方法,访问“已选择键集(selected
key set)”中的就绪通道。当向Selector注册Channel时,Channel.register()
方法会返回一个SelectionKey 对象。这个对象代表了注册到该Selector的通道。

Buffer

NIO中的关键Buffer实现有:ByteBuffer, CharBuffer, DoubleBuffer,
FloatBuffer, IntBuffer, LongBuffer, ShortBuffer,分别对应基本数
据类型: byte, char, double, float, int, long, short。
使用Buffer一般遵循下面几个步骤:

  1. 分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); )
  2. 写入数据到Buffer(int bytesRead = fileChannel.read(buf);)
  3. 调用filp()方法( buf.flip();)在将缓冲区的数据写到输出通道之前
    调用
  4. 从Buffer中读取数据(System.out.print((char)buf.get());)
  5. 调用clear()方法或者compact()方法,调用clear()方法:position将
    被设回0limit设置成capacity,换句话说Buffer被清空了,使用compact()
    方法。该方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最
    后一个未读元素正后面

Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。Channel提供
从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。

  1. 向Buffer中写数据:
  • 从Channel写到Buffer (fileChannel.read(buf))
  • 通过Buffer的put()方法 (buf.put(…))
  1. 从Buffer中读取数据:
  • 从Buffer读取到Channel (channel.write(buf))
  • 使用get()方法从Buffer中读取数据 (buf.get())

可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来
保存这个数据的当前位置状态:capacity, position, limit, mark:

  1. capacity 缓冲区数组的总长度
  2. position 下一个要操作的数据元素的位置
  3. limit 缓冲区数组中不可操作的下一个元素的位置:limit<=capacity
  4. mark用于记录当前position的前一个位置或者默认是-1

Selector

Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每
个连接的流量都很低,使用Selector 就会很方便。例如在一个聊天服务器中。
要使用Selector, 得向Selector注册Channel,然后调用它的select() 方
法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,
线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选
择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让
一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还
未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找
到 IO 事件已经到达的 Channel 执行。

  1. 创建选择器
    1
    Selector selector = Selector.open();
  2. 将通道注册到选择器上
    1
    2
    3
    ServerSocketChannel ssChannel = ServerSocketChannel.open();
    ssChannel.configureBlocking(false);
    ssChannel.register(selector, SelectionKey.OP_ACCEPT);
    通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道
    在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理
    完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
    在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
  • SelectionKey.OP_CONNECT
  • SelectionKey.OP_ACCEPT
  • SelectionKey.OP_READ
  • SelectionKey.OP_WRITE
  1. 监听事件
    1
    int num = selector.select();
    使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
  2. 获取到达的事件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
    // ...
    } else if (key.isReadable()) {
    // ...
    }
    keyIterator.remove();
    }
  3. 事件循环
    因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一
    直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    while (true) {
    int num = selector.select();
    Set<SelectionKey> keys = selector.selectedKeys();
    Iterator<SelectionKey> keyIterator = keys.iterator();
    while (keyIterator.hasNext()) {
    SelectionKey key = keyIterator.next();
    if (key.isAcceptable()) {
    // ...
    } else if (key.isReadable()) {
    // ...
    }
    keyIterator.remove();
    }
    }

内存映射文件

JAVA处理大文件,一般用BufferedReader,BufferedInputStream这类带缓冲
的IO类,不过如果文件超大的话,更快的方式是采用MappedByteBuffer。
MappedByteBuffer是NIO引入的文件内存映射方案,读写性能极高。NIO最主要
的就是实现对异步操作支持。其中一种通过把一个套接字通道(SocketChannel)
注册到一个选择器(Selector)中,不时调用后者的选择(select)方法就能返回
满足的选择键(SelectionKey),键中包含了SOCKET事件信息。这就是select模
型。
SocketChannel的读写是通过一个类叫ByteBuffer来操作的。ByteBuffer有两
种模式:直接/间接。间接模式最典型(也只有这么一种)的就是HeapByteBuffer
,即操作堆内存 (byte[]).但是内存毕竟有限,这时就必须使用”直接”模式,即
MappedByteBuffer,文件映射。
FileChannel提供了map方法来把文件影射为内存映像文件:MappedByteBuffer
map(int mode,long position,long size);可以把文件的从position开始的
size大小的区域映射为内存映像文件

NIO实现零拷贝

NIO的零拷贝由transferTo()方法实现。该方法将数据从FileChannel对象传
送到可写的字节通道(如Socket Channel等)。在内部实现中,由native方
法transferTo0()来实现,它依赖底层操作系统的支持。在UNIX和Linux系统
中,调用这个方法将会引起sendfile()系统调用。

1
2
3
4
5
6
File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
SocketChannel socketChannel= SocketChannel.open(new InetSocketAddress("", 1234));
// 直接使用了transferTo()进行通道间的数据传输
fileChannel.transferTo(0, fileChannel.size(), socketChannel);

NIO的直接内存

它的作用位置处于传统IO(BIO)与零拷贝之间

  1. IO,可以把磁盘的文件经过内核空间,读到JVM空间,然后进行各种操作,
    最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操作
  2. 零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。
    由于它没有读取文件数据到JVM这一环,因此程序无法操作该文件数据,尽
    管效率很高

而直接内存则介于两者之间,效率一般且可操作文件数据。直接内存(mmap技
术)将文件直接映射到内核空间的内存,返回==一个操作地址(address)==
,它解决了文件数据需要拷贝到JVM才能进行操作的窘境。而是直接在内核空间
直接进行操作,省去了内核空间拷贝到用户空间这一步操作。

1
2
3
4
5
File file = new File("test.zip");
RandomAccessFile raf = new RandomAccessFile(file, "rw");
FileChannel fileChannel = raf.getChannel();
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_ONLY,
0, fileChannel.size());

由于MappedByteBuffer申请的是堆外内存,因此不受Minor GC控制,只能在
发生Full GC时才能被回收。
直接内存的大小可通过jvm参数来设置:-XX:MaxDirectMemorySize

RPC具体过程

当远程过程调用,简称为RPC,是一个计算机通信协议,它允许运行于一台
计算机的程序调用另一台计算机的子程序,而无需额外地为这个交互作用
编程

  1. 传输效率高(二进制传输),发起调用的一方无需知道RPC的具体实现,如同
    调用本地函数般调用
  2. 通用性不如HTTP好(HTTP是标准协议)
  3. RPC适合内部服务间的通信调用;HTTP适合面向用户与服务间的通信调用

Java IO分为哪几种?

  1. BIO:同步阻塞IO
  2. NIO:同步非阻塞IO
  3. AIO:异步非阻塞IO

BIO 是什么?

  1. 同步阻塞IO,每个客户端的Socket连接请求,服务端都会对应有个处理
    线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当
    于是一个连接一个线程
  2. 使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚
    拟机造成一定压力
  3. 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待
    ,会造成资源的浪费

NIO 是什么?

  1. 同步非阻塞,也就是说如果你调用NIO接口去执行IO操作,其实还是同步
    等待的,但是在底层的IO操作上 ,会对系统内核发起非阻塞IO请求,以非
    阻塞的形式来执行IO。也就是说,如果底层数据没到位,那么内核返回异常
    信息,不会阻塞住,但是NIO接口内部会采用非阻塞方式过一会儿再次调用
    内核发起IO请求,直到成功为止。但是之所以说是同步非阻塞,这里的“同
    步”指的就是因为在你的Java代码调用NIO接口层面是同步的,你还是要同
    步等待底层IO操作真正完成了才可以返回,只不过在执行底层IO的时候采
    用了非阻塞的方式来执行罢了
  2. 服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发
    现某个Socket端口上有数据可读时说明读就绪,则调用该socket连接的相
    应读操作。如果发现某个 Socket端口上有数据可写时说明写就绪,则调用
    该socket连接的相应写操作。如果某个端口的Socket连接已经中断,则调
    用相应的析构方法关闭该端口
  3. 每个线程中包含一个Selector对象,它相当于一个通道管理器,可以实
    现在一个线程中处理多个通道的目的,减少线程的创建数量。远程连接对应
    一个channel,数据的读写通过buffer均在同一个channel中完成,并且
    数据的读写是非阻塞的
  4. 通道创建后需要注册在selector中,同时需要为该通道注册感兴趣事件
    (客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件),
    selector线程需要采用轮训的方式调用selector的select函数,直到所
    有注册通道中有兴趣的事件发生,则返回,否则一直阻塞。而后循环处理
    所有就绪的感兴趣事件。以上步骤解决BIO的两个瓶颈:
  • 不必对每个连接分别创建线程
  • 数据读写非阻塞

Java NIO由以下三个核心部分组成

  1. selector:Selector 允许单线程处理多个Channel。如果你的应用打开
    了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便
    。要使用Selector,得向Selector注册Channel,然后调用他的select 方
    法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回
    ,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等
  2. Channel:基本上所有的IO在NIO中都从一个Channel开始。Channel有点
    像流,数据可以从channel读到buffer,也可以从buffer写到channel
  3. Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个
    容器对象( 含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓
    冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel
    提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由Buffer

NIO 和IO 多路复用的关系?

  1. 实际上,如果基于NIO进行网络通信,采取的就是多路复用的IO模型,这个
    多路复用IO模型针对的是网络通信中的IO场景来说的
  2. 简单来说,就是在基于Socket进行网络通信的时候,如果有多个客户端跟
    你的服务端建立了Socket连接,那你就需要维护多个Socket连接
  3. 而所谓的多路复用IO模型,就是说你的Java代码直接通过一个select函数
    调用,直接会进入一个同步等待的状态。必须在这里同步等待某个Socket连接
    有请求到来,接着你就要同步等着select函数去对底层的多个Socket 连接
    进行轮询,不断的查看各个 Socket 连接谁有请求到达,就可以让select
    函数返回
  4. select函数在底层会通过非阻塞的方式轮询各个Socket,任何一个Socket
    如果没有数据到达,那么非阻塞的特性会立即返回一个信息,然后select函数
    可以轮询下一个Socket,不会阻塞在某个Socket上
  5. 这就是所谓的“同步非阻塞”,但是因为操作系统把上述工作都封装在一个
    select函数调用里了,可以对多路Socket连接同时进行监视,所以就把这种
    模型称之为“IO多路复用”模型
  6. 通过这种IO多路复用的模型,就可以用一个线程,调用一个select函数
    ,然后监视大量的客户端连接

AIO 是什么?

  1. AIO是异步非阻塞IO,相比NIO更进一步,进程读取数据时只负责发送跟接
    收指令,数据的准备工作完全由操作系统来处理
  2. 可以基于AIO API发起一个请求,比如说接收网络数据,AIO API底层会
    基于异步IO模型来调用操作系统内核,此时不需要去管这个IO是否成功了,
    AIO接口会直接返回
  3. BIO、NIO都是同步的,你发起IO请求,都必须同步等待IO操作完成
  4. 不过你需要提供一个回调函数给AIO接口,一旦底层系统内核完成了具体
    的IO请求,比如网络读写之类的,就会回调你提供的回调函数

Reactor模式

多线程IO的致命缺陷

最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新
的套接字连接,如果有那么就调用一个处理函数处理

1
2
3
4
while(true){
socket = accept();
handle(socket)
}

这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么
后面的请求只能被阻塞,服务器的吞吐量太低。之后想到了使用多线程,也就是
很经典的connection per thread,每一个连接用一个线程处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
class BasicModel implements Runnable {
public void run() {
try {
ServerSocket ss =
new ServerSocket(SystemConfig.SOCKET_SERVER_PORT);
while (!Thread.interrupted())
new Thread(new Handler(ss.accept())).start();
//创建新线程来handle
// or, single-threaded, or a thread pool
} catch (IOException ex) { /* ... */ }
}

static class Handler implements Runnable {
final Socket socket;
Handler(Socket s) { socket = s; }
public void run() {
try {
byte[] input = new byte[SystemConfig.INPUT_SIZE];
socket.getInputStream().read(input);
byte[] output = process(input);
socket.getOutputStream().write(output);
} catch (IOException ex) { /* ... */ }
}
private byte[] process(byte[] input) {
byte[] output=null;
/* ... */
return output;
}
}
}

对于每一个请求都分发给一个线程,每个线程中都独自处理上面的流程。tomcat
服务器的早期版本确实是这样实现的。
多线程并发模式,一个连接一个线程的优点是:
一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不
会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线
程只能对应一个socket”的原因。另外有个问题如果一个线程中对应多个socket
连接不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的
,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个
socket被阻塞了,后面的是无法被执行到的。
多线程并发模式,一个连接一个线程的缺点是:
缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数
太高,系统无法承受,而且线程的反复创建-销毁也需要代价。
改进方法是:采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行
数据处理。使用Reactor模式对线程的数量进行控制,一个线程处理大量的事件。

单线程Reactor模型

Java的NIO模式的Selector网络通讯,其实就是一个简单的Reactor模型。可以
说是Reactor模型的朴素原型。负责多路分离套接字,Accept新连接,并分派请
求到Handler处理器中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
static class Server
{
public static void testServer() throws IOException
{
// 1、获取Selector选择器
Selector selector = Selector.open();
// 2、获取通道
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
// 3.设置为非阻塞
serverSocketChannel.configureBlocking(false);
// 4、绑定连接
serverSocketChannel.bind(new InetSocketAddress(SystemConfig.
SOCKET_SERVER_PORT));
// 5、将通道注册到选择器上,并注册的操作为:“接收”操作
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 6、采用轮询的方式,查询获取“准备就绪”的注册过的操作
while (selector.select() > 0)
{
// 7、获取当前选择器中所有注册的选择键(“已经准备就绪的操作”)
Iterator<SelectionKey> selectedKeys = selector.selectedKeys().
iterator();
while (selectedKeys.hasNext())
{
// 8、获取“准备就绪”的时间
SelectionKey selectedKey = selectedKeys.next();
// 9、判断key是具体的什么事件
if (selectedKey.isAcceptable())
{
// 10、若接受的事件是“接收就绪” 操作,就获取客户端连接
SocketChannel socketChannel = serverSocketChannel.
accept();
// 11、切换为非阻塞模式
socketChannel.configureBlocking(false);
// 12、将该通道注册到selector选择器上
socketChannel.register(selector, SelectionKey.OP_READ);
}
else if (selectedKey.isReadable())
{
// 13、获取该选择器上的“读就绪”状态的通道
SocketChannel socketChannel = (SocketChannel)
selectedKey.channel();
// 14、读取数据
ByteBuffer byteBuffer = ByteBuffer.allocate(1024);
int length = 0;
while ((length = socketChannel.read(byteBuffer)) != -1)
{
byteBuffer.flip();
System.out.println(new String(byteBuffer.array(),
0, length));
byteBuffer.clear();
}
socketChannel.close();
}
// 15、移除选择键
selectedKeys.remove();
}
}

// 7、关闭连接
serverSocketChannel.close();
}
public static void main(String[] args) throws IOException
{
testServer();
}
}

实际上的Reactor模式,是基于Java NIO的,在他的基础上,抽象出来两个组件
——Reactor和Handler两个组件:

  1. Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的
    Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。
  2. Handler:将自身(handler)与事件绑定,负责事件的处理,完成channel
    的读入,完成处理业务逻辑后,负责将结果写出channel。

单线程模式的缺点:

  1. 当其中某个 handler 阻塞时,会导致其他所有的 client 的 handler都
    得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收
    新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷,因
    此单线程Reactor模型用的比较少。这种单线程模型不能充分利用多核资源,
    所以实际使用的不多。
  2. 因此单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景

多线程的Reactor

在单线程Reactor模式基础上,做如下改进:

  1. 将Handler处理器的执行放入线程池,多线程进行业务处理。
  2. 而对于Reactor而言,可以仍为单个线程。如果服务器为多核的CPU,为充分
    利用系统资源,可以将Reactor拆分为两个线程。

Java命名规范

驼峰命名法

  1. 大驼峰命名法
    1
    2
    //类名需要使用大驼峰命名法
    ServiceDiscovery、ServiceInstance、LruCacheFactory
  2. 小驼峰命名法
    1
    2
    3
    //方法名、参数名、成员变量、局部变量需要使用小驼峰命名法
    getUserInfo()、createCustomThreadPool()、setNameFormat(String nameFormat)
    uservice userService;

蛇形命名法

测试方法名、常量、枚举名称需要使用蛇形命名法,在蛇形命名法中,各个单词
之间通过下划线“_”连接

1
2
3
4
@Test
void should_get_200_status_code_when_request_is_valid() {
......
}

常见规范

  1. 包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 “.” 分隔符连
    接,并且各个单词必须为单数
  2. 抽象类命名使用 Abstract 开头
  3. 异常类命名使用 Exception 结尾
  4. 测试类命名以它要测试的类的名称开始,以Test 结尾
  5. 项目名全部小写,多个单词用中划线分隔‘-’
Author: 高明
Link: https://skysea-gaoming.github.io/2021/05/30/Java%E6%80%BB%E7%BB%932/
Copyright Notice: All articles in this blog are licensed under CC BY-NC-SA 4.0 unless stating additionally.