同步异步指的是通信模式,而阻塞和非阻塞指的是在接收和发送时是否等待动作完成才返回所以不能混淆这四个词。
首先是通信的同步,主要是指客户端在发送请求后,必须得在服务端有回应后才发送下一个请求。所以这个时候的所有请求将会在服务端得到同步
其次是通信的异步,指客户端在发送请求后,不必等待服务端的回应就可以发送下一个请求,这样对于所有的请求动作来说将会在服务端得到异步,这条请求的链路就象是一个请求队列,所有的动作在这里不会得到同步的。
阻塞和非阻塞只是应用在请求的读取和发送。
在实现过程中,如果服务端是异步的话,客户端也是异步的话,通信效率会很高,但如果服务端在请求的返回时也是返回给请求的链路时,客户端是可以同步的,这种情况下,服务端是兼容同步和异步的。相反,如果客户端是异步而服务端是同步的也不会有问题,只是处理效率低了些。
同步=阻塞式,异步=非阻塞式
同步和异步都只针对于本机SOCKET而言的
同步模式下,比如RECIEV和SEND,都要确保收到或发送完才返回,继续执行下面的代码不然就阻塞在哪里,所以,同步模式下,一般要用到线程来处理。
Bytebuffer buffer = ByteBuffer.allocate(512); //给bytebuffer分配空间长度: 512的空间长度
Bytebuffer buffer = ByteBuffer.wrap(byte[] array); //给定一个字节数组,调用Bytebuffer的静态方法wrap回返回一个 Bytebuffer对象,
这个返回的对象的字节数组底层的字节数组就是作为参数传进来的数组;
需要注意:听过bytebuffer对象可以操作底层的数组中的里面的内容,也可以通过直接修改既有的字节数组内容
(两种途径来修改bytebuffer底层的所维护的字节数组的内容)是比较危险的操作,除非确保不会的既有的字节数组做任何改动,都是通过bytebuffer
来操作,那么就不会有问题,这个方法起到的作用就是这些,不要直接通过修改数组的方式去去修改里面的内容(一般通过bytebuffer的方式去修修改)
Bytebuffer buffer = ByteBuffer.allocateDirect(512);
allocateDirect 和 allocate方法的区别:
创建的对象不同: allocate底层new的是 teapBytebuffer
allocateDirect底层创建的对象是DirectBytebuffer(); 直接缓冲
DirectBytebuffer();是位于Java的堆上面的,堆这个区域中;
Unsafe.class 文件:反编译出来的,JDK分为openJDK和OracleJDK一个是开源的一个是收费的
Unsafe.allocateMemory()方法是一个 native 修饰的方法 实际上就是本地的方法,是通过JNI的方式去实现获取数据的
native 涉及到Java内存模型(操作系统)上的概念 ,native其实就是JVM的堆外内存,不是由JVM来操作内存的
调用了本地c或者是c++的代码来生产数据放到了堆外的内存(操作系统)上,Java通过一个指针
address指针指向native这个修饰词找到对外内存(由c或者c++生成的数据)来得到数据,实际上
DirectBytebuffer的数据存放在native这个系统本地的里的,能访问到堆外内存的成员变量存放在 DirectBytebuffer的父类MappedByteBuffer
的父类Bytebuffer(抽象类)的父类Bufer里面long 类型 的address成员变量,之所以提升到超级父类Bytebuffer是因为提升
( JNI(Java native interface)本地接口) 的方法调用的速率、效率、速度
address是对外内存的地址,通过这个指针地址就能找到对外内存所生成的数据,
如果使用间接的bytebuffer(heapBytebuffer),里面封装了字节数组都是再Java堆上面的,对于操作系统中,
操作系统并不直接的处理heapByebuffer在堆上面所封装的那个字节数组,并不是直接操作的那个字节数组,实际上是Java内存外面优惠开辟一块内存区域
比如对数据进行写入,实际上是将Java堆上面的heapBytebuffer里面的字节数组的内容拷贝到Java内存模型之外所开辟的这个内存空间里面的某一个空间
然后再将里的数据拿出来进行跟IO设备直接打交道,进行数据的读取和写入,如果使用了间接缓存bytebuffer(heapBytebuffer)
实际上是多了一步数据拷贝的过程,放到Java内存模型之外的,也就是不在JVM堆里面的的操作系统中的某一块内存区域当中,然后这个内存区域会直接跟
IO设备进行交互,这个过程有一个名字就是zero copy 零拷贝 零拷贝的概念就是这个意思
如果是使用了DirectBytebuffer变成什么场景了呢?
Java堆上面就不会再存在一个所谓的字节数组了,因为正真的数据就已经再对外放着了,如果对数据进行读写的话直接就由
操作系统去跟堆外的这个内存进行打交道,少了一次数据拷贝的过程,
为什么堆外的操作不会直接操作堆内的数据减少一次零拷贝的一个数据拷贝过程呢?
其实并不是操作系统不能访问堆内的那块内存的(操作系统是可以访问(比如在内核态的情况下/场景下)他是可以访问任何的一块内存区域的),
所以操纵系统是可以访问到JVM虚拟机上面的堆内存区域的
既然可以访问为什么不直接访问呢?
实际上没有那么简单的,操作系统真正进行IO操作的时候都是跟外设进行打交道的,一定会调用操作系统相应的实现东西,就相当于通过JNI的方式
去访问JVM的那块内存区域,通过JNI访问JVM虚拟机内存模型的内存区域的话这个地址一定是已经确定了才会访问到这个JVM虚拟机的内存区域,
然而你正在访问JVM那块内存区域的时候,突然再JVM内存区域发生了G-C(垃圾回收),发生了垃圾回收的时候,(垃圾回收有多种实现垃圾
回收算法,除了CMS垃圾回收算法【并发的标记清除算法】之外 ,其他的垃圾回收算法的方式都涉及一个先标记然后自取压缩这么一个过程),
什么叫做先标记在压缩呢?比如说有一个块若干个内存区域,会标记需要回收的内存空间地址,比如有八块连在一起的内存,虚拟机会发现1,3,5这
几块内存需要垃圾回收掉,标记的内存需要清除,不标记的既不需要;等虚拟机把这三块内存清空掉之后,会执行一次压缩,而这个压缩的过程会涉及到
内存对象的移动,移动的目的是为了腾出一块更大的完整的连续的内存空间,让这个内存可以容纳更大额Java对象,所以他会涉及到剩下没有清除的内存空间
向左移动,如果native正在执行操作这VM这个内存中的数据的时候,突然发生了数据移动的话,整个数据都乱套了,所以不能进行这种操纵,不能进行G-C
这种操作,很可能就会出现AitoMemoryErro这样的错误。
所有有两种办法:
第一种办法:让native这个本地的方法来操作的时候,让JVM虚拟机的某一个内存区域需要操作的对象的地址固定再那里不能改变,不要让其发生G-C操作
或者是内存地址移动的现象,
但是对于JVM虚拟机来说让对象的内存地址不发生改变,是不可太可能的,这种方案是行不通的,通过原生的代码来去操作java堆上面的内存对象的这个方式
是不可行的,被堵死了
第二种办法:将JVM虚拟机上的内存对象给拷贝到堆外操作系统上去,基于这些条件:拷贝对象的速度其实是比进行IO操作的速度快的,IO操作的速度没有那么快
所以这种性价比是比较好的事情,跟外界进行IO操作(和物理设备进行打交道)的这个时间可能会很长,拷贝的这个过程时间综合来说会快一些,所以会执行拷贝的过程
这个拷贝就涉及到JNI的一个相应的拷贝内存的方法的一个调用
拷贝过程中是不会产生G-C的,JVM会做相应的保证,所以采用的是这样的方案
这个拷贝完使用IO交互完成之后的这个拷贝的内存什么时候会被释放呢?
用完之后实际上释放掉了,这个拷贝的内存是由操作系统来进行维护的,这个内存区域本来就是操作系统来创建的,那么他就会相应的进行释放
对于DirectBytebuffer这个native的修饰的对象的堆外内存数据是怎么释放的呢?
因为address维护着这块数据的引用,当位于Java堆内的对象DirectBytebuffer进行内存释放的时候,通过address这个成员变量指针就能找到对外
的操作系统上的内存数据地址,通过JNI的形式可以把对外的内存数据进行释放回收,因此不会出现内存泄露的情况,内存泄露的情况是不会发生的
对于Java这个维度只考虑直接或者间接实际上有两种实现类,一种叫直接缓冲区另外一种是间接缓冲区或者非缓冲区,对于直接缓冲区来说,Java虚拟机就可以直接
进行本地IO操作,避免了在操作系统原生的IO操作上还要复制内容到一个中间的一个缓冲区,这就中间缓冲区
39、Scattering与Gathering深度解析
RandomAccessFile randomAccessFile = new RamdomAccessFile("NioTest9.txt","rw"); // 第一个字符串表示文件的名字 第二个参数表示读写状态 能读能写
FileChannel fileChannel = randomAccessFile.getChannel(); //通过getChanel得到一个 FileChannel对象
通过FileChannel的map方法得到内存映射文件的这个对象MappedByteBuffer 是一个堆外内存
MappedByteBuffer mappedByteBuffer = fileChannel.map(FileChannel.MapMapMode.READ_WRITE,0,5); //三个参数 第一个参数表示映射的这个模式(读模式、写模式、读写模式)
第二个参数position表示读写位置,第三个参数size表示映射多少到内存当中
//修改内存当中的信息,注意是在内存当中,具体怎么从文件在内存中修改保存到硬盘当中由操作系统操作去完成
mappedByteBuffer.put(0,(byte)'a');
mappedByteBuffer .put(0,(byte)'b');
//改完之后把文件关闭掉
randomAccessFile.close();
通过fileChannel这个对象可以锁定文件整体或者部分
而且锁有两种:第一种叫共享锁(都可以读),第二种叫排它锁(只能有一个写)
RandomAccessFile randomAccessFile = new RandomAccessFile("NioTest10.txt","rw");
FileChannel fileChannel = randomAccessFile.getChannel(); //通过getChanel得到一个 FileChannel对象
//获取文件锁 fileChannel有多个构造方法 这里使用三个参数的构造方法
这个lock通过调用构造方法 构造方法有三个参数 1、位置position,从哪个位置开始锁 2、锁多长 3、boolean shared 判断是共享锁还是排他锁
FileLock fileLock = fileChannel.lock(3,6,true);
//判断有效性
System.out.println(“valid” + fileLock.isVaild()); //true
//判断锁的类型
System.out.println(“lock type” + fileLock.isShared); //ture
//释放锁
fileLock.release();
//关闭锁
ramdomAccess.close();
/*
*关于Buffer 的 Scattering (分散、散开,分散成多个)与 Gathering (收集,合并到一起)
*/
public class NioTest{
public void static main(Stirng[] args) thows Exception {
//网络程序需要连接客户端(需要一些工具NC、NistCat、HelNit)和服务端(就是Java里面写的)
ServiceSocketChannel ssc = ServiceSocketChannel.open();
//设置端口号8899
IntetSocketAddress address = new InetSocketAddress(8899);
//绑定端口号8899 监听端口号
ssc.socket().bind(address);
//定义长度
int messageLength = 2+ 3 + 4;
//byte Buffer数组
ByteBuffer[] buffers = new ByteBuffer[3];
//最多读取多少个字节 2 3 4 字节数组
byteBuffers[0] = ByteBuffer.allocate(2);
byteBuffers[1] = ByteBuffer.allocate(3);
byteBuffers[2] = ByteBuffer.allocate(4);
SocketChannel socketChannel = ssc.accept();
while(true) {
int byteRead = 0;
//从socketChannel获取信息不断的读,写一个死循环 接送的总数不能小于这个总的信息所要读的
while(byteRead < messageLength) {
long r = socketChannel.read(buffers);
byteRead += r ;
System.out.println("byteRead:" + byteRead);
//每次读完之后就把 byteBuffers上面三个数组对象的每个position的位置和limit的限制数输出出来
}
}
}
}