Selector
NIO最大的亮点就是选择器和非阻塞I/O操作的使用,这一点在SelectableChannel上尤为重要(Pipe也可使用选择器),先看一个传统的连接方式:
1 public class TCPServer { 2 public static void main(String[] args) throws IOException { 3 int serverPort = 12000; 4 ServerSocket serverSocket = new ServerSocket(serverPort,1); 5 6 Socket connectionSocket = serverSocket.accept(); 7 8 InputStream inputStream = connectionSocket.getInputStream(); 9 byte[] bytes = new byte[1024]; 10 inputStream.read(bytes); 11 String s = new String(bytes); 12 System.out.println("服务器收到:"+s); 13 14 OutputStream outputStream = connectionSocket.getOutputStream(); 15 outputStream.write(s.toUpperCase().getBytes()); 16 17 inputStream.close(); 18 outputStream.close(); 19 connectionSocket.close(); 20 serverSocket.close(); 21 } 22 } 23
该例中serverSocket.accept( )会一直阻塞,直到有请求连接;连接建立以后,inputStream.read( )又会产生阻塞,直到有数据可读为止。如果要处理大量连接,我们可以让一个线程去处理一个连接,就可以避免单线程在inputStream.read( )阻塞时无法处理其他连接的问题。显然,程序员和Java虚拟机需要为管理所有线程的复杂性和性能损耗付出巨大代价,特别是在线程数量的增长失控时。
传统方式事实上将每个被阻塞的线程当作一个socket监控器,并将Java虚拟机的线程调度当作了通知机制。但两者本来都不是为了这种目的而设计的,因此选择器的出现就很有必要。
一、选择器基础
我们先看正选择器使用的整个流程:首先,将创建的一个或多个可选择的通道注册到选择器对象中,然后一个表示通道和选择器关系的键将会被返回。选择键会记住注册的通道,也会追踪对应的通道是否已经就绪。然后,当调用一个选择器对象的select( )方法时,相关的键建会被更新,用来检查所有被注册到该选择器的通道。此时,可以获取一个键的集合,从而找到当时已经就绪的通道。通过遍历这些键,就可以选择出每个从上次您调用select( )开始直到现在,已经就绪的通道。
整个过程是一个就绪选择的过程,就绪选择可以询问通道是否已经准备好执行每个I/0操作的能力(例如,选择器可以监控ServerSocketChannel是否有需要准备接受的连接),并使大量的通道可以同时进行就绪状态的检查,调用者可以轻松地决定多个通道中的哪一个准备好要运行。就绪选择是由操作系统完成的,操作系统的一项最重要的功能就是处理I/O请求并通知各个线程它们的数据已经准备好了,选择器类提供了这种抽象,使得Java代码能够以可移植的方式,请求底层的操作系统提供就绪选择服务。
1.1 选择就绪的三个组件
选择器(Selector):该类管理着一个被注册的通道集合的信息和它们的就绪状态。通道和其感兴趣的事件(即通道要处理的事件或执行的操作)一起被注册到选择器,当通道感兴趣的事件产生时(即通道要处理的对象准备好了),选择器会更新通道的就绪状态。
可选择通道(SelectableChannel):该类提供了实现通道的可选择性所需要的公共方法,是所有支持就绪检查的通道类的父类。SelectableChannel被注册到Selector对象上,同时可以指定对那个选择器而言,哪种操作是感兴趣的(该通道是要处理哪种事件的)。一个通道可以被注册到多个选择器上,但对每个选择器而言只能被注册一次。
选择键(SelectionKey):选择键封装了特定的通道与特定的选择器的注册关系。选择键对象被SelectableChannel.register( )返回并提供一个表示这种注册关系的标记。选择键包含了 两个比特集(以整数的形式进行编码),指示了该注册关系所关心的通道操作,以及通道已经准备好的操作。
需要注意的是:SelectableChannel在注册之前必须设置为非阻塞模式,否则会抛出IllegalBlockingModeException异常;通道被注册后,就不能回到阻塞状态,否则在调用configureBlocking( )方法时也会抛出未检查异常;试图注册一个已经关闭的SelectableChannel实例的话,会抛出ClosedChannelException异常。
1.2 建立选择器
1 Selector selector = Selector.open( ); 2 channel1.register (selector, SelectionKey.OP_READ);
Selector对象是通过调用静态工厂方法open( )来实例化的,类方法open( )向SPI发出请求,通过默认的SelectorProvider对象获取一个新的实例。通过调用一个自定义的SelectorProvider对象的openSelector( )方法来创建一个Selector实例也可行。
1 public abstract class Selector{ 2 // This is a partial API listing 3 public static Selector open( ) throws IOException 4 public abstract boolean isOpen( ); 5 public abstract void close( ) throws IOException; 6 public abstract SelectionProvider provider( ); 7 }
close( )方法释放选择器可能占用的资源并将所有相关的选择键设置为无效,一个选择器被关闭,试图调用它的大多数方法都将导致ClosedSelectorException。
1 public abstract class SelectableChannel 2 extends AbstractChannel 3 implements Channel{ 4 // This is a partial API listing 5 public abstract SelectionKey register (Selector sel, int ops) throws ClosedChannelException; 6 public abstract SelectionKey register (Selector sel, int ops,Object att) throws ClosedChannelException; 7 public abstract boolean isRegistered( ); 8 public abstract SelectionKey keyFor (Selector sel); 9 public abstract int validOps( ); 10 }
这里我们主要关注register( )方法,它总共有三个参数,sel参数即注册的选择器,选择器包含了注册到它们之上的通道的集合。在任意给定的时间里,对于一个给定的选择器和一个给定的通道而言,只有一种注册关系是有效的。但将一个通道注册到多于一个的选择器上允许的。
ops参数表示通道感兴趣的事件,这是一个表示选择器在检查通道就绪状态时需要关心的操作的比特掩码(在SelectonKey类中定义)。并非所有的操作都在所有的可选择通道上被支持,例如,SocketChannel不支持accept。可以通过调用validOps( )方法来获取特定的通道所支持的操作集合。
att参数可以传递您提供的对象引用,在调用新生成的选择键的attach( )方法时会将这个对象引用返回,在SelectionKey中详解。
二、选择键
1 public abstract class SelectionKey { 2 //a part of API 3 public static final int OP_READ 4 public static final int OP_WRITE 5 public static final int OP_CONNECT 6 public static final int OP_ACCEPT 7 public abstract void cancel( ); 8 public abstract boolean isValid( ); 9 public abstract int interestOps( ); 10 public abstract void interestOps (int ops); 11 public abstract int readyOps( ); 12 public final Object attach (Object ob) 13 public final Object attachment( ) 14 }
SelectionKey是需要单独拿出来说的,一个键表示一个特定的通道对象和一个特定的选择器对象之间的注册关系。当应该终结这种关系的时候,可以调用cancel( )方法,可以通过调用isValid( )方法来检查它是否仍然表示一种有效的关系。
- 当键被取消时,它将被放在相关的选择器的已取消的键的集合里。注册不会立即被取消,但键会立即失效(这种设计是有有利的)。当再次调用 select( )方法时(或者一个正在进行的select()调用结束时),已取消的键的集合中的被取消的键将被清理掉,并且相应的注销也将完成,通道会被注销,而新的 SelectionKey 将被返回。
- 当通道关闭时,所有相关的键会自动取消。
- 当选择器关闭时,所有被注册到该选择器的通道都将被注销,并且相关的键将立即被无效化(取消)。键被无效化后,调用它的与选择相关的方法就将抛出CancelledKeyException。
2.1 操作集
SelectionKey包含两个表示为整数值的操作集,每一位都表示该键的通道所支持的一类可选择操作:
- interest集合:用于指示那些通道/ 选择器组合体所关心的操作,它确定了下一次调用某个选择器的选择方法时,将测试哪类操作的准备就绪信息。创建该键时使用给定的值初始化interest集合;interset集合永远不会被选择器改变,之后可通过interestOps(int)方法对其进行更改。当相关的Selector上的select( )操作正在进行时,去改变键的interest集合,不会影响那个正在进行的选择操作,所有更改将会在select( )的下一个调用中体现出来。
- ready集合:标识了这样一类操作,即某个键的选择器检测到该键的通道已为此类操作准备就绪。创建该键时ready集合被初始化为零;可以在之后的选择操作中通过选择器对其进行更新,但不能直接更新它。 readyOps( )方法可以获取相关的通道的已经就绪的操作,ready集合是interest集合的子集,并且表示了interest集合中从上次调用 select( )以来已经就绪的那些操作。例
这里的操作集并不是如List这样的容器,它只是用不同的整数表示不同的操作,它标识的是该键对应通道的支持的操作有哪些(这里需要与Selector维护的集合相区别)。例如,一个Channel支持(OP_READ | OP_WRITE)两个操作,此时OP_READ操作就绪,因此ready是interest的子集,而很多方法的返回值均是int。
需要注意的是,通过相关的选择键的readyOps( )方法返回的就绪状态指示只是一个提示,并不保证线程可执行此类别中的操作而不导致被阻塞。ready集合很可能一完成选择操作就是准确的。ready集合可能由于外部事件和在相应通道上调用的 I/O 操作而变得不准确。
2.2 附件
1 public abstract class SelectionKey { 2 // This is a partial API listing 3 public final Object attach (Object ob) 4 public final Object attachment( ) 5 }
上一节说到register( )方法中的att参数,它是一个传递的对象引用,在SelectionKey中也可通过这两个方法设置和获取。SelectionKey类除了保存它之外,不会将它用于任何其他用途。任何一个之前保存在键中的附件引用都会被新传入的附件替换,可以传入null值来清除附件。
1 Selector open = Selector.open(); 2 SocketChannel channel = SocketChannel.open(); 3 ByteBuffer allocate = ByteBuffer.allocate(1024); 4 5 SelectionKey register = channel.register(open, SelectionKey.OP_READ, allocate); 6 ByteBuffer attachment = (ByteBuffer)register.attachment(); 7 register.attach(null); 8
例如,假如存在必要需要保留ByteBuffer对象,就可以通过这种方式。要注意的是,如果选择键的存续时间很长,但附加的对象不应该存在那么长时间,请记得在完成后清理附件。否则,您附加的对象将不能被垃圾回收,您将会面临内存泄漏问题。
2.3 并发性
总体上说,SelectionKey对象是线程安全的,修改interest集合的操作是通过Selector对象进行同步的(这可能会导致interestOps( )方法的调用会阻塞不确定长的一段时间)。
三、使用选择器
3.1 内部集合
1 public abstract class Selector { 2 // This is a partial API listing 3 public abstract Set keys( ); 4 public abstract Set selectedKeys( ); 5 public abstract int select( ) throws IOException; 6 public abstract int select (long timeout) throws IOException; 7 public abstract int selectNow( ) throws IOException; 8 public abstract void wakeup( ); 9 }
选择器维护着注册过的通道的集合,其注册关系都是封装在SelectionKey对象中。每一个Selector对象维护三个键的集合:
- 已注册的键的集合(Registered key set):与选择器关联的已经注册的键的集合。并不是所有注册过的键都仍然有效。这个集合通过keys( )方法返回,并且可能是空的。这个已注册的键的集合不是可以直接修改的;试图这么做的话将导致UnsupportedOperationException。
- 已选择的键的集合(Selected key set):已注册的键的集合的子集。这个集合的每个成员都是相关的通道被选择器(在前一个选择操作中)判断为已经准备好的,并且每个键都关联一个已经准备好至少一种操作(Insterst操作集中的一个)的通道。这个集合通过selectedKeys( )方法返回(并有可能是空的)。键可以直接从该集合中移除,不能添加。试图向该集合中添加元素将抛出UnsupportedOperationException。
- 已取消的键的集合(Cancelled key set):已注册的键的集合的子集,这个集合包含了cancel( )方法被调用过的键(这个键已经被无效化),但它们还没有被注销 。这个集合是选择器对象的私有成员,因而无法直接访问。
3.2 选择过程
结合三个键集,我们看Selector的选择过程,这个过程实际是调用select( )方法产生的变化,在每次选择操作期间,都可以将键添加到选择器的已选择键集以及从中将其移除,并且可以从其键集和已取消键集中将其移除。select( )方法有三种形式,但无论哪种都会执行以下步骤:
(1)已取消的键的集合将会被检查。如果它是非空的,每个已取消的键的集合中的键将从另外两个集合中移除,并且相关的通道将被注销。这个步骤结束后,已取消的键的集合将是空的。
(2)已注册的键的集合中的键的interest集合将被检查。在这个步骤中的检查执行过后,其他线程对interest集合的改动不会影响剩余的检查过程。一旦就绪条件被定下来,底层操作系统将会查询每个通道所感兴趣的事件的就绪状态。
依赖于特定的select( )方法调用,如果没有通道已经准备好,线程可能会在这时阻塞,通常会有一个超时值。直到系统调用完成为止,这个过程可能会使得调用线程睡眠一段时间,然后当前每个通道的就绪状态将确定下来(某个事件已经就绪)。对于那些还没准备好的通道将不会执行任何的操作。对于那些操作系统指示至少已经准备好interest集合中的一种操作的通道,将执行以下两种操作中的一种:
- 如果通道的键还没有处于已选择的键的集合中,那么键的ready集合将被清空,然后表示操作系统发现的当前通道已经准备好的操作的比特掩码将被设置。
- 否则,也就是键在已选择的键的集合中。键的ready集合将被表示操作系统发现的当前已经准备好的操作的比特掩码更新。所有之前的已经不再是就绪状态的操作不会被清除。事实上,所有的比特位都不会被清理。它的ready集合将是累积的。比特位只会被设置,不会被清理。
(3)步骤2可能会花费很长时间,特别是调用的select( )是一个阻塞方法时。与该选择器相关的键可能会在此过程中被取消。当步骤2结束时,将再次重新执行一次步骤1,以完成任意一个在选择进行的过程中,键已经被取消的通道的注销。
整个select( )涉及几个问题:
问题一:select操作返回的值是ready集合在步骤2中被修改的键的数量,而不是已选择的键的集合中的通道的总数。返回值不是已准备好的通道的总数,而是从上一个 select( )调用之后进入就绪状态的通道的数量。之前的调用中就绪的,并且在本次调用中仍然就绪的通道不会被计入,而那些在前一次调用中已经就绪但已经不再处于就绪状态的通道也不会被计入。这些通道可能仍然在已选择的键的集合中,但不会被计入返回值中。返回值可能是 0。
问题二:步骤2中select( )并不会清除已选择键集,并且键在已选择键集时,其ready集合不会被清除。这样是否会造成很多问题,我们在“管理选择键”时讨论。
问题三:我们可以看到,当我们使用SelectionKey.close( )或其他方法时,注册的通道并不是立刻注销的,而是在使用select( )时注销。使用内部的已取消的键的集合来延迟注销,是一种防止线程在取消键时阻塞,并防止与正在进行的选择操作冲突的优化。注销通道是一个潜在的代价很高的操作,这可能需要重新分配资源。清理已取消的键,并在选择操作之前和之后立即注销通道,可以消除它们可能正好在选择的过程中执行的潜问题。
3.3 停止选择过程
select( )有三种形式,select( )和带有超时时间参数的都会造成阻塞,Selector的wakeup( )方法提供了使线程从被阻塞的select( )方法中优雅地退出的能力。事实上,有三种方式可以唤醒在select( )方法中睡眠的线程:
(1)wakeup( )
wakeup( )方法将使得选择器上的第一个还没有返回的选择操作立即返回。如果当前没有在进行中的选择,那么下一次对select( )方法的一种形式的调用将立即返回。后续的选择操作将正常进行。在选择操作之间多次调用 wakeup( )方法与调用它一次没有什么不同。
1 Selector open = Selector.open(); 2 open.wakeup(); 3 open.select();//立即返回
也存在这种情况,调用wakeup( )只是想唤醒一个睡眠中的线程,而后续的select( )能继续阻塞,这时可以在wakeup( )方法后调用selectNow( )方法来绕过这个问题。
1 open.wakeup(); 2 open.selectNow(); 3 open.select();
(2)close( )
如果选择器的close( )方法被调用,那么任何一个在选择操作中阻塞的线程都将被唤醒,并且与选择器相关的通道将被注销,而键将被取消。
(3)interrupt( )
如果睡眠中的线程的interrupt( )方法被调用,它的返回状态将被设置。此时线程被唤醒,如果该线程试图在通道上执行I/O操作,通道将立即关闭,然后线程将捕捉到一个异常。
3.4 管理选择键
在3.2说到过,select( )并不会清理已选择键集,并且在更新ready集合时,之前ready集合不会被清除。这里有两个问题:一是不清理已选键集,下一次调用selectedKeys( )时,则还能拿到之前的Channel,但该Channel可能在此期间又变成了非就绪状态。二是之前ready不清理,则新设置后,Channel怎么知道这次是哪个操作准备好了。
这种设计的确会造成这些问题,但这是权衡下的选择,它提供了极大的灵活性,把合理地管理键以确保它们表示的状态信息不会变得陈旧的任务交给了程序员。为了避免这些问题,我们需要自己去清理已选择键集,但ready的更新操作却不用自己设置。注:以上第一个问题,虽然可能拿到非就绪的Channel,但使用它并没有什么问题,因为它是非阻塞的,会直接返回。而ready是按位分开的,可能类似于,比如上一个状态是OP_WRITE==4,新的就绪状态也是OP_READ==1,则更新后是(OP_WRITE | OP_READ)==5,就保留了两个状态。
通常的做法是在选择器上调用一次select操作(这将更新已选择的 键的集合),然后遍历selectKeys( )方法返回的键的集合。在按顺序进行检查每个键的过程中,相关的通道也根据键的就绪集合进行处理。然后键将从已选择的键的集合中被移除(通过在Iterator对象上调用remove( )方法),然后检查下一个键。
1 public class SelectSockets { 2 public static int PORT_NUMBER = 12000; 3 public static void main(String[] argv) throws Exception { 4 new SelectSockets().go(argv); 5 } 6 public void go(String[] argv) throws Exception { 7 int port = PORT_NUMBER; 8 if (argv.length > 0) { 9 port = Integer.parseInt(argv[0]); 10 } 11 System.out.println("Listening on port " + port); 12 13 ServerSocketChannel serverChannel = ServerSocketChannel.open(); 14 ServerSocket serverSocket = serverChannel.socket(); 15 Selector selector = Selector.open(); 16 serverSocket.bind(new InetSocketAddress(port)); 17 serverChannel.configureBlocking(false); 18 serverChannel.register(selector, SelectionKey.OP_ACCEPT); 19 20 while (true) { 21 int n = selector.select(); 22 if (n == 0) { 23 continue; 24 } 25 Iterator it = selector.selectedKeys().iterator(); 26 while (it.hasNext()) { 27 SelectionKey key = (SelectionKey) it.next(); 28 if (key.isAcceptable()) { 29 ServerSocketChannel server = 30 (ServerSocketChannel) key.channel(); 31 SocketChannel channel = server.accept(); 32 registerChannel(selector, channel, 33 SelectionKey.OP_READ); 34 sayHello(channel); 35 } 36 if (key.isReadable()) { 37 readDataFromSocket(key); 38 } 39 it.remove(); 40 } 41 } 42 } 43 44 45 protected void registerChannel(Selector selector, 46 SelectableChannel channel, int ops) throws Exception { 47 if (channel == null) { 48 return; 49 } 50 channel.configureBlocking(false); 51 channel.register(selector, ops); 52 } 53 private ByteBuffer buffer = ByteBuffer.allocateDirect(1024); 54 55 protected void readDataFromSocket(SelectionKey key) throws Exception { 56 SocketChannel socketChannel = (SocketChannel) key.channel(); 57 int count; 58 buffer.clear(); 59 while ((count = socketChannel.read(buffer)) > 0) { 60 buffer.flip(); 61 while (buffer.hasRemaining()) { 62 socketChannel.write(buffer); 63 } 64 buffer.clear(); // Empty buffer 65 } 66 if (count < 0) { 67 // Close channel on EOF, invalidates the key 68 socketChannel.close(); 69 } 70 } 71 72 private void sayHello(SocketChannel channel) throws Exception { 73 buffer.clear(); 74 buffer.put("Hi there!\r\n".getBytes()); 75 buffer.flip(); 76 channel.write(buffer); 77 } 78 }
上例采用Selector,则accept( )和read( )均不会阻塞,并且Selector监听这两个事件,只有当两个事件准备好时,去执行该方法,避免了传统调用下accept( )和read( )一直阻塞。
3.5 并发性
选择器对象是线程安全的,但它们包含的键集合不是。通过keys( )和selectKeys( )返回的键的集合是Selector对象内部的私有的Set对象集合的直接引用。
这些集合可能在任意时间被改变。已注册的键的集合是只读的,如果试图修改它,将会抛出UnsupportedOperationException。但并不代表绝对修改不了(反射),如果修改就会造成很大问题。Iterator对象是快速失败的(fail-fast):如果底层的Set改变,将会抛出ConcurrentModificationException。因此在多个线程间共享选择器和/或键,需要谨慎。
如果直接修改选择键,这么做可能也会彻底破坏另一个线程的Iterator。如果在多个线程并发地访问一个选择器的键合的时候存在任何问题,可以采取一些步骤合理地同步访问。在执行选择操作时,选择器在Selector对象上进行同步,然后是已注册的键的集合,最后是已选择的键的集合。按照这样的顺序,已取消的键的集合也在选择过程的的第1步和第3步之间保持同步。注意同步顺序,避免发生死锁。
Selector类的close( )方法与select( )方法的同步方式是一样的,因此也有一直阻塞的可能性。在选择过程还在进行的过程中,所有对close( )的调用都会被阻塞,直到选择过程结束,或者执行选择的线程进入睡眠。
四、异步关闭能力
任何时候都有可能关闭一个通道或者取消一个选择键,除非采取步骤进行同步,否则键的状态及相关的通道将发生意料之外的改变。一个特定的键集中的一个键的存在并不保证键仍然是有效的,或者它相关的通道仍然是打开的。
关闭通道的过程不应该是一个耗时的操作,NIO 的设计者们特别想要阻止一种可能性:一个线程在关闭一个处于选择操作中的通道时,被阻塞于无限期的等待。当一个通道关闭时,它相关的键也就都被取消了。这并不会影响正在进行的select( ),但这意味着在您调用select( )之前仍然是有效的键,在返回时可能会变为无效。所以,不要自己维护键的集合。
如果试图使用一个已经失效的键,大多数方法将抛出CancelledKeyException。但可以安全地从已取消的键中获取通道的句柄。如果通道已经关闭,使用时大多数情况下将引发ClosedChannelException。
五、选择过程的可扩展性
选择器可以简化用单线程同时管理多个可选择通道的实现。使用一个线程来为多个通道提供服务,通过消除管理各个线程的额外开销,可能会降低复杂性并可能大幅提升性能。这在单核CPU中可能很好,但假如有这样的场景,存在很多不同的请求,并且不同请求的响应优先级也不同(实时or延时较大)。这时,一个线程在处理一个事件时,其他事件只能等待,特别是对于实时请求可能是很糟糕的。
(1)可以使用多个线程提供服务,但避免使用多个选择器。在大量通道上执行就绪选择并不会有很大的开销,大多数工作是由底层操作系统完成的,但管理多个选择器并随机地将通道分派给它们当中的一个并不是这个问题的合理的解决方案,因为这只是将上面的场景缩小了(键集变小了,但问题还在)。
更优的策略是,所有的可选择通道使用一个选择器,并将对就绪通道的服务委托给其他线程。只用一个线程监控通道的就绪状态并使用一个协调好的工作线程池来处理共接收到的数据。这里可以用线程池来处理服务。
(2)对于实时任务,通道的响应速度要更高,可以通过使用两个选择器来解决:一个为命令连接服务,另一个为普通连接服务。这个场景也可以使用与(1)相似的办法来解决。与将所有准备好的通道放到同一个线程池的做法不同,通道可以根据功能由不同的工作线程来处理。它们可能可以是日志线程池,命令/控制线程池,状态请求线程池。
1 public class SelectSocketsThreadPool extends SelectSockets { 2 private static final int MAX_THREADS = 5; 3 private ThreadPool pool = new ThreadPool(MAX_THREADS); 4 5 public static void main(String[] argv) throws Exception { 6 new SelectSocketsThreadPool().go(argv); 7 } 8 9 protected void readDataFromSocket(SelectionKey key) throws Exception { 10 WorkerThread worker = pool.getWorker(); 11 if (worker == null) { 12 return; 13 } 14 worker.serviceChannel(key); 15 } 16 17 private class ThreadPool { 18 List idle = new LinkedList(); 19 ThreadPool(int poolSize) { 20 // Fill up the pool with worker threads 21 for (int i = 0; i < poolSize; i++) { 22 WorkerThread thread = new WorkerThread(this); 23 // Set thread name for debugging. Start it. 24 thread.setName("Worker" + (i + 1)); 25 thread.start(); 26 idle.add(thread); 27 } 28 } 29 /** 30 * Find an idle worker thread, if any. Could return null. 31 */ 32 WorkerThread getWorker() { 33 WorkerThread worker = null; 34 synchronized (idle) { 35 if (idle.size() > 0) { 36 worker = (WorkerThread) idle.remove(0); 37 } 38 } 39 return (worker); 40 } 41 /** 42 * Called by the worker thread to return itself to the idle pool. 43 */ 44 void returnWorker(WorkerThread worker) { 45 synchronized (idle) { 46 idle.add(worker); 47 } 48 } 49 } 50 51 private class WorkerThread extends Thread { 52 private ByteBuffer buffer = ByteBuffer.allocate(1024); 53 private ThreadPool pool; 54 private SelectionKey key; 55 WorkerThread(ThreadPool pool) { 56 this.pool = pool; 57 } 58 // Loop forever waiting for work to do 59 public synchronized void run() { 60 System.out.println(this.getName() + " is ready"); 61 while (true) { 62 try { 63 // Sleep and release object lock 64 this.wait(); 65 } catch (InterruptedException e) { 66 e.printStackTrace(); 67 // Clear interrupt status 68 this.interrupted(); 69 } 70 if (key == null) { 71 continue; // just in case 72 } 73 System.out.println(this.getName() + " has been awakened"); 74 try { 75 drainChannel(key); 76 } catch (Exception e) { 77 System.out.println("Caught '" + e 78 + "' closing channel"); 79 // Close channel and nudge selector 80 try { 81 key.channel().close(); 82 } catch (IOException ex) { 83 ex.printStackTrace(); 84 } 85 key.selector().wakeup(); 86 } 87 key = null; 88 // Done. Ready for more. Return to pool 89 this.pool.returnWorker(this); 90 } 91 } 92 93 synchronized void serviceChannel(SelectionKey key) { 94 this.key = key; 95 key.interestOps(key.interestOps() & (~SelectionKey.OP_READ)); 96 this.notify(); // Awaken the thread 97 } 98 99 void drainChannel(SelectionKey key) throws Exception { 100 SocketChannel channel = (SocketChannel) key.channel(); 101 int count; 102 buffer.clear(); // Empty buffer 103 // Loop while data is available; channel is nonblocking 104 while ((count = channel.read(buffer)) > 0) { 105 buffer.flip(); 106 107 while (buffer.hasRemaining()) { 108 channel.write(buffer); 109 } 110 buffer.clear(); 111 } 112 if (count < 0) { 113 channel.close(); 114 return; 115 } 116 117 key.interestOps(key.interestOps() | SelectionKey.OP_READ); 118 key.selector().wakeup(); 119 } 120 } 121 }
我们将这个程序与SelectSocket做一个对比,在唤醒线程去执行任务前,我们将键的interest集合修改,将interest(感兴趣的操作)从读取就绪(read-rreadiness)状态中移除:key.interestOps(key.interestOps() & (~SelectionKey.OP_READ))。
这是由于执行选择过程的线程将重新循环并几乎立即再次调用select( ),这就会出现这种情况:任务还未执行完,实际上该通道还是就绪的,select( )又会把该通道置于已选择键集中,选择器就会重复地调用readDataFromSocket( )。
当工作线程结束为通道提供的服务时,它将再次更新键的ready集合,将interest重新放到读取就绪集合中。它也会在选择器上显式地调用wakeup( ),阻塞被打断,就会再执行一次选择循环并带着被更新的键重新进入select( )。