枚举
enum关键字在java5中引入表示一种特殊类型的类,其总是继承java.lang.Enum
类,以这种方式定义的常量使代码更具可读性,允许进行编译时检查,预先记录
可接受值的列表,并避免由于传入无效值而引起的意外行为
枚举类
当定义一个枚举类型时,每一个枚举类型成员都可以看作是Enum 类的实例,这些
枚举成员默认都被 final、public、static 修饰,当使用枚举类型成员时,直接
使用枚举名称调用成员即可
1 | values() 以数组形式返回枚举类型的所有成员 |
Java 注解是什么?
Java 注解用于为Java 代码提供元数据。作为元数据,注解不直接影响你的
代码执行,但也有一些类型的注解实际上可以用于这一目的,注解可以放在
类或者方法上,在类、方法、成员变量、形参位置
- 自定义注解 自定义注解就是我们自己写的注解
- JDK内置注解 @Override @Deprecated
- 还有第三方框架提供的注解 SpringMVC的 @Controller
1 | public MyTestAnnotation { |
- @Override,表示当前的方法定义将覆盖超类中的方法
- @Deprecated,如果程序员使用了注解为它的元素,那么编译器会发出警告信息
- @SuppressWarnings,关闭不当的编译器警告信息。在java SE5之前的
版本中,也可以使用该注解,不过会被忽略不起作用。
Java 注解有什么作用?
- 生成帮助文档
- 在编译时进行格式检查。如@override 放在方法前,如果你这个方法并
不是覆盖了超类方法,则编译时就能检查出,@Deprecated标识方法过期 - 提供信息给编译器 编译器可以利用注解来检测出错误或者警告信息,
打印出日志
元注解是什么?
注解的注解,它是作用在注解中,方便我们使用注解实现想要的功能
- @Retention 表示注解存在阶段是保留在源码(编译期),字节码(类
加载)或者运行期(JVM中运行),使用枚举来表示注解保留时期 - @Target 表示注解作用的范围可以是类,方法,方法参数变量
- @Documented 将注解中的元素包含到 Javadoc 中去
- @Inherited 该注解了的注解修饰了一个父类,如果他的子类没有被其
他注解修饰,则它的子类也继承了父类的注解 - @Repeatable 被这个元注解修饰的注解可以同时作用一个对象多次,但
是每次作用注解又可以代表不同的含义
Java IO
I/O(Input/Outpu)即输入/输出。指应用程序和外部设备之间的数据传递,
常见的外部设备包括文件、管道、网络连接。用户空间的程序不能直接访问内核
空间必须通过系统调用来间接访问内核空间,主要是磁盘IO(读写文件)和网
络IO(网络请求和响应)
什么是流
流是一个抽象的概念,是指一连串的数据(字符或字节),是以先进先出的方式
发送信息的通道。流的特性有三点
- 先进先出:最先写入输出流的数据最先被输入流读取到
- 顺序存取:可以一个接一个地往流中写入一串字节,读出时也将按写入顺序
读取一串字节,不能随机访问中间的数据。(RandomAccessFile除外) - 只读或只写:每个流只能是输入流或输出流的一种,不能同时具备两个功能
,输入流只能进行读操作,对输出流只能进行写操作。在一个数据传输通道中,
如果既要写入数据,又要读取数据,则要分别提供两个流
Java IO流的分类
IO流主要的分类方式有以下3种:
- 按数据流的方向:输入流、输出流
- 按处理数据单位:字节流、字符流
- 按功能:节点流、处理流
输入流与输出流
输入与输出是相对于应用程序而言的,比如文件读写,读取文件是输入流,写文
件是输出流,这点很容易搞反
字节流与字符流
字节流和字符流的用法几乎完全一样,区别在于字节流和字符流所操作的数据
单元不同,字节流操作的单元是数据单元是8位的字节,字符流操作的是数据
单元为16位的字符
为什么要有字符流
Java中字符是采用Unicode标准,Unicode 编码中,一个英文为一个字节,一
个中文为两个字节。而在UTF-8编码中,一个中文字符是3个字节。如果使用字节
流处理中文,如果一次读写一个字符对应的字节数就不会有问题,一旦将一个
字符对应的字节分裂开来,就会出现乱码了。并且如果我们不知道编码类型就
很容易出现乱码问题。为了更方便地处理中文这些字符,Java 就推出了字符
流。字节流和字符流的其他区别如下
- 字节流一般用来处理图像、视频、音频、PPT、Word等类型的文件。字符流一
般用于处理纯文本类型的文件,如TXT文件等,但不能处理图像视频等非文本
文件。用一句话说就是:字节流可以处理一切文件,而字符流只能处理纯文本
文件 - 字节流本身没有缓冲区,缓冲字节流相对于字节流,效率提升非常高。而字
符流本身就带有缓冲区,缓冲字符流相对于字符流效率提升就不是那么大了
节点流和处理流
- 节点流:直接操作数据读写的流类,比如FileInputStream
- 处理流:对一个已存在的流的链接和封装,通过对数据进行处理为程序提供
功能强大、灵活的读写功能,例如BufferedInputStream(缓冲字节流)
程序与磁盘的交互相对于内存运算是很慢的,容易成为程序的性能瓶颈。减少程
序与磁盘的交互,是提升程序效率一种有效手段。缓冲流,就应用这种思路:普
通流每次读写一个字节,而缓冲流在内存中设置一个缓存区,缓冲区先存储足
够的待操作数据后,再与内存或磁盘进行交互。这样在总数据量不变的情况下
,通过提高每次交互的数据量,减少了交互次数
字节流
InputStream与OutputStream是两个抽象类,是字节流的基类,所有具体的字节
流实现类都是分别继承了这两个类
字符流
与字节流类似,字符流也有两个抽象基类,分别是Reader和Writer。其他的字符
流实现类都是继承了这两个类
Java IO使用的设计模式
使用了适配器模式和装饰器模式
- 适配器模式 把一个类的接口变换成客户端所期待的另一种接口,从而
使原本因接口不匹配而无法在一起工作的两个类能够在一起工作1
2
3
4//实现从字节流解码成字符流;
Reader reader = new InputStreamReader(inputStream);
//字节输出流转字符输出流通过
Writer writer = new OutputStreamWriter(outputStream); - 装饰器模式 一种动态地往一个类中添加新的行为的设计模式。就功能而
言,装饰器模式相比生成子类更为灵活,这样可以给某个对象而不是整个
类添加一些功能,BufferedInputStream 为FileInputStream 提供缓
存的功能1
new BufferedInputStream(new FileInputStream(inputStream));
获取用键盘输入常用的两种方法
- 通过 Scanner
1
2
3Scanner input = new Scanner(System.in);
String s = input.nextLine();
input.close(); - 通过 BufferedReader
1
2BufferedReader input = new BufferedReader(new InputStreamReader(System.in));
String s = input.readLine();
阻塞和非阻塞IO
IO操作包括:对硬盘的读写、对socket的读写以及外围设备的读写。
当用户线程发起一个IO请求操作,内核会去查看要读取的数据是否就绪,对于阻
塞IO来说,如果数据没有就绪,则会一直在那等待,直到数据就绪,对于非阻塞
IO来说,如果数据没有就绪,则会返回一个标志信息告知用户线程当前要读的数
据没有就绪。当数据就绪之后,便将数据拷贝到用户线程,这样才完成了一个完
整的IO读请求操作,也就是说一个完整的IO读请求操作包括两个阶段:
- 查看数据是否就绪
- 进行数据拷贝(内核将数据拷贝到用户线程)
那么阻塞和非阻塞的区别就在于第一个阶段,如果数据没有就绪,在查看数据
是否就绪的过程中是一直等待,还是直接返回一个标志信息。Java中传统的IO
都是阻塞IO,比如通过socket来读数据,调用read()方法之后,如果数据没
有就绪,当前线程就会一直阻塞在read 方法调用那里,直到有数据才返回,
而如果是非阻塞IO的话,当数据没有就绪,read()方法应该返回一个标志信
息,告知当前线程数据没有就绪,而不是一直在那里等待
同步和异步IO
- 同步IO:当用户发出IO请求操作后,内核会去查看要读取的数据是否就
绪,如果没有,就一直等待。期间用户线程或内存会不断地轮询数据是否就
绪。当数据就绪时,再把数据从内核拷贝到用户空间 - 异步IO:用户线程只需发出IO请求和接收IO操作完成通知,期间的IO操
作由内核自动完成,并发送通知告知用户线程IO操作已经完成。也就是说,
在异步IO中,并不会对用户线程产生任何阻塞
Java 常见IO 模型
- BIO(Blocking I/O) BIO属于同步阻塞IO 模型。同步阻塞IO 模型中,应用
程序发起 read 调用后会一直阻塞,直到在内核把数据拷贝到用户空间。在客户
端连接数量不高的情况下是没问题的。但是当面对十万甚至百万级连接的时候,
传统的 BIO 模型是无能为力的。因此我们需要一种更高效的I/O 处理模型来应
对更高的并发量 - NIO(Non-blocking/New I/O) 对应java.nio包,提供了Channel, Selector
,Buffer 等抽象。NIO 中的N 可以理解为Non-blocking,不单纯是New。它支持面
向缓冲的,基于通道的 I/O 操作方法。 对于高负载、高并发的(网络)应用,应使
用NIO。NIO 可以看作是I/O 多路复用模型 - AIO (Asynchronous I/O) 异步IO 模型。异步IO 是基于事件和回调机制实
现的,也就是应用操作之后会直接返回,不会堵塞在那里,当后台处理完成,操
作系统会通知相应的线程进行后续的操作
BIO,NIO,AIO 有什么区别
- BIO:Block IO 同步阻塞式IO,就是我们平常使用的传统IO,它的特点
是模式简单使用方便,并发处理能力低。在服务器中实现的模式为一个连接
一个线程。也就是说客户端有连接请求的时候,服务器就需要启动一个线程
进行处理,如果这个连接不做任何事情会造成不必要的线程开销,当然这
也可以通过线程池机制改善。BIO 一般适用于连接数目小且固定的架构,
这种方式对于服务器资源要求比较高,而且并发局限于应用中,是
JDK1.4之前的唯一选择,但好在程序直观简单,易理解 - NIO:Non IO 同步非阻塞 IO,是传统 IO 的升级,客户端和服务器端
通过Channel(通道)通讯,实现了多路复用。同步并非阻塞,在服务器中
实现的模式为一个请求一个线程,也就是说客户端发送的连接请求都会注
册到多路复用器上,多路复用器轮询到有连接IO请求时才会启动一个线程
进行处理。NIO一般适用于连接数目多且连接比较短(轻操作)的架构,并
发局限于应用中,编程比较复杂,从JDK1.4开始支持 - AIO:Asynchronous IO 是 NIO 的升级,也叫 NIO2,实现了异步非
阻塞IO ,异步 IO 的操作基于事件和回调机制。异步并非阻塞,在服务器
中实现的模式为一个有效请求一个线程,也就是说,客户端的IO请求都是
通过操作系统先完成之后,再通知服务器应用去启动线程进行处理。AIO
一般适用于连接数目多且连接比较长(重操作)的架构,充分调用操作
系统参与并发操作,编程比较复杂,从JDK1.7开始支持
有了解过Java中的NIO吗?原理是什么?
- 基于直接内存实现
- NIO底层原理,同步非阻塞的IO模型,它是面向缓冲区的
NIO
NIO实现高性能处理的原理是使用较少的线程处理更多的任务
NIO和IO的区别
- IO是面向流的,NIO是面向缓冲区的。 Java IO 面向流意味着每次从流中
读一个或多个字节,直至读取所有字节,它们没有被缓存在任何地方。NIO 的
缓冲导向方法略有不同。数据读取到一个它稍后处理的缓冲区,一次处理一个
数据块 - IO的各种流是阻塞的。这意味着当一个线程调用read()或write()时,该
线程被阻塞,直到有一些数据被读取或写入。该线程在此期间不能再干任何事
情了。 NIO 的非阻塞模式使一个线程从某通道发送请求读取数据,但是它仅
能得到目前可用的数据,如果目前没有数据可用时就什么都不会获取。而不
是保持线程阻塞,所以直至数据变得可以读取之前,该线程可以继续做其他
的事情。 非阻塞写也是如此。一个线程请求写入一些数据到某通道,但不需
要等待它完全写入,这个线程同时可以去做别的事情。 线程通常将非阻塞IO
的空闲时间用于在其它通道上执行IO操作,所以一个单独的线程现在可以管
理多个输入和输出通道 - 常规io使用的byte[]、char[]进行封装,而NIO采用ByteBuffer类来操
作数据,再结合针对File或socket技术的channel,采用同步非阻塞技术来
实现高性能处理,而Netty正是采用ByteBuffer(缓冲区)、Channel(通
道)、Selector(选择器)进行封装的 - 传统IO基于字节流和字符流进行操作,NIO基于Channel和Buffer(缓冲区)
进行操作,数据总是从通道读取到缓冲区中,或者从缓冲区写入到通道中。
Selector(选择区)用于监听多个通道的事件(比如:连接打开,数据到
达)。因此单个线程可以监听多个数据通道。
为什么使用NIO
- 传统的服务器端同步阻塞 I/O 处理使用多线程,主要原因在于socket.
accept()、socket.read()、socket.write() 三个主要函数都是同步阻塞
的,当一个连接在处理I/O 的时候,系统是阻塞的,如果是单线程的话必然
就挂死在那里;但CPU是被释放出来的,开启多线程,就可以让CPU去处理更
多的事情。其实这也是所有使用多线程的本质: 1. 利用多核。 2. 当I/O
阻塞系统,但CPU空闲的时候,可以利用多线程使用CPU资源。 - 现在的多线程一般都使用线程池,可以让线程的创建和回收成本相对较低
。在活动连接数不是特别高(小于单机1000)的情况下,这种模型是比较不
错的,可以让每一个连接专注于自己的I/O并且编程模型简单,也不用过多
考虑系统的过载、限流等问题。线程池本身就是一个天然的漏斗,可以缓冲
一些系统处理不了的连接或请求。 - 这个模型最本质的问题在于严重依赖于线程。但线程是很”贵”的资源,主
要表现在: 1. 线程的创建和销毁成本很高,在Linux这样的操作系统中,
线程本质上就是一个进程。创建和销毁都是重量级的系统函数。 2. 线程本
身占用较大内存,像Java的线程栈,一般至少分配512K~1M的空间,如果
系统中的线程数过千,恐怕整个JVM的内存都会被吃掉一半。 3. 线程的切
换成本是很高的。操作系统发生线程切换的时候,需要保留线程的上下文,
然后执行系统调用。如果线程数过高,可能执行线程切换的时间甚至会大于
线程执行的时间,这时候带来的表现往往是系统load偏高、CPU sy使用率
特别高(超过20%以上),导致系统几乎陷入不可用的状态。 4. 容易造成
锯齿状的系统负载。因为系统负载是用活动线程数或CPU核心数,一旦线程
数量高但外部网络环境不是很稳定,就很容易造成大量请求的结果同时返
回,激活大量阻塞线程从而使系统负载压力过大。 - NIO一个重要的特点是:socket主要的读、写、注册和接收函数,在等待
就绪阶段都是非阻塞的,真正的I/O 操作是同步阻塞的(消耗CPU但性能非
常高)。回忆BIO模型,之所以需要多线程,是因为在进行I/O操作的时候,
一是没有办法知道到底能不能写、能不能读,只能”傻等”,即使通过各种估
算,算出来操作系统没有能力进行读写,也没法在socket.read()和socket
.write()函数中返回,这两个函数无法进行有效的中断。所以除了多开线程
另起炉灶,没有好的办法利用CPU - 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 | //IO线程主循环: |
优化线程模型
NIO由原来的阻塞读写(占用线程)变成了单线程轮询事件,找到可以进行读
写的网络描述符进行读写。除了事件的轮询是阻塞的(没有可干的事情必须要
阻塞),剩余的I/O操作都是纯CPU操作,没有必要开启多线程。
Proactor与Reactor
一般情况下,I/O 复用机制需要事件分发器。事件分发器的作用,即将那些读写
事件源分发给各读写事件的处理者,开发人员在开始的时候需要在分发器那里
注册感兴趣的事件,并提供相应的处理者(event handler),或者是回调函
数;事件分发器在适当的时候,会将请求的事件分发给这些handler或者回调
函数。涉及到事件分发器的两种模式称为:Reactor和Proactor。
- Reactor模式是基于同步I/O的,而Proactor模式是和异步I/O相关的。在
Reactor 模式中,事件分发器等待某个事件或者可应用或可操作的状态发生
(比如文件描述符可读写,或者是socket可读写),事件分发器就把这个事
件传给事先注册的事件处理函数或者回调函数,由后者来做实际的读写操作。 - 而在Proactor模式中,事件处理者(或者代由事件分发器发起)直接发起
一个异步读写操作(相当于请求),而实际的工作是由操作系统来完成的。发
起时,需要提供的参数包括用于存放读到数据的缓存区、读的数据大小或用于
存放外发数据的缓存区,以及这个请求完后的回调函数等信息。事件分发器得
知了这个请求,它默默等待这个请求的完成,然后转发完成事件给相应的事
件处理者或者回调。举例来说,在Windows上事件处理者投递了一个异步IO
操作(称为overlapped技术),事件分发器等IO Complete事件完成。这
种异步模式的典型实现是基于操作系统底层异步API的,所以我们可称之
为“系统级别”的或者“真正意义上”的异步,因为具体的读写是由操作系
统代劳的。 - Reactor模式:主动模式,所谓主动,是指应用程序不断轮询,询问操作系
统或者网络框架,IO是否就绪。其中java的NIO就属于这种模式。在这种模式下
,实际的IO操作还是应用程序执行的。 - Procator模式:被动模式,应用程序的读写函数操作交给操作系统或者
网络框架,实际IO操作由操作系统或者网络框架完成,之后再回调应用程序
。微软的asio库就是这种模式。
事件驱动模型
- 采用轮询的方式:线程不断轮询询问相关事件发生源有没有发生事件,有发
生事件就调用事件处理逻辑 - 事件驱动方式:发生事件,主线程把事件加入到事件队列,在另外线程不断
循环消费事件列表的事件,调用事件对应的处理逻辑事件。事件驱动方式也被称
为消息通知方式,其实是设计模式中观察者模式的思路。基于事件驱动的优点:
可扩展性好,高性能。
在Reactor中实现读
- 注册读就绪事件和相应的事件处理器。
- 事件分发器等待事件。
- 事件到来,激活分发器,分发器调用事件对应的处理器。
- 事件处理器完成实际的读操作,处理读到的数据,注册新的事件,然后返
还控制权。
Reactor线程模型中2个关键组成:
- Reactor:Reactor在一个单独的线程运行,负责监听和分发事件,分发给适
当的处理程序来对IO事件做出反应。 - Handlers:处理程序执行I/O事件要完成的实际事件。
Reactor的三种模型:单Reactor单线程、单Reactor多线程、主从Reactor多线程。
- 单 Reactor 单线程,前台接待员和服务员是同一个人,全程为顾客服
- 单 Reactor 多线程,1 个前台接待员,多个服务员,接待员只负责接待
- 主从 Reactor 多线程,多个前台接待员,多个服务生
Netty主要基于主从Reactor多线程模型,主从Reactor主从模型有多个Reactor:
1 | // MainReactor负责客户端的连接请求,并将请求转交给SubReactor |
bossGroup线程池则只是在bind某个端口后,获得其中一个线程作为MainReactor
,专门处理端口的Accept事件,每个端口对应一个Boss线程,workerGroup线程
池会被各个SubReactor和Worker线程充分利用。
- NioEventLoop:维护了一个线程和任务队列,支持异步提交执行任务,线程启动时会调用
- NioEventLoop的run方法,执行I/O任务和非I/O任务:
任务分为两种
- I/O任务:即selectionKey中的ready的事件,如accept、connect、read、
write等,由processSelectKeys方法触发。 - 非I/O任务:添加到taskQueue中的任务,如register0、bind0等任务,由
runAllTasks方法触发。
在Proactor中实现读
- 处理器发起异步读操作(注意:操作系统必须支持异步IO)。在这种情况下
,处理器无视IO就绪事件,它关注的是完成事件。 - 事件分发器等待操作完成事件。
- 在分发器等待过程中,操作系统利用并行的内核线程执行实际的读操作,并
将结果数据存入用户自定义缓冲区,最后通知事件分发器读操作完成。 - 事件分发器呼唤处理器。
- 事件处理器处理用户自定义缓冲区中的数据,然后启动一个新的异步操作,
并将控制权返回事件分发器。
Channel
Channel和IO中的Stream(流)是差不多一个等级的。只不过Stream是单向的
,譬如:InputStream, OutputStream。而Channel是双向的,既可以用来
进行读操作,又可以用来进行写操作。
在Java NIO 中的通道(Channel)就相当于操作系统的内核空间(kernel
space)的缓冲区,而缓冲区(Buffer)对应的相当于操作系统的用户空间
(user space)中的用户缓冲区(user buffer)
- FileChannel 对应文件IO
- DatagramChannel 对应UDP
- SocketChannel 对应TCP Client
- 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一般遵循下面几个步骤:
- 分配空间(ByteBuffer buf = ByteBuffer.allocate(1024); )
- 写入数据到Buffer(int bytesRead = fileChannel.read(buf);)
- 调用filp()方法( buf.flip();)在将缓冲区的数据写到输出通道之前
调用 - 从Buffer中读取数据(System.out.print((char)buf.get());)
- 调用clear()方法或者compact()方法,调用clear()方法:position将
被设回0limit设置成capacity,换句话说Buffer被清空了,使用compact()
方法。该方法将所有未读的数据拷贝到Buffer起始处。然后将position设到最
后一个未读元素正后面
Buffer顾名思义:缓冲区,实际上是一个容器,一个连续数组。Channel提供
从文件、网络读取数据的渠道,但是读写的数据都必须经过Buffer。
- 向Buffer中写数据:
- 从Channel写到Buffer (fileChannel.read(buf))
- 通过Buffer的put()方法 (buf.put(…))
- 从Buffer中读取数据:
- 从Buffer读取到Channel (channel.write(buf))
- 使用get()方法从Buffer中读取数据 (buf.get())
可以把Buffer简单地理解为一组基本数据类型的元素列表,它通过几个变量来
保存这个数据的当前位置状态:capacity, position, limit, mark:
- capacity 缓冲区数组的总长度
- position 下一个要操作的数据元素的位置
- limit 缓冲区数组中不可操作的下一个元素的位置:limit<=capacity
- mark用于记录当前position的前一个位置或者默认是-1
Selector
Selector运行单线程处理多个Channel,如果你的应用打开了多个通道,但每
个连接的流量都很低,使用Selector 就会很方便。例如在一个聊天服务器中。
要使用Selector, 得向Selector注册Channel,然后调用它的select() 方
法。这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回,
线程就可以处理这些事件,事件的例子有如新的连接进来、数据接收等。
NIO 实现了 IO 多路复用中的 Reactor 模型,一个线程 Thread 使用一个选
择器 Selector 通过轮询的方式去监听多个通道 Channel 上的事件,从而让
一个线程就可以处理多个事件。
通过配置监听的通道 Channel 为非阻塞,那么当 Channel 上的 IO 事件还
未到达时,就不会进入阻塞状态一直等待,而是继续轮询其它 Channel,找
到 IO 事件已经到达的 Channel 执行。
- 创建选择器
1
Selector selector = Selector.open();
- 将通道注册到选择器上通道必须配置为非阻塞模式,否则使用选择器就没有任何意义了,因为如果通道
1
2
3ServerSocketChannel ssChannel = ServerSocketChannel.open();
ssChannel.configureBlocking(false);
ssChannel.register(selector, SelectionKey.OP_ACCEPT);
在某个事件上被阻塞,那么服务器就不能响应其它事件,必须等待这个事件处理
完毕才能去处理其它事件,显然这和选择器的作用背道而驰。
在将通道注册到选择器上时,还需要指定要注册的具体事件,主要有以下几类:
- SelectionKey.OP_CONNECT
- SelectionKey.OP_ACCEPT
- SelectionKey.OP_READ
- SelectionKey.OP_WRITE
- 监听事件使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
1
int num = selector.select();
- 获取到达的事件
1
2
3
4
5
6
7
8
9
10
11Set<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();
} - 事件循环
因为一次 select() 调用不能处理完所有的事件,并且服务器端有可能需要一
直监听事件,因此服务器端处理事件的代码一般会放在一个死循环内1
2
3
4
5
6
7
8
9
10
11
12
13
14while (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 | File file = new File("test.zip"); |
NIO的直接内存
它的作用位置处于传统IO(BIO)与零拷贝之间
- IO,可以把磁盘的文件经过内核空间,读到JVM空间,然后进行各种操作,
最后再写到磁盘或是发送到网络,效率较慢但支持数据文件操作 - 零拷贝则是直接在内核空间完成文件读取并转到磁盘(或发送到网络)。
由于它没有读取文件数据到JVM这一环,因此程序无法操作该文件数据,尽
管效率很高
而直接内存则介于两者之间,效率一般且可操作文件数据。直接内存(mmap技
术)将文件直接映射到内核空间的内存,返回==一个操作地址(address)==
,它解决了文件数据需要拷贝到JVM才能进行操作的窘境。而是直接在内核空间
直接进行操作,省去了内核空间拷贝到用户空间这一步操作。
1 | File file = new File("test.zip"); |
由于MappedByteBuffer申请的是堆外内存,因此不受Minor GC控制,只能在
发生Full GC时才能被回收。
直接内存的大小可通过jvm参数来设置:-XX:MaxDirectMemorySize
RPC具体过程
当远程过程调用,简称为RPC,是一个计算机通信协议,它允许运行于一台
计算机的程序调用另一台计算机的子程序,而无需额外地为这个交互作用
编程
- 传输效率高(二进制传输),发起调用的一方无需知道RPC的具体实现,如同
调用本地函数般调用 - 通用性不如HTTP好(HTTP是标准协议)
- RPC适合内部服务间的通信调用;HTTP适合面向用户与服务间的通信调用
Java IO分为哪几种?
- BIO:同步阻塞IO
- NIO:同步非阻塞IO
- AIO:异步非阻塞IO
BIO 是什么?
- 同步阻塞IO,每个客户端的Socket连接请求,服务端都会对应有个处理
线程与之对应,对于没有分配到处理线程的连接就会被阻塞或者拒绝。相当
于是一个连接一个线程 - 使用一个独立的线程维护一个socket连接,随着连接数量的增多,对虚
拟机造成一定压力 - 使用流来读取数据,流是阻塞的,当没有可读/可写数据时,线程等待
,会造成资源的浪费
NIO 是什么?
- 同步非阻塞,也就是说如果你调用NIO接口去执行IO操作,其实还是同步
等待的,但是在底层的IO操作上 ,会对系统内核发起非阻塞IO请求,以非
阻塞的形式来执行IO。也就是说,如果底层数据没到位,那么内核返回异常
信息,不会阻塞住,但是NIO接口内部会采用非阻塞方式过一会儿再次调用
内核发起IO请求,直到成功为止。但是之所以说是同步非阻塞,这里的“同
步”指的就是因为在你的Java代码调用NIO接口层面是同步的,你还是要同
步等待底层IO操作真正完成了才可以返回,只不过在执行底层IO的时候采
用了非阻塞的方式来执行罢了 - 服务器端保存一个Socket连接列表,然后对这个列表进行轮询,如果发
现某个Socket端口上有数据可读时说明读就绪,则调用该socket连接的相
应读操作。如果发现某个 Socket端口上有数据可写时说明写就绪,则调用
该socket连接的相应写操作。如果某个端口的Socket连接已经中断,则调
用相应的析构方法关闭该端口 - 每个线程中包含一个Selector对象,它相当于一个通道管理器,可以实
现在一个线程中处理多个通道的目的,减少线程的创建数量。远程连接对应
一个channel,数据的读写通过buffer均在同一个channel中完成,并且
数据的读写是非阻塞的 - 通道创建后需要注册在selector中,同时需要为该通道注册感兴趣事件
(客户端连接服务端事件、服务端接收客户端连接事件、读事件、写事件),
selector线程需要采用轮训的方式调用selector的select函数,直到所
有注册通道中有兴趣的事件发生,则返回,否则一直阻塞。而后循环处理
所有就绪的感兴趣事件。以上步骤解决BIO的两个瓶颈:
- 不必对每个连接分别创建线程
- 数据读写非阻塞
Java NIO由以下三个核心部分组成
- selector:Selector 允许单线程处理多个Channel。如果你的应用打开
了多个连接(通道),但每个连接的流量都很低,使用Selector就会很方便
。要使用Selector,得向Selector注册Channel,然后调用他的select 方
法,这个方法会一直阻塞到某个注册的通道有事件就绪。一旦这个方法返回
,线程就可以处理这些事件,事件的例子入有新连接接进来,数据接收等 - Channel:基本上所有的IO在NIO中都从一个Channel开始。Channel有点
像流,数据可以从channel读到buffer,也可以从buffer写到channel - Buffer:缓冲区本质上是一个可以读写数据的内存块,可以理解成是一个
容器对象( 含数组),该对象提供了一组方法,可以更轻松的使用内存块,缓
冲区对象内置了一些机制,能够跟踪和记录缓冲区的状态变换情况,Channel
提供从文件,网络读取数据的渠道,但是读取或者写入的数据都必须经由Buffer
NIO 和IO 多路复用的关系?
- 实际上,如果基于NIO进行网络通信,采取的就是多路复用的IO模型,这个
多路复用IO模型针对的是网络通信中的IO场景来说的 - 简单来说,就是在基于Socket进行网络通信的时候,如果有多个客户端跟
你的服务端建立了Socket连接,那你就需要维护多个Socket连接 - 而所谓的多路复用IO模型,就是说你的Java代码直接通过一个select函数
调用,直接会进入一个同步等待的状态。必须在这里同步等待某个Socket连接
有请求到来,接着你就要同步等着select函数去对底层的多个Socket 连接
进行轮询,不断的查看各个 Socket 连接谁有请求到达,就可以让select
函数返回 - select函数在底层会通过非阻塞的方式轮询各个Socket,任何一个Socket
如果没有数据到达,那么非阻塞的特性会立即返回一个信息,然后select函数
可以轮询下一个Socket,不会阻塞在某个Socket上 - 这就是所谓的“同步非阻塞”,但是因为操作系统把上述工作都封装在一个
select函数调用里了,可以对多路Socket连接同时进行监视,所以就把这种
模型称之为“IO多路复用”模型 - 通过这种IO多路复用的模型,就可以用一个线程,调用一个select函数
,然后监视大量的客户端连接
AIO 是什么?
- AIO是异步非阻塞IO,相比NIO更进一步,进程读取数据时只负责发送跟接
收指令,数据的准备工作完全由操作系统来处理 - 可以基于AIO API发起一个请求,比如说接收网络数据,AIO API底层会
基于异步IO模型来调用操作系统内核,此时不需要去管这个IO是否成功了,
AIO接口会直接返回 - BIO、NIO都是同步的,你发起IO请求,都必须同步等待IO操作完成
- 不过你需要提供一个回调函数给AIO接口,一旦底层系统内核完成了具体
的IO请求,比如网络读写之类的,就会回调你提供的回调函数
Reactor模式
多线程IO的致命缺陷
最最原始的网络编程思路就是服务器用一个while循环,不断监听端口是否有新
的套接字连接,如果有那么就调用一个处理函数处理
1 | while(true){ |
这种方法的最大问题是无法并发,效率太低,如果当前的请求没有处理完,那么
后面的请求只能被阻塞,服务器的吞吐量太低。之后想到了使用多线程,也就是
很经典的connection per thread,每一个连接用一个线程处理
1 | class BasicModel implements Runnable { |
对于每一个请求都分发给一个线程,每个线程中都独自处理上面的流程。tomcat
服务器的早期版本确实是这样实现的。
多线程并发模式,一个连接一个线程的优点是:
一定程度上极大地提高了服务器的吞吐量,因为之前的请求在read阻塞以后,不
会影响到后续的请求,因为他们在不同的线程中。这也是为什么通常会讲“一个线
程只能对应一个socket”的原因。另外有个问题如果一个线程中对应多个socket
连接不行吗?语法上确实可以,但是实际上没有用,每一个socket都是阻塞的
,所以在一个线程里只能处理一个socket,就算accept了多个也没用,前一个
socket被阻塞了,后面的是无法被执行到的。
多线程并发模式,一个连接一个线程的缺点是:
缺点在于资源要求太高,系统中创建线程是需要比较高的系统资源的,如果连接数
太高,系统无法承受,而且线程的反复创建-销毁也需要代价。
改进方法是:采用基于事件驱动的设计,当有事件触发时,才会调用处理器进行
数据处理。使用Reactor模式对线程的数量进行控制,一个线程处理大量的事件。
单线程Reactor模型
Java的NIO模式的Selector网络通讯,其实就是一个简单的Reactor模型。可以
说是Reactor模型的朴素原型。负责多路分离套接字,Accept新连接,并分派请
求到Handler处理器中。
1 | static class Server |
实际上的Reactor模式,是基于Java NIO的,在他的基础上,抽象出来两个组件
——Reactor和Handler两个组件:
- Reactor:负责响应IO事件,当检测到一个新的事件,将其发送给相应的
Handler去处理;新的事件包含连接建立就绪、读就绪、写就绪等。 - Handler:将自身(handler)与事件绑定,负责事件的处理,完成channel
的读入,完成处理业务逻辑后,负责将结果写出channel。
单线程模式的缺点:
- 当其中某个 handler 阻塞时,会导致其他所有的 client 的 handler都
得不到执行, 并且更严重的是, handler 的阻塞也会导致整个服务不能接收
新的 client 请求(因为 acceptor 也被阻塞了)。 因为有这么多的缺陷,因
此单线程Reactor模型用的比较少。这种单线程模型不能充分利用多核资源,
所以实际使用的不多。 - 因此单线程模型仅仅适用于handler 中业务处理组件能快速完成的场景
多线程的Reactor
在单线程Reactor模式基础上,做如下改进:
- 将Handler处理器的执行放入线程池,多线程进行业务处理。
- 而对于Reactor而言,可以仍为单个线程。如果服务器为多核的CPU,为充分
利用系统资源,可以将Reactor拆分为两个线程。
Java命名规范
驼峰命名法
- 大驼峰命名法
1
2//类名需要使用大驼峰命名法
ServiceDiscovery、ServiceInstance、LruCacheFactory - 小驼峰命名法
1
2
3//方法名、参数名、成员变量、局部变量需要使用小驼峰命名法
getUserInfo()、createCustomThreadPool()、setNameFormat(String nameFormat)
uservice userService;
蛇形命名法
测试方法名、常量、枚举名称需要使用蛇形命名法,在蛇形命名法中,各个单词
之间通过下划线“_”连接
1 |
|
常见规范
- 包名统一使用小写,尽量使用单个名词作为包名,各个单词通过 “.” 分隔符连
接,并且各个单词必须为单数 - 抽象类命名使用 Abstract 开头
- 异常类命名使用 Exception 结尾
- 测试类命名以它要测试的类的名称开始,以Test 结尾
- 项目名全部小写,多个单词用中划线分隔‘-’