关于 HandshakeCompletedNotify-Thread 线程

284 阅读3分钟

对于 Java 应用来说, 一个常见的数值指标就是创建的总线程数. 线程相对来说是一个比较珍贵的资源, 创建和销毁它都会带来内存和CPU的开销. 所以, 我们经常见到各种关于线程的调优的指导方针是: 尽量重用创建的线程, 尽量使用线程池. 可是我们却在 JDK 的代码里看到了一种线程, 它没有遵循这个指导原则.

起源

我们为了调查一个Java应用为什么创建很多线程, 于是写了下面的 Btrace 代码:

import org.openjdk.btrace.core.annotations.BTrace;
import org.openjdk.btrace.core.annotations.OnMethod;
import org.openjdk.btrace.core.annotations.Self;
import static org.openjdk.btrace.core.BTraceUtils.*;

@BTrace
public class ThreadStart {
    @OnMethod(
            clazz = "java.lang.Thread",
            method = "start"
    )
    public static void onnewThread(@Self Thread t) {
        println("starting " + Threads.name(t));
        println(jstackStr());
    }
}

当我们注入应用之后, 发现了大量下面的日志:

starting HandshakeCompletedNotify-Thread
java.lang.Thread.start(Thread.java)
sun.security.ssl.TransportContext.finishHandshake(TransportContext.java:620)
sun.security.ssl.Finished$T12FinishedConsumer.onConsumeFinished(Finished.java:546)
sun.security.ssl.Finished$T12FinishedConsumer.consume(Finished.java:515)
sun.security.ssl.SSLHandshake.consume(SSLHandshake.java:377)
sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:444)
sun.security.ssl.HandshakeContext.dispatch(HandshakeContext.java:422)
sun.security.ssl.TransportContext.dispatch(TransportContext.java:182)
sun.security.ssl.SSLTransport.decode(SSLTransport.java:156)
sun.security.ssl.SSLSocketImpl.decode(SSLSocketImpl.java:1409)
sun.security.ssl.SSLSocketImpl.readHandshakeRecord(SSLSocketImpl.java:1315)
sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:439)
sun.security.ssl.SSLSocketImpl.startHandshake(SSLSocketImpl.java:410)
sun.net.www.protocol.https.HttpsClient.afterConnect(HttpsClient.java:559)
     ... 省略 ...

分析

从上面的日志可以很容易的看到:

  1. 新起的线程名字是 HandshakeCompletedNotify-Thread
  2. 它是在一个 SSL 连接的 finishHandshake 的时候创建出来的.

为什么需要这个线程?

根据上面的日志, 很容易找到了 JDK 这段源代码:
github.com/AdoptOpenJD…

// Tell folk about handshake completion, but do it in a separate thread.
if (transport instanceof SSLSocket &&
        sslConfig.handshakeListeners != null &&
        !sslConfig.handshakeListeners.isEmpty()) {
    HandshakeCompletedEvent hce =
        new HandshakeCompletedEvent((SSLSocket)transport, conSession);
    Thread thread = new Thread(
        null,
        new NotifyHandshake(sslConfig.handshakeListeners, hce),
        "HandshakeCompletedNotify-Thread",
        0,
        false);
    thread.start();
}

这段代码也比较容易理解: 如果有人注册了SSL的 handshake 完成事件, 那么就要通知(执行)这些事件. 可以让当前线程去执行这些通知, 也可以另起一个线程去执行这些通知. 但是通常不知道执行这些通知需要多久, 是一个不确定事件, 所以这里选择新起一个线程去执行通知. 于是就有了这些 HandshakeCompletedNotify 线程.

为什么不用线程池?

当遇到这个问题后, 第一反应是: 为什么 JDK 允许这样的代码存在? 于是去 google 一把. 发现早在2020年就有人提了一个 bug 给 openJDK: bugs.openjdk.org/browse/JDK-…, 但是直到现在, 结论仍然是: 不会修复.

原因是: 没有一个完美的方案.

从这个bug给的反馈看:

  1. 如果用线程池:
    1. 那么就要保持线程池必须有一个活着的线程 -> JDK 的测试用例会失败.
    2. 如果用无上限线程池, 那么当有很多SSL建立完连接并且需要执行通知的时候, 照样会有很多线程.
    3. 如果用固定数量线程池, 当有很多SSL建立完连接并且需要执行通知的时候, 会排队, 并且不确定有些会执行很久, 导致执行到通知很晚, 并且增加了处理队列的逻辑.
  2. 如果不用线程池:
    1. 那就是我们看到的很多 HandshakeCompletedNotify 被创建.

结论

从上面的分析可以看到, 并没有一个完美的方案. JDK 也没有一个规范去规定如何面对这种情况. 所以, 从 JDK 1.4 到现在, 一直保持现状. 所以, 遇到这种线程导致的创建线程总数一直在增加, 也算一种正常现象.