1. 背景
上周有小伙伴反饋zk連接很慢躯概。
整理出zk連接的關(guān)鍵邏輯如下:
public class ClientZkAgent {
//單例模式
private static final ClientZkAgent instance = new ClientZkAgent();
private ZooKeeper zk; //zk客戶端
private ClientZkAgent() {
connect(); //初始化并連接zk
}
public static ClientZkAgent getInstance() {
return instance;
}
/**
* zk常用模式: 由于zookeeper的連接是異步的允睹,為防止zk對象在建立有效連接之前就返回行您,
* 我們阻塞主線程卸察,并通過zookeeper的EventThread在連接事件中喚醒主線程
*/
private void connect() {
CountDownLatch semaphore = new CountDownLatch(1);
zk = new ZooKeeper(zkHost, timeout, watchEvent -> { // #_1
switch (e.getState()) {
case SyncConnected:
semaphore.countDown();
break;
// 其它邏輯 ....
}
});
semaphore.await(10000, TimeUnit.MILLISECONDS);
}
}
上面的代碼造成第一次調(diào)用ClientZkAgent.getInstance
的時候,需耗時10s璧微, 這個時間恰好跟semaphore
的超時時間相當. 在此期間,整個世界好像停滯了一樣硬梁。
2. 分析
在本地重現(xiàn)后前硫,通過jstack
獲得系統(tǒng)停滯期間的線程棧,發(fā)現(xiàn)這個時候zookeeper
的EventThread
有個比較奇怪的現(xiàn)象:
"main-EventThread" #13 daemon prio=5 os_prio=0 tid=0x000000001fe36800 nid=0xf0c in Object.wait() [0x000000002032f000]
java.lang.Thread.State: RUNNABLE
at com.github.dapeng.registry.zookeeper.ClientZkAgent.lambda$connect$0(ClientZkAgent.java:154)
at com.github.dapeng.registry.zookeeper.ClientZkAgent$$Lambda$1/116211441.process(Unknown Source)
at org.apache.zookeeper.ClientCnxn$EventThread.processEvent(ClientCnxn.java:533)
at org.apache.zookeeper.ClientCnxn$EventThread.run(ClientCnxn.java:508)
Locked ownable synchronizers:
- None
客戶端實際上很快就連上了zookeeper
并返回后生成了SyncConnected
事件荧止,而且EventThread
已經(jīng)在回調(diào)Watcher.process
方法了屹电,但似乎事件線程就一直hold在上面#_1
的位置無法往下走, 同時跃巡,lambda表達式變成了ClientZkAgent
的一個方法了:lambda$connect$0
危号。
了解了一下Java中lambda的實現(xiàn)方式,事情水落石出了素邪。
簡而言之外莲,jvm會把lambda表達式轉(zhuǎn)換成所在類的一個方法lambda${method}${seq}
(method為該lambda所在的方法名,例如上面的connect方法)兔朦,同時通過動態(tài)代理生成一個代理類(該代理類實現(xiàn)了lambda表達式所代表的具體接口)偷线,在該代理類中調(diào)用lambda${method}${seq}
。
在上面的例子中沽甥,生成的代理類大概如下:
final class ClientZkAgent$$Lambda$1 implements Watcher {
final ClientZkAgent clientZkAgent;
public void process(WatchedEvent event) {
clientZkAgent.lambda$connect$0(event);
}
}
再梳理一下:
業(yè)務(wù)線程:
- 通過靜態(tài)方法
ClientZkAgent.getInstance()
獲取實例声邦,第一次訪問的時候會觸發(fā)類ClientZkAgent
的裝載。 - 裝載過程中摆舟,裝載靜態(tài)成員instance亥曹,這時候會嘗試創(chuàng)建一個
ClientZkAgent
對象邓了。 - 在
ClientZkAgent
的構(gòu)造函數(shù)中連接zk,并通過CountdownLatch
進入阻塞狀態(tài)媳瞪。 注意這時候類裝載還沒完成骗炉。 -
CountdownLatch
超時后完成對象的初始化以及整個類的加載
zk事件線程:
-
SyncConnected
事件觸發(fā)后,調(diào)用ClientZkAgent.lambda$connect$0(event)
, 試圖喚醒業(yè)務(wù)線程(喚醒邏輯在lambda中)材失。 - 然而這時候
ClientZkAgent
還沒加載完痕鳍,事件線程只能等待類加載流程的結(jié)束。 - 業(yè)務(wù)線程加載完
ClientZkAgent
后龙巨,事件線程完成事件的處理笼呆。
可見,在這個過程中旨别,兩個線程相互等待(類似死鎖但不是死鎖)诗赌,直至業(yè)務(wù)線程超時后才化解這個局面。
3. 改進
修改ClientZkAgent
的初始化邏輯如下:
public class ClientZkAgent {
//單例模式
private static final ClientZkAgent instance = new ClientZkAgent();
private ZooKeeper zk; //zk客戶端
private ClientZkAgent() {
}
public static ClientZkAgent getInstance() {
if (instance.zk == null) {
synchronized(ClientZkAgent.class) {
if (instance.zk == null) {
instance.connect();
}
}
}
return instance;
}