当前位置: 首页 > news >正文

庄行网站建设阿里云wordpress建站教程

庄行网站建设,阿里云wordpress建站教程,湖南建设厅官网平台,网站定制开发一般多久【笔记来自#xff1a;it白马】 NIO基础 **注意#xff1a;**推荐完成JavaSE篇、JavaWeb篇的学习再开启这一部分的学习#xff0c;如果在这之前完成了JVM篇#xff0c;那么看起来就会比较轻松了。 在JavaSE的学习中#xff0c;我们了解了如何使用IO进行数据传输#xf…【笔记来自it白马】 NIO基础 **注意**推荐完成JavaSE篇、JavaWeb篇的学习再开启这一部分的学习如果在这之前完成了JVM篇那么看起来就会比较轻松了。 在JavaSE的学习中我们了解了如何使用IO进行数据传输Java IO是阻塞的如果在一次读写数据调用时数据还没有准备好或者目前不可写那么读写操作就会被阻塞直到数据准备好或目标可写为止。Java NIO则是非阻塞的每一次数据读写调用都会立即返回并将目前可读或可写的内容写入缓冲区或者从缓冲区中输出即使当前没有可用数据调用仍然会立即返回并且不对缓冲区做任何操作。 NIO框架是在JDK1.4推出的它的出现就是为了解决传统IO的不足这一期视频我们就将围绕着NIO开始讲解。 缓冲区 一切的一切还要从缓冲区开始讲起包括源码在内其实这个不是很难只是需要理清思路。 Buffer类及其实现 Buffer类是缓冲区的实现类似于Java中的数组也是用于存放和获取数据的。但是Buffer相比Java中的数组功能就非常强大了它包含一系列对于数组的快捷操作。 Buffer是一个抽象类它的核心内容 public abstract class Buffer {// 这四个变量的关系: mark position limit capacity// 这些变量就是Buffer操作的核心了之后我们学习的过程中可以看源码是如何操作这些变量的private int mark -1;private int position 0;private int limit;private int capacity;// 直接缓冲区实现子类的数据内存地址之后会讲解long address;我们来看看Buffer类的子类包括我们认识到的所有基本类型除了boolean类型之外 IntBuffer - int类型的缓冲区。ShortBuffer - short类型的缓冲区。LongBuffer - long类型的缓冲区。FloatBuffer - float类型的缓冲区。DoubleBuffer - double类型的缓冲区。ByteBuffer - byte类型的缓冲区。CharBuffer - char类型的缓冲区。 注意我们之前在JavaSE中学习过的StringBuffer虽然也是这种命名方式但是不属于Buffer体系这里不会进行介绍 这里我们以IntBuffer为例我们来看看如何创建一个Buffer类 public static void main(String[] args) {//创建一个缓冲区不能直接new而是需要使用静态方法去生成有两种方式//1. 申请一个容量为10的int缓冲区IntBuffer buffer IntBuffer.allocate(10);//2. 可以将现有的数组直接转换为缓冲区包括数组中的数据int[] arr new int[]{1, 2, 3, 4, 5, 6};IntBuffer buffer IntBuffer.wrap(arr); }那么它的内部是本质上如何进行操作的呢我们来看看它的源码 public static IntBuffer allocate(int capacity) {if (capacity 0) //如果申请的容量小于0那还有啥意思throw new IllegalArgumentException();return new HeapIntBuffer(capacity, capacity); //可以看到这里会直接创建一个新的IntBuffer实现类//HeapIntBuffer是在堆内存中存放数据本质上就数组一会我们可以在深入看一下 }public static IntBuffer wrap(int[] array, int offset, int length) {try {//可以看到这个也是创建了一个新的HeapIntBuffer对象并且给了初始数组以及截取的起始位置和长度return new HeapIntBuffer(array, offset, length);} catch (IllegalArgumentException x) {throw new IndexOutOfBoundsException();} }public static IntBuffer wrap(int[] array) {return wrap(array, 0, array.length); //调用的是上面的wrap方法 }那么这个HeapIntBuffer又是如何实现的呢我们接着来看 HeapIntBuffer(int[] buf, int off, int len) { // 注意这个构造方法不是public是默认的访问权限super(-1, off, off len, buf.length, buf, 0); //你会发现这怎么又去调父类的构造方法了绕来绕去//mark是标记off是当前起始下标位置offlen是最大下标位置buf.length是底层维护的数组真正长度buf就是数组最后一个0是起始偏移位置 }我们又来看看IntBuffer中的构造方法是如何定义的 final int[] hb; // 只有在堆缓冲区实现时才会使用 final int offset; boolean isReadOnly; // 只有在堆缓冲区实现时才会使用IntBuffer(int mark, int pos, int lim, int cap, // 注意这个构造方法不是public是默认的访问权限int[] hb, int offset) {super(mark, pos, lim, cap); //调用Buffer类的构造方法this.hb hb; //hb就是真正我们要存放数据的数组堆缓冲区底层其实就是这么一个数组this.offset offset; //起始偏移位置 }最后我们来看看Buffer中的构造方法 Buffer(int mark, int pos, int lim, int cap) { // 注意这个构造方法不是public是默认的访问权限if (cap 0) //容量不能小于0小于0还玩个锤子throw new IllegalArgumentException(Negative capacity: cap);this.capacity cap; //设定缓冲区容量limit(lim); //设定最大position位置position(pos); //设定起始位置if (mark 0) { //如果起始标记大于等于0if (mark pos) //并且标记位置大于起始位置那么就抛异常至于为啥不能大于我们后面再说throw new IllegalArgumentException(mark position: ( mark pos ));this.mark mark; //否则设定mark位置mark默认为-1} }通过对源码的观察我们大致可以得到以下结构了 现在我们来总结一下上面这些结构的各自职责划分 Buffer缓冲区的一些基本变量定义比如当前的位置position、容量 (capacity)、最大限制 (limit)、标记 (mark)等你肯定会疑惑这些变量有啥用别着急这些变量会在后面的操作中用到我们逐步讲解。IntBuffer等子类定义了存放数据的数组只有堆缓冲区实现子类才会用到、是否只读等也就是说数据的存放位置、以及对于底层数组的相关操作都在这里已经定义好了并且已经实现了Comparable接口。HeapIntBuffer堆缓冲区实现子类数据存放在堆中实际上就是用的父类的数组在保存数据并且将父类定义的所有底层操作全部实现了。 这样我们对于Buffer类的基本结构就有了一个大致的认识。 缓冲区写操作 前面我们了解了Buffer类的基本操作现在我们来看一下如何向缓冲区中存放数据以及获取数据数据的存放包括以下四个方法 public abstract IntBuffer put(int i); - 在当前position位置插入数据由具体子类实现public abstract IntBuffer put(int index, int i); - 在指定位置存放数据也是由具体子类实现public final IntBuffer put(int[] src); - 直接存放所有数组中的内容数组长度不能超出缓冲区大小public IntBuffer put(int[] src, int offset, int length); - 直接存放数组中的内容同上但是可以指定存放一段范围public IntBuffer put(IntBuffer src); - 直接存放另一个缓冲区中的内容 我们从最简的开始看是在当前位置插入一个数据那么这个当前位置是怎么定义的呢我们来看看源码 public IntBuffer put(int x) {hb[ix(nextPutIndex())] x; //这个ix和nextPutIndex()很灵性我们来看看具体实现return this; }protected int ix(int i) {return i offset; //将i的值加上我们之前设定的offset偏移量值但是默认是0非0的情况后面会介绍 }final int nextPutIndex() {int p position; //获取Buffer类中的position位置一开始也是0if (p limit) //位置肯定不能超过底层数组最大长度否则越界throw new BufferOverflowException();position p 1; //获取之后会使得Buffer类中的position1return p; //返回当前的位置 }所以put操作实际上是将底层数组hb在position位置上的数据进行设定。 设定完成后position自动后移 我们可以编写代码来看看 public static void main(String[] args) {IntBuffer buffer IntBuffer.allocate(10);buffer.put(1).put(2).put(3); //我们依次存放三个数据试试看System.out.println(buffer); }通过断点调试我们来看看实际的操作情况 可以看到我们不断地put操作position会一直向后移动当然如果超出最大长度那么会直接抛出异常 接着我们来看看第二个put操作是如何进行它能够在指定位置插入数据 public IntBuffer put(int i, int x) {hb[ix(checkIndex(i))] x; //这里依然会使用ix但是会检查位置是否合法return this; }final int checkIndex(int i) { // package-privateif ((i 0) || (i limit)) //插入的位置不能小于0并且不能大于等于底层数组最大长度throw new IndexOutOfBoundsException();return i; //没有问题就把i返回 }实际上这个比我们之前的要好理解一些注意全程不会操作position的值这里需要注意一下。 我们接着来看第三个put操作它是直接在IntBuffer中实现的是基于前两个put方法的子类实现来完成的 public IntBuffer put(int[] src, int offset, int length) {checkBounds(offset, length, src.length); //检查截取范围是否合法给offset、调用者指定长度、数组实际长度if (length remaining()) //接着判断要插入的数据量在缓冲区是否容得下装不下也不行throw new BufferOverflowException();int end offset length; //计算出最终读取位置下面开始forfor (int i offset; i end; i)this.put(src[i]); //注意是直接从postion位置开始插入直到指定范围结束return this; //ojbk }public final IntBuffer put(int[] src) {return put(src, 0, src.length); //因为不需要指定范围所以直接0和length然后调上面的多捞哦 }public final int remaining() { //计算并获取当前缓冲区的剩余空间int rem limit - position; //最大容量减去当前位置就是剩余空间return rem 0 ? rem : 0; //没容量就返回0 }static void checkBounds(int off, int len, int size) { // package-privateif ((off | len | (off len) | (size - (off len))) 0) //让我猜猜看不懂了是吧throw new IndexOutOfBoundsException();//实际上就是看给定的数组能不能截取出指定的这段数据如果都不够了那肯定不行啊 }大致流程如下首先来了一个数组要取一段数据全部丢进缓冲区 在检查没有什么问题并且缓冲区有容量时就可以开始插入了 最后我们通过代码来看看 public static void main(String[] args) {IntBuffer buffer IntBuffer.allocate(10);int[] arr new int[]{1,2,3,4,5,6,7,8,9};buffer.put(arr, 3, 4); //从下标3开始截取4个元素System.out.println(Arrays.toString(buffer.array())); //array方法可以直接获取到数组 }可以看到最后结果为 当然我们也可以将一个缓冲区的内容保存到另一个缓冲区 public IntBuffer put(IntBuffer src) {if (src this) //不会吧不会吧不会有人保存自己吧throw new IllegalArgumentException();if (isReadOnly()) //如果是只读的话那么也是不允许插入操作的我猜你们肯定会问为啥就这里会判断只读前面四个呢throw new ReadOnlyBufferException();int n src.remaining(); //给进来的src看看容量注意这里不remaining的结果不是剩余容量是转换后的之后会说if (n remaining()) //这里判断当前剩余容量是否小于src容量throw new BufferOverflowException();for (int i 0; i n; i) //也是从position位置开始继续写入put(src.get()); //通过get方法一个一个读取数据出来具体过程后面讲解return this; }我们来看看效果 public static void main(String[] args) {IntBuffer src IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});IntBuffer buffer IntBuffer.allocate(10);buffer.put(src);System.out.println(Arrays.toString(buffer.array())); }但是如果是这样的话会出现问题 public static void main(String[] args) {IntBuffer src IntBuffer.allocate(5);for (int i 0; i 5; i) src.put(i); //手动插入数据IntBuffer buffer IntBuffer.allocate(10);buffer.put(src);System.out.println(Arrays.toString(buffer.array())); }我们发现结果和上面的不一样并没有成功地将数据填到下面的IntBuffer中这是为什么呢实际上就是因为remaining()的计算问题因为这个方法是直接计算postion的位置但是由于我们在写操作完成之后position跑到后面去了也就导致remaining()结果最后算出来为0。 因为这里不是写操作是接下来需要从头开始进行读操作所以我们得想个办法把position给退回到一开始的位置这样才可以从头开始读取那么怎么做呢一般我们在写入完成后需要进行读操作时后面都是这样不只是这里会使用flip()方法进行翻转 public final Buffer flip() {limit position; //修改limit值当前写到哪里下次读的最终位置就是这里limit的作用开始慢慢体现了position 0; //position归零mark -1; //标记还原为-1但是现在我们还没用到return this; }这样再次计算remaining()的结果就是我们需要读取的数量了这也是为什么put方法中要用remaining()来计算的原因我们再来测试一下 public static void main(String[] args) {IntBuffer src IntBuffer.allocate(5);for (int i 0; i 5; i) src.put(i);IntBuffer buffer IntBuffer.allocate(10);src.flip(); //我们可以通过flip来翻转缓冲区buffer.put(src);System.out.println(Arrays.toString(buffer.array())); }翻转之后再次进行转移就正常了。 缓冲区读操作 前面我们看完了写操作现在我们接着来看看读操作。读操作有四个方法 public abstract int get(); - 直接获取当前position位置的数据由子类实现public abstract int get(int index); - 获取指定位置的数据也是子类实现public IntBuffer get(int[] dst) - 将数据读取到给定的数组中public IntBuffer get(int[] dst, int offset, int length) - 同上加了个范围 我们还是从最简单的开始看第一个get方法的实现在IntBuffer类中 public int get() {return hb[ix(nextGetIndex())]; //直接从数组中取就完事 }final int nextGetIndex() { // 好家伙这不跟前面那个一模一样吗int p position;if (p limit)throw new BufferUnderflowException();position p 1;return p; }可以看到每次读取操作之后也会将postion1直到最后一个位置如果还要继续读那么就直接抛出异常。 我们来看看第二个 public int get(int i) {return hb[ix(checkIndex(i))]; //这里依然是使用checkIndex来检查位置是否非法 }我们来看看第三个和第四个 public IntBuffer get(int[] dst, int offset, int length) {checkBounds(offset, length, dst.length); //跟put操作一样也是需要检查是否越界if (length remaining()) //如果读取的长度比可以读的长度大那肯定是不行的throw new BufferUnderflowException();int end offset length; //计算出最终读取位置for (int i offset; i end; i)dst[i] get(); //开始从position把数据读到数组中注意是在数组的offset位置开始return this; }public IntBuffer get(int[] dst) {return get(dst, 0, dst.length); //不指定范围的话那就直接用上面的 }我们来看看效果 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});int[] arr new int[10];buffer.get(arr, 2, 5);System.out.println(Arrays.toString(arr)); }可以看到成功地将数据读取到了数组中。 当然如果我们需要直接获取数组也可以使用array()方法来拿到 public final int[] array() {if (hb null) //为空那说明底层不是数组实现的肯定就没法转换了throw new UnsupportedOperationException();if (isReadOnly) //只读也是不让直接取出的因为一旦取出去岂不是就能被修改了throw new ReadOnlyBufferException();return hb; //直接返回hb }我们来试试看 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});System.out.println(Arrays.toString(buffer.array())); }当然既然都已经拿到了底层的hb了我们来看看如果直接修改之后是不是读取到的就是我们的修改之后的结果了 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});int[] arr buffer.array();arr[0] 99999; //拿到数组对象直接改System.out.println(buffer.get()); }可以看到这种方式由于是直接拿到的底层数组所有修改会直接生效在缓冲区中。 当然除了常规的读取方式之外我们也可以通过mark()来实现跳转读取这里需要介绍一下几个操作 public final Buffer mark() - 标记当前位置public final Buffer reset() - 让当前的position位置跳转到mark当时标记的位置 我们首先来看标记方法 public final Buffer mark() {mark position; //直接标记到当前位置mark变量终于派上用场了当然这里仅仅是标记return this; }我们再来看看重置方法 public final Buffer reset() {int m mark; //存一下当前的mark位置if (m 0) //因为mark默认是-1要是没有进行过任何标记操作那reset个毛throw new InvalidMarkException();position m; //直接让position变成mark位置return this; }那比如我们在读取到1号位置时进行标记 接着我们使用reset方法就可以直接回退回去了 现在我们来测试一下 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});buffer.get(); //读取一位那么position就变成1了buffer.mark(); //这时标记那么mark 1buffer.get(); //又读取一位那么position就变成2了buffer.reset(); //直接将position mark也就是变回1System.out.println(buffer.get()); }可以看到读取的位置根据我们的操作进行了变化有关缓冲区的读操作就暂时讲到这里。 缓冲区其他操作 前面我们大致了解了一下缓冲区的读写操作那么我们接着来看看除了常规的读写操作之外还有哪些其他的操作 public abstract IntBuffer compact() - 压缩缓冲区由具体实现类实现public IntBuffer duplicate() - 复制缓冲区会直接创建一个新的数据相同的缓冲区public abstract IntBuffer slice() - 划分缓冲区会将原本的容量大小的缓冲区划分为更小的出来进行操作public final Buffer rewind() - 重绕缓冲区其实就是把position归零然后mark变回-1public final Buffer clear() - 将缓冲区清空所有的变量变回最初的状态 我们先从压缩缓冲区开始看起它会将整个缓冲区的大小和数据内容变成position位置到limit之间的数据并移动到数组头部 public IntBuffer compact() {int pos position(); //获取当前位置int lim limit(); //获取当前最大position位置assert (pos lim); //断言表达式position必须小于最大位置肯定的int rem (pos lim ? lim - pos : 0); //计算pos距离最大位置的长度System.arraycopy(hb, ix(pos), hb, ix(0), rem); //直接将hb数组当前position位置的数据拷贝到头部去然后长度改成刚刚计算出来的空间position(rem); //直接将position移动到rem位置limit(capacity()); //pos最大位置修改为最大容量discardMark(); //mark变回-1return this; }比如现在的状态是 那么我们在执行 compact()方法之后会进行截取此时limit - position 6那么就会截取第4、5、6、7、8、9这6个数据然后丢到最前面接着position跑到7表示这是下一个继续的位置 现在我们通过代码来检验一下 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});for (int i 0; i 4; i) buffer.get(); //先正常读4个buffer.compact(); //压缩缓冲区System.out.println(压缩之后的情况Arrays.toString(buffer.array()));System.out.println(当前position位置buffer.position());System.out.println(当前limit位置buffer.limit()); }可以看到最后的结果没有问题 我们接着来看第二个方法那么如果我们现在需要复制一个内容一模一样的的缓冲区该怎么做直接使用duplicate()方法就可以复制了 public IntBuffer duplicate() { //直接new一个新的但是是吧hb给丢进去了而不是拷贝一个新的return new HeapIntBuffer(hb,this.markValue(),this.position(),this.limit(),this.capacity(),offset); }那么各位猜想一下如果通过这种方式创了一个新的IntBuffer那么下面的例子会出现什么结果 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});IntBuffer duplicate buffer.duplicate();System.out.println(buffer duplicate);System.out.println(buffer.array() duplicate.array()); }由于buffer是重新new的所以第一个为false而底层的数组由于在构造的时候没有进行任何的拷贝而是直接传递因此实际上两个缓冲区的底层数组是同一个对象。所以一个发生修改那么另一个就跟着变了 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5});IntBuffer duplicate buffer.duplicate();buffer.put(0, 66666);System.out.println(duplicate.get()); }现在我们接着来看下一个方法slice()方法会将缓冲区进行划分 public IntBuffer slice() {int pos this.position(); //获取当前positionint lim this.limit(); //获取position最大位置int rem (pos lim ? lim - pos : 0); //求得剩余空间return new HeapIntBuffer(hb, //返回一个新的划分出的缓冲区但是底层的数组用的还是同一个-1,0,rem, //新的容量变成了剩余空间的大小rem,pos offset); //可以看到offset的地址不再是0了而是当前的position加上原有的offset值 }虽然现在底层依然使用的是之前的数组但是由于设定了offset值我们之前的操作似乎变得不太一样了 回顾前面我们所讲解的内容在读取和存放时会被ix方法进行调整 protected int ix(int i) {return i offset; //现在offset为4那么也就是说逻辑上的i是0但是得到真实位置却是4 }public int get() {return hb[ix(nextGetIndex())]; //最后会经过ix方法转换为真正在数组中的位置 }当然在逻辑上我们可以认为是这样的 现在我们来测试一下 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});for (int i 0; i 4; i) buffer.get();IntBuffer slice buffer.slice();System.out.println(划分之后的情况Arrays.toString(slice.array()));System.out.println(划分之后的偏移地址slice.arrayOffset());System.out.println(当前position位置slice.position());System.out.println(当前limit位置slice.limit());while (slice.hasRemaining()) { //将所有的数据全部挨着打印出来System.out.print(slice.get(), );} }可以看到最终结果 最后两个方法就比较简单了我们先来看rewind()它相当于是对position和mark进行了一次重置 public final Buffer rewind() {position 0;mark -1;return this; }接着是clear()它相当于是将整个缓冲区回归到最初的状态了 public final Buffer clear() {position 0; //同上limit capacity; //limit变回capacitymark -1;return this; }到这里关于缓冲区的一些其他操作我们就讲解到此。 缓冲区比较 缓冲区之间是可以进行比较的我们可以看到equals方法和compareTo方法都是被重写了的我们首先来看看equals方法注意它是判断两个缓冲区剩余的内容是否一致 public boolean equals(Object ob) {if (this ob) //要是两个缓冲区是同一个对象肯定一样return true;if (!(ob instanceof IntBuffer)) //类型不是IntBuffer那也不用比了return false;IntBuffer that (IntBuffer)ob; //转换为IntBufferint thisPos this.position(); //获取当前缓冲区的相关信息int thisLim this.limit();int thatPos that.position(); //获取另一个缓冲区的相关信息int thatLim that.limit();int thisRem thisLim - thisPos; int thatRem thatLim - thatPos;if (thisRem 0 || thisRem ! thatRem) //如果剩余容量小于0或是两个缓冲区的剩余容量不一样也不行return false;//注意比较的是剩余的内容for (int i thisLim - 1, j thatLim - 1; i thisPos; i--, j--) //从最后一个开始倒着往回比剩余的区域if (!equals(this.get(i), that.get(j)))return false; //只要发现不一样的就不用继续了直接falsereturn true; //上面的比较都没问题那么就true }private static boolean equals(int x, int y) {return x y; }那么我们按照它的思路来验证一下 public static void main(String[] args) {IntBuffer buffer1 IntBuffer.wrap(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});IntBuffer buffer2 IntBuffer.wrap(new int[]{6, 5, 4, 3, 2, 1, 7, 8, 9, 0});System.out.println(buffer1.equals(buffer2)); //直接比较buffer1.position(6);buffer2.position(6);System.out.println(buffer1.equals(buffer2)); //比较从下标6开始的剩余内容 }可以看到结果就是我们所想的那样 那么我们接着来看比较compareTo方法它实际上是Comparable接口提供的方法它实际上比较的也是pos开始剩余的内容 public int compareTo(IntBuffer that) {int thisPos this.position(); //获取并计算两个缓冲区的pos和remainint thisRem this.limit() - thisPos;int thatPos that.position();int thatRem that.limit() - thatPos;int length Math.min(thisRem, thatRem); //选取一个剩余空间最小的出来if (length 0) //如果最小的小于0那就返回-1return -1;int n thisPos Math.min(thisRem, thatRem); //计算n的值当前的pos加上剩余的最小空间for (int i thisPos, j thatPos; i n; i, j) { //从两个缓冲区的当前位置开始一直到n结束int cmp compare(this.get(i), that.get(j)); //比较if (cmp ! 0)return cmp; //只要出现不相同的那么就返回比较出来的值}return thisRem - thatRem; //如果没比出来个所以然那么就比长度 }private static int compare(int x, int y) {return Integer.compare(x, y); }这里我们就不多做介绍了。 只读缓冲区 接着我们来看看只读缓冲区只读缓冲区就像其名称一样它只能进行读操作而不允许进行写操作。 那么我们怎么创建只读缓冲区呢 public abstract IntBuffer asReadOnlyBuffer(); - 基于当前缓冲区生成一个只读的缓冲区。 我们来看看此方法的具体实现 public IntBuffer asReadOnlyBuffer() {return new HeapIntBufferR(hb, //注意这里并不是直接创建了HeapIntBuffer而是HeapIntBufferR并且直接复制的hb数组this.markValue(),this.position(),this.limit(),this.capacity(),offset); }那么这个HeapIntBufferR类跟我们普通的HeapIntBuffer有什么不同之处呢 可以看到它是继承自HeapIntBuffer的那么我们来看看它的实现有什么不同 protected HeapIntBufferR(int[] buf,int mark, int pos, int lim, int cap,int off) {super(buf, mark, pos, lim, cap, off);this.isReadOnly true; }可以看到在其构造方法中除了直接调用父类的构造方法外还会将isReadOnly标记修改为true我们接着来看put操作有什么不同之处 public boolean isReadOnly() {return true; }public IntBuffer put(int x) {throw new ReadOnlyBufferException(); }public IntBuffer put(int i, int x) {throw new ReadOnlyBufferException(); }public IntBuffer put(int[] src, int offset, int length) {throw new ReadOnlyBufferException(); }public IntBuffer put(IntBuffer src) {throw new ReadOnlyBufferException(); }可以看到所有的put方法全部凉凉只要调用就会直接抛出ReadOnlyBufferException异常。但是其他get方法依然没有进行重写也就是说get操作还是可以正常使用的但是只要是写操作就都不行 public static void main(String[] args) {IntBuffer buffer IntBuffer.wrap(new int[]{1, 2, 3, 4, 5, 6, 7, 8, 9, 0});IntBuffer readBuffer buffer.asReadOnlyBuffer();System.out.println(readBuffer.isReadOnly());System.out.println(readBuffer.get());readBuffer.put(0, 666); }可以看到结果为 这就是只读状态下的缓冲区。 ByteBuffer和CharBuffer 通过前面的学习我们基本上已经了解了缓冲区的使用但是都是基于IntBuffer进行讲解现在我们来看看另外两种基本类型的缓冲区ByteBuffer和CharBuffer因为ByteBuffer底层存放的是很多单个byte字节所以会有更多的玩法同样CharBuffer是一系列字节所以也有很多便捷操作。 我们先来看看ByteBuffer我们可以直接点进去看 public abstract class ByteBuffer extends Buffer implements ComparableByteBuffer {final byte[] hb; // Non-null only for heap buffersfinal int offset;boolean isReadOnly; // Valid only for heap buffers....可以看到如果也是使用堆缓冲区子类实现那么依然是一个byte[]的形式保存数据。我们来尝试使用一下 public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(10);//除了直接丢byte进去之外我们也可以丢其他的基本类型注意容量消耗buffer.putInt(Integer.MAX_VALUE); //丢个int的最大值进去注意一个int占4字节System.out.println(当前缓冲区剩余字节数buffer.remaining()); //只剩6个字节了//我们来尝试读取一下记得先翻转buffer.flip();while (buffer.hasRemaining()) {System.out.println(buffer.get()); //一共四个字节} }最后的结果为 可以看到第一个byte为127、然后三个都是-1我们来分析一下 127 转换为二进制补码形式就是 01111111而-1转换为二进制补码形式为11111111 那也就是说第一个字节是01111111而后续字节就是11111111把它们拼接在一起 二进制补码表示01111111 11111111 11111111 11111111 转换为十进制就是2147483647也就是int的最大值。 那么根据我们上面的推导各位能否计算得到下面的结果呢 public static void main(String[] args) {ByteBuffer buffer ByteBuffer.allocate(10);buffer.put((byte) 0);buffer.put((byte) 0);buffer.put((byte) 1);buffer.put((byte) -1);buffer.flip(); //翻转一下System.out.println(buffer.getInt()); //以int形式获取那么就是一次性获取4个字节 }经过上面的计算得到的结果就是 上面的数据以二进制补码的形式表示为00000000 00000000 00000001 11111111将其转换为十进制那么就是256 255 511 好吧再来个魔鬼问题把第一个换成1呢10000000 00000000 00000001 11111111自己算。 我们接着来看看CharBuffer这种缓冲区实际上也是保存一大堆char类型的数据 public static void main(String[] args) {CharBuffer buffer CharBuffer.allocate(10);buffer.put(lbwnb); //除了可以直接丢char之外字符串也可以一次性丢进入System.out.println(Arrays.toString(buffer.array())); }但是正是得益于char数组它包含了很多的字符串操作可以一次性存放一整个字符串。我们甚至还可以将其当做一个String来进行处理 public static void main(String[] args) {CharBuffer buffer CharBuffer.allocate(10);buffer.put(lbwnb);buffer.append(!); //可以像StringBuilder一样使用append来继续添加数据System.out.println(剩余容量buffer.remaining()); //已经用了6个字符了buffer.flip();System.out.println(整个字符串为buffer); //直接将内容转换为字符串System.out.println(第3个字符是buffer.charAt(2)); //直接像String一样charAtbuffer //也可以转换为IntStream进行操作.chars().filter(i - i l).forEach(i - System.out.print((char) i)); }当然除了一些常规操作之外我们还可以直接将一个字符串作为参数创建 public static void main(String[] args) {//可以直接使用wrap包装一个字符串但是注意包装出来之后是只读的CharBuffer buffer CharBuffer.wrap(收藏等于学会~);System.out.println(buffer);buffer.put(111); //这里尝试进行一下写操作 }可以看到结果也是我们预料中的 对于这两个比较特殊的缓冲区我们就暂时讲解到这里。 直接缓冲区 **注意**推荐学习完成JVM篇再来学习这一部分。 最后我们来看一下直接缓冲区我们前面一直使用的都是堆缓冲区也就是说实际上数据是保存在一个数组中的如果你已经完成了JVM篇的学习一定知道实际上占用的是堆内存而我们也可以创建一个直接缓冲区也就是申请堆外内存进行数据保存采用操作系统本地的IO相比堆缓冲区会快一些。 那么怎么使用直接缓冲区呢我们可以通过allocateDirect方法来创建 public static void main(String[] args) {//这里我们申请一个直接缓冲区ByteBuffer buffer ByteBuffer.allocateDirect(10);//使用方式基本和之前是一样的buffer.put((byte) 66);buffer.flip();System.out.println(buffer.get()); }我们来看看这个allocateDirect方法是如何创建一个直接缓冲区的 public static ByteBuffer allocateDirect(int capacity) {return new DirectByteBuffer(capacity); }这个方法直接创建了一个新的DirectByteBuffer对象那么这个类又是怎么进行创建的呢 可以看到它并不是直接继承自ByteBuffer而是MappedByteBuffer并且实现了接口DirectBuffer我们先来看看这个接口 public interface DirectBuffer {public long address(); //获取内存地址public Object attachment(); //附加对象这是为了保证某些情况下内存不被释放我们后面细谈public Cleaner cleaner(); //内存清理类 }public abstract class MappedByteBuffer extends ByteBuffer {//这三个方法目前暂时用不到后面文件再说public final MappedByteBuffer load();public final boolean isLoaded();public final MappedByteBuffer force(); }接着我们来看看DirectByteBuffer类的成员变量 // 把Unsafe类取出来 protected static final Unsafe unsafe Bits.unsafe();// 在内存中直接创建的内存空间地址 private static final long arrayBaseOffset (long)unsafe.arrayBaseOffset(byte[].class);// 是否具有非对齐访问能力根据CPU架构而定intel、AMD、AppleSilicon 都是支持的 protected static final boolean unaligned Bits.unaligned();// 直接缓冲区的内存地址为了提升速度就放到Buffer类中去了 // protected long address;// 附加对象一会有大作用 private final Object att;接着我们来看看构造方法 DirectByteBuffer(int cap) { // package-privatesuper(-1, 0, cap, cap);boolean pa VM.isDirectMemoryPageAligned(); //是否直接内存分页对齐需要额外计算int ps Bits.pageSize();long size Math.max(1L, (long)cap (pa ? ps : 0)); //计算出最终需要申请的大小//判断堆外内存是否足够够的话就作为保留内存Bits.reserveMemory(size, cap);long base 0;try {//通过Unsafe申请内存空间并得到内存地址base unsafe.allocateMemory(size);} catch (OutOfMemoryError x) {//申请失败就取消一开始的保留内存Bits.unreserveMemory(size, cap);throw x;}//批量将申请到的这一段内存每个字节都设定为0unsafe.setMemory(base, size, (byte) 0);if (pa (base % ps ! 0)) {// Round up to page boundaryaddress base ps - (base (ps - 1));} else {//将address变量在Buffer中定义设定为base的地址address base;}//创建一个针对于此缓冲区的Cleaner由于是堆外内存所以现在由它来进行内存清理cleaner Cleaner.create(this, new Deallocator(base, size, cap));att null; }可以看到在构造方法中是直接通过Unsafe类来申请足够的堆外内存保存数据那么当我们不使用此缓冲区时内存会被如何清理呢我们来看看这个Cleaner public class Cleaner extends PhantomReferenceObject{ //继承自鬼引用也就是说此对象会存放一个没有任何引用的对象//引用队列PhantomReference构造方法需要private static final ReferenceQueueObject dummyQueue new ReferenceQueue();//执行清理的具体流程private final Runnable thunk;static private Cleaner first null; //Cleaner双向链表每创建一个Cleaner对象都会添加一个结点private Cleanernext null,prev null;private static synchronized Cleaner add(Cleaner cl) { //添加操作会让新来的变成新的头结点if (first ! null) {cl.next first;first.prev cl;}first cl;return cl;}//可以看到创建鬼引用的对象就是传进的缓冲区对象private Cleaner(Object referent, Runnable thunk) {super(referent, dummyQueue);//清理流程实际上是外面的Deallocatorthis.thunk thunk;}//通过此方法创建一个新的Cleanerpublic static Cleaner create(Object ob, Runnable thunk) {if (thunk null)return null;return add(new Cleaner(ob, thunk)); //调用add方法将Cleaner添加到队列}//清理操作public void clean() {if (!remove(this))return; //进行清理操作时会从双向队列中移除当前Cleanerfalse说明已经移除过了直接returntry {thunk.run(); //这里就是直接执行具体清理流程} catch (final Throwable x) {...}}那么我们先来看看具体的清理程序在做些什么Deallocator是在直接缓冲区中声明的 private static class Deallocator implements Runnable {private static Unsafe unsafe Unsafe.getUnsafe();private long address; //内存地址private long size; //大小private int capacity; //申请的容量private Deallocator(long address, long size, int capacity) {assert (address ! 0);this.address address;this.size size;this.capacity capacity;}public void run() { //具体的清理操作if (address 0) {// Paranoiareturn;}unsafe.freeMemory(address); //这里是直接调用了Unsafe进行内存释放操作address 0; //内存地址改为0NULLBits.unreserveMemory(size, capacity); //取消一开始的保留内存} }好了现在我们可以明确在清理的时候实际上也是调用Unsafe类进行内存释放操作那么这个清理操作具体是在什么时候进行的呢首先我们要明确如果是普通的堆缓冲区由于使用的数组那么一旦此对象没有任何引用时就随时都会被GC给回收掉但是现在是堆外内存只能我们手动进行内存回收那么当DirectByteBuffer也失去引用时会不会触发内存回收呢 答案是可以的还记得我们刚刚看到Cleaner是PhantomReference的子类吗而DirectByteBuffer是被鬼引用的对象而具体的清理操作是Cleaner类的clean方法莫非这两者有什么联系吗 你别说还真有我们直接看到PhantomReference的父类Reference我们会发现这样一个类 private static class ReferenceHandler extends Thread {...static {// 预加载并初始化 InterruptedException 和 Cleaner 类// 以避免出现在循环运行过程中时由于内存不足而无法加载ensureClassInitialized(InterruptedException.class);ensureClassInitialized(Cleaner.class);}public void run() {while (true) {tryHandlePending(true); //这里是一个无限循环调用tryHandlePending方法}} }private T referent; /* 会被GC回收的对象也就是我们给过来被引用的对象 */volatile ReferenceQueue? super T queue; //引用队列可以和下面的next搭配使用形成链表 //Reference对象也是一个一个连起来的节点这样才能放到ReferenceQueue中形成链表 volatile Reference next;//即将被GC的引用链表 transient private ReferenceT discovered; /* 由虚拟机操作 *///pending与discovered一起构成了一个pending单向链表标记为static类所有pending为链表的头节点discovered为链表当前 //Reference节点指向下一个节点的引用这个队列是由JVM构建的当对象除了被reference引用之外没有其它强引用了JVM就会将指向 //需要回收的对象的Reference对象都放入到这个队列里面这个队列会由下面的 Reference Hander 线程来处理。 private static ReferenceObject pending null;static { //Reference类的静态代码块ThreadGroup tg Thread.currentThread().getThreadGroup();for (ThreadGroup tgn tg;tgn ! null;tg tgn, tgn tg.getParent());Thread handler new ReferenceHandler(tg, Reference Handler); //在一开始的时候就会创建handler.setPriority(Thread.MAX_PRIORITY); //以最高优先级启动handler.setDaemon(true); //此线程直接作为一个守护线程handler.start(); //也就是说在一开始的时候这个守护线程就会启动... }那么也就是说Reference Handler线程是在一开始就启动了那么我们的关注点可以放在tryHandlePending方法上看看这玩意到底在做个啥 static boolean tryHandlePending(boolean waitForNotify) {ReferenceObject r;Cleaner c;try {synchronized (lock) { //加锁办事//当Cleaner引用的DirectByteBuffer对象即将被回收时pending会变成此Cleaner对象//这里判断到pending不为null时就需要处理一下对象销毁了if (pending ! null) {r pending;// instanceof 有时会导致内存溢出所以将r从链表中移除之前就进行类型判断// 如果是Cleaner类型就给到cc r instanceof Cleaner ? (Cleaner) r : null;// 将pending更新为链表下一个待回收元素pending r.discovered;r.discovered null; //r不再引用下一个节点} else {//否则就进入等待if (waitForNotify) {lock.wait();}return waitForNotify;}}} catch (OutOfMemoryError x) {Thread.yield();return true;} catch (InterruptedException x) {return true;}// 如果元素是Cleaner类型c在上面就会被赋值这里就会执行其clean方法破案了if (c ! null) {c.clean();return true;}ReferenceQueue? super Object q r.queue;if (q ! ReferenceQueue.NULL) q.enqueue(r); //这个是引用队列实际上就是我们之前在JVM篇中讲解的入队机制return true; }通过对源码的解读我们就了解了直接缓冲区的内存加载释放整个流程。和堆缓冲区一样当直接缓冲区没有任何强引用时就有机会被GC正常回收掉并自动释放申请的内存。 我们接着来看看直接缓冲区的读写操作是如何进行的 public byte get() {return ((unsafe.getByte(ix(nextGetIndex())))); //直接通过Unsafe类读取对应地址上的byte数据 }private long ix(int i) {return address ((long)i 0); //ix现在是内存地址再加上i }我们接着来看看写操作 public ByteBuffer put(byte x) {unsafe.putByte(ix(nextPutIndex()), ((x)));return this; }可以看到无论是读取还是写入操作都是通过Unsafe类操作对应的内存地址完成的。 那么它的复制操作是如何实现的呢 public ByteBuffer duplicate() {return new DirectByteBuffer(this,this.markValue(),this.position(),this.limit(),this.capacity(),0); }DirectByteBuffer(DirectBuffer db, // 这里给的db是进行复制操作的DirectByteBuffer对象int mark, int pos, int lim, int cap,int off) {super(mark, pos, lim, cap);address db.address() off; //直接继续使用之前申请的内存空间cleaner null; //因为用的是之前的内存空间已经有对应的Cleaner了这里不需要再搞一个att db; //将att设定为此对象 }可以看到如果是进行复制操作那么会直接会继续使用执行复制操作的DirectByteBuffer申请的内存空间。不知道各位是否能够马上联想到一个问题我们知道如果执行复制操作的DirectByteBuffer对象失去了强引用被回收那么就会触发Cleaner并进行内存释放但是有个问题就是这段内存空间可能复制出来的DirectByteBuffer对象还需要继续使用这时肯定是不能进行回收的所以说这里使用了att变量将之前的DirectByteBuffer对象进行引用以防止其失去强引用被垃圾回收所以只要不是原来的DirectByteBuffer对象和复制出来的DirectByteBuffer对象都失去强引用时就不会导致这段内存空间被回收。 这样我们之前的未解之谜为啥有个att也就得到答案了有关直接缓冲区的介绍就到这里为止。 通道 前面我们学习了NIO的基石——缓冲区那么缓冲区具体用在什么地方呢在本板块我们学习通道之后相信各位就能知道了。那么什么是通道呢 在传统IO中我们都是通过流进行传输数据会源源不断从流中传出而在NIO中数据是放在缓冲区中进行管理再使用通道将缓冲区中的数据传输到目的地。 通道接口层次 通道的根基接口是Channel所以的派生接口和类都是从这里开始的我们来看看它定义了哪些基本功能 public interface Channel extends Closeable {//通道是否处于开启状态public boolean isOpen();//因为通道开启也需要关闭所以实现了Closeable接口所以这个方法懂的都懂public void close() throws IOException; }我们接着来看看它的一些子接口首先是最基本的读写操作 public interface ReadableByteChannel extends Channel {//将通道中的数据读取到给定的缓冲区中public int read(ByteBuffer dst) throws IOException; }public interface WritableByteChannel extends Channel {//将给定缓冲区中的数据写入到通道中public int write(ByteBuffer src) throws IOException; }有了读写功能后最后整合为了一个ByteChannel接口 public interface ByteChannel extends ReadableByteChannel, WritableByteChannel{}在ByteChannel之下还有更多的派生接口 //允许保留position和更改position的通道以及对通道连接实体的相关操作 public interface SeekableByteChannel extends ByteChannel {...//获取当前的positionlong position() throws IOException;//修改当前的positionSeekableByteChannel position(long newPosition) throws IOException;//返回此通道连接到的实体比如文件的当前大小long size() throws IOException;//将此通道连接到的实体截断比如文件截断之后文件后面一半就没了为给定大小SeekableByteChannel truncate(long size) throws IOException; }接着我们来看除了读写之外Channel还可以具有响应中断的能力 public interface InterruptibleChannel extends Channel {//当其他线程调用此方法时在此通道上处于阻塞状态的线程会直接抛出 AsynchronousCloseException 异常public void close() throws IOException; }//这是InterruptibleChannel的抽象实现完成了一部分功能 public abstract class AbstractInterruptibleChannel implements Channel, InterruptibleChannel {//加锁关闭操作用到private final Object closeLock new Object();//当前Channel的开启状态private volatile boolean open true;protected AbstractInterruptibleChannel() { }//关闭操作实现public final void close() throws IOException {synchronized (closeLock) { //同时只能有一个线程进行此操作加锁if (!open) //如果已经关闭了那么就不用继续了return;open false; //开启状态变成falseimplCloseChannel(); //开始关闭通道}}//该方法由 close 方法调用以执行关闭通道的具体操作仅当通道尚未关闭时才调用此方法不会多次调用。protected abstract void implCloseChannel() throws IOException;public final boolean isOpen() {return open;}//开始阻塞有可能一直阻塞下去操作之前需要调用此方法进行标记protected final void begin() {...}//阻塞操作结束之后也需要需要调用此方法为了防止异常情况导致此方法没有被调用建议放在finally中protected final void end(boolean completed)...}... }而之后的一些实现类都是基于这些接口定义的方法去进行实现的比如FileChannel 这样我们就大致了解了一下通道相关的接口定义那么我来看看具体是如何如何使用的。 比如现在我们要实现从输入流中读取数据然后打印出来那么之前传统IO的写法 public static void main(String[] args) throws IOException {//数组创建好一会用来存放从流中读取到的数据byte[] data new byte[10];//直接使用输入流InputStream in System.in;while (true) {int len;while ((len in.read(data)) 0) { //将输入流中的数据一次性读取到数组中System.out.print(读取到一批数据new String(data, 0, len)); //读取了多少打印多少}} }而现在我们使用通道之后 public static void main(String[] args) throws IOException {//缓冲区创建好一会就靠它来传输数据ByteBuffer buffer ByteBuffer.allocate(10);//将System.in作为输入源一会Channel就可以从这里读取数据然后通过缓冲区装载一次性传递数据ReadableByteChannel readChannel Channels.newChannel(System.in);while (true) {//将通道中的数据写到缓冲区中缓冲区最多一次装10个readChannel.read(buffer);//写入操作结束之后需要进行翻转以便接下来的读取操作buffer.flip();//最后转换成String打印出来康康System.out.println(读取到一批数据new String(buffer.array(), 0, buffer.remaining()));//回到最开始的状态buffer.clear();} }乍一看好像感觉也没啥区别不就是把数组换成缓冲区了吗效果都是一样的数据也是从Channel中读取得到并且通过缓冲区进行数据装载然后得到结果但是Channel不像流那样是单向的它就像它的名字一样一个通道可以从一端走到另一端也可以从另一端走到这一端我们后面进行介绍。 文件传输FileChannel 前面我们介绍了通道的基本情况这里我们就来尝试实现一下文件的读取和写入在传统IO中文件的写入和输出都是依靠FileOutputStream和FileInputStream来完成的 public static void main(String[] args) throws IOException {try(FileOutputStream out new FileOutputStream(test.txt);FileInputStream in new FileInputStream(test.txt)){String data 伞兵一号卢本伟准备就绪;out.write(data.getBytes()); //向文件的输出流中写入数据也就是把数据写到文件中out.flush();byte[] bytes new byte[in.available()];in.read(bytes); //从文件的输入流中读取文件的信息System.out.println(new String(bytes));} }而现在我们只需要通过一个FileChannel就可以完成这两者的操作获取文件通道的方式有以下几种 public static void main(String[] args) throws IOException {//1. 直接通过输入或输出流获取对应的通道FileInputStream in new FileInputStream(test.txt);//但是这里的通道只支持读取或是写入操作FileChannel channel in.getChannel();//创建一个容量为128的缓冲区ByteBuffer buffer ByteBuffer.allocate(128);//从通道中将数据读取到缓冲区中channel.read(buffer);//翻转一下接下来要读取了buffer.flip();System.out.println(new String(buffer.array(), 0, buffer.remaining())); }可以看到通过输入流获取的文件通道读取是没有任何问题的但是写入操作 public static void main(String[] args) throws IOException {//1. 直接通过输入或输出流获取对应的通道FileInputStream in new FileInputStream(test.txt);//但是这里的通道只支持读取或是写入操作FileChannel channel in.getChannel();//尝试写入一下channel.write(ByteBuffer.wrap(伞兵一号卢本伟准备就绪.getBytes())); }直接报错说明只支持读取操作那么输出流呢 public static void main(String[] args) throws IOException {//1. 直接通过输入或输出流获取对应的通道FileOutputStream out new FileOutputStream(test.txt);//但是这里的通道只支持读取或是写入操作FileChannel channel out.getChannel();//尝试写入一下channel.write(ByteBuffer.wrap(伞兵一号卢本伟准备就绪.getBytes())); }可以看到能够正常进行写入但是读取呢 public static void main(String[] args) throws IOException {//1. 直接通过输入或输出流获取对应的通道FileOutputStream out new FileOutputStream(test.txt);//但是这里的通道只支持读取或是写入操作FileChannel channel out.getChannel();ByteBuffer buffer ByteBuffer.allocate(128);//从通道中将数据读取到缓冲区中channel.read(buffer);//翻转一下接下来要读取了buffer.flip();System.out.println(new String(buffer.array(), 0, buffer.remaining())); }可以看到输出流生成的Channel又不支持读取所以说本质上还是保持着输入输出流的特性但是之前不是说Channel又可以输入又可以输出吗这里我们来看看第二种方式 //RandomAccessFile能够支持文件的随机访问并且实现了数据流 public class RandomAccessFile implements DataOutput, DataInput, Closeable {我们可以通过RandomAccessFile来创建通道 public static void main(String[] args) throws IOException {/*通过RandomAccessFile进行创建注意后面的mode有几种r 以只读的方式使用rw 读操作和写操作都可以rws 每当进行写操作同步的刷新到磁盘刷新内容和元数据rwd 每当进行写操作同步的刷新到磁盘刷新内容*/try(RandomAccessFile f new RandomAccessFile(test.txt, )){} }现在我们来测试一下它的读写操作 public static void main(String[] args) throws IOException {/*通过RandomAccessFile进行创建注意后面的mode有几种r 以只读的方式使用rw 读操作和写操作都可以rws 每当进行写操作同步的刷新到磁盘刷新内容和元数据rwd 每当进行写操作同步的刷新到磁盘刷新内容*/try(RandomAccessFile f new RandomAccessFile(test.txt, rw); //这里设定为支持读写这样创建的通道才能具有这些功能FileChannel channel f.getChannel()){ //通过RandomAccessFile创建一个通道channel.write(ByteBuffer.wrap(伞兵二号马飞飞准备就绪.getBytes()));System.out.println(写操作完成之后文件访问位置channel.position()); //注意读取也是从现在的位置开始channel.position(0); //需要将位置变回到最前面这样下面才能从文件的最开始进行读取ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer);buffer.flip();System.out.println(new String(buffer.array(), 0, buffer.remaining()));} }可以看到一个FileChannel既可以完成文件读取也可以完成文件的写入。 除了基本的读写操作我们也可以直接对文件进行截断 public static void main(String[] args) throws IOException {try(RandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel()){//截断文件只留前20个字节channel.truncate(20);ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer);buffer.flip();System.out.println(new String(buffer.array(), 0, buffer.remaining()));} }可以看到文件的内容直接被截断了文件内容就只剩一半了。 当然如果我们要进行文件的拷贝也是很方便的只需要使用通道就可以比如我们现在需要将一个通道的数据写入到另一个通道就可以直接使用transferTo方法 public static void main(String[] args) throws IOException {try(FileOutputStream out new FileOutputStream(test2.txt);FileInputStream in new FileInputStream(test.txt)){FileChannel inChannel in.getChannel(); //获取到test文件的通道inChannel.transferTo(0, inChannel.size(), out.getChannel()); //直接将test文件通道中的数据转到test2文件的通道中} }可以看到执行后文件的内容全部被复制到另一个文件了。 当然反向操作也是可以的 public static void main(String[] args) throws IOException {try(FileOutputStream out new FileOutputStream(test2.txt);FileInputStream in new FileInputStream(test.txt)){FileChannel inChannel in.getChannel(); //获取到test文件的通道out.getChannel().transferFrom(inChannel, 0, inChannel.size()); //直接将从test文件通道中传来的数据转给test2文件的通道} }当我们要编辑某个文件时通过使用MappedByteBuffer类可以将其映射到内存中进行编辑编辑的内容会同步更新到文件中 //注意一定要是可写的不然无法进行修改操作 try(RandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel()){//通过map方法映射文件的某一段内容创建MappedByteBuffer对象//比如这里就是从第四个字节开始映射10字节内容到内存中//注意这里需要使用MapMode.READ_WRITE模式其他模式无法保存数据到文件MappedByteBuffer buffer channel.map(FileChannel.MapMode.READ_WRITE, 4, 10);//我们可以直接对在内存中的数据进行编辑也就是编辑Buffer中的内容//注意这里写入也是从pos位置开始的默认是从0开始相对于文件就是从第四个字节开始写//注意我们只映射了10个字节也就是写的内容不能超出10字节了buffer.put(yyds.getBytes());//编辑完成后通过force方法将数据写回文件的映射区域buffer.force(); }可以看到文件的某一个区域已经被我们修改了并且这里实际上使用的就是DirectByteBuffer直接缓冲区效率还是很高的。 文件锁FileLock 我们可以创建一个跨进程文件锁来防止多个进程之间的文件争抢操作注意这里是进程不是线程FileLock是文件锁它能保证同一时间只有一个进程程序能够修改它或者都只可以读这样就解决了多进程间的同步文件保证了安全性。但是需要注意的是它进程级别的不是线程级别的他可以解决多个进程并发访问同一个文件的问题但是它不适用于控制同一个进程中多个线程对一个文件的访问。 那么我们来看看如何使用文件锁 public static void main(String[] args) throws IOException, InterruptedException {//创建RandomAccessFile对象并拿到ChannelRandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel();System.out.println(new Date() 正在尝试获取文件锁...);//接着我们直接使用lock方法进行加锁操作如果其他进程已经加锁那么会一直阻塞在这里//加锁操作支持对文件的某一段进行加锁比如这里就是从0开始后的6个字节加锁false代表这是一把独占锁//范围锁甚至可以提前加到一个还未写入的位置上FileLock lock channel.lock(0, 6, false);System.out.println(new Date() 已获取到文件锁);Thread.sleep(5000); //假设要处理5秒钟System.out.println(new Date() 操作完毕释放文件锁);//操作完成之后使用release方法进行锁释放lock.release(); }有关共享锁和独占锁 进程对文件加独占锁后当前进程对文件可读可写独占此文件其它进程是不能读该文件进行读写操作的。进程对文件加共享锁后进程可以对文件进行读操作但是无法进行写操作共享锁可以被多个进程添加但是只要存在共享锁就不能添加独占锁。 现在我们来启动两个进程试试看我们需要在IDEA中配置一下两个启动项 现在我们依次启动它们 可以看到确实是两个进程同一时间只能有一个进行访问而另一个需要等待锁释放。 那么如果我们申请的是文件的不同部分呢 //其中一个进程锁 0 - 5 FileLock lock channel.lock(0, 6, false); //另一个进程锁 6 - 11 FileLock lock channel.lock(6, 6, false);可以看到两个进程这时就可以同时进行加锁操作了因为它们锁的是不同的段落。 那么要是交叉呢 //其中一个进程锁 0 - 5 FileLock lock channel.lock(0, 6, false); //另一个进程锁 3 - 8 FileLock lock channel.lock(3, 6, false);可以看到交叉的情况下也是会出现阻塞的。 接着我们来看看共享锁共享锁允许多个进程同时加锁但是不能进行写操作 public static void main(String[] args) throws IOException, InterruptedException {RandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel();System.out.println(new Date() 正在尝试获取文件锁...);//现在使用共享锁FileLock lock channel.lock(0, Long.MAX_VALUE, true);System.out.println(new Date() 已获取到文件锁);//进行写操作channel.write(ByteBuffer.wrap(new Date().toString().getBytes()));System.out.println(new Date() 操作完毕释放文件锁);//操作完成之后使用release方法进行锁释放lock.release();}当我们进行写操作时 可以看到直接抛出异常说另一个程序已锁定文件的一部分进程无法访问某些系统或是环境实测无效比如UP主的arm架构MacOS就不生效这个异常是在Windows环境下运行得到的 当然我们也可以测试一下多个进行同时加共享锁 public static void main(String[] args) throws IOException, InterruptedException {RandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel();System.out.println(new Date() 正在尝试获取文件锁...);FileLock lock channel.lock(0, Long.MAX_VALUE, true);System.out.println(new Date() 已获取到文件锁);Thread.sleep(5000); //假设要处理5秒钟System.out.println(new Date() 操作完毕释放文件锁);lock.release(); }可以看到结果是多个进程都能加共享锁 当然除了直接使用lock()方法进行加锁之外我们也可以使用tryLock()方法以非阻塞方式获取文件锁但是如果获取锁失败会得到null public static void main(String[] args) throws IOException, InterruptedException {RandomAccessFile f new RandomAccessFile(test.txt, rw);FileChannel channel f.getChannel();System.out.println(new Date() 正在尝试获取文件锁...);FileLock lock channel.tryLock(0, Long.MAX_VALUE, false);System.out.println(lock);Thread.sleep(5000); //假设要处理5秒钟lock.release(); }可以看到两个进程都去尝试获取独占锁 第一个成功加锁的进程获得了对应的锁对象而第二个进程直接得到的是null。 到这里有关文件锁的相关内容就差不多了。 多路复用网络通信 前面我们已经介绍了NIO框架的两大核心Buffer和Channel我们接着来看看最后一个内容。 传统阻塞I/O网络通信 说起网络通信相信各位并不陌生正是因为网络的存在我们才能走进现代化的社会在JavaWeb阶段我们学习了如何使用Socket建立TCP连接进行网络通信 public static void main(String[] args) {try(ServerSocket server new ServerSocket(8080)){ //将服务端创建在端口8080上System.out.println(正在等待客户端连接...);Socket socket server.accept();System.out.println(客户端已连接IP地址为socket.getInetAddress().getHostAddress());BufferedReader reader new BufferedReader(new InputStreamReader(socket.getInputStream())); //通过System.out.print(接收到客户端数据);System.out.println(reader.readLine());OutputStreamWriter writer new OutputStreamWriter(socket.getOutputStream());writer.write(已收到);writer.flush();}catch (IOException e){e.printStackTrace();} }public static void main(String[] args) {try (Socket socket new Socket(localhost, 8080);Scanner scanner new Scanner(System.in)){System.out.println(已连接到服务端);OutputStream stream socket.getOutputStream();OutputStreamWriter writer new OutputStreamWriter(stream); //通过转换流来帮助我们快速写入内容System.out.println(请输入要发送给服务端的内容);String text scanner.nextLine();writer.write(text\n); //因为对方是readLine()这里加个换行符writer.flush();System.out.println(数据已发送text);BufferedReader reader new BufferedReader(new InputStreamReader(socket.getInputStream()));System.out.println(收到服务器返回reader.readLine());}catch (IOException e){System.out.println(服务端连接失败);e.printStackTrace();}finally {System.out.println(客户端断开连接);} }当然我们也可以使用前面讲解的通道来进行通信 public static void main(String[] args) {//创建一个新的ServerSocketChannel一会直接使用SocketChannel进行网络IO操作try (ServerSocketChannel serverChannel ServerSocketChannel.open()){//依然是将其绑定到8080端口serverChannel.bind(new InetSocketAddress(8080));//同样是调用accept()方法阻塞等待新的连接到来SocketChannel socket serverChannel.accept();//因为是通道两端的信息都是可以明确的这里获取远端地址当然也可以获取本地地址System.out.println(客户端已连接IP地址为socket.getRemoteAddress());//使用缓冲区进行数据接收ByteBuffer buffer ByteBuffer.allocate(128);socket.read(buffer); //SocketChannel同时实现了读写通道接口所以可以直接进行双向操作buffer.flip();System.out.print(接收到客户端数据new String(buffer.array(), 0, buffer.remaining()));//直接向通道中写入数据就行socket.write(ByteBuffer.wrap(已收到.getBytes()));//记得关socket.close();} catch (IOException e) {throw new RuntimeException(e);} }public static void main(String[] args) {//创建一个新的SocketChannel一会通过通道进行通信try (SocketChannel channel SocketChannel.open(new InetSocketAddress(localhost, 8080));Scanner scanner new Scanner(System.in)){System.out.println(已连接到服务端);System.out.println(请输入要发送给服务端的内容);String text scanner.nextLine();//直接向通道中写入数据真舒服channel.write(ByteBuffer.wrap(text.getBytes()));ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer); //直接从通道中读取数据buffer.flip();System.out.println(收到服务器返回new String(buffer.array(), 0, buffer.remaining()));} catch (IOException e) {throw new RuntimeException(e);} }虽然可以通过传统的Socket进行网络通信但是我们发现如果要进行IO操作我们需要单独创建一个线程来进行处理比如现在有很多个客户端服务端需要同时进行处理那么如果我们要处理这些客户端的请求那么我们就只能单独为其创建一个线程来进行处理 虽然这样看起来比较合理但是随着客户端数量的增加如果要保持持续通信那么就不能摧毁这些线程而是需要一直保留但是实际上很多时候只是保持连接一直在阻塞等待客户端的读写操作IO操作的频率很低这样就白白占用了一条线程很多时候都是站着茅坑不拉屎但是我们的线程不可能无限制的进行创建总有一天会耗尽服务端的资源那么现在怎么办呢关键是现在又有很多客户端源源不断地连接并进行操作这时我们就可以利用NIO为我们提供的多路复用编程模型。 我们来看看NIO为我们提供的模型 服务端不再是一个单纯通过accept()方法来创建连接的机制了而是根据客户端不同的状态Selector会不断轮询只有客户端在对应的状态时比如真正开始读写操作时才会创建线程或进行处理这样就不会一直阻塞等待某个客户端的IO操作了而不是创建之后需要一直保持连接即使没有任何的读写操作。这样就不会因为占着茅坑不拉屎导致线程无限制地创建下去了。 通过这种方式甚至单线程都能做到高效的复用最典型的例子就是Redis了因为内存的速度非常快多线程上下文的开销就会显得有些拖后腿还不如直接单线程简单高效这也是为什么Redis单线程也能这么快的原因。 因此我们就从NIO框架的第三个核心内容Selector开始讲起。 选择器与I/O多路复用 前面我们大概了解了一下选择器我们知道选择器是当具体有某一个状态比如读、写、请求已经就绪时才会进行处理而不是让我们的程序主动地进行等待。 既然我们现在需要实现IO多路复用那么我们来看看常见的IO多路复用模型也就是Selector的实现方案比如现在有很多个用户连接到我们的服务器 select当这些连接出现具体的某个状态时只是知道已经就绪了但是不知道详具体是哪一个连接已经就绪每次调用都进行线性遍历所有连接时间复杂度为O(n)并且存在最大连接数限制。poll同上但是由于底层采用链表所以没有最大连接数限制。epoll采用事件通知方式当某个连接就绪能够直接进行精准通知这是因为在内核实现中epoll是根据每个fd上面的callback函数实现的只要就绪会会直接回调callback函数实现精准通知但是只有Linux支持这种方式时间复杂度O(1)Java在Linux环境下正是采用的这种模式进行实现的。 好了既然多路复用模型了解完毕了那么我们就来看看如何让我们的网络通信实现多路复用 public static void main(String[] args) {try (ServerSocketChannel serverChannel ServerSocketChannel.open();Selector selector Selector.open()){ //开启一个新的Selector这玩意也是要关闭释放资源的serverChannel.bind(new InetSocketAddress(8080));//要使用选择器进行操作必须使用非阻塞的方式这样才不会像阻塞IO那样卡在accept()而是直接通过让选择器去进行下一步操作serverChannel.configureBlocking(false);//将选择器注册到ServerSocketChannel中后面是选择需要监听的时间只有发生对应事件时才会进行选择多个事件用 | 连接注意并不是所有的Channel都支持以下全部四个事件可能只支持部分//因为是ServerSocketChannel这里我们就监听accept就可以了等待客户端连接//SelectionKey.OP_CONNECT --- 连接就绪事件表示客户端与服务器的连接已经建立成功//SelectionKey.OP_ACCEPT --- 接收连接事件表示服务器监听到了客户连接服务器可以接收这个连接了//SelectionKey.OP_READ --- 读 就绪事件表示通道中已经有了可读的数据可以执行读操作了//SelectionKey.OP_WRITE --- 写 就绪事件表示已经可以向通道写数据了这玩意比较特殊一般情况下因为都是可以写入的所以可能会无限循环serverChannel.register(selector, SelectionKey.OP_ACCEPT);while (true) { //无限循环等待新的用户网络操作//每次选择都可能会选出多个已经就绪的网络操作没有操作时会暂时阻塞int count selector.select();System.out.println(监听到 count 个事件);SetSelectionKey selectionKeys selector.selectedKeys();IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {SelectionKey key iterator.next();//根据不同的事件类型执行不同的操作即可if(key.isAcceptable()) { //如果当前ServerSocketChannel已经做好准备处理AcceptSocketChannel channel serverChannel.accept();System.out.println(客户端已连接IP地址为channel.getRemoteAddress());//现在连接就建立好了接着我们需要将连接也注册选择器比如我们需要当这个连接有内容可读时就进行处理channel.configureBlocking(false);channel.register(selector, SelectionKey.OP_READ);//这样就在连接建立时完成了注册} else if(key.isReadable()) { //如果当前连接有可读的数据并且可以写那么就开始处理SocketChannel channel (SocketChannel) key.channel();ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer);buffer.flip();System.out.println(接收到客户端数据new String(buffer.array(), 0, buffer.remaining()));//直接向通道中写入数据就行channel.write(ByteBuffer.wrap(已收到.getBytes()));//别关说不定用户还要继续通信呢}//处理完成后一定记得移出迭代器不然下次还有iterator.remove();}}} catch (IOException e) {throw new RuntimeException(e);} }接着我们来编写一下客户客户端 public static void main(String[] args) {//创建一个新的SocketChannel一会通过通道进行通信try (SocketChannel channel SocketChannel.open(new InetSocketAddress(localhost, 8080));Scanner scanner new Scanner(System.in)){System.out.println(已连接到服务端);while (true) { //咱给它套个无限循环这样就能一直发消息了System.out.println(请输入要发送给服务端的内容);String text scanner.nextLine();//直接向通道中写入数据真舒服channel.write(ByteBuffer.wrap(text.getBytes()));System.out.println(已发送);ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer); //直接从通道中读取数据buffer.flip();System.out.println(收到服务器返回new String(buffer.array(), 0, buffer.remaining()));}} catch (IOException e) {throw new RuntimeException(e);} }我们来看看效果 可以看到成功实现了当然各位也可以跟自己的室友一起开客户端进行测试现在我们只用了一个线程就能够同时处理多个请求可见多路复用是多么重要。 实现Reactor模式 前面我们简单实现了多路复用网络通信我们接着来了解一下Reactor模式对我们的服务端进行优化。 现在我们来看看如何进行优化我们首先抽象出两个组件Reactor线程和Handler处理器 Reactor线程负责响应IO事件并分发到Handler处理器。新的事件包含连接建立就绪、读就绪、写就绪等。Handler处理器执行非阻塞的操作。 实际上我们之前编写的算是一种单线程Reactor的朴素模型面向过程的写法我们来看看标准的写法 客户端还是按照我们上面的方式连接到Reactor并通过选择器走到Acceptor或是HandlerAcceptor主要负责客户端连接的建立Handler负责读写操作代码如下首先是Handler public class Handler implements Runnable{private final SocketChannel channel;public Handler(SocketChannel channel) {this.channel channel;}Overridepublic void run() {try {ByteBuffer buffer ByteBuffer.allocate(128);channel.read(buffer);buffer.flip();System.out.println(接收到客户端数据new String(buffer.array(), 0, buffer.remaining()));channel.write(ByteBuffer.wrap(已收到.getBytes()));}catch (IOException e){e.printStackTrace();}} }接着是Acceptor实际上就是把上面的业务代码搬个位置罢了 /*** Acceptor主要用于处理连接操作*/ public class Acceptor implements Runnable{private final ServerSocketChannel serverChannel;private final Selector selector;public Acceptor(ServerSocketChannel serverChannel, Selector selector) {this.serverChannel serverChannel;this.selector selector;}Overridepublic void run() {try{SocketChannel channel serverChannel.accept();System.out.println(客户端已连接IP地址为channel.getRemoteAddress());channel.configureBlocking(false);//这里在注册时创建好对应的Handler这样在Reactor中分发的时候就可以直接调用Handler了channel.register(selector, SelectionKey.OP_READ, new Handler(channel));}catch (IOException e){e.printStackTrace();}} }这里我们在注册时丢了一个附加对象进去这个附加对象会在选择器选择到此通道上时可以通过attachment()方法进行获取对于我们简化代码有大作用一会展示我们接着来看看Reactor public class Reactor implements Closeable, Runnable{private final ServerSocketChannel serverChannel;private final Selector selector;public Reactor() throws IOException{serverChannel ServerSocketChannel.open();selector Selector.open();}Overridepublic void run() {try {serverChannel.bind(new InetSocketAddress(8080));serverChannel.configureBlocking(false);//注册时将Acceptor作为附加对象存放当选择器选择后也可以获取到serverChannel.register(selector, SelectionKey.OP_ACCEPT, new Acceptor(serverChannel, selector));while (true) {int count selector.select();System.out.println(监听到 count 个事件);SetSelectionKey selectionKeys selector.selectedKeys();IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {this.dispatch(iterator.next()); //通过dispatch方法进行分发iterator.remove();}}}catch (IOException e) {e.printStackTrace();}}//通过此方法进行分发private void dispatch(SelectionKey key){Object att key.attachment(); //获取attachmentServerSocketChannel和对应的客户端Channel都添加了的if(att instanceof Runnable) {((Runnable) att).run(); //由于Handler和Acceptor都实现自Runnable接口这里就统一调用一下} //这样就实现了对应的时候调用对应的Handler或是Acceptor了}//用了记得关保持好习惯就像看完视频要三连一样Overridepublic void close() throws IOException {serverChannel.close();selector.close();} }最后我们编写一下主类 public static void main(String[] args) {//创建Reactor对象启动完事try (Reactor reactor new Reactor()){reactor.run();}catch (IOException e) {e.printStackTrace();} }这样我们就实现了单线程Reactor模式注意全程使用到的都只是一个线程没有创建新的线程来处理任何事情。 但是单线程始终没办法应对大量的请求如果请求量上去了单线程还是很不够用接着我们来看看多线程Reactor模式它创建了多个线程处理我们可以将数据读取完成之后的操作交给线程池来执行 其实我们只需要稍微修改一下Handler就行了 public class Handler implements Runnable{//把线程池给安排了10个线程private static final ExecutorService POOL Executors.newFixedThreadPool(10);private final SocketChannel channel;public Handler(SocketChannel channel) {this.channel channel;}Overridepublic void run() {try {ByteBuffer buffer ByteBuffer.allocate(1024);channel.read(buffer);buffer.flip();POOL.submit(() - {try {System.out.println(接收到客户端数据new String(buffer.array(), 0, buffer.remaining()));channel.write(ByteBuffer.wrap(已收到.getBytes()));}catch (IOException e){e.printStackTrace();}});} catch (IOException e) {throw new RuntimeException(e);}} }这样在数据读出之后就可以将数据处理交给线程池执行。 但是这样感觉还是划分的不够一个Reactor需要同时处理来自客户端的所有操作请求显得有些乏力那么不妨我们将Reactor做成一主多从的模式让主Reactor只负责Accept操作而其他的Reactor进行各自的其他操作 现在我们来重新设计一下我们的代码Reactor类就作为主节点不进行任何修改我们来修改一下其他的 //SubReactor作为从Reactor public class SubReactor implements Runnable, Closeable {//每个从Reactor也有一个Selectorprivate final Selector selector;//创建一个4线程的线程池也就是四个从Reactor工作private static final ExecutorService POOL Executors.newFixedThreadPool(4);private static final SubReactor[] reactors new SubReactor[4];private static int selectedIndex 0; //采用轮询机制每接受一个新的连接就轮询分配给四个从Reactorstatic { //在一开始的时候就让4个从Reactor跑起来for (int i 0; i 4; i) {try {reactors[i] new SubReactor();POOL.submit(reactors[i]);} catch (IOException e) {e.printStackTrace();}}}//轮询获取下一个SelectorAcceptor用public static Selector nextSelector(){Selector selector reactors[selectedIndex].selector;selectedIndex (selectedIndex 1) % 4;return selector;}private SubReactor() throws IOException {selector Selector.open();}Overridepublic void run() {try { //启动后直接等待selector监听到对应的事件即可其他的操作逻辑和Reactor一致while (true) {int count selector.select();System.out.println(Thread.currentThread().getName() 监听到 count 个事件);SetSelectionKey selectionKeys selector.selectedKeys();IteratorSelectionKey iterator selectionKeys.iterator();while (iterator.hasNext()) {this.dispatch(iterator.next());iterator.remove();}}}catch (IOException e) {e.printStackTrace();}}private void dispatch(SelectionKey key){Object att key.attachment();if(att instanceof Runnable) {((Runnable) att).run();}}Overridepublic void close() throws IOException {selector.close();} }我们接着来修改一下Acceptor类 public class Acceptor implements Runnable{private final ServerSocketChannel serverChannel; //只需要一个ServerSocketChannel就行了public Acceptor(ServerSocketChannel serverChannel) {this.serverChannel serverChannel;}Overridepublic void run() {try{SocketChannel channel serverChannel.accept(); //还是正常进行Accept操作得到SocketChannelSystem.out.println(Thread.currentThread().getName() 客户端已连接IP地址为channel.getRemoteAddress());channel.configureBlocking(false);Selector selector SubReactor.nextSelector(); //选取下一个从Reactor的Selectorselector.wakeup(); //在注册之前唤醒一下防止卡死channel.register(selector, SelectionKey.OP_READ, new Handler(channel)); //注意现在注册的是从Reactor的Selector}catch (IOException e){e.printStackTrace();}} }现在SocketChannel相关的操作就由从Reactor进行处理了而不是一律交给主Reactor进行操作。 至此我们已经了解了NIO的三大组件Buffer、Channel、Selector有关NIO基础相关的内容就讲解到这里。下一章我们将继续讲解基于NIO实现的高性能网络通信框架Netty。
http://www.sadfv.cn/news/285731/

相关文章:

  • 普通高等学校健康驿站建设指引php 公司网站
  • 河北省建设执业资格注册管理中心网站举报网站建设情况总结
  • 电子商务网站建设自服务器个人网站建设程序设计
  • 做网站哪便宜wordpress修改站标在哪个文件
  • 外贸最大电子元器件交易网站商贸有限公司企业简介
  • 电商网站建设建议优化教育培训
  • 设计自己的网站网站策划方案800字
  • 如何做网络营销推广服务机构嘉兴优化网站价格
  • 网站标题写什么作用是什么网站搭建好之后提示网页走丢了
  • 北京免费网站设计网站建设推广员工资
  • xml网站地图格式电子商务网站建设及推广方案
  • 泰州网站建设价格网页设计下载免费
  • 中国做网站的公司有哪些外贸会计做账流程
  • 一般网站建设公司有哪些西安网站开发高端网站开发
  • 遵义做百度网站一年多少钱郑州平面设计公司排行榜
  • 天津网站建设首选津坤科技上海资格证报名网站
  • 通州网站建设全包做网站和做网店哪个好
  • 使用apmserv本地搭建多个网站需要网站建设
  • 用word文档做网站详情页页面页面
  • 温州网站设计图片大全网站开发 聊天窗口
  • 沈阳网站建设费用网站认证要钱
  • 旅游网站建设网站推广龙岗高端建设网站建设
  • 加快建设企业门户网站建唐山网站制作网络公司
  • 艺术设计类网站seo流量增长策略
  • 厦门建设网站公司惠州企业建站系统
  • 搭建网站服务器需要什么配置wap网站快速开发
  • 咸阳网站建设学校网站建设培训学院
  • 如何更换网站服务器怎么制作游戏地图
  • 濮阳网站建设熊掌号网站设计过程怎么写
  • 手机里面的网站怎么制作往网站上传照片怎么做