Java死锁——以NIO网络编程为例

事故背景

我在写 Java1.4从BIO模型发展到NIO模型 时,就有个问题:

  • 是否可以用 Acceptor线程I/O 线程分别处理 接收连接读写

于是我想到"acceptor"线程循环执行 ServerSocketChannel.accept(),然后再注册事件。
另外启动一个"selector-io"线程刷新键集 Selector.select() 和处理键集 Set<SelectionKey> 。结果却出现了死锁,囧。

编写服务端

我们设计将 accept() 和 select() 分别放在主线程"selector-io"线程中循环,一旦 accept() 获取到 SocketChannel 对象就立刻注册 OP_READ 事件到 Selector。

public class JavaChannelDeadLock {

    // TCP 事件
    @Test
    public void tcpSocketTest() throws IOException {
        ServerSocketChannel channel = ServerSocketChannel.open();
        channel.bind(new InetSocketAddress(8080));
        Selector selector = Selector.open();
        new Thread(() -> {
            try {
                dispatch(selector);
            } catch (IOException | InterruptedException e) {
                e.printStackTrace();
            }
        },"selector-io").start();

        while (true) {
            SocketChannel socket = channel.accept();
            socket.configureBlocking(false);
            socket.register(selector,SelectionKey.OP_READ);
            System.out.println("已注册"+socket);
        }
    }
    private void dispatch( Selector selector) throws IOException, InterruptedException {
        while (true) {
            int count = selector.select();
            if (count==0) {
                continue;
            }
            //客户端断开 事件处理
            Set<SelectionKey> keys = selector.selectedKeys();
            Iterator<SelectionKey> iterator = keys.iterator();
            while (iterator.hasNext()) {
                SelectionKey key = iterator.next();
                iterator.remove();
                if (!key.isValid()) {
                    continue;
                } else if (key.isReadable()) {
                    SocketChannel channel = (SocketChannel) key.channel();
                    ByteBuffer dst=ByteBuffer.allocate(1024);
                    channel.read(dst);
                    System.out.println("收到消息");
                }
            }
        }
    }
}

启动服务端

运行上一节的服务端代码,我们启动之后来看一下线程 dump 情况:

如上图所示,主线程("main")拿到了锁- locked <0x00000000d6092df0> (a java.lang.Object),然后调用 native accept0 方法,发生CPU中断。

  • 这把锁即 sun.nio.ch.ServerSocketChannelImpl 的成员变量 private final Object lock = new Object();


I/O 线程("selector-io")在执行 sun.nio.ch.SelectorImpl.lockAndDoSelect 时依次拿到了三把锁:

  • 第一把锁 - locked <0x00000000d609c1b0> (a sun.nio.ch.WindowsSelectorImpl) 是通过 synchronized(this) {} 获取到的;
  • 第二把锁 - locked <0x00000000d609e728> (a java.util.Collections$UnmodifiableSet) 是通过 synchronized(this.publicKeys) {} 获取到的;
  • 第三把锁 - locked <0x00000000d609efc0> (a sun.nio.ch.Util$3) 是通过 synchronized(this.publicSelectedKeys) {} 获取到的;

最后调用 native poll0 方法,发生CPU中断。

补充一下
我们 sun.nio.ch.SelectorImpl 的构造函数中看到成员变量 publicKeys:Set<SelectionKey>publicSelectedKeys:Set<SelectionKey>的初始化过程。代码如下:

protected SelectorImpl(SelectorProvider var1) {
      super(var1);
      if (Util.atBugLevel("1.4")) {
            this.publicKeys = this.keys;
            this.publicSelectedKeys = this.selectedKeys;
      } else {
            this.publicKeys = Collections.unmodifiableSet(this.keys);
            this.publicSelectedKeys = Util.ungrowableSet(this.selectedKeys);
      }
}

启动Telnet服务端发生死锁


回车之后,我们随便输入几个字符,发送给服务端,此时服务端完全没有响应!此时,已经产生死锁!
我们来观察一下此时的线程 dump 文件:

此时线程已经由 RUNNABLE 变为 BLOCKED,主线程正在等待 publicKeys 对象锁的释放。
在类 sun.nio.ch.SelectorImplprotected final SelectionKey register(AbstractSelectableChannel channel, int ops, Object attachment) 方法中,
语句 synchronized(this.publicKeys) {} 等待的是lock <0x00000000d609e728> (a java.util.Collections$UnmodifiableSet)


Selector.select() 获得 publicKeys 对象锁,等待就绪事件。就绪事件的前提是注册事件,但是 SocketChannel.register() 方法尝试竞争 publicKeys 对象锁时进入阻塞状态,无法成功注册事件。
"selector-io"线程等待就绪事件,陷入无限等待。"main"线程等待 publicKeys 对象锁,陷入无限等待。

修改方案

改用 Selector.select(int timeout) 代替 Selector.select(),那么 SelectableChannel.register(Selector sel, int ops) 就有机会竞争锁了,但是还是可能出现“饿死”现象,即长时间竞争不到锁而等待。
因此再加上一个 Thread.sleep(5),保证在 select(500) 超时并且释放锁之后,register 方法有足够的时间来获取到锁。

// int count = selector.select();
int count = selector.select(500);
Thread.sleep(5);// 防止死锁 导致注册不上

这样做,虽然能够避免死锁,但是性能上还是有损耗。

原文地址:https://www.cnblogs.com/kendoziyu/p/java-nio-deadlock-example.html