Linux CentOS7 原始碼安裝Redis
事故背景
我在寫 Java1.4從BIO模型發展到NIO模型 時,就有個問題:
- 是否可以用 Acceptor執行緒 和 I/O 執行緒分別處理 接收連線 和 讀寫 ?
於是我想到"acceptor"執行緒迴圈執行 ServerSocketChannel.accept(),然後再註冊事件。
另外啟動一個"selector-io"執行緒重新整理鍵集 Selector.select() 和處理鍵集 Set<SelectionKey> 。結果卻出現了死鎖,囧。
編寫服務端
我們設計將 accept() 和 select() 分別放在主執行緒和"selector-io"執行緒
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.SelectorImpl 的 protected 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);// 防止死鎖 導致註冊不上
這樣做,雖然能夠避免死鎖,但是效能上還是有損耗。