概述
-
I/O模型就是用什么样的通道或者通信方式进行数据的发送和接收,这很大程度上决定了程序通信的性能
-
Java支持3种I/O模型:BIO、NIO、AIO
- BIO(Blocking I/O):同步阻塞,一个线程专门处理一个连接
- NIO(Non-blocking I/O):同步非阻塞,一个线程处理多个连接(请求),多个client注册到一个selector上,一个线程在一个selector上轮询是否有新的请求
- AIO(Asynchronous I/O)(NIO.2):异步非阻塞,一个线程处理多个连接(请求),多个client注册到一个selector上,一个线程在一个selector上监听是否有新的请求,如果有则触发异步回调(OS已经为IO操作注册好回调函数了,上层应用程序只需要去使用即可)
-
I/O多路复用是一种允许单个进程或线程监视多个I/O流的活动状态,并能在一个或多个流准备好进行读写时得到通知的技术
测试代码仓库:gitee.com/csuyth/io-t…
Java BIO
package java.io;
Java 1.0引入java.io包,并且在Java 1.1引入了Reader。
InputStream、OutputStream:输出输出流的抽象基类Reader、Writer:流包装类- 阻塞模式:总是等待完整的数据
Java NIO
概要介绍
package java.nio;
Java 1.4引入了java.nio包,并且在Java 1.7更新了NIO.2(增强文件操作、ASynchronousSocketChannel)
Buffer:原始类型的数据的容器charset.CharsetDecoder、charset.CharsetEncoder:字符集解码/编码器channels.Channel:IO通道,代表与外界(硬件设备、文件、socket等)的连接channels.Selector:SelectableChannel的多路复用器- 非阻塞模式:随时读取已经准备好的数据
NIO中Buffer、Channel、Selector关系图:
Server
|
+-----------------+-----------+-------------+
| | |
Thread Thread ...
| |
Selector Selector
| |
+---------+-----+----+-------+ +-------+-------+
| | | | | |
Channel Channel ... Channel Channel ...
| | | |
Buffer Buffer Buffer Buffer
| | | |
Client Client Client Client
Buffer
@see java.nio.Buffer
-
定义:存储具体原始类型(比如:byte类型、int类型)的数据的容器
-
buffer存储的数据是线性的、有限的
-
buffer最关键的属性为:
capacity、limit、positioncapacity:容量,buffer存储的元素个数,一旦定义不可改变,capacity >= 0limit:边界,第一个不该被读取或写入的元素的下标,0 <= limit <= capacityposition:当前位置,下一个该被读取或写入的元素的下标
+-------------------+------------------+------------------+
| used bytes | remaining bytes | out of bounds |
| | | |
+-------------------+------------------+------------------+
| | | |
0 <= position <= limit <= capacity
-
传输数据:由子类定义的
get、put方法- 相对操作:如果下标越界,
get操作抛出BufferUnderflowException异常,put操作抛出BufferOverflowException - 绝对操作:如果下标越界,抛出
IndexOutOfBoundsException异常
- 相对操作:如果下标越界,
-
mark属性和reset方法
- 如果
mark == -1,代表标记位未定义,使用未定义的mark,则抛出InvalidMarkException - 规定:
0 <= mark <= position <= limit <= capacity mark:标记,把mark标记为position的数值reset:重置,把position重置为mark的数值
- 如果
-
Clearing、 flipping、 rewinding
clear:清空数据,准备好接收新的数据,把limit设置为capacity,把position设置为0flip:反转模式,准备好读取buffer数据,把limit设置为当前position,把position设置为0rewind:倒回操作,准备好从头覆盖写数据,不改变limit,把position设置为0
-
compact:整理buffer,来更好的利用碎片空间,即利用
[0, position - 1]之间的空间- 把
[position, limit - 1]之间的数据拷贝到[0, limit - position - 1] - 把新
position设置为limit - position,把新limit设置为capacity
- 把
-
每个Buffer都是可读的,但并不是每个Buffer都是可写的
- 操作只读Buffer,会抛出异常
ReadOnlyBufferException isReadOnly方法,返回当前Buffer是否只读
- 操作只读Buffer,会抛出异常
-
Buffer不是线程安全的
-
Buffer支持链式编程,例如:
myBuffer.flip().position(23).limit(42); -
额外:
- remaining:指的是
position和limit中间的空间 - isDirect:分配的空间是不是直接内存
- hasArray、array:背后是不是靠Java数组实现的(HeapBuffer)
- remaining:指的是
DirectByteBuffer
@see java.nio.DirectByteBuffer
分配的内存空间是直接内存,也可以称为狭义上的堆外内存,通过unsafe实例调用native方法进行内存分配。
为什么要用直接内存呢?
通信过程中,如果使用堆内内存,那么需要把内存数据拷贝一份出去,再进行发送,所以直接使用堆外内存既提高了通信效率,又减少了GC负担。
- 属性
protected static final Unsafe unsafe = Bits.unsafe();
/** byte数组的对象头所占的字节数 */
private static final long arrayBaseOffset = (long)unsafe.arrayBaseOffset(byte[].class);
/** 是否线性 */
protected static final boolean unaligned = Bits.unaligned();
/**
* 附属对象,用于GC时避免附属对象被清理
* 执行duplicating, slicing这类派生操作时,源ByteBuffer即为att附属对象
*/
private final Object att;
/** 用于清理直接内存,避免内存泄露的 */
private final Cleaner cleaner;
- 内部类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) {
// Paranoia
return;
}
unsafe.freeMemory(address); // 释放直接内存
address = 0;
Bits.unreserveMemory(size, capacity); // 更新直接内存的使用量统计,释放空间
}
}
class Bits {
...
static void unreserveMemory(long size, int cap) {
long cnt = count.decrementAndGet();
long reservedMem = reservedMemory.addAndGet(-size);
long totalCap = totalCapacity.addAndGet(-cap);
assert cnt >= 0 && reservedMem >= 0 && totalCap >= 0;
}
...
}
- 构造器
DirectByteBuffer(int cap) { // package-private
super(-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 {
base = unsafe.allocateMemory(size); // 真实分配直接内存空间
} catch (OutOfMemoryError x) {
Bits.unreserveMemory(size, cap); // 释放空间
throw x;
}
unsafe.setMemory(base, size, (byte) 0); // 把数据初始化为0
if (pa && (base % ps != 0)) {
// Round up to page boundary
address = base + ps - (base & (ps - 1));
} else {
address = base;
}
// 创建内存cleaner,并添加到Cleaner链表里去
cleaner = Cleaner.create(this, new Deallocator(base, size, cap));
att = null;
}
class Bits {
...
// These methods should be called whenever direct memory is allocated or
// freed. They allow the user to control the amount of direct memory
// which a process may access. All sizes are specified in bytes.
static void reserveMemory(long size, int cap) {
if (!memoryLimitSet && VM.isBooted()) {
maxMemory = VM.maxDirectMemory();
memoryLimitSet = true;
}
// optimist!
// 直接尝试分配空间
if (tryReserveMemory(size, cap)) {
return;
}
final JavaLangRefAccess jlra = SharedSecrets.getJavaLangRefAccess();
// retry while helping enqueue pending Reference objects
// which includes executing pending Cleaner(s) which includes
// Cleaner(s) that free direct buffer memory
// 尝试清理DirectByteBuffer对应的堆外内存
while (jlra.tryHandlePendingReference()) {
// 清理成功,则再尝试分配
if (tryReserveMemory(size, cap)) {
return;
}
}
// trigger VM's Reference processing
// 触发GC,清理堆内的DirectByteBuffer
System.gc();
// a retry loop with exponential back-off delays
// (this gives VM some time to do it's job)
boolean interrupted = false;
try {
long sleepTime = 1;
int sleeps = 0;
while (true) {
// 等待守护线程清理直接内存,循环尝试分配内存
if (tryReserveMemory(size, cap)) {
return;
}
if (sleeps >= MAX_SLEEPS) {
break;
}
if (!jlra.tryHandlePendingReference()) {
try {
Thread.sleep(sleepTime);
sleepTime <<= 1;
sleeps++;
} catch (InterruptedException e) {
interrupted = true;
}
}
}
// no luck
// 实在是没有空间来分配了,抛出异常
throw new OutOfMemoryError("Direct buffer memory");
} finally {
if (interrupted) {
// don't swallow interrupts
Thread.currentThread().interrupt();
}
}
}
...
}
-
直接内存清理机制
- 依赖于Cleaner,把堆外内存的清理与堆内的VM GC关联起来。
- PhantomReference(虚引用)只能用于跟踪对象是何时被回收的。
- 但是如果gc过程中发现某个对象除了只有PhantomReference引用它之外,并没有其他的地方引用它了,那么VM会把这个引用放到java.lang.ref.Reference.pending队列里,在gc完毕的时候通知ReferenceHandler这个守护线程去执行一些后置处理。
- DirectByteBuffer关联的Cleaner是PhantomReference的一个子类,在最终的处理时,会通过Unsafe的free接口来释放DirectByteBuffer对应的堆外内存块。
- 由于young gc不会清理老年代对象,所以如果一直没有做full gc,那么老年代的DirectByteBuffer(冰山对象 Iceberg Object)背后关联的大量直接内存将无法释放,偷偷耗尽我们的物理内存。
public class Cleaner extends PhantomReference<Object> {
private static final ReferenceQueue<Object> dummyQueue = new ReferenceQueue();
private static Cleaner first = null;
private Cleaner next = null;
private Cleaner prev = null;
private final Runnable thunk;
private static synchronized Cleaner add(Cleaner var0) {
...
}
private static synchronized boolean remove(Cleaner var0) {
...
}
private Cleaner(Object var1, Runnable var2) {
super(var1, dummyQueue);
this.thunk = var2;
}
public static Cleaner create(Object var0, Runnable var1) {
// var0是directByteBuffer对象,var1是Deallocator对象
return var1 == null ? null : add(new Cleaner(var0, var1));
}
public void clean() {
if (remove(this)) { // 从链表中移除cleaner
try {
this.thunk.run(); // 执行清理逻辑
} catch (final Throwable var2) {
AccessController.doPrivileged(new PrivilegedAction<Void>() {
public Void run() {
if (System.err != null) {
(new Error("Cleaner terminated abnormally", var2)).printStackTrace();
}
System.exit(1);
return null;
}
});
}
}
}
}
public abstract class Reference<T> {
...
/* When active: next element in a discovered reference list maintained by GC (or this if last)
* pending: next element in the pending list (or null if last)
* otherwise: NULL
*/
transient private Reference<T> discovered; /* used by VM */
...
/* List of References waiting to be enqueued. The collector adds
* References to this list, while the Reference-handler thread removes
* them. This list is protected by the above lock object. The
* list uses the discovered field to link its elements.
*/
private static Reference<Object> pending = null; // JVM会把引用放入这个队列
/* High-priority thread to enqueue pending References
*/
private static class ReferenceHandler extends Thread {
private static void ensureClassInitialized(Class<?> clazz) {
try {
Class.forName(clazz.getName(), true, clazz.getClassLoader());
} catch (ClassNotFoundException e) {
throw (Error) new NoClassDefFoundError(e.getMessage()).initCause(e);
}
}
static {
// pre-load and initialize InterruptedException and Cleaner classes
// so that we don't get into trouble later in the run loop if there's
// memory shortage while loading/initializing them lazily.
ensureClassInitialized(InterruptedException.class);
ensureClassInitialized(Cleaner.class);
}
ReferenceHandler(ThreadGroup g, String name) {
super(g, name);
}
public void run() {
// 循环尝试处理cleaner,清理直接内存
while (true) {
tryHandlePending(true);
}
}
}
/**
* Try handle pending {@link Reference} if there is one.<p>
* Return {@code true} as a hint that there might be another
* {@link Reference} pending or {@code false} when there are no more pending
* {@link Reference}s at the moment and the program can do some other
* useful work instead of looping.
*
* @param waitForNotify if {@code true} and there was no pending
* {@link Reference}, wait until notified from VM
* or interrupted; if {@code false}, return immediately
* when there is no pending {@link Reference}.
* @return {@code true} if there was a {@link Reference} pending and it
* was processed, or we waited for notification and either got it
* or thread was interrupted before being notified;
* {@code false} otherwise.
*/
static boolean tryHandlePending(boolean waitForNotify) {
Reference<Object> r;
Cleaner c;
try {
synchronized (lock) {
if (pending != null) {
// 从链表中取出cleaner
r = pending;
// 'instanceof' might throw OutOfMemoryError sometimes
// so do this before un-linking 'r' from the 'pending' chain...
c = r instanceof Cleaner ? (Cleaner) r : null;
// unlink 'r' from 'pending' chain
pending = r.discovered;
r.discovered = null;
} else {
// The waiting on the lock may cause an OutOfMemoryError
// because it may try to allocate exception objects.
if (waitForNotify) {
lock.wait(); // 等待JVM来唤醒自己
}
// retry if waited
return waitForNotify;
}
}
} catch (OutOfMemoryError x) {
// Give other threads CPU time so they hopefully drop some live references
// and GC reclaims some space.
// Also prevent CPU intensive spinning in case 'r instanceof Cleaner' above
// persistently throws OOME for some time...
Thread.yield();
// retry
return true;
} catch (InterruptedException x) {
// retry
return true;
}
// Fast path for cleaners
if (c != null) {
c.clean(); // 清理直接内存
return true;
}
ReferenceQueue<? super Object> q = r.queue;
if (q != ReferenceQueue.NULL) q.enqueue(r);
return true;
}
static {
ThreadGroup tg = Thread.currentThread().getThreadGroup();
for (ThreadGroup tgn = tg;
tgn != null;
tg = tgn, tgn = tg.getParent());
// 创建一个守护线程handler,处理cleaner
Thread handler = new ReferenceHandler(tg, "Reference Handler");
/* If there were a special system-only priority greater than
* MAX_PRIORITY, it would be used here
*/
handler.setPriority(Thread.MAX_PRIORITY);
handler.setDaemon(true);
handler.start();
// 给SharedSecrets提供主动尝试清理的方法
// provide access in SharedSecrets
SharedSecrets.setJavaLangRefAccess(new JavaLangRefAccess() {
@Override
public boolean tryHandlePendingReference() {
// 返回true,则说明成功释放了部分直接内存
return tryHandlePending(false);
}
});
}
...
}
- get、put:逻辑很简单,基地址寻址操作
private long ix(int i) {
return address + ((long)i << 0);
}
public byte get() {
return ((unsafe.getByte(ix(nextGetIndex()))));
}
public byte get(int i) {
return ((unsafe.getByte(ix(checkIndex(i)))));
}
public ByteBuffer put(byte x) {
unsafe.putByte(ix(nextPutIndex()), ((x)));
return this;
}
public ByteBuffer put(int i, byte x) {
unsafe.putByte(ix(checkIndex(i)), ((x)));
return this;
}
Channel
@see java.nio.channels.Channel
/**
* Channel通道,代表连接到一个实体(比如:硬件设备、文件、网络套接字)的连接。
* 能够支持一个或多个I/O操作(比如:读、写)。
* 通道要么是关闭的,要么是开启的。
* 创建通道时,通道即为开启状态,一旦通道被关闭,则永远维持关闭状态。
* 一般来说,Channel需要是线程安全的。
*/
public interface Channel extends Closeable {
/**
* Tells whether or not this channel is open.
*/
public boolean isOpen();
/**
* Closes this channel.
*/
public void close() throws IOException;
}
常见的Channel子类:
FileChannel:文件操作DatagramChannel:UDP/IPServerSocketChannel:B/S、TCP/IPSocketChannel:C/S、TCP/IP
相关比较基本的子接口:
Jdk中接口是怎么定义的,怎么划分层次的,值得学习参考。
/** A channel that can write bytes. */
public interface WritableByteChannel extends Channel {
public int write(ByteBuffer src) throws IOException;
}
/** A channel that can read bytes. */
public interface ReadableByteChannel extends Channel {
public int read(ByteBuffer dst) throws IOException;
}
/** A channel that can read and write bytes. */
public interface ByteChannel extends ReadableByteChannel, WritableByteChannel {
}
/** A channel that can read bytes into a sequence of buffers. */
public interface ScatteringByteChannel extends ReadableByteChannel {
public long read(ByteBuffer[] dsts, int offset, int length) throws IOException;
public long read(ByteBuffer[] dsts) throws IOException;
}
/** A channel that can write bytes from a sequence of buffers. */
public interface GatheringByteChannel extends WritableByteChannel {
public long write(ByteBuffer[] srcs, int offset, int length) throws IOException;
public long write(ByteBuffer[] srcs) throws IOException;
}
SeekableByteChannel
@see java.nio.channels.SeekableByteChannel
- FileChannel:继承InterruptibleChannel的骨架实现类,实现SeekableByteChannel接口
/** A channel for reading, writing, mapping, and manipulating a file. */
public abstract class FileChannel
extends AbstractInterruptibleChannel
implements SeekableByteChannel, GatheringByteChannel, ScatteringByteChannel {
...
}
- InterruptibleChannel:作为标记接口(Marker Interface),对channel的方法重新进行了规定,本身没有新增方法
/** A channel that can be asynchronously closed and interrupted. */
public interface InterruptibleChannel extends Channel {
/**
* Closes this channel.
* 当前在该通道上的I/O操作中阻塞的任何线程都将收到一个AsynchronousCloseException。
*/
@Override
public void close() throws IOException;
}
/** InterruptibleChannel的基本骨架实现 */
public abstract class AbstractInterruptibleChannel
implements Channel, InterruptibleChannel {
... // 省略
}
- SeekableByteChannel:基本上来说,这个接口是专门为了文件通道操作而生的
/** A byte channel that maintains a current position and allows the position to be changed. */
public interface SeekableByteChannel extends ByteChannel {
@Override
int read(ByteBuffer dst) throws IOException;
@Override
int write(ByteBuffer src) throws IOException;
/**
* Returns this channel's position.
*/
long position() throws IOException;
/**
* Sets this channel's position.
*/
SeekableByteChannel position(long newPosition) throws IOException;
/**
* Returns the current size of entity to which this channel is connected.
*/
long size() throws IOException;
/**
* Truncates the entity, to which this channel is connected, to the given
* size.
*/
SeekableByteChannel truncate(long size) throws IOException;
}
怎么理解SeekableByteChannel的position?
通常,position是指从文件开头到当前读取或写入位置的字节数。
- 当你打开一个
SeekableByteChannel通道时,初始位置通常会被设置为0(从文件开头)。 - 当你进行读取或写入操作时,
position()方法会返回当前的读写位置。 - 每次读取或写入操作完成后,
position()方法会更新当前位置,指向已读取或已写入数据之后的位置。
FileChannel
@see java.nio.channels.FileChannel
- Zero-copy
在Java中,零拷贝技术(zero-copy)通常与网络IO和文件系统IO相关。它是一种减少或消除CPU参与数据传输过程的技术,允许操作系统直接将数据从文件系统缓存传输到网络堆栈,而不需要CPU将数据从一个内存区域复制到另一个内存区域。
FileChannel sourceChannel = new FileInputStream("source.txt").getChannel();
FileChannel targetChannel = new FileOutputStream("target.txt").getChannel();
sourceChannel.transferTo(0, sourceChannel.size(), targetChannel);
// or
targetChannel.transferFrom(sourceChannel, 0, sourceChannel.size());
sourceChannel.close();
targetChannel.close();
- Memory-mapped files
Java NIO的 FileChannel 还可以创建内存映射文件。当文件内容被映射到内存时,应用程序可以直接在内存中操作这些内容,操作系统会负责将修改后的内容写回到文件中。这个过程中减少了数据在用户空间和内核空间之间的拷贝。
FileChannel fileChannel = new RandomAccessFile("largeFile.txt", "rw").getChannel();
MappedByteBuffer buffer = fileChannel.map(FileChannel.MapMode.READ_WRITE, 0, fileChannel.size());
// 在这个buffer上进行数据操作,这些操作将直接反映到文件上
buffer.put(0, (byte) 97); // example of write operation
fileChannel.close();
SelectableChannel
@see java.nio.channels.SelectableChannel
SelectableChannel
-
SelectableChannel是一个通过Selector进行多路复用的Channel。
-
需要被selector使用,首先需要这个channel通过register方法来进行注册,这个register方法会返回一个SelectionKey代表注册证。
-
一旦注册了,就将保持注册状态,直到取消注册diregistered,取消注册意味着释放所有由selector分配的资源。
-
channel不能直接被取消注册,注册证selectionKey需要调用cannel方法后,channel才可以取消注册。cancel一个SelectionKey的条件是:对应的channel在selector下一次选择操作之前进行取消注册。当channel执行了close操作,则这个channel全部的selectionKey都绝对会cannel。
-
SelectableChannel最多只能注册一个特定的selector
-
SelectableChannel是线程安全的
-
SelectableChannel要么处于“阻塞(blocking)”模式,要么处于“非阻塞(non-blocking)”模式
- blocking:任何I/O操作都将阻塞直到被完成
- non-blocking:任何I/O操作都不会阻塞,但是可能只能传输比请求更少的字节数,甚至0个字节数
-
新创建的SelectableChannel总是处于blocking模式。non-blocking模式在与selector协调工作时发挥最佳效果,SelectableChannel必须在注册到一个selector之前被设置为non-blocking模式,并且在取消注册之前不能回退到blocking模式。
@see java.nio.channels.SelectionKey // 代表channel注册到selector到注册证
@see java.nio.channels.spi.AbstractSelectionKey // 骨架实现基类
@see java.nio.channels.spi.SelectorProvider // 工厂基类,提供构造channel对象的方法
@see java.nio.channels.spi.AbstractSelectableChannel // 骨架实现基类
/**
* A channel that can be multiplexed via a {@link Selector}.
*/
public abstract class SelectableChannel
extends AbstractInterruptibleChannel
implements Channel {
protected SelectableChannel() { }
/**
* @return The provider that created this channel
*/
public abstract SelectorProvider provider();
/**
* 一个具体实现类的这个方法总是返回相同的数值。
* 操作掩码见SelectionKey的静态变量,有四种:OP_READ、OP_WRITE、OP_CONNECT、OP_ACCEPT
*
* @return 合法操作合集
*/
public abstract int validOps();
// 下面的变量定义在java.nio.channels.spi.AbstractSelectableChannel中
// Internal state:
// keySet, 一个数值实现的set,存放SelectionKey
// boolean isRegistered, protected by key set
// regLock, 一个锁,用于防止重复注册
// boolean isBlocking, protected by regLock
/**
* Tells whether or not this channel is currently registered with any
* selectors. A newly-created channel is not registered.
*
* <p> Due to the inherent delay between key cancellation and channel
* deregistration, a channel may remain registered for some time after all
* of its keys have been cancelled. A channel may also remain registered
* for some time after it is closed. </p>
*
* @return <tt>true</tt> if, and only if, this channel is registered
*/
public abstract boolean isRegistered();
//
// sync(keySet) { return isRegistered; }
/**
* Retrieves the key representing the channel's registration with the given
* selector.
*
* @param sel
* The selector
*
* @return The key returned when this channel was last registered with the
* given selector, or <tt>null</tt> if this channel is not
* currently registered with that selector
*/
public abstract SelectionKey keyFor(Selector sel);
//
// sync(keySet) { return findKey(sel); }
/**
* 用selecrot注册channel,返回表示注册关系的selectionKey
*/
public abstract SelectionKey register(Selector sel, int ops, Object att)
throws ClosedChannelException;
//
// sync(regLock) {
// sync(keySet) { look for selector }
// if (channel found) { set interest ops -- may block in selector;
// return key; }
// create new key -- may block somewhere in selector;
// sync(keySet) { add key; }
// attach(attachment);
// return key;
// }
/** 重载 */
public final SelectionKey register(Selector sel, int ops)
throws ClosedChannelException
{
return register(sel, ops, null);
}
/**
* 修改channel的阻塞模式
*/
public abstract SelectableChannel configureBlocking(boolean block)
throws IOException;
//
// sync(regLock) {
// sync(keySet) { throw IBME if block && isRegistered; }
// change mode;
// }
/**
* @return <tt>true</tt> if, and only if, this channel is in blocking mode
*/
public abstract boolean isBlocking();
/**
* @return The blocking-mode lock object,即为regLock
*/
public abstract Object blockingLock();
}
java.nio.channels.SelectionKey
-
代表一个SelectableChannel注册到一个Selector到注册证。
-
当channel注册到selector时,就会创建一个selectionKey,这个selectionKey维持有效(valid)状态直到被取消(cancel)。
-
当客户端直接调用cancel方法时、当关联的channel进行close时、当关联的selecor进行close时,selectionKey转变为无效状态。
-
一个selectionKey包含2种操作集,分别是
interestOps和readyOps,两种都分别通过一个整数来表示。- interestOps:哪些操作是感兴趣的,随时可能随着
interestOps(int)方法的调用而更新 - readyOps:哪些操作是准备就绪的,一般来说是interestOps的子集,一般在selector执行select时更新。readyOps作为一种客户端使用提示,但不能保证总是准确的,一般来说紧跟在select操作后面查询readOps则会得到准确的结果,外部事件I/O操作可能会让readyOps不准确。
- interestOps:哪些操作是感兴趣的,随时可能随着
-
四种操作:
SelectionKey.OP_ACCEPT:通道接受连接的操作。SelectionKey.OP_CONNECT:通道连接完成的操作。SelectionKey.OP_READ:通道可读的操作。SelectionKey.OP_WRITE:通道可写的操作。
-
selectionKey是线程安全的
/**
* A token representing the registration of a SelectableChannel with a Selector.
*/
public abstract class SelectionKey {
protected SelectionKey() { }
// -- Channel and selector operations --
/**
* @return This key's channel
*/
public abstract SelectableChannel channel();
/**
* @return This key's selector
*/
public abstract Selector selector();
/**
* @return 如果selectionKey有效则返回true
*/
public abstract boolean isValid();
/**
* 取消注册,永久进入invalid状态
* 这个方法需要同步来保证线程安全,并发时可能短暂阻塞
*/
public abstract void cancel();
// -- Operation-set accessors --
/**
* @return This key's interest set
*/
public abstract int interestOps();
/**
* 设置感兴趣的操作集合
*/
public abstract SelectionKey interestOps(int ops);
/**
* @return This key's ready-operation set
*/
public abstract int readyOps();
// -- Operation bits and bit-testing convenience methods --
public static final int OP_READ = 1 << 0;
public static final int OP_WRITE = 1 << 2;
public static final int OP_CONNECT = 1 << 3;
public static final int OP_ACCEPT = 1 << 4;
public final boolean isReadable() {
return (readyOps() & OP_READ) != 0;
}
public final boolean isWritable() {
return (readyOps() & OP_WRITE) != 0;
}
public final boolean isConnectable() {
return (readyOps() & OP_CONNECT) != 0;
}
public final boolean isAcceptable() {
return (readyOps() & OP_ACCEPT) != 0;
}
// -- Attachments --
// selectionKey可以携带一个附件对象
private volatile Object attachment = null;
private static final AtomicReferenceFieldUpdater<SelectionKey,Object>
attachmentUpdater = AtomicReferenceFieldUpdater.newUpdater(
SelectionKey.class, Object.class, "attachment"
);
public final Object attach(Object ob) {
return attachmentUpdater.getAndSet(this, ob);
}
public final Object attachment() {
return attachment;
}
}
SocketChannel、ServerSocketChannel、DatagramChannel
/** A selectable channel for stream-oriented connecting sockets. */
public abstract class SocketChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, NetworkChannel {
...
}
/** A selectable channel for stream-oriented listening sockets. */
public abstract class ServerSocketChannel
extends AbstractSelectableChannel
implements NetworkChannel {
...
}
/** A selectable channel for datagram-oriented sockets. */
public abstract class DatagramChannel
extends AbstractSelectableChannel
implements ByteChannel, ScatteringByteChannel, GatheringByteChannel, MulticastChannel {
...
}
Selector
@see java.nio.channels.Selector
-
定义:给SelectableChannel实例使用的多路复用器
-
创建selector实例:
- 调用
Selector.open()静态方法,通过系统默认的provider创建 - 调用特定的
provider的openSelector()方法创建
- 调用
-
selector将维持“开启”状态直到调用
close()方法“关闭” -
一个selector中包含3个不同的selectionKey集合:
- key set:这个集合包括了当前所有在该selector上注册的channel对应的SelectionKey。你可以通过调用
keys()方法来访问这个集合。 - selected-key set:每当selector执行
select()操作时,所有检测到已经准备就绪至少一个interesed操作的channel的SelectionKey都会被收集到这个集合中。这个集合可以通过selectedKeys()方法获取。你可以使用remove方法来从集合中移除特定的key。如果在处理过程中一个key被取消了,那么在随后的select操作期间,它会自动从集合中被清除。这个集合始终是key set的一个子集。 - cancelled-key set:这个集合包含了所有已经被取消但是其关联channel尚未取消注册(deregistered)的SelectionKey。这些已取消的key将在下一次select操作期间被selector自动清除。这个集合也始终是key set的一个子集。
- key set:这个集合包括了当前所有在该selector上注册的channel对应的SelectionKey。你可以通过调用
-
可以通过
select()、select(long)、selectNow()执行selection operation,其执行过程包含三个阶段:- cancelled-key set中的全部key都从3个集合中删除(意味着cancelled-key set清空),并且把这些key关联的channel取消注册
- 查询底层操作系统,更新每个剩余的channel准备就绪的操作集合(readyOps,interestOps的子集)。对于这些更新后的channel,如果channel的key不在selected-key set中,则加入这个set。
- 在步骤2期间加入cancelled-key set中的key会像步骤1一样,从3个集合中删除并把关联channel取消注册。
-
selectNow()、select(long)、select()的区别与联系:selectNow():非阻塞,执行select操作,并立即返回结果select(long):阻塞,允许阻塞至多一段时间(除非中断或被唤醒)来等待channel就绪select():阻塞,等同于select(0),允许永久阻塞(除非中断或被唤醒)来等待channel就绪wakeup():唤醒selector,取消阻塞状态,让select(long)或select()立即返回结果
-
并发:selector自身是线程安全的,但是它的3个key集合并不是,如果在并发场景下修改key集合,则可能抛出
java.util.ConcurrentModificationException。由于selectionKey和channel可能在任何时间关闭,所以应用代码需要谨慎使用同步机制来检查key是否valid,channel是否open。
/**
* A multiplexor of {@link SelectableChannel} objects.
*
* @see SelectableChannel
* @see SelectionKey
*/
public abstract class Selector implements Closeable {
protected Selector() { }
/**
* Opens a selector.
*
* <p> The new selector is created by invoking the {@link
* java.nio.channels.spi.SelectorProvider#openSelector openSelector} method
* of the system-wide default {@link
* java.nio.channels.spi.SelectorProvider} object. </p>
*
* @return A new selector
*/
public static Selector open() throws IOException {
return SelectorProvider.provider().openSelector();
}
/**
* @return 如果selector处于开启状态,则返回true
*/
public abstract boolean isOpen();
/**
* @return The provider that created this channel
*/
public abstract SelectorProvider provider();
/**
* key set 不能被客户端修改元素,否则抛出异常
* key set 不是线程安全的
*
* @return This selector's key set
*/
public abstract Set<SelectionKey> keys();
/**
* selected-key set 可以被客户端删除元素,但不能直接新增
* selected-key set 不是线程安全的
*
* @return This selector's selected-key set
*/
public abstract Set<SelectionKey> selectedKeys();
/**
* Selects a set of keys whose corresponding channels are ready for I/O
* operations.
*
* <p> This method performs a non-blocking <a href="#selop">selection
* operation</a>. If no channels have become selectable since the previous
* selection operation then this method immediately returns zero.
*
* <p> Invoking this method clears the effect of any previous invocations
* of the {@link #wakeup wakeup} method. </p>
*
* @return 在本次select操作中,更新了“准备就绪操作集合”的key的数量
*/
public abstract int selectNow() throws IOException;
/**
* Selects a set of keys whose corresponding channels are ready for I/O
* operations.
*
* This method performs a blocking selection operation.
* It returns only after at least one channel is selected,
* this selector's {@link #wakeup wakeup} method is invoked, the current
* thread is interrupted, or the given timeout period expires, whichever
* comes first.
*
* <p> This method does not offer real-time guarantees: It schedules the
* timeout as if by invoking the {@link Object#wait(long)} method. </p>
*
* @param timeout 如果timeout > 0, 最多阻塞${timeout}毫秒来等待有至少一个channel就绪;
* 如果timeout == 0, 允许永久阻塞,来等待至少一个channel就绪;
* 如果timeout < 0,抛出非法参数异常
*
* @return 在本次select操作中,更新了“准备就绪操作集合”的key的数量
*/
public abstract int select(long timeout)
throws IOException;
/**
* Selects a set of keys whose corresponding channels are ready for I/O
* operations.
*
* This method performs a blocking selection operation.
* It returns only after at least one channel is selected,
* this selector's {@link #wakeup wakeup} method is invoked, or the current
* thread is interrupted, whichever comes first. </p>
*
* @return 在本次select操作中,更新了“准备就绪操作集合”的key的数量
*/
public abstract int select() throws IOException;
/**
* Causes the first selection operation that has not yet returned to return
* immediately.
*
* <p> If another thread is currently blocked in an invocation of the
* {@link #select()} or {@link #select(long)} methods then that invocation
* will return immediately. If no selection operation is currently in
* progress then the next invocation of one of these methods will return
* immediately unless the {@link #selectNow()} method is invoked in the
* meantime. In any case the value returned by that invocation may be
* non-zero. Subsequent invocations of the {@link #select()} or {@link
* #select(long)} methods will block as usual unless this method is invoked
* again in the meantime.
*
* <p> Invoking this method more than once between two successive selection
* operations has the same effect as invoking it just once. </p>
*
* @return This selector
*/
public abstract Selector wakeup();
/**
* Closes this selector.
*
* <p> If a thread is currently blocked in one of this selector's selection
* methods then it is interrupted as if by invoking the selector's {@link
* #wakeup wakeup} method.
*
* <p> Any uncancelled keys still associated with this selector are
* invalidated, their channels are deregistered, and any other resources
* associated with this selector are released.
*
* <p> If this selector is already closed then invoking this method has no
* effect.
*
* <p> After a selector is closed, any further attempt to use it, except by
* invoking this method or the {@link #wakeup wakeup} method, will cause a
* {@link ClosedSelectorException} to be thrown. </p>
*
* @throws IOException
* If an I/O error occurs
*/
public abstract void close() throws IOException;
}
JavaNIO编程示例
package com.yth.nio;
import org.junit.Assert;
import org.junit.Test;
import java.io.IOException;
import java.net.InetSocketAddress;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.channels.SelectionKey;
import java.nio.channels.Selector;
import java.nio.channels.ServerSocketChannel;
import java.nio.channels.SocketChannel;
import java.nio.charset.Charset;
import java.nio.charset.CharsetDecoder;
import java.nio.charset.StandardCharsets;
import java.util.Iterator;
import java.util.Set;
/**
* {@link Selector}, {@link SelectionKey}, {@link ServerSocketChannel}
*
* @author yutianhong
* @version 1.0
* @since 2023/11/23 19:53
*/
public class ServerSocketTest {
/**
* 服务器的ip
*/
private final String HOST = "127.0.0.1";
/**
* 服务器端口
*/
private final int PORT = 8080;
@Test
public void testServer() throws IOException {
// 创建Selector
Selector selector = Selector.open();
// 创建ServerSocketChannel,并绑定监听端口
ServerSocketChannel serverSocketChannel = ServerSocketChannel.open();
serverSocketChannel.socket().bind(new InetSocketAddress(PORT));
serverSocketChannel.configureBlocking(false);
// 将Channel注册到Selector上,并指定监听事件为“接收”事件
serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT);
// 创建缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);
Charset charset = StandardCharsets.UTF_8;
CharsetDecoder charsetDecoder = charset.newDecoder();
CharBuffer charBuffer = CharBuffer.allocate(1024);
while (true) {
// 阻塞等待就绪的Channel
if (selector.select() == 0) {
continue;
}
// 获取就绪的Channel集合
Set<SelectionKey> selectionKeys = selector.selectedKeys();
Iterator<SelectionKey> iterator = selectionKeys.iterator();
while (iterator.hasNext()) {
SelectionKey key = iterator.next();
// 移除Set中的当前key
iterator.remove();
// 根据就绪状态,调用对应方法处理业务逻辑
// 如果是“接收”事件
if (key.isAcceptable()) {
ServerSocketChannel server = (ServerSocketChannel) key.channel();
// 接收客户端的连接
SocketChannel clientChannel = server.accept();
clientChannel.configureBlocking(false);
// 注册到Selector,等待连接
clientChannel.register(selector, SelectionKey.OP_READ);
System.out.println("Accepted connection from " + clientChannel);
}
// 如果是“读取”事件
if (key.isReadable()) {
SocketChannel clientChannel = (SocketChannel) key.channel();
buffer.clear();
// 读取数据
StringBuilder stringBuilder = new StringBuilder();
while (clientChannel.read(buffer) != -1 || buffer.position() != 0) {
buffer.flip();
charsetDecoder.decode(buffer, charBuffer, true);
charBuffer.flip();
stringBuilder.append(charBuffer);
charBuffer.clear();
buffer.compact();
}
String message = stringBuilder.toString();
System.out.println("Received message from " + clientChannel + ": " + message);
// 回写数据
clientChannel.write(charset.encode("Echo: " + message));
// 客户端断开连接
clientChannel.close();
}
}
}
}
@Test
public void testClient() {
ByteBuffer byteBuffer = ByteBuffer.allocate(8192);
Charset charset = StandardCharsets.UTF_8;
CharsetDecoder charsetDecoder = charset.newDecoder();
// 连接服务器
try (SocketChannel socketChannel = SocketChannel.open(new InetSocketAddress(HOST, PORT))) {
// send message to server
byteBuffer.put(charset.encode("hello server"));
byteBuffer.flip();
socketChannel.write(byteBuffer);
socketChannel.shutdownOutput();
byteBuffer.clear();
// receive response from server
StringBuilder stringBuilder = new StringBuilder();
while (socketChannel.read(byteBuffer) != -1 || byteBuffer.position() != 0) {
byteBuffer.flip();
// decode and store
CharBuffer decode = charsetDecoder.decode(byteBuffer);
stringBuilder.append(decode);
byteBuffer.compact();
}
System.out.println("Received message from server " + socketChannel + ": " + stringBuilder);
} catch (IOException e) {
Assert.fail();
}
}
}