Java NIO 类库Selector机制解析(续)

在前些天的《 Java NIO 类库Selector 机制解析 》文章中,我们知道了下面的事情:
 
1)Sun JVM 在实现Selector 上,在Linux Windows 平台下的细节。
2)Selector 类的wakeup() 方法如何唤醒阻塞在select() 系统调用上的细节。
 
先给大家做一个简单的回顾,在Windows 下,Sun Java 虚拟机在Selector.open() 时会自己和自己建立loopback TCP 链接;在Linux 下,Selector 会创建pipe 。这主要是为了Selector.wakeup() 可以方便唤醒阻塞在select() 系统调用上的线程(通过向自己所建立的TCP 链接和管道上随便写点什么就可以唤醒阻塞线程)
 
我们知道,无论是建立TCP 链接还是建立管道都会消耗系统资源,而在Windows 上,某些Windows 上的防火墙设置还可能会导致Java Selector 因为建立不起loopback TCP 链接而出现异常。
 
而在我的另一篇文章《 GDB 调试Java 程序 》中介绍了另一个Java 的解释器――GNU gij ,以及编译器gcj ,不但可以比较高效地运行Java 程序,而且还可以把Java 程序直接编译成可执行文件。
 
GNU 的之所以要重做一个Java 的编译和解释器,其一个重要原因就是想解释Sun JVM 的效率和资源耗费问题。当然,GNU Java 编译/ 解释器并不需要考虑太多复杂的平台,他们只需要专注于Linux 和衍生自Unix System V 的操作系统,对于开发人员来说,离开了Windows ,一切都会变得简单起来。在这里,让我们看看GNU gij 是如何解释Selector.open() Selector.wakeup() 的。
 
同样,我们需要一个测试程序。在这里,为了清晰,我不会例出所有的代码,我只给出我所使用的这个程序的一些关键代码。
 
我的这个测试程序中,和所有的Socket 程序一样,下面是一个比较标准的框架,当然,这个框架应该是在一个线程中,也就是一个需要继承Runnable 接口,并实现run() 方法的一个类。(注意:其中的s 是一个成员变量,是Selector 类型,以便主线程序使用)
 
 
         // 生成一个侦听端
        ServerSocketChannel ssc = ServerSocketChannel.open();
         // 将侦听端设为异步方式
        ssc.configureBlocking( false);
        // 生成一个信号监视器
        s = Selector.open();
        // 侦听端绑定到一个端口
        ssc.socket().bind( new InetSocketAddress(port));
        // 设置侦听端所选的异步信号 OP_ACCEPT
        ssc. register(s,SelectionKey.OP_ACCEPT);
  
        System.out.println( "echo server has been set up ......");
 
        while( true){
            int n = s.select();
            if (n == 0) { // 没有指定的 I/O 事件发生
               continue;
            }     
            Iterator it = s.selectedKeys().iterator();     
            while (it.hasNext()) {
                SelectionKey key = (SelectionKey) it.next();
                if (key.isAcceptable()) { // 侦听端信号触发
                     …… …… ……
                     …… …… ……
                }   
                if (key.isReadable()) { // socket 可读信号
                     …… …… ……
                     …… …… ……                    
                }     
                it.remove();
            }
         }


 
而在主线程中,我们可以通过Selector.wakeup() 来唤醒这个阻塞在select() 上的线程,下面是写在主线程中的唤醒程序:
 
 
new Thread( this).start();
try{
    //Sleep 30 seconds
    Thread.sleep( 30000);
    System.out.println(" wakeup the select");
    s.wakeup();
} catch(Exception e){
        e.printStackTrace();
}
 
 
这个程序在主线程中,先启动一个线程,也就是上面那个Socket 线程,然后休息30 秒,为的是让上面的那个线程有阻塞在select() ,然后打印出一条信息,这是为了我们用strace 命令查看具体的系统调用时能够快速定位。之后调用的是Selector wakeup() 方法来唤醒侦听线程。
 
接下来,我们可以通过两种方式来编译这个程序:
1) 使用gcj 或是sun javac 编译成class 文件,然后使用gij 解释执行。
2) 使用gcj 直接编译成可执行文件。
(无论你用那种方法,都是一样的结果,本文使用第二种方法,关于gcj 的编译方法,请参看我的《 GDB 调试Java 程序 》)
 
编译成可执行文件后,执行程序时,使用lsof 命令,我们可以看到没有任何pipe 的建立。可见GNU 的解释更为的节省资源。而对于一个Unix C 程序员来说,这意味着如果要唤醒select() 只能使用pthread_kill() 来发送一个信号了。下面就让我们使用strace 命令来验证这个想法。
 
下图是使用strace 命令来跟踪整个程序运行时的系统调用,我们利用我们的输出的“wakeup the select ”字符串快速的找到了wakeup 的实际系统调用。
 
 
果然,我们可可以看到,tgkill(5829, 5831, SIGHUP) 这个系统调用,第一个参数是“源线程id ”,第二个参数是“目的线程id ”,第三个参数是“信号SIGHUP” 。通过每一行前面的线程号我们可以看到紧接着tgkill 后面的5831 线程的“… select resumed ”字样。
 
可见,GNU 的确是使用最为传统的pthread_kill kill 系统调用向阻塞线程发信号的方法来实现Selector.wakeup() 的,这也证明了GNU Java 编译/ 解释器是不会消耗系统文件描述符的。而我们也终于看到了回归经典的Java 实现机制。
 
欢迎使用MSN 和邮件和我联系: [email protected]

你可能感兴趣的:(java,职场,类库,休闲,机制解析)