Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@
- [Java 并发](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20并发.md)
- [Java 虚拟机](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20虚拟机.md)
- [Java I/O](https://github.com/CyC2018/CS-Notes/blob/master/notes/Java%20IO.md)
- [Spring](https://github.com/CyC2018/CS-Notes/blob/master/notes/Spring.md)

## :bulb: 系统设计

Expand Down
37 changes: 37 additions & 0 deletions notes/HTTP.md
Original file line number Diff line number Diff line change
Expand Up @@ -481,6 +481,40 @@ Session 可以存储在服务器上的文件、数据库或者内存中。也可
- Cookie 存储在浏览器中,容易被恶意查看。如果非要将一些隐私数据存在 Cookie 中,可以将 Cookie 值进行加密,然后在服务器进行解密;
- 对于大型网站,如果用户所有的信息都存储在 Session 中,那么开销是非常大的,因此不建议将所有的用户信息都存储到 Session 中。

#### 11. JWT

JSON Web Token(JWT)是一种自带签名的令牌。用户登录成功后,服务器把用户 ID、过期时间等信息签名后生成 JWT 返回给客户端,客户端之后在请求的 Authorization 首部中携带它。服务器验证签名,就能确认令牌是自己签发的、内容没有被修改,不需要像 Session 那样在服务器保存会话。

JWT 由三部分组成,用 . 连接:

```html
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1aWQiOjEwMjQsInJvbGUiOiJ1c2VyIiwiZXhwIjoxNzY3MjI1NjAwfQ.35OYguRMXfGxb4cYZPiP9KDH3-Ny6vMNwrPe-lNHBBw
```

- 头部:{"alg":"HS256","typ":"JWT"} 的 Base64URL 编码,表示用 HMAC-SHA256 计算签名;
- 载荷:{"uid":1024,"role":"user","exp":1767225600} 的 Base64URL 编码,exp 是过期时间 2026-01-01 00:00:00(UTC);
- 签名:用只有服务器知道的密钥,对“头部.载荷”计算 HMAC-SHA256。

头部和载荷只是编码而不是加密,任何人都能解码看到内容,所以载荷中不能放密码等敏感信息。签名的作用是防篡改:如果把载荷中的 role 改成 admin,没有密钥就算不出新的签名,服务器按篡改后的内容重新计算的签名与令牌中的不一致,请求会被拒绝:

```text
令牌中的签名 35OYguRMXfGxb4cYZPiP9KDH3-Ny6vMNwrPe-lNHBBw
按篡改后的内容计算 Az4UO74FtqslT1MDLrbi7CMlKtd4KICR1jcwA7rJE4U
```

JWT 与 Session 的区别:

- Session 的状态保存在服务器,删除 Session 就能让用户立即下线;集群中需要共享 Session,见 [集群下的 Session 管理](集群.md#二集群下的-session-管理);
- JWT 的状态保存在令牌中,任何持有密钥的服务都能独立验证,适合多个服务共用登录状态;代价是令牌签发后在过期前一直有效,服务器无法主动让它失效。

无法主动失效是 JWT 最常被追问的问题,常用的解决方法是双令牌:访问令牌(Access Token)的有效期很短,例如 15 分钟,每次请求都携带;刷新令牌(Refresh Token)的有效期较长,服务器记录已签发的刷新令牌,只在访问令牌过期时用它换取新的访问令牌。用户退出或者被封禁时撤销刷新令牌,已经发出的访问令牌最多 15 分钟后自然失效。需要立即失效时,可以把令牌的 ID 放入 Redis 黑名单,过期时间设为令牌的剩余有效期,但这又回到了在服务器保存状态。

令牌在浏览器中的存放位置:放在 localStorage 中,页面上的任何脚本都能读取,一旦存在 XSS 漏洞,令牌就会被盗走;放在 HttpOnly 的 Cookie 中,脚本读不到,但浏览器会自动携带 Cookie,需要防范 [跨站请求伪造](攻击技术.md#二跨站请求伪造)。

需要注意的是,服务器验证时要固定使用的签名算法,不能接受头部中 alg 为 none 的令牌;HMAC 的密钥要随机生成,长度不少于 256 位。

JWT 只是令牌的格式,而 OAuth 2.0 是授权框架,规定第三方应用如何获得访问令牌,这个访问令牌可以用 JWT 表示。

### 缓存

#### 1. 优点
Expand Down Expand Up @@ -920,6 +954,9 @@ DELETE /idX/delete HTTP/1.1 -> Returns 404

## 参考资料

- [RFC 7519: JSON Web Token (JWT)](https://www.rfc-editor.org/rfc/rfc7519)
- [OWASP: JSON Web Token Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/JSON_Web_Token_Cheat_Sheet.html)
- [OWASP: HTML5 Security Cheat Sheet](https://cheatsheetseries.owasp.org/cheatsheets/HTML5_Security_Cheat_Sheet.html)
- 上野宣. 图解 HTTP[M]. 人民邮电出版社, 2014.
- [MDN : HTTP](https://developer.mozilla.org/en-US/docs/Web/HTTP)
- [HTTP/2 简介](https://developers.google.com/web/fundamentals/performance/http2/?hl=zh-cn)
Expand Down
147 changes: 147 additions & 0 deletions notes/Java 并发.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
* [实现接口 VS 继承 Thread](#实现接口-vs-继承-thread)
* [二、基础线程机制](#二基础线程机制)
* [Executor](#executor)
* [ThreadPoolExecutor](#threadpoolexecutor)
* [Daemon](#daemon)
* [sleep()](#sleep)
* [yield()](#yield)
Expand All @@ -24,6 +25,7 @@
* [join()](#join)
* [wait() notify() notifyAll()](#wait-notify-notifyall)
* [await() signal() signalAll()](#await-signal-signalall)
* [交替打印](#交替打印)
* [六、线程状态](#六线程状态)
* [新建(NEW)](#新建new)
* [可运行(RUNNABLE)](#可运行runnable)
Expand Down Expand Up @@ -167,6 +169,78 @@ public static void main(String[] args) {
}
```

### ThreadPoolExecutor

上面的三种 Executor 都是通过 ThreadPoolExecutor 创建的,它的构造函数有以下参数:

- corePoolSize:核心线程数,核心线程空闲时也不会被回收,除非调用了 allowCoreThreadTimeOut(true);
- maximumPoolSize:最大线程数;
- keepAliveTime 和 unit:线程数超过核心线程数时,多出的线程空闲超过这个时间就会被回收;
- workQueue:保存等待执行的任务的阻塞队列;
- threadFactory:创建线程的工厂,常用来给线程起有意义的名字,方便排查问题;
- handler:拒绝策略,线程数达到最大并且队列已满时,用来处理新提交的任务。

提交一个任务时,线程池按以下顺序处理:

1. 线程数小于核心线程数时,新建线程执行任务,即使其它线程是空闲的;
2. 否则把任务放入队列;
3. 队列已满时,如果线程数小于最大线程数,新建线程执行这个任务;
4. 否则执行拒绝策略。

例如核心线程数为 2、最大线程数为 4、队列容量为 2,依次提交 7 个各执行 1 秒的任务:

<div align="center"> <img src="pics/thread-pool.png" width="700px"/> </div><br>

```java
ThreadPoolExecutor pool = new ThreadPoolExecutor(2, 4, 60, TimeUnit.SECONDS,
new ArrayBlockingQueue<>(2), new ThreadPoolExecutor.AbortPolicy());
for (int i = 1; i <= 7; i++) {
try {
pool.execute(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException ignored) {
}
});
System.out.println("提交任务 " + i + ":线程数 " + pool.getPoolSize()
+ ",队列中 " + pool.getQueue().size() + " 个");
} catch (RejectedExecutionException e) {
System.out.println("提交任务 " + i + ":被拒绝");
}
}
```

```text
提交任务 1:线程数 1,队列中 0 个
提交任务 2:线程数 2,队列中 0 个
提交任务 3:线程数 2,队列中 1 个
提交任务 4:线程数 2,队列中 2 个
提交任务 5:线程数 3,队列中 2 个
提交任务 6:线程数 4,队列中 2 个
提交任务 7:被拒绝
```

需要注意的是,任务 5、6 比先提交的任务 3、4 更早执行:队列满后新建的线程直接执行当前提交的任务,队列中的任务要等有线程空闲后才会被取出。

拒绝策略:

- AbortPolicy:默认策略,抛出 RejectedExecutionException;
- CallerRunsPolicy:由提交任务的线程自己执行这个任务,提交方被拖慢,从而降低了提交速度;
- DiscardPolicy:直接丢弃任务;
- DiscardOldestPolicy:丢弃队列中最早的任务,再重新提交当前任务。

不建议用 Executors 创建线程池,而是直接创建 ThreadPoolExecutor 并使用有界队列:

- newFixedThreadPool() 和 newSingleThreadExecutor() 使用没有容量限制的 LinkedBlockingQueue,任务堆积时可能耗尽内存,最大线程数也不再起作用;
- newCachedThreadPool() 的最大线程数为 Integer.MAX_VALUE,任务多时会创建大量线程。

线程数可以按任务类型估算:

- CPU 密集型:取 CPU 核数 + 1,多出的一个线程用于在某个线程因缺页等原因暂停时继续使用 CPU;
- I/O 密集型:线程等待 I/O 时不占用 CPU,可以按 核数 × (1 + 等待时间 / 计算时间) 估算。例如 8 核 CPU,每个任务计算 10 ms、等待数据库 90 ms,约为 8 × (1 + 90 / 10) = 80 个线程。

估算值只是起点,最终要通过压测确定。上线后监控活跃线程数、队列长度和拒绝次数,必要时调用 setCorePoolSize()、setMaximumPoolSize() 在运行时调整。

### Daemon

守护线程是程序运行时在后台提供服务的线程,不属于程序中不可或缺的部分。
Expand Down Expand Up @@ -678,6 +752,76 @@ before
after
```

### 交替打印

多个线程按固定顺序轮流执行是常见的手写题,例如三个线程依次打印 A、B、C,共打印 10 轮。思路是用一个共享变量记录轮到哪个线程:没轮到的线程等待,轮到的线程打印后把轮次交给下一个线程,并唤醒等待的线程。

```java
public class PrintABC {
private final Object lock = new Object();
private int turn = 0; // 轮到第几个线程:0、1、2

public void print(int id, char c, int times) throws InterruptedException {
for (int i = 0; i < times; i++) {
synchronized (lock) {
while (turn != id) {
lock.wait();
}
System.out.print(c);
turn = (turn + 1) % 3;
lock.notifyAll();
}
}
}

public static void main(String[] args) {
PrintABC p = new PrintABC();
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
try {
p.print(id, (char) ('A' + id), 10);
} catch (InterruptedException ignored) {
}
}).start();
}
}
}
```

```text
ABCABCABCABCABCABCABCABCABCABC
```

有两处容易写错:

- 等待条件要用 while 而不是 if:线程被唤醒时不一定轮到了自己,例如和其它线程一起被 notifyAll() 唤醒,或者发生了虚假唤醒,醒来后必须重新检查条件;
- 要用 notifyAll() 而不是 notify():notify() 只唤醒任意一个等待的线程,例如 B 打印完后唤醒的是 A,A 发现没轮到自己又继续等待,C 就再也不会被唤醒,三个线程都在等待。

把上面代码中的 notifyAll() 改成 notify() 后运行 100 次,每次都在打印出几个字母后卡住;把 while 改成 if 后,输出变成了 ABAABAAB 这样的乱序。

用 Semaphore 实现更直观:每个线程对应一个信号量,只有第一个线程的许可为 1。线程打印前获取自己的许可,打印后释放下一个线程的许可,就像传递接力棒:

```java
Semaphore[] s = {new Semaphore(1), new Semaphore(0), new Semaphore(0)};
for (int i = 0; i < 3; i++) {
int id = i;
new Thread(() -> {
for (int k = 0; k < 10; k++) {
try {
s[id].acquire();
} catch (InterruptedException e) {
return;
}
System.out.print((char) ('A' + id));
s[(id + 1) % 3].release();
}
}).start();
}
```

两个线程交替打印奇偶数也是同样的做法,把轮次改为两个即可。使用 ReentrantLock 时,可以为每个线程创建一个 Condition,打印后只唤醒下一个线程。

## 六、线程状态

一个线程只能处于一种状态,并且这里的线程状态特指 Java 虚拟机的线程状态,不能反映线程在特定操作系统下的状态。
Expand Down Expand Up @@ -1621,6 +1765,9 @@ JDK 1.6 引入了偏向锁和轻量级锁,从而让锁拥有了四个状态:

## 参考资料

- [Object (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/lang/Object.html)
- Brian Goetz. Java 并发编程实战 [M]. 机械工业出版社, 2012.
- [ThreadPoolExecutor (Java SE 21)](https://docs.oracle.com/en/java/javase/21/docs/api/java.base/java/util/concurrent/ThreadPoolExecutor.html)
- BruceEckel. Java 编程思想: 第 4 版 [M]. 机械工业出版社, 2007.
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Threads and Locks](https://docs.oracle.com/javase/specs/jvms/se6/html/Threads.doc.html)
Expand Down
84 changes: 84 additions & 0 deletions notes/Java 虚拟机.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,10 @@
* [类加载器分类](#类加载器分类)
* [双亲委派模型](#双亲委派模型)
* [自定义类加载器实现](#自定义类加载器实现)
* [五、线上问题排查](#五线上问题排查)
* [内存溢出与内存泄漏](#内存溢出与内存泄漏)
* [CPU 占用过高](#cpu-占用过高)
* [死锁](#死锁)
* [参考资料](#参考资料)
<!-- GFM-TOC -->

Expand Down Expand Up @@ -738,8 +742,88 @@ public class FileSystemClassLoader extends ClassLoader {
}
```

## 五、线上问题排查

### 内存溢出与内存泄漏

OutOfMemoryError 的详细信息指明了是哪一块内存不足,常见的有:

- Java heap space:堆中无法再分配对象,可能是堆设置得太小,也可能是内存泄漏;
- GC overhead limit exceeded:GC 占用了约 98% 的时间,却只回收了不到 2% 的堆,说明存活的对象几乎占满了堆;
- Metaspace:加载的类太多,元空间超过了 MaxMetaspaceSize,常见于运行时动态生成大量的类。

内存泄漏指的是对象已经不再使用,但仍然被引用着,GC 无法回收,堆内存持续增长,最终导致内存溢出。常见原因:

- 静态集合只添加不清理,例如用 static Map 做缓存却没有淘汰策略;
- 线程池中的线程使用 ThreadLocal 后没有调用 remove(),线程一直存活,值也一直无法被回收;
- 连接、流等资源没有关闭;
- 注册的监听器、回调没有注销。

排查步骤:

1. 启动参数加上 -XX:+HeapDumpOnOutOfMemoryError,发生内存溢出时自动保存堆转储,用 -XX:HeapDumpPath 指定保存位置;进程还在运行时,可以用 jcmd <pid> GC.heap_dump <文件名> 手动导出;
2. 用 Eclipse MAT 等工具打开堆转储,找到占用内存最多的对象,查看它们到 GC Roots 的引用链,确定是谁持有了这些对象;
3. 引用链指向不应该存活的对象(例如上面的静态集合)就是内存泄漏,需要修改代码;否则说明确实需要这么多内存,应该增大堆或者减少一次加载的数据量。

例如下面的程序不断向静态列表中添加 1 MB 的数组,静态变量引用的对象属于 GC Roots,这些数组都无法被回收:

```java
public class Leak {
static final List<byte[]> CACHE = new ArrayList<>();

public static void main(String[] args) {
while (true) {
CACHE.add(new byte[1024 * 1024]);
}
}
}
```

```text
$ java -Xmx32m -XX:+HeapDumpOnOutOfMemoryError Leak
java.lang.OutOfMemoryError: Java heap space
Dumping heap to java_pid24607.hprof ...
Heap dump file created [19154891 bytes in 0.007 secs]
```

### CPU 占用过高

1. 用 top 找到 CPU 占用高的 Java 进程;
2. 用 top -H -p <pid> 查看这个进程中每个线程的 CPU 占用,记下占用最高的线程 ID;
3. 用 jstack <pid> 或者 jcmd <pid> Thread.print 导出所有线程的调用栈,按线程 ID 找到对应的线程;
4. 查看这个线程正在执行的代码,常见原因有死循环、频繁 GC(这时占用 CPU 的是 GC 线程)。

调用栈中的 nid 就是操作系统中的线程 ID。JDK 18 起 nid 以十进制显示,可以直接和 top 中的线程 ID 对应;更早的版本以 0x 开头的十六进制显示,需要先用 printf '%x\n' <线程 ID> 转换。JDK 11 起每个线程还带有累计占用的 CPU 时间 cpu=。例如一个执行死循环的线程:

```text
"busy-worker" #20 [26883] prio=5 os_prio=31 cpu=2148.39ms elapsed=2.15s tid=0x000000012f811200 nid=26883 runnable [0x000000016dfda000]
java.lang.Thread.State: RUNNABLE
at Busy.lambda$main$0(Busy.java:3)
```

### 死锁

jstack 会自动检测死锁,在输出的末尾给出互相等待的线程,以及每个线程持有和等待的锁:

```text
Found one Java-level deadlock:
=============================
"t1":
waiting to lock monitor 0x0000000140e63bb0 (object 0x000000061fc144c8, a java.lang.Object),
which is held by "t2"

"t2":
waiting to lock monitor 0x0000000141818190 (object 0x000000061fc144b8, a java.lang.Object),
which is held by "t1"
```

后面还会列出这两个线程的调用栈,指出它们在哪一行获取锁。避免死锁的方法见 [计算机操作系统 - 死锁](计算机操作系统%20-%20死锁.md)。

## 参考资料

- [Troubleshooting Memory Leaks (Java SE 21)](https://docs.oracle.com/en/java/javase/21/troubleshoot/troubleshooting-memory-leaks.html)
- [JDK-8268425: Show decimal nid of OSThread instead of hex format one](https://bugs.openjdk.org/browse/JDK-8268425)
- [JDK-8200720: Print additional information in thread dump](https://bugs.openjdk.org/browse/JDK-8200720)
- 周志明. 深入理解 Java 虚拟机 [M]. 机械工业出版社, 2011.
- [Chapter 2. The Structure of the Java Virtual Machine](https://docs.oracle.com/javase/specs/jvms/se8/html/jvms-2.html#jvms-2.5.4)
- [Jvm memory](https://www.slideshare.net/benewu/jvm-memory)
Expand Down
Loading
Loading