zoukankan      html  css  js  c++  java
  • 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);// 防止死锁 导致注册不上
    

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

  • 相关阅读:
    浏览器 cookie
    c# 委托
    并不对劲的loj3106:p5339:[TJOI2019]唱、跳、rap 和篮球
    并不对劲的loj3095:p5329:[SNOI2019]字符串
    并不对劲的CF1365D&E&F: Solve The Maximum Subsequence Again
    并不对劲的loj3123:p5404[CTS2019]重复
    并不对劲的loj3046:p5327:[ZJOI2019]语言
    并不对劲的loj3115:p5362:[SDOI2019]连续子序列
    并不对劲的loj3113:p5360:[SDOI2019]热闹的聚会与尴尬的聚会
    并不对劲的bzoj2521:p5039:[SHOI2010]最小生成树
  • 原文地址:https://www.cnblogs.com/kendoziyu/p/java-nio-deadlock-example.html
Copyright © 2011-2022 走看看