`
dingjun1
  • 浏览: 207849 次
  • 性别: Icon_minigender_1
  • 来自: 北京
社区版块
存档分类
最新评论

java nio 笔记

阅读更多
一、基础知识
操作系统借助直接内存访问,移动一大块数据,JVM 操作一小片数据,比如一行或者单个字节。操作系统传递的缓冲区数据,会被java.io包中相关的流类分割成小片,还常常会在多层对象之间拷贝。
传统的I/O模型也可以移动大量的数据,比如RandomAccessFile 使用基于数组的read() 和write()方法,也可以高效地移动数据。尽管这些方法会至少遗留一个缓冲区的拷贝,但是也十分接近底层系统调用。
磁盘控制器通过DMA(direct memory access)直接写数据到内核存储缓冲区,不需要主CPU的协助。当磁盘控制器完成缓冲区的填充,内核会从内核空间中的临时缓冲区拷贝到进程读操作指定的地方。

用户区域是没有特权的区域,代码不可以直接访问硬件设备,内核区是操作系统存活的地方,内核代码有特殊的权力,可以与设备控制器进行通信,操作用户区进程的状态。更重要的是所有的I/O流都通过内核区域。

当进程请求一个I/O操作,它会执行一个系统调用,常常是作为一个中断,把控制权交给内核。
C程序员熟悉的底层open() read() write() 和close()函数,除装配和执行一合适的系统调用外,什么也不做。
当内核通过这种方式被调用时,找到的数据不管需要多少步,进程会被请求,转移数据到用户空间中指定的缓冲区中,内核会试图缓存也可能预读。
如果用户请求的数据已经在内核空间中,进程会进行拷贝,如果没有可用的数据,进程会暂停,同时内核把数据加载到内存中。

为什么不直接让磁盘控制器发送数据到用户区的缓存中。有以下三个原因:
1、硬件一般不能直接访问用户区域。
2、以块为操作单位的硬件设备操作一个固定大小的数据块。用户可能请求非规则大小或是不连续的数据块,内核充当一个中间角色,在用户空间区和存储设备之间移动数据对数据进行分割和重新组合。
3、硬件设备不能访问虚拟内存地址。

虚拟内存:
现代的操作系统可以使用虚拟内存,是人为的或是虚构的(用于物理内存地址)地址空间。
1、多个虚拟地址可以关联一个相同的物理内存地址
2、虚拟内存空间比真实的硬件内存更大。

前面的小节提到设备控制器不可能使用DMA 直接进入用户空间,但是相同的效果可以达成,映射内核空间地址到相同的物理地址作为用户区域的虚拟地址。DMA硬件可以填充一个缓冲区同时对内核和用户进程可见。这样消除了内核与用户空间之间的拷贝。这要求内核和用户缓冲区分享相同的页框page alignment。


scatter/gather概念 允许一个进程在一个系统调用中传递一组缓冲区地址给操作系统。内核可以按顺序填充或输出多个缓冲区。在一次读中可以分散数据到多个用户缓冲区。或者从多个缓冲区中收集数据。
会保存用户进程使用的系统调用(有些可能花销比较大)允许内核优化处理数据,因为它有整个传换的信息。如果有多个可用的CPU,它可能同时填充或输出多个缓存区。



-----------------------------------------------------------------------------------
传统I/O的缺点:
1、轮询检查每一个等待的channel,至少需要一个系统调用测试channel的就绪状态。
2、检查不是原子性的,可能在检查后,channel变为可用状态,但是需要等待下一次轮询检果时才会发现,没有办法在channel变为可用状态时发出一个通知。
3、传统的JAVA解决监听多个socket需要为第个创建一个线程,允许线程在read()时阻塞,直到有数据可用。每一个阻塞的线程是一个socket监听器,JVM的线程调度变成了通知机制。会导致线程过快增长。
seletor.select()工作原理:
就绪状态的选择由操作系统完成,操作系统最重要的执行功能之一是处理I/O请求和在数据就绪时通知对应进程。

1、检查“被取消的KEY集合”cancelled key set,如果不为空,每个在"被取消的KEY集合"中的KEY会从另两个集合(Registered key set,Selected key set)中移除,与该KEY关联的channel会被注销,完成这两用人才步后, cancelled key set会被清空。
2、测试Registered key set中的每一个KEY被关注的操作,测试后会改变关注的(操作位)集合,但是不会注意剩下的选择操作。
操作系统在就绪状态的标准确认后,只会处理每个channel关注的操作状态。 依靠特定的select()方法调用,线索可能被阻塞,如果没有一个channel是就绪的,可能阻塞设定的“超时时间”长度,完成系统调用后,会激活挂起的线程。每一个channel的就绪状态已经确定好了的。
操作系统指示至少有一个关注的操作就绪的channel会做如下两件事:
    a、如果channel关联的key没有在selected key set中,key的就绪set被清除,显示已确定的当前就绪的操作位会被设置。
    b、如果已经在selected key set中,key的就绪集合会被更新。以前在就绪显示操作的位不会被清理,一旦一个KEY被置于selected key set,它的就绪位集合是累积的,位被设置不会被清理。

3、每二步潜在地会花费一长段时间,特别是激活sleep的线程。在这段时间与这个selector关联的key有可能被取消,当第二步完成后会重复第一步的操作。

4、select操作返回的值是操作就绪集合被修改的key数目。这个值不是就绪channels的数据,仅仅是最近一次调用select由非就绪状态变为就绪状态的channel数目。以前已经就绪,当前调用select仍然就绪的channel不会被统计,以前就绪,现在不再就绪的channel也不会被统计。
select()方法会一直阻塞直到key sets中有新就绪的SelectableChannel。
这时如果另一个线程在该selector上注册,也会被阻塞(selector 需要同步其内部的key set,在获取同步锁时被阻塞)。所以应该在register()方法前调用selector.wakeup()方法。


selector对象是线程安全的,但是key sets不是。keys()和selectedKeys()返回的是一个私有的Set 对象。这些Sets可以在任何时间被改变。
the registered key set 是只读的,试图修改会抛出UnsupportedOperationException。Iterator对象是快速失败的,如果底层的Set被修改。
----------------------
FileChannel是线程安全的,多个线程可以并发调用在同一个实例上,不会引起任何问题,但是不是所有操作是多线程的。
影响channel's位置和文件大小是单线程的。线程试图调用这些操作中的一个,会等待另一个正在执行影响channel位置或文件大小的操作。

FileChannel类保证所有在相同JVM上的实例看到文件一致的视图,但是多个JVM或是非JAVA程序就不能保证了。
文件锁模式是应用于每一个文件上,不是某个channel或是线程上,不适合在相同JVM中的多线程的情况。
一个线程请求一个给定文件的排它锁,第二个线程使用独立打开的channel请求该文件相同区域的排它锁,第二个线程还是会获取,允许访问的。
如果两个线程是运行在不同JVM上的,那么第二个线程会被阻塞。这是因为锁最根本是在进程中决定,而不是线程级别。
锁是与文件联系的,不是和独立的file handle或是channel.


FileChannel可以创建虚拟内存映射磁盘文件,封装一个MappedByteBuffer关于虚拟内存空间的对象,MappedByteBuffer在很多方面像一个基于内存的buffer。
但是它的数据元素是存放在磁盘上的文件中。调用get会从磁盘文件中取回数据。这个数据反映了当前文件的内容,尽管文件是在创建好映射关系后被其它的进程
修改。通过put往mapped buffer中添加数据,会更新到磁盘上,改变的内容会对其它的reader也是可见的。

通过内存映射机制访问文件比传统方式读写要更有效率,尽管使用channel。没有外在的系统调用,更重要的是操作系统会原子性的缓存内存页。
这些页使用系统内存缓存,不会消耗JVM的内存堆空间。一但内存页可以使用,它可以通过磁盘的速度访问,不需要任何其它的系统调用来获得数据。包含索引或其它节的大的结构化文件频繁更新可以从内存映射中获得巨大效益。


FileChannel position()方法设置一个超过文件大小的位置是被允许的,但是不会改变文件的大小。如果读取一个被设置为超出文件大小的值,会返回文件结束条件(比如:-1)
如果调用write(),写数据到一个超出文件大小的位置,会引起文件增加,以适应新字节的写入。这个行为与绝对写文件一样。这个结果会导致文件漏洞。
(文件系统可能不会为空白的一段文件设置存储空间,比如直接从某处跳过10K的长度再写数据,那么这段空白的数据并没有被真正存储在磁盘上。但是文件大小是会包含的,
读取这段文件中的数据会返回0)
FileChannel的位置position是由底层的file descriptor反映的,同一个文件是共享的。如果在一个对象上更新的位置,对另一个对象是可见的。
RandomAccessFile randomAccessFile = new RandomAccessFile ("filename", "r");
// Set the file position
randomAccessFile.seek (1000);
// Create a channel from the file
FileChannel fileChannel = randomAccessFile.getChannel();
// This will print "1000"
System.out.println ("file pos: " + fileChannel.position());
// Change the position using the RandomAccessFile object
randomAccessFile.seek (500);
// This will print "500"
System.out.println ("file pos: " + fileChannel.position());
// Change the position using the FileChannel object
fileChannel.position (200);
// This will print "200"
System.out.println ("file pos: " + randomAccessFile.getFilePointer());


---------------------------
Channels可以以阻塞或非阻塞模式操作, 仅有像socket和pipes面向流的channels可以设置为非阻塞模式。
非阻塞模式,不会引起线程睡眠,请求的操作要么立即完成,或者返回一个什么也没有做的结果。

调用channel's close方法,channel完成关闭底层I/O设备可能引起线程阻塞。对同一个channel多次调用close()没有副作用,如果第一个线程正在被close方法阻塞,任一其它的线程调用close方法会被阻塞,直到第一个线程完成关闭channel。对已关闭的channel调用close,立即返回,不会做任何事。

如果一个channel实现了InterruptibleChannel接口,如果一个线程在channel上阻塞,这个线程被其它线程中断,channel会被关闭,阻塞的线程会抛出ClosedByInterruptException.
如果channel关闭,尽管有其它的线程被阻塞外,使用该channel完成I/O,睡眠的线程会唤醒并收到AsynchronousCloseException, channel会被关闭不再被使用。

文件channel一直是阻塞的,不能被置于非阻塞模式。现代的操作系统有成熟的缓存和获取算法,本地I/O操作是非常低的延迟。
网络文件系统常常有高的延迟,但是常常有相同的优化。

I/O操作可以归为两类,一类是面向流的,一类是面向文件的。对于文件系统,其获益于异步I/O,它可以让一个进程请求
一个或多个I/O操作,但是不用等待它们完成。当I/O完成后该进程会被通知。这也是将来NIO提高的地方。

FileChannel不能被直接创建,FileChannel实例仅支持从一个打开的文件对象中获取(RandomAccessFile,FileInputStream,FileOutputStream).
调用getChannel()返回FileChannel对象,连接到相同的文件,有相同的访问权限。

RandomAccessFile raf = new RandomAccessFile(file,mode);
FileChannel fc1 = raf.getChannel();
FileChannel fc2 = raf.getChannel();

fc1与fc2是相同的对象,raf、fc1、fc2是共用同一个底层的FileDescriptor对象,因此修改位置,文件大小,这三个对象会同时可见的。调用三个中的任一一个对象的close()对象,都会关闭底层的通道。

force()方法是强制文件的修改从缓存输出到磁盘。可以保证该方法返回后所有的修改都会被写到磁盘上,但是对远程文件系统不能保证。

force(boolean metadata) 提示方法返回前,是否要同步中间数据,比如(文件的所属用户、访问权限、最后修改时间等)。如果为false表示仅文件数据同步后就返回。

----------------------------------
MapMode.PRIVATE:指写入时拷贝一份,以后底层文件该区域(页)修改时就不可见了,该模式下的MappedByteBuffer做的修改不会被保存到底层的文件,做的修改只对该对象可见,当底层文件的内容改变时,该对象有可能见到修改的内容。当底层文件修改的页区域中的内容并没有被该对象修改,也就不会生成拷贝,内容反映的是底层文件的内容。
只有在底层文件修改的区域在该对象中有拷贝时才不可见,见到的时自己私有修改的内容。

memory-mapping(MappedByteBuffer):1、操作系统的虚拟内存系统会自动缓存内存页。这个页的缓存在系统的内存空间里。2、没有显示的系统调用。buffer = fileChannel.map (FileChannel.MapMode.READ_ONLY, 100, 200);
映射文件的范围不像文件锁(不会修改文件大小),如果超过实际的文件大小,那么会文件会增长以匹配映射的大小。如果是只读模式会导致IOException。如果映射的范围超过文件大小也会导致文件漏洞。

MappedByteBuffer对象,当映射建立后会一直保持着影响,直到被垃圾回收,FileChannel关闭也不会破坏映射。只能由buffer自己处理,解除映射关系。
如果MappedByteBuffer对象与文件的映射关系建立后,文件的大小改变,小于映射的区域会引起MappedByteBuffer不能访问(如果访问会抛出异常)。当刚建立关联关系时,通常不会自动从文件中加载数据。就好像是打开文件,获得文件的位置和操作句柄。虚拟内存系统会加载文件块到MappedByteBuffer中,如果加载所有的页到内存文件中,访问速度就好像基于内存的buffer.

load()方法会试图加载整个文件到内存中。但是不能保证把整个文件都加载到内存。一般情况下最近使用的内存页才会加载到主存中,其它的会被换出。


Channel to Channel Transfer:
public abstract long transferTo (long position, long count,
WritableByteChannel target)
public abstract long transferFrom (ReadableByteChannel src,
long position, long count)

transferTo()和transferFrom()方法,允许连接两个channel,消除了用于传递数据的中间环节buffer。注意这两个方法只在FileChannel中有。因此其中一个必须是FileChannel。
直接的channel转换并不更新关联的FileChannel的当前position(与使用绝对定位的写和读相同)。这两个方法会返回实际传递的值,但是不会超过count.

transferTo()如果接收源是一个文件,position+count大于文件的大小时,转换会停止在文件的结束处,并不会扩大文件。如果接收的源是一个非阻塞的Socket,仅仅转换一个当前队列长度的数据(也可能不转换数据)。阻塞模式会做部分的转换,这个依赖于操作系统。
-------------------------------------------------------
关于Socket超时的说明:
连接超时的设置,先生成一个未连接的套接字,然后使用connect方法建立连接。
new Socket().connect(SocketAddress endpoint, int timeout);
connect(SocketAddress endpoint, int timeout)
          将此套接字连接到具有指定超时值的服务器。

setSoTimeout
public void setSoTimeout(int timeout)
                  throws SocketException启用/禁用带有指定超时值的 SO_TIMEOUT,以毫秒为单位。将此选项设为非零的超时值时,在与此 Socket 关联的 InputStream 上调用 read() 将只阻塞此时间长度。如果超过超时值,将引发 java.net.SocketTimeoutException,虽然 Socket 仍旧有效。选项必须在进入阻塞操作前被启用才能生效。超时值必须是 > 0 的数。超时值为 0 被解释为无穷大超时值。
-----------------------
注意:这里是对读操作,不能对写进行控制超时。
--------------------------------
设置SO_SNDBUF SO_RCVBUF 都是一程建议值。
setSendBufferSize
public void setSendBufferSize(int size)
                       throws SocketException将此 Socket 的 SO_SNDBUF 选项设置为指定的值。平台的网络连接代码将 SO_SNDBUF 选项用作设置基础网络 I/O 缓存的大小的提示。
由于 SO_SNDBUF 是一种提示,想要验证缓冲区设置大小的应用程序应该调用 getSendBufferSize()。


setReceiveBufferSize
public void setReceiveBufferSize(int size)
                          throws SocketException将此 Socket 的 SO_RCVBUF 选项设置为指定的值。平台的网络连接代码将 SO_RCVBUF 选项用作设置基础网络 I/O 缓存的大小的提示。
增大接收缓存大小可以增大大量连接的网络 I/O 的性能,而减小它有助于减少传入数据的 backlog。

由于 SO_RCVBUF 是一种提示,想要验证缓冲区设置大小的应用程序应该调用 getReceiveBufferSize()。

SO_RCVBUF 的值还用于设置公布到远程同位体的 TCP 接收窗口。一般情况下,当连接套接字时,可以在任意时间更改窗口大小。然而,如果需要的接收窗口大于 64K,则必须在将套接字连接到远程同位体之前请求。下面是需要知道的两种情况:

(连接握手时会设置最大的连接窗口的大小,在TCP首部有此信息项的位)

对于从 ServerSocket 接受的套接字,必须在将 ServerSocket 绑定到本地地址前通过调用 ServerSocket.setReceiveBufferSize(int) 执行此操作。

对于客户端套接字,则必须在将套接字连接到其远程同位体前调用 setReceiveBufferSize()。


Socket implement information
SOCKET实例包含的信息:
1、本地和远程网络地址及端口,远程地址和端口标识远程SOCKET。
2、FIFO队列:等待派送(给应用层消费)的已接收数据(RecvQ),等待发送的数据(SendQ)。
3、对于TCP SOCKET ,与TCP握手状态关联的协议状态信息,是打开还是关闭。

TCP提供了可靠的字节流服务,写到SOCKET输出流中的数据拷贝,会一直保存直到连接的另一端成功接收。
往输出流中写数据并不表示数据被实际发送,仅仅是把数据放到了本地缓存中。既使调用socket的flush方法也不能保证会被立即发送。
此外,字节流的性质意味着输入流没有提供边界。
SendQ:在发送端的字节缓冲区,这些数据已经写到输出流中,但是还没有成功发送给接受主机。
RecvQ:接收端的字节缓冲区,这些数据等待发送给接收应用程序,应用程序从输入流中读取。
Delivered:由接收者从输入流中已读取的字节。

发送者调用out.write()添加字节到SendQ中,TCP协议的责任是按顺序从SendQ移送到RecvQ。用户程序并不能显示地直接控制这个转换过程。
SendQ ---TCP PROTOCOL--->RecvQ----recv()---->Delivered。

死锁:
举例说明:
1、当连接建立后,客户端和服务端都等待从对方获取数据。
2、受到RecvQ和SendQ缓冲区大小的影响,当RecvQ填充满后,TCP流控制机制会阻制发送者从SendQ中交换数据到接收方,直到RecvQ中有可用的空间,即需要等待接收方调用read()方法。
发送方可以一直调用out.write()方法向SendQ中填充数据,直到SendQ被填充满,这时out.write()方法会被阻塞。
如果接收方的程序一直没有调用read()方法,当发送方发送大小超过SendQ_size+RecvQ_size大小的数据时,就会被阻塞。
如果接收方和发送方同时往对方输出数据,且大小分别大于SendQ_size_a+RecvQ_size_b,RecvQ_size_a+SendQ_size_b时就会出现死锁,双方会被out.write()阻塞。

public class TestBlockSelect {
	public Selector sel;
	
	
	public TestBlockSelect() throws IOException{
		sel = Selector.open();
	}
	
	public int listen() throws IOException{
		System.out.println("start call select() ...");
		//sel.select(1000*10);
		int i = sel.select();
		System.out.println("return select() "+i);
		return i;
	}
	
	public void addServer() throws IOException{
		ServerSocketChannel serverCh = ServerSocketChannel.open();
		serverCh.configureBlocking(false);
		System.out.println("prepare register server ...");
		//唤醒当前等待的对象sel所在的线程
		//sel.wakeup();
		serverCh.register(sel, OP_ACCEPT);
		System.out.println("register server successfully.");
	}
	
	
	/**
	 * @param args
	 */
	public static void main(String[] args) {
		// TODO Auto-generated method stub
		TestBlockSelect tbs = null;
		try{
			tbs = new TestBlockSelect();
		}catch(Exception e){
			e.printStackTrace();
		}
		SelectorListen lst = new SelectorListen();
		Register rgt = new Register();
		lst.setTbs(tbs);
		rgt.setTbs(tbs);
		
		//Selector.select()阻塞,
		lst.start();
		//由于select()阻塞,当register()同一个selector对象时,需要同步register key set,不能获取对应的锁。也会被阻塞
		rgt.start();
		
		/**解决办法,在调用register方法前,调用selector.wakeup();,select()方法立即返回使其处于非等待状态释放同步锁
		 * select()方法可以改为select(timeout),使其有一个超时时间
		 * 
		 */
	}
}

class SelectorListen extends Thread{
	public void run(){
		try{
			System.out.println("SelectorListen start...");
			tbs.listen();
			System.out.println("SelectorListen end.");
		}catch(Exception e){
			e.printStackTrace();
		}
		
		
	}
	public void setTbs(TestBlockSelect tbs){
		this.tbs = tbs;
	}
	TestBlockSelect tbs;
}

class Register extends Thread{
	public void run(){
		try{
			System.out.println("Register start...");
			tbs.addServer();
			System.out.println("Register end.");
		}catch(Exception e){
			e.printStackTrace();
		}
		
		
	}
	public void setTbs(TestBlockSelect tbs){
		this.tbs = tbs;
	}
	TestBlockSelect tbs;
}


二、别人的经验
1、The Rox Java NIO Tutorial


1、使用单一selecting线程
2、只在selecting线程中修改selector相关的内容
3、只有在准备好发送数据时才设置OP_WRITE

1、Use a single selecting thread
Although NIO selectors are threadsafe their key sets are not. The upshot of this is that if you try to build a solution that depends on multiple threads accessing your selector you very quickly end up in one of two situations:

Plagued by deadlocks and race conditions as you build up an increasingly fragile house of cards (or rather house of locks) to avoid stepping on your own toes while accessing the selector and its key sets.
Effectively single-threading access to the selector and its key sets in an attempt to avoid, well, stepping on your own toes.
The upshot of this is that if you want to build a solution based on NIO then trust me and stick to a single selecting thread. Offload events as you see fit but stick to one thread on the selector.
I tend to handle all I/O within the selecting thread too. This means queuing writes and having the selecting thread perform the actual I/O, and having the selecting thread read off ready channels and offload the read data to a worker thread. In general I've found this scales well enough so I've not yet had a need to have other threads perform the I/O on the side.

2、Modify the selector from the selecting thread only
If you look closely at the NIO documentation you'll come across the occasional mention of naive implementations blocking where more efficient implementations might not, usually in the context of altering the state of the selector from another thread. If you plan on writing code against the NIO libraries that must run on multiple platforms you have to assume the worst. This isn't just hypothetical either, a little experimentation should be enough to convince you that Sun's Linux implementation is "naive". If you plan on targeting one platform only feel free to ignore this advice but I'd recommend against it. The thing about code is that it oftens ends up in the oddest of places.

As a result, if you plan to hang onto your sanity don't modify the selector from any thread other than the selecting thread. This includes modifying the interest ops set for a selection key, registering new channels with the selector, and cancelling existing channels.

A number of these changes are naturally initiated by threads that aren't the selecting thread. Think about sending data. This pretty much always has to be initiated by a calling thread that isn't the selecting thread. Don't try to modify the selector (or a selection key) from the calling thread. Queue the operation where the selecting thread can get to it and call Selector.wakeup. This will wake the selecting thread up. All it needs to do is check if there are any pending changes, and if there are apply them and go back to selecting on the selector. There are variations on this but that's the general idea.

3、Set OP_WRITE only when you have data ready
A common mistake is to enable OP_WRITE on a selection key and leave it set. This results in the selecting thread spinning because 99% of the time a socket channel is ready for writing. In fact the only times it's not going to be ready for writing is during connection establishment or if the local OS socket buffer is full. The correct way to do this is to enable OP_WRITE only when you have data ready to be written on that socket channel. And don't forget to do it from within the selecting thread.

Alternate between OP_READ and OP_WRITE
If you try to mix OP_READ and OP_WRITE you'll quickly get yourself into trouble. The Sun Windows implementation has been seen to deadlock if you do this. Other implementations may fare better, but I prefer to play it safe and alternate between OP_READ and OP_WRITE, rather than trying to use them together.

With those out of the way, let's take a look at some actual code. The code presented here could do with a little cleaning up. I've simplified a few things in an attempt to stick to the core issue, using the NIO libraries, without getting bogged down in too many abstractions or design discussions.

java nio在多线程环境下的恶梦之终结

有人说java nio在多线程环境下编程简直就是个恶梦,其实你如果能把握住java nio API的要领,你就可以将之驾驭.

0. 一个 channal 对应一个SelectionKey in the same selector.
e.g:
SelectionKey sk=sc.register(selector, SelectionKey.OP_READ, handler);
sk==sc.register(selector, SelectionKey.OP_WRITE, handler) true?
selector.select() 每次返回的对同一channal的sk是否相同?

1.channel.register(...) may block if invoked concurrently with another registration[another.register(...)] or selection operation[selector.select(...)] involving *****the same selector*****.
这个是register方法jdk src上的原文,
e.g:
如果一个selection thread已经在select方法上等待ing,那么这个时候如果有另一条线程调用channal.register方法的话,那么它将被blocking.

2.selectionKey.cancel() : The key will be removed from all of the selector's key sets during *****the next selection operation[selector.select(...)]*****.
may block briefly if invoked concurrently with a cancellation[cancel()] or selection operation[select(...)] involving ***the same selector***.
这个也是cancel方法jdk src上的原文,
e.g:
你先将一个selectionKey.cancel(),然后随即再channel.register to the same selector,
在cancel和register之间,如果没有线程(包括当前线程)进行select操作的话,
那么 throws java.nio.channels.CancelledKeyException.
所以 cancel-->select-->re-register. 

3.if don't remove the current selectedKey from selector.selectedKeys()[Set] 将导致 selector.select(...) not block [may be cpu 100%,specially when client cut the current channel(connection)].
e.g:
Iterator<SelectionKey> it=selector.selectedKeys().iterator();
...for/while it.hasNext()...
it.remove();<------*****must do it. or Keys' Set.clear() finally;

if remove the current selectedKey from selector.selectedKeys()[Set] but don't sk.interestOps(sk.interestOps()& (~sk.readyOps()));将导致 selector.select(...) not block [select() not block several times, or excepted exception]

4.op_write should not be registered to the selector.   [may be cpu100%]

5. if involving  wakeup() before select() [wakeup called several times >=1],the next select() not block [not block just once].

尽管以前有些人分析了nio的wakeup性能及not block in linux的bug,但是java nio依然是高效的,那些c/c++的牛人们去看看jre/bin目录下的nio.dll/nio.so吧,java nio是基于select模型(这个是c/c++中常用网络编程模型之一)的.

基于java nio的服务器:mina,girzzly[glassfish],jetty(基于girzzly),tomcat6[可以配置Http11NioProtocol]...

其中从本人对girzzly,tomcat6的源码分析来看,它们都还没有真正发挥出nio异步处理请求的优点,它们的读写还都是blocking的虽然使用了selectorPool,此外tomcat6要剥离出socket通信还要花费一定的功夫.而mina却是名不符其实,还有bug哦.
分享到:
评论

相关推荐

Global site tag (gtag.js) - Google Analytics