Java IO
JavaWeb应用中最主要的技术之一就是IO流,甚至可以这样说,大部分的web系统的瓶颈就是流的瓶颈,由流这一个技术,就可以延伸出磁盘、网络、同步异步、阻塞非阻塞等;
IO流说白了就是管着人机交互的使者!
JavaIO流的基本结构
Java如何和磁盘交互的
这就要设计到操作系统的知识了,无论是读文件还是写文件都要用到操作系统提供的接口,先不看Java如何做,先复习一下应用程序都是怎么干的?
应用程序访问设备都是用过系统调用的方式来做的:read()
和write()
,都知道只要是涉及到系统调用就存在内核空间地址和用户空间地址切换的问题,IO操作不是一般的耗时,数据从磁盘找出来再复制到内核空间,再从内核复制到用户空间,步骤多再加上本身速度就不快,所以就相当费时间,为了提高速度加上了缓存;
一般来说应用程序访问磁盘都是用以下几种方式:
1、标准访问文件的方式
读操作也就是应用程序调用Read()
接口,操作系统会先检查缓存中有没有数据,有就直接从缓存拿,如果没有再从磁盘中读取,读取完之后再缓存起来;
写操作也一样,应用程序访问Write()
,数据就会从用户空间地址复制到内核空间地址的缓存中;
2、直接IO的方式
应用程序不经过操作系统的内核数据缓冲区,直接访问直接访问磁盘数据,也就是省略了从内核缓冲区到用户程序缓存的数据复制的步骤,这种方式常常用于数据库管理系统中,毕竟数据库要效率嘛,不过跳过缓存有的时候就要牺牲下时间了,因为都是从磁盘里读取数据,整个计算机就数着磁盘的速度慢,提升效率的方式就是和异步IO结合起来使用;
3、同步访问文件的方式
顾名思义就是数据的读取和写入都是同步进行的,只有当数据被写入到磁盘的时候才返回给应用程序成功的标志,这这方式安全性高但费时间;
4、异步访问文件的方式
当访问数据的线程发出请求之后,线程不一定当时就处理,也就是采取非阻塞的方式,这种方式明显对应用程序很有好处,但是跟访问文件的效率没有什么关系;
5、内存映射的方式
这是操作系统把内存中的某一块区域与磁盘中的文件结合起来,当要访问内存中的一段数据时,就可以转化为访问文件中的某一段数据;
Java都是怎么访问磁盘文件的?一图胜千言!
Java都是如何做序列化的?
所谓序列化就是把一个对象转化成一串由二进制表示的字节数组,那么反序列化就是从数组到对象的过程;
序列化主要是作用是在于持久化的;总结一下:
- 如果序列化的属性是对象,则这个对象也必须实现
Serializable
接口; - 如果子类实现了
Serializable
接口,子类的属性是可以序列化的,但是父类没有实现,那么父类中的属性是不能序列化的,虽然不会报错,但是数据会消失,一定要注意这一点! - 如果父类实现了
Serializable
接口,那么这个父类及其下面的所有的子类的属性都可以序列化; - 在反序列化的时候,如果对象的属性有修改或者是减少,那么修改的部分属性会消失,不过这个不会报错;
- 反序列化的时候还有一点就是不能随便更改
serialVesionUID
,要不然就会反序列化失败;网络IO
先上图:
Java Socket就好比是两个城市之间的交通工具,那么相应的,每种交通工具都会有自己的交通规则,一般都是用的基于TCP/IP的流套接字,原因无他,稳定!
具体使用的时候,客户端建立一个Socket
实例,操作系统会给这个Socket
分配一个没有被使用过的本地端口号,同时服务端建立一个ServerSocket
实例,操作系统则会为ServerSocket
创建一个包含指定监听的端口号和包含监听地址的通配符的底层数据结构;
当调用到accept()
方法时,服务端会进入阻塞状态来等待客户端的请求,当一个新的请求到来的时候,将会为这个连接创建一个新的包含地址和端口信息的套接数据结构,而这个地址和端口信息正是请求源地址和端口;
这个新创建的数据结构会关联到ServerSocket
实例的一个未完成的连接数据结构列表中,不过,这个时候服务端的与之对应的Socket
实例可并没有完成创建,必须完成家喻户晓的三次握手才行,服务端的Socket
实例返回之后,会把该实例对应的数据结构从未完成列表中移到完成列表中;与ServerSocket
所关联的列表中的每个数据结构都代表与一个客户端建立的TCP连接;
连接成功之后,服务端和 客户端都会有一个Socket
实例,Socket
都有InputStream
和OutputStream
。并通过这两个对象来交换数据,网络IO都是以字节流传输的,每创建一个Socket
对象,操作系统就会为InputStream
和OutputStream
分别量身分配缓冲区,之后的数据读取和数据写入都是在缓冲区完成;
写入端就是把数据写到OutputStream
对应的SendQ
队列中,那如果队列满了怎么办呢?那就再把数据转移到InputStream
的RecvQ队列中,那如果RecvQ也满了怎么办?不可能永远为数据腾地方,空间不够用就用时间来凑,这就用到阻塞了;OutputStream
的Write()
方法会一直阻塞到RecvQ队列有足够的空间来容纳SendQ的数据,这就带来了新的问题,缓冲区的大小与写入端和读取端的速度非常影响这个链接的传输数据的效率;
阻塞就好比是交通信号灯,交通信号灯就是为了在有限的道路资源下做好无限的交通流量的协调,就是因为有阻塞的情况,所以必须要解决协调网络IO 和磁盘IO不同的数据写入和读取,两边如果同时进行就会发生死锁,死锁可不是一件好事情,下面,来看NIO!NIO
BIO就是烧上第一壶水之后啥也不干就在旁边是守着,等水开关上火之后再去忙别的,NIO则是把所有的水全都烧上,来回走着,看哪壶开了就关哪壶,AIO就是在水壶上放个哨子,烧上水该干嘛干嘛,等哨子响了就可以停下手头的工作去关火,关火之后再继续工作;
BIO如果数据量少了还好,多了就不行了,就好比烧100壶水,还用这法子我甭干别的,BIO在大规模访问量以及有高性能要求的情况下就不行了,网络IO的一些解决办法:让一个客户端对应一个线程、采用线程池等只是缓解了困境,并不能真正的解决问题;
问题是高维度的问题,解决的手段也必须实现降维打击,NIO就是要的降维打击;
NIO有两个关键的类:channel
和Selector
,如果说Socket
是泛指一切交通工具的话,那么channel
就是特指某一种交通工具,而Selector
就好比是交通局负责实时调度和监控每一种交通工具的运行状况,Buffer
则更加具体,如果说channel
是大客车,那么Buffer
就是客车上的座位;
基于NIO工作方式的Socket请求的处理过程是这样的: