1. 程式人生 > 實用技巧 >Linux CentOS7 原始碼安裝Redis

Linux CentOS7 原始碼安裝Redis

事故背景

我在寫 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);// 防止死鎖 導致註冊不上

這樣做,雖然能夠避免死鎖,但是效能上還是有損耗。