《Java 网络编程基础》12.3 上下文传播与线程安全
本节处理跨线程的请求上下文:ThreadLocal 在线程池中的残留与泄漏、ScopedValue 在 JDK 25 的绑定与恢复、事件循环到业务线程的显式传播,以及不可变消息与 happens-before 如何建立可见性;给出可运行的 RequestContext 与本机输出。
tag
本节处理跨线程的请求上下文:ThreadLocal 在线程池中的残留与泄漏、ScopedValue 在 JDK 25 的绑定与恢复、事件循环到业务线程的显式传播,以及不可变消息与 happens-before 如何建立可见性;给出可运行的 RequestContext 与本机输出。
本节给 linkd 补业务线程池:用有界队列和命名线程隔离阻塞任务,按 CPU 与等待比估算线程数,解释 CallerRunsPolicy 为何会把阻塞带回事件循环,并给出指标与拒绝路径;最后用 JDK 25 虚拟线程做同负载对照,附可复现命令与本机单次数据。
本节拆开 linkd 事件循环的调度层:任务队列为什么必须是多生产者单消费者、execute 的线程归属与重入边界、selector.wakeup 的合并条件、定时任务的最小堆与 select 超时算法,以及一次 500 毫秒阻塞如何让心跳延迟到任务结束;给出可运行的 EventLoopImpl 与本机输出。
在帧编解码之上补齐连接生命周期:用 ping/pong 检测半开连接,按 1000、1001、1002 等关闭码完成关闭握手,并用 ConcurrentHashMap 会话表与每会话有界发送队列实现线程安全广播,给出 WebSocketSessionImpl 与 WebSocketHub。
从 101 之后的第一段字节开始,拆开 WebSocket 的两字节帧头:FIN、RSV、opcode、MASK 与 7/16/64 位长度编码,讲清客户端掩码的 XOR 算法和控制帧的 125 字节上限,并用 WebSocketFrameCodec 处理半帧、continuation 分片与 UTF-8 校验。
从 linkd-ws 的 /ws/echo 升级请求出发,逐条校验 Upgrade、Connection、Sec-WebSocket-Version 与 Sec-WebSocket-Key,手写 SHA-1 加 GUID 再 Base64 的 Accept 计算,讲清 101 响应写完后协议切换点在哪里,并给出 WebSocketHandshake 的完整实现。
完整握手之后,linkd 还要决定复用哪条会话、用 h2 还是 http/1.1、以及证书到底属于谁。本节讲 TLS 1.3 session ticket 的复用口径、ALPN 协商与主机名校验,给出 TlsSessionPolicy,并用 PKIXParameters 校验证书链。
阻塞式 SSLSocket 会把握手绑在一条线程上,linkd 的 EventLoop 不能这样等。本节把 SSLEngine 封装成 NonBlockingTlsHandshake:逐轮驱动 wrap/unwrap,处理 BUFFER_OVERFLOW、BUFFER_UNDERFLOW 与 NEED_TASK,最后用 TlsHandshakeHandler 接进 6.2 的事件循环。
从 linkd 明文代理必须加密的约束出发,拆解 TLS 1.3 的 1-RTT 握手、ClientHello/ServerHello 与证书链;用 keytool 生成带 SAN 的 PKCS12 自签证书,再用 TlsHandshakeInspector 打印协议、套件、证书链与 SAN,并给出证书不匹配的排查顺序。
从 linkd 一条连接上 6 个并发请求抢 65535 字节窗口出发,讲清 stream id 分配、WINDOW_UPDATE、连接级与流级窗口的区别,给出 Http2Connection 的写数据、窗口回收与轮转调度骨架,并说明 RFC 9113 弃用优先树后的取舍。
手写 HPACK 的最小可用实现:静态表、动态表、整数编码与 Huffman 解码,用 RFC 7541 的 C.4.1、C.6.1 测试向量逐字节验证解码结果,并说明编码端不支持 Huffman 时如何通过 H=0 字面量合法降级、解码端为何仍必须支持。
从 linkd 一条连接只能串行处理请求的瓶颈出发,拆开 HTTP/2 的 9 字节帧头,逐字节解析 SETTINGS、HEADERS、DATA、RST_STREAM 与 GOAWAY,给出 Http2Frame、Http2FrameCodec 的编解码实现,并用 preface 握手把帧接进 9090 的读循环。
路由决定一个 HttpRequest 最终由谁处理。本节实现 HttpRouter 与 HttpRouterImpl,注册 /healthz 与 /books/{id},区分 404、405 与 500,并在静态文件服务里做百分号解码、路径规范化与根目录前缀校验,挡住 ../、%2e%2e、符号链接与编码斜杠。
HTTP/1.1 的连接复用依赖消息边界。本节实现 ChunkedCodec 的 chunk 解码与响应编码,用 KeepAliveHandler 处理 Connection: keep-alive/close、HTTP/1.0 兼容、分块请求与按序回写,并在 loopback 的真实 SocketChannel 上验证两个请求的响应顺序。
HTTP/1.1 的第一层是字节到消息的边界。本节实现 HttpRequest、HttpResponse 与 HttpParser,解析请求行和头部,给请求行、头部总字节与消息体设上限,用 400、413、431、414 区分格式错误与超限,并用分片读取、管线化输入验证解析器的增量行为。
连接卡在 CLOSE_WAIT、空闲连接占着内存,靠 if-else 堆状态迟早漏分支。本节用 linkd 的 ConnectionStateMachine 枚举 CONNECTED、READING、CLOSING、CLOSED 四种状态,配合 lastReadNanos 与 IdleTimeoutHandler 定时扫描,给出半关闭与资源释放顺序。
一次 read 可能只拿到半个帧,一次 write 也可能只写出一部分。本节用 linkd 的 AccumulatingReader 累积字节、compact 保留残余、按帧上限扩容,并用 WriteQueue 暂存未写完的 ByteBuffer,给出半包、粘包与背压的可编译实现和实测输出。
非阻塞通道上的 accept、read、write 不再阻塞,返回值变成 null、0 和 -1 三种信号。本节用 linkd 的 NonBlockingEchoServer 实测这三种返回值,讲清读循环、写队列与 OP_WRITE 开关,以及漏摘 OP_WRITE 导致 CPU 空转的排查方法。
本节把单线程 Reactor 扩成 accept 线程加多个 I/O 线程的主从模型:round-robin 分配连接、跨线程 execute 提交注册任务、ConcurrentLinkedQueue 与 volatile 的可见性边界。给出与单线程版同一压测脚本的对照实验和真实耗时。
本节用 JDK 25 手写 linkd 的单线程 Reactor:BufferPool、Handler、Connection、EventLoop 四个共享类型,完整实现 accept、read、写队列与 interestOps 开关。给出可运行的 Echo 服务、真实往返输出,并说明线程归属、阻塞 handler 的代价与常见错误。