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 实现机制。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值