对于 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)
... 省略 ...
分析
从上面的日志可以很容易的看到:
- 新起的线程名字是
HandshakeCompletedNotify-Thread - 它是在一个
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给的反馈看:
- 如果用线程池:
- 那么就要保持线程池必须有一个活着的线程 -> JDK 的测试用例会失败.
- 如果用无上限线程池, 那么当有很多SSL建立完连接并且需要执行通知的时候, 照样会有很多线程.
- 如果用固定数量线程池, 当有很多SSL建立完连接并且需要执行通知的时候, 会排队, 并且不确定有些会执行很久, 导致执行到通知很晚, 并且增加了处理队列的逻辑.
- 如果不用线程池:
- 那就是我们看到的很多
HandshakeCompletedNotify被创建.
- 那就是我们看到的很多
结论
从上面的分析可以看到, 并没有一个完美的方案. JDK 也没有一个规范去规定如何面对这种情况. 所以, 从 JDK 1.4 到现在, 一直保持现状. 所以, 遇到这种线程导致的创建线程总数一直在增加, 也算一种正常现象.