diff --git a/notes/HTTP.md b/notes/HTTP.md
index 2d61a243e..97ddc5cf0 100644
--- a/notes/HTTP.md
+++ b/notes/HTTP.md
@@ -510,7 +510,7 @@ Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMjQsInJvb
JWT 与 Session 的区别:
- Session 的状态保存在服务器,删除 Session 就能让用户立即下线;集群中需要共享 Session,见 [集群下的 Session 管理](集群.md#二集群下的-session-管理);
-- JWT 的状态保存在令牌中,任何持有密钥的服务都能独立验证,适合多个服务共用登录状态;代价是令牌签发后在过期前一直有效,服务器无法主动让它失效。
+- JWT 的状态保存在令牌中,任何持有密钥的服务都能独立验证(用 HS256 时持有密钥也就能签发,只需要验证的服务应改用 RS256 等非对称算法,只拿公钥),适合多个服务共用登录状态;代价是令牌签发后在过期前一直有效,服务器无法主动让它失效。
无法主动失效是 JWT 最常被追问的问题,常用的解决方法是双令牌:访问令牌(Access Token)的有效期很短,例如 15 分钟,每次请求都携带;刷新令牌(Refresh Token)的有效期较长,服务器记录已签发的刷新令牌,只在访问令牌过期时用它换取新的访问令牌。用户退出或者被封禁时撤销刷新令牌,已经发出的访问令牌最多 15 分钟后自然失效:
@@ -675,7 +675,7 @@ If-Modified-Since: Wed, 21 Oct 2015 07:28:00 GMT
浏览器使用缓存分为两种情况:
-- 强缓存:缓存还在 max-age(或 Expires)规定的有效期内,直接使用,不发送请求;
+- 强缓存:缓存还在 max-age(或 Expires)规定的有效期内,直接使用,不发送请求;两者都没有时,浏览器可以按 Last-Modified 估算有效期,例如取距上次修改时间的 10%;
- 协商缓存:缓存已经过期,或者响应中有 no-cache,浏览器带上 If-None-Match 或 If-Modified-Since 向服务器确认,资源没有变化就返回 304,只传输首部,浏览器继续使用本地的缓存。
@@ -944,7 +944,7 @@ HTTPS 并不是新协议,而是在 HTTP 和 TCP 之间加了一层 SSL(Secur
通过使用 SSL,HTTPS 具有了加密(防窃听)、认证(防伪装)和完整性保护(防篡改)。
-例如同一个登录请求 POST /login,表单为 user=alice&password=123456,分别用 HTTP 和 HTTPS 发给 shop.example,同时抓包(OpenSSL 3.0 实测,见[附录 A8](#a8-http-与-https-的抓包对比)):HTTP 的报文中可以直接读到请求行和密码;HTTPS 的报文中找不到 /login 和 password,只能看到域名 shop.example。这是因为客户端在 ClientHello 的 SNI(Server Name Indication)扩展中以明文告诉服务器要访问哪个域名,服务器据此选择证书。需要注意的是,HTTPS 保护的是报文的内容,访问了哪个 IP 和域名、传输了多少数据,仍然可以被网络中的设备看到。
+例如同一个登录请求 POST /login,表单为 user=alice&password=123456,分别用 HTTP 和 HTTPS 发给 shop.example,同时抓包(OpenSSL 3.0 实测,见[附录 A8](#a8-http-与-https-的抓包对比)):HTTP 的报文中可以直接读到请求行和密码;HTTPS 的报文中找不到 /login 和 password,只能看到域名 shop.example。这是因为未启用加密 ClientHello(Encrypted Client Hello, ECH)时,客户端在 ClientHello 的 SNI(Server Name Indication)扩展中以明文告诉服务器要访问哪个域名,服务器据此选择证书。需要注意的是,HTTPS 保护的是报文的内容,访问了哪个 IP、传输了多少数据,仍然可以被网络中的设备看到,未启用 ECH 时域名也是如此。
@@ -1014,7 +1014,7 @@ TLS 1.3 的套件名称只包含后两项,例如访问 www.jd.com 时为 TLS_A
证书中的签名是 CA 用自己的私钥对证书内容(域名、公钥、有效期、签发者等)的哈希值计算出来的。客户端用上一级证书中的公钥验证签名:证书内容只要被改动一个字节,哈希值就会变化,签名验证就会失败。例如把 baidu.com 证书中的 CN 改成 baidu.con,再用中间证书验证,结果为 certificate signature failure(OpenSSL 3.0 实测,见[附录 A9](#a9-证书验证))。
-根证书是信任的起点,它是自签名的,无法再向上验证,因此不在握手中发送,而是预装在操作系统或浏览器的信任列表中。客户端沿证书链逐级验证,链的最上端必须是信任列表中的根证书。除了签名,客户端还要检查证书是否在有效期内、是否已被吊销,以及访问的域名是否在证书的 Subject Alternative Name 中,例如 baidu.com 的证书列出了 53 个域名,其中的 *.baidu.com 匹配 www.baidu.com。
+根证书是信任的起点,它是自签名的,无法再向上验证,因此通常不在握手中发送,而是预装在操作系统或浏览器的信任列表中。客户端沿证书链逐级验证,链的最上端必须是信任列表中的根证书。除了签名,客户端还要检查证书是否在有效期内、是否已被吊销,以及访问的域名是否在证书的 Subject Alternative Name 中,例如 baidu.com 的证书列出了 53 个域名,其中的 *.baidu.com 匹配 www.baidu.com。
证书在有效期内也可能被提前吊销,例如私钥泄露。查询方式有两种,证书中可以写明相应的地址(baidu.com 的证书两者都有,见[附录 A9](#a9-证书验证)):证书吊销列表(Certificate Revocation List, CRL)是 CA 定期发布的全部被吊销证书的序列号;在线证书状态协议(Online Certificate Status Protocol, OCSP)只查询这一张证书。每次连接都去问 CA 既慢又暴露用户的访问记录,所以服务器可以在握手时附带 CA 签过名的 OCSP 响应,称为 OCSP Stapling;浏览器也大多改为定期下载厂商汇总的吊销列表。
@@ -1043,7 +1043,7 @@ TLS 1.2 要 2 个 RTT 之后才能发送 HTTP 请求。TLS 1.3 把 ECDHE 公钥
- ClientKeyExchange:客户端的 ECDHE 公钥,同样是 32 字节;
- ChangeCipherSpec 和 Finished:通知对方之后的报文都已加密,然后发送第一个加密的报文 Finished。
-客户端验证证书,再用证书中的公钥验证 ServerKeyExchange 中的签名,确认 ECDHE 公钥确实来自证书的持有者。之后双方用自己的私钥和对方的公钥算出相同的预主密钥(pre-master secret),再把预主密钥和两个随机数一起输入伪随机函数,得到 48 字节的主密钥,最后由主密钥派生出加密和完整性校验用的会话密钥。
+客户端验证证书,再用证书中的公钥验证 ServerKeyExchange 中的签名,确认 ECDHE 公钥确实来自证书的持有者。之后双方用自己的私钥和对方的公钥算出相同的预主密钥(pre-master secret),再把预主密钥和两个随机数一起输入伪随机函数(启用扩展主密钥时两个随机数换成从 ClientHello 到 ClientKeyExchange 这些握手报文的哈希,OpenSSL 3 默认启用),得到 48 字节的主密钥,最后由主密钥派生出加密和完整性校验用的会话密钥。
#### 2. 密钥交换
@@ -1124,7 +1124,7 @@ HTTP/2.0 将报文分成 HEADERS 帧和 DATA 帧,它们都是二进制格式
-在通信过程中,只会有一个 TCP 连接存在,它承载了任意数量的双向数据流(Stream)。
+在通信过程中,客户端与同一个服务器通常只建立一个 TCP 连接,它承载了任意数量的双向数据流(Stream)。
- 每个数据流(Stream)都有一个唯一标识符,用于承载双向信息(RFC 7540 定义的优先级信号已被 RFC 9113 废弃,需要时改用 RFC 9218 的 Priority 首部);
- 消息(Message)是与逻辑请求或响应对应的完整的一系列帧;
diff --git a/notes/Java IO.md b/notes/Java IO.md
index f5fee689e..5f468e909 100644
--- a/notes/Java IO.md
+++ b/notes/Java IO.md
@@ -455,7 +455,7 @@ int interestSet = SelectionKey.OP_READ | SelectionKey.OP_WRITE;
int num = selector.select();
```
-使用 select() 来监听到达的事件,它会一直阻塞直到有至少一个事件到达。
+使用 select() 来监听到达的事件,它会阻塞到至少一个通道就绪,或者被 wakeup() 或线程中断唤醒,因此返回值可能为 0。
#### 4. 获取到达的事件
diff --git "a/notes/Java \345\237\272\347\241\200.md" "b/notes/Java \345\237\272\347\241\200.md"
index be68c41df..0ce7cb0cd 100644
--- "a/notes/Java \345\237\272\347\241\200.md"
+++ "b/notes/Java \345\237\272\347\241\200.md"
@@ -157,7 +157,7 @@ public final class String
}
```
-value 数组被声明为 final,这意味着 value 数组初始化之后就不能再引用其它数组。并且 String 内部没有改变 value 数组的方法,因此可以保证 String 不可变。
+value 数组被声明为 final,这只意味着 value 初始化之后不能再引用其它数组,数组中的内容仍然可以修改。真正保证 String 不可变的是:value 为 private,String 内部没有改变 value 数组的方法,也不把它交给外部代码。
### 不可变的好处
@@ -1358,7 +1358,7 @@ getMethod()、getField() 只能找到 public 成员,包括继承的;getDecla
## 八、异常
-Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,例如 OutOfMemoryError、StackOverflowError,程序一般不捕获。Exception 分为两种:
+Throwable 可以用来表示任何可以作为异常抛出的类,分为两种:Error 和 Exception。其中 Error 用来表示 JVM 无法处理的错误,例如 OutOfMemoryError、StackOverflowError,程序一般不捕获,编译器也不要求捕获或声明。Exception 分为两种:
- 受检异常:例如 IOException、SQLException,需要用 try...catch... 语句捕获并进行处理,或者用 throws 声明抛出,否则编译不通过,这类异常通常可以恢复;
- 非受检异常:RuntimeException 及其子类,是程序运行时错误,例如除 0 会引发 ArithmeticException,访问 null 的成员会引发 NullPointerException。编译器不强制处理这类异常,通常应该修改代码来避免,而不是捕获之后恢复。
diff --git "a/notes/Java \345\256\271\345\231\250.md" "b/notes/Java \345\256\271\345\231\250.md"
index ce83f9fa7..20c40b01b 100644
--- "a/notes/Java \345\256\271\345\231\250.md"
+++ "b/notes/Java \345\256\271\345\231\250.md"
@@ -347,7 +347,7 @@ List list = new CopyOnWriteArrayList<>();
#### 1. 读写分离
-写操作先加锁,把当前数组复制一份,在副本上修改,最后把 volatile 字段 array 指向副本;加锁是为了防止两个写操作各自复制同一个旧数组,后完成的覆盖先完成的。已经发布的数组不再被修改,所以读操作直接读当前的数组,不需要加锁。JDK 21 中写操作改为用 synchronized 加锁,过程相同。
+写操作先加锁,把当前数组复制一份,在副本上修改,最后把 volatile 字段 array 指向副本;加锁是为了防止两个写操作各自复制同一个旧数组,后完成的覆盖先完成的。已经发布的数组不再被修改,所以读操作直接读当前的数组,不需要加锁。JDK 9 起写操作改为用 synchronized 加锁,过程相同。
迭代器遍历的是创建时的数组快照:遍历期间可以修改列表,不会抛出 ConcurrentModificationException,但看不到这些修改,也不支持通过迭代器 remove(JDK 21 实测,见[附录 A8](#a8-copyonwritearraylist-的迭代器))。
diff --git "a/notes/Java \345\271\266\345\217\221.md" "b/notes/Java \345\271\266\345\217\221.md"
index 6d8ac3f9f..065ed3d1f 100644
--- "a/notes/Java \345\271\266\345\217\221.md"
+++ "b/notes/Java \345\271\266\345\217\221.md"
@@ -89,6 +89,8 @@
* [A23 拒绝策略的执行线程与 Future](#a23-拒绝策略的执行线程与-future)
* [A24 volatile 的自增与机器码](#a24-volatile-的自增与机器码)
* [A25 线程池空闲线程的状态与 submit()](#a25-线程池空闲线程的状态与-submit)
+ * [A26 CompletableFuture 的回调线程与异常](#a26-completablefuture-的回调线程与异常)
+ * [A27 traceId 随任务传递](#a27-traceid-随任务传递)
* [参考资料](#参考资料)
@@ -235,7 +237,7 @@ Executor 把任务的提交和执行分开,提交者只管提交任务,不
- CachedThreadPool:有空闲线程时复用,没有时为任务新建一个线程,线程空闲 60 秒后被回收;
- FixedThreadPool:所有任务只能使用固定大小的线程;
-- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool。
+- SingleThreadExecutor:相当于大小为 1 的 FixedThreadPool,但不能再被调大线程数。
```java
public static void main(String[] args) {
@@ -303,7 +305,7 @@ pool.shutdown(); // 不关闭的话,核心线程一直存活,程序不会
需要注意的是,任务 5、6 比先提交的任务 3、4 更早执行:队列满后新建的线程直接执行当前提交的任务,队列中的任务要等有线程空闲后才会被取出。
-先放入队列、队列满了才增加线程,是因为线程比排队的任务昂贵得多:核心线程承担平时的负载,短时间的高峰由队列缓冲,队列也放不下说明负载持续超出了处理能力,才临时增加线程,高峰过后再减少。Tomcat 的线程池则反过来,线程数没有达到最大值时先增加线程,让等待 I/O 的请求尽快开始处理。
+先放入队列、队列满了才增加线程,是因为线程比排队的任务昂贵得多:核心线程承担平时的负载,短时间的高峰由队列缓冲,队列也放不下说明负载持续超出了处理能力,才临时增加线程,高峰过后再减少。Tomcat 的线程池则反过来:没有空闲线程、线程数又没到最大值时,它的队列在 offer() 中返回 false,线程池当作队列已满而先增加线程,让等待 I/O 的请求尽快开始处理。
核心线程数为 0 时,任务直接进入队列,execute() 发现没有线程时只创建一个。例如核心线程数为 0、最大线程数为 4、使用无界队列时,提交 3 个任务只有 1 个线程逐个执行(JDK 21 实测,见[附录 A7](#a7-线程池的异常与参数调整))。
@@ -347,6 +349,8 @@ ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
- CPU 密集型:取 CPU 核数 + 1,多出的一个线程用于在某个线程因缺页等原因暂停时继续使用 CPU;
- I/O 密集型:线程等待 I/O 时不占用 CPU,可以按 核数 × (1 + 等待时间 / 计算时间) 估算。例如 8 核 CPU,每个任务计算 10 ms、等待数据库 90 ms,约为 8 × (1 + 90 / 10) = 80 个线程。
+核数取 availableProcessors() 的值,JDK 10 起在容器中按 CPU 配额计算,也可以用 -XX:ActiveProcessorCount 指定。
+
也可以从吞吐量估算:同时在执行的任务数 = 每秒到达的任务数 × 每个任务的耗时(利特尔法则)。例如每秒 200 个请求,每个耗时 100 ms,平均有 200 × 0.1 = 20 个任务同时在执行,线程数至少为 20,再按高峰留出余量。
CPU 或下游资源(例如数据库连接池)饱和后,再加线程也不能提高吞吐量。
@@ -355,7 +359,7 @@ CPU 或下游资源(例如数据库连接池)饱和后,再加线程也不
I/O 密集、估出的线程数成百上千时,JDK 21 可以改用虚拟线程(Executors.newVirtualThreadPerTaskExecutor(),每个任务一个,不池化),对下游的并发数用 [Semaphore](#semaphore) 限制。
-估算值只是起点,最终要通过压测确定。上线后用 getActiveCount()、getQueue().size() 等方法监控活跃线程数和队列长度,在拒绝策略中统计拒绝次数,必要时调用 setCorePoolSize()、setMaximumPoolSize() 在运行时调整,参数放在配置中心、变更时调用这两个方法就是动态线程池。需要注意的是,JDK 9 起 setCorePoolSize() 的新值大于最大线程数时抛出 IllegalArgumentException,两者都要调大时先调最大线程数(JDK 21 实测,见[附录 A7](#a7-线程池的异常与参数调整));LinkedBlockingQueue 的容量是 final 字段,队列长度要可调时只能自己实现队列。
+估算值只是起点,最终要通过压测确定。上线后用 getActiveCount()、getQueue().size() 等方法监控活跃线程数和队列长度,在拒绝策略中统计拒绝次数,必要时调用 setCorePoolSize()、setMaximumPoolSize() 在运行时调整,参数放在配置中心、变更时调用这两个方法就是动态线程池。需要注意的是,JDK 9 起 setCorePoolSize() 的新值大于最大线程数时抛出 IllegalArgumentException,两者都要调大时先调最大线程数;调小核心线程数后,多出的线程要再空闲满 keepAliveTime 才退出,调大时只按队列中排队的任务数补建线程(JDK 21 实测,见[附录 A7](#a7-线程池的异常与参数调整));LinkedBlockingQueue 的容量是 final 字段,队列长度要可调时只能自己实现队列。
不同业务最好使用各自的线程池(线程池隔离),例如导出报表变慢时只占满自己的线程,不影响下单。
@@ -1194,8 +1198,11 @@ public class CountdownLatchExample {
ExecutorService executorService = Executors.newCachedThreadPool();
for (int i = 0; i < totalThread; i++) {
executorService.execute(() -> {
- System.out.print("run..");
- countDownLatch.countDown();
+ try {
+ System.out.print("run..");
+ } finally {
+ countDownLatch.countDown(); // 任务抛出异常也要减 1
+ }
});
}
countDownLatch.await();
@@ -1379,17 +1386,19 @@ pool.shutdown();
需要注意的是:
-- 带 Async 的方法不传入线程池时,任务在 ForkJoinPool.commonPool() 中执行,它的线程数默认为 CPU 核数 − 1,并且被并行流等代码共用,阻塞的 I/O 任务应该使用自己的线程池(commonPool 并行度小于 2 时改为每个任务新建一个线程,见[附录 A18](#a18-completablefuture-的默认线程));
-- 异常不会自动抛出,要用 exceptionally、handle 处理,否则只有调用 join() 时才会得到包装后的 CompletionException;
-- 远程调用要设置超时:JDK 9 起 orTimeout() 超时后以 TimeoutException 结束,可以接 exceptionally 降级,但它不中断正在执行的任务,所以 HTTP 客户端、数据库驱动本身也要设置超时(JDK 21 实测,见[附录 A21](#a21-优先级队列与超时));
+- 带 Async 的方法不传入线程池时,任务在 ForkJoinPool.commonPool() 中执行,它的线程数默认为 CPU 核数 − 1,并且被并行流等代码共用,阻塞的 I/O 任务应该使用自己的线程池(commonPool 并行度小于 2 时改为每个任务新建一个线程,见[附录 A18](#a18-completablefuture-的默认线程));不带 Async 的回调通常由完成上一步的线程执行,也可能由其他调用 get()、join() 等方法的线程执行,注册时上一步已完成则由注册的线程直接执行,所以不要依赖回调所在的线程;
+- 异常不会自动抛出,要用 exceptionally、handle 处理,否则 get()、join() 分别抛出包装后的 ExecutionException、CompletionException,exceptionally 收到的异常在任务中抛出时被包装成 CompletionException,要用 getCause() 取原始异常,而 orTimeout() 的 TimeoutException 没有包装(JDK 21 实测,见[附录 A26](#a26-completablefuture-的回调线程与异常));
+- 远程调用要设置超时:JDK 9 起 orTimeout() 超时后以 TimeoutException 结束,可以接 exceptionally 降级,但它和 cancel() 一样不中断正在执行的任务,所以 HTTP 客户端、数据库驱动本身也要设置超时(JDK 21 实测,见[附录 A21](#a21-优先级队列与超时));
- thenApply 的函数返回普通的值,thenCompose 的函数返回另一个 CompletableFuture,用来串联两个异步调用,避免得到 CompletableFuture<CompletableFuture<T>>。
### BlockingQueue
java.util.concurrent.BlockingQueue 接口有以下阻塞队列的实现:
-- FIFO 队列:LinkedBlockingQueue、ArrayBlockingQueue(固定长度);
-- 优先级队列:PriorityBlockingQueue。
+- FIFO 队列:ArrayBlockingQueue(固定长度,入队出队共用一把锁)、LinkedBlockingQueue(入队出队各一把锁,默认容量 Integer.MAX_VALUE);
+- 优先级队列:PriorityBlockingQueue;
+- 延迟队列:DelayQueue,元素到期后才能取出;
+- 直接交接:SynchronousQueue,不保存元素,put() 要等另一个线程 take()。
提供了阻塞的 take() 和 put() 方法:如果队列为空 take() 将阻塞,直到队列中有内容;如果队列为满 put() 将阻塞,直到队列有空闲位置。
@@ -2042,7 +2051,38 @@ try {
}
```
-子线程读不到父线程设置的 ThreadLocal 值,因为值存在父线程自己的 Map 中。InheritableThreadLocal 在 new Thread() 时把父线程的值复制给子线程(复制的是引用),但只复制这一次,而线程池中的线程会被反复使用。例如主线程先后设置 alice 和 bob,每次都向只有一个线程的线程池提交任务,两次读到的都是 alice(JDK 21 实测,见[附录 A5](#a5-inheritablethreadlocal-与线程池))。需要把上下文传给线程池中的任务时,可以在提交任务时显式传递,或者使用阿里开源的 TransmittableThreadLocal,它在提交任务时捕获当前的值,执行任务前设置,执行后恢复。
+子线程读不到父线程设置的 ThreadLocal 值,因为值存在父线程自己的 Map 中。InheritableThreadLocal 在 new Thread() 时把父线程的值复制给子线程(复制的是引用),但只复制这一次,而线程池中的线程会被反复使用。例如主线程先后设置 alice 和 bob,每次都向只有一个线程的线程池提交任务,两次读到的都是 alice(JDK 21 实测,见[附录 A5](#a5-inheritablethreadlocal-与线程池))。
+线程池中的任务同样读不到提交者的值。例如过滤器把链路追踪的 traceId 放进请求线程的 ThreadLocal(日志框架的 MDC 也存在 ThreadLocal 中),业务代码把发短信交给线程池,任务读的是工作线程自己的 Map,得到 null 或者之前某个任务没清理的值,这条日志就和请求对不上了。
+
+解决办法是让任务带着上下文走:提交时在调用者线程中捕获,执行前在工作线程中设置,执行后在 finally 中恢复工作线程原来的值:
+
+```java
+static final ThreadLocal TRACE_ID = new ThreadLocal<>();
+
+static Runnable wrap(Runnable task) {
+ String captured = TRACE_ID.get(); // 提交时,在调用者线程中捕获
+ return () -> {
+ String backup = TRACE_ID.get(); // 工作线程原来的值
+ TRACE_ID.set(captured); // 执行前设置
+ try {
+ task.run();
+ } finally {
+ if (backup == null) TRACE_ID.remove(); else TRACE_ID.set(backup); // 恢复
+ }
+ };
+}
+
+pool.execute(wrap(() -> log.info("traceId={} 发送短信", TRACE_ID.get())));
+```
+
+最后恢复而不是直接 remove(),是因为任务也可能在调用者线程中执行(例如 CallerRunsPolicy),直接 remove() 会把调用者自己的 traceId 清掉(JDK 21 实测,见[附录 A27](#a27-traceid-随任务传递))。
+
+每个提交点都手写 wrap() 容易漏,一般在线程池上统一处理:
+
+- Spring 的 ThreadPoolTaskExecutor 用 setTaskDecorator() 设置一个 TaskDecorator,在 decorate() 中写同样的逻辑,经这个线程池执行的任务(包括 @Async 方法)都会被包装;MDC 用 MDC.getCopyOfContextMap() 捕获、MDC.setContextMap() 设置;
+- 把 traceId 存进阿里开源的 TransmittableThreadLocal(TTL)而不是普通 ThreadLocal,再用 TtlExecutors 包装线程池,由它完成捕获、设置和恢复;MDC 不会随之传递,需要另外处理。
+
+需要注意的是,只有被包装的任务才带着上下文。CompletableFuture 中没有指定线程池的 thenRunAsync() 这类方法在 ForkJoinPool.commonPool() 中执行,这一步读不到请求的 traceId(附录中读到的是 null)。
#### 3. 可重入代码(Reentrant Code)
@@ -2380,6 +2420,18 @@ p.setCorePoolSize(8);
ThreadPoolExecutor ps = new ThreadPoolExecutor(5, 10, 60, TimeUnit.SECONDS, new SynchronousQueue<>());
for (int i = 0; i < 15; i++) ps.execute(() -> sleep(1000)); // 捕获 RejectedExecutionException 并计数
+
+ThreadPoolExecutor p1 = new ThreadPoolExecutor(10, 10, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+p1.prestartAllCoreThreads();
+p1.setCorePoolSize(2); // 0.5 s 和 3 s 时查看线程数
+
+ThreadPoolExecutor p2 = new ThreadPoolExecutor(2, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+p2.prestartAllCoreThreads();
+p2.setCorePoolSize(6); // 队列为空
+
+ThreadPoolExecutor p3 = new ThreadPoolExecutor(2, 10, 60, TimeUnit.SECONDS, new LinkedBlockingQueue<>());
+for (int i = 0; i < 5; i++) p3.execute(() -> release.await()); // 2 个在执行、3 个排队,省略了 try/catch
+p3.setCorePoolSize(6);
```
```text
@@ -2396,9 +2448,12 @@ future.get() 抛出 java.lang.IllegalStateException: submit 中的异常
最大线程数为 4 时 setCorePoolSize(8):IllegalArgumentException
先 setMaximumPoolSize(8) 再 setCorePoolSize(8):core=8 max=8
核心 5、最大 10、SynchronousQueue,提交 15 个 1 s 的任务:线程数 10,被拒绝 5 个
+keepAliveTime 2 s,核心线程数 10 → 2:0.5 s 时线程数 10,3 s 时线程数 2
+队列为空时核心线程数 2 → 6:线程数 2
+队列中 3 个任务时核心线程数 2 → 6:线程数 5
```
-JDK 8 的 setCorePoolSize() 只检查新值是否小于 0,JDK 9 起增加了与最大线程数的比较(见两个版本的 ThreadPoolExecutor.java)。
+JDK 8 的 setCorePoolSize() 只检查新值是否小于 0,JDK 9 起增加了与最大线程数的比较(见两个版本的 ThreadPoolExecutor.java)。调小核心线程数时 setCorePoolSize() 只中断空闲线程,被唤醒的线程回到 getTask() 后仍用 poll(keepAliveTime) 等待,超时才退出;调大时最多补建 min(增量, 队列长度) 个线程,上例增量为 4、队列中 3 个,所以补建 3 个。
### A8 重排序与 volatile
@@ -3131,6 +3186,86 @@ execute() 收到 FutureTask
submit() 返回 FutureTask,get() = 3
```
+### A26 CompletableFuture 的回调线程与异常
+
+JDK 21.0.13,Apple M1 Pro。pool 是 2 个线程的线程池,线程名以 biz- 开头;下面省略了 try/catch 和 sleep() 的定义:
+
+```java
+// 回调线程
+CompletableFuture.completedFuture("x").thenApply(v -> Thread.currentThread().getName()).join();
+CompletableFuture.supplyAsync(() -> { sleep(200); return "y"; }, pool)
+ .thenApply(v -> Thread.currentThread().getName()).join();
+
+// 异常的包装
+CompletableFuture