Lambdaによるピット

3894 ワード

1.背景
先週、zk接続が遅いというパートナーのフィードバックがありました.zk接続を整理するキーロジックは次のとおりです.
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);
 }
}

上記のコードにより、ClientZkAgent.getInstanceが初めて呼び出される場合、10 sの時間がかかり、この時間はsemaphoreのタイムアウト時間に相当する.その間、世界全体が停滞したようだ.
2.分析
ローカル再生後,jstackによりシステムの停滞期間のスレッドスタックが得られ,このときzookeeperEventThreadには比較的奇妙な現象があることが分かった.
"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はすでにWatcher.processメソッドをコールバックしているが、イベントスレッドは上の#_1の位置でずっと下に行けないようだ.同時に、lambda式はClientZkAgentのメソッドになった.lambda$connect$0.
Javaにおけるlambdaの実現方法を知って、事は明らかになった.
簡単に言えば、jvmは、lambda式をクラスに変換するメソッドlambda${method}${seq}(methodは、上記のconnectメソッドなどのlambdaが存在するメソッド名)と、動的エージェントによってlambda式が表す特定のインタフェースを実現するエージェントクラスを生成し、エージェントクラスでlambda${method}${seq}を呼び出す.上記の例では、生成されたエージェントクラスは次のようになります.
final class ClientZkAgent$$Lambda$1 implements Watcher {
     final ClientZkAgent clientZkAgent;
    
     public void process(WatchedEvent event) {
        clientZkAgent.lambda$connect$0(event);
    }
}

もう一度整理します:ビジネススレッド:
  • は、静的方法ClientZkAgent.getInstance()によってインスタンスを取得し、最初のアクセス時にクラスClientZkAgentのマウントがトリガーされる.
  • マウント中に静的メンバーinstanceがマウントされ、ClientZkAgentオブジェクトが作成されます.
  • は、ClientZkAgentの構造関数にzkを接続し、CountdownLatchを介して閉塞状態に入る.注意この時点でクラスのマウントはまだ完了していません.
  • CountdownLatchタイムアウト後にオブジェクトの初期化とクラス全体のロード
  • が完了する.
    zkイベントスレッド:
  • SyncConnectedイベントがトリガーされた後、ClientZkAgent.lambda$connect$0(event)が呼び出され、ビジネススレッド(lambdaにおける起動ロジック)を起動しようとする.
  • しかし、この時点でClientZkAgentはまだロードが完了していないため、イベントスレッドはクラスロードプロセスの終了を待つしかない.
  • トラフィックスレッドがClientZkAgentをロードした後、イベントスレッドはイベントの処理を完了する.

  • この過程で、2つのスレッドは互いに待機し(デッドロックに似ているがデッドロックではない)、ビジネススレッドがタイムアウトするまでこの局面を解消していることがわかる.
    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;
      }