8.21学会

1.实现单例模式 (Singleton Pattern)

懒汉式:

使用双重检查锁定 (DCL) 实现线程安全的懒汉式单例模式。这种模式在第一次调用时才创建实例,并且通过两次检查和同步块确保了线程安全和性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Singleton {
// 1. volatile 关键字确保多线程环境下,instance 变量的修改能够立即被其他线程看到
private static volatile Singleton instance;

// 2. 私有化构造器,防止外部直接 new 实例
private Singleton() {}

// 3. 提供一个全局访问点,使用 DCL 确保线程安全
public static Singleton getInstance() {
// 第一次检查:如果实例已经存在,直接返回,避免不必要的同步
if (instance == null) {
// 第一次检查失败,进入同步块
synchronized (Singleton.class) {
// 第二次检查:在同步块内部再次检查,防止多个线程同时通过第一次检查,
// 导致创建多个实例
if (instance == null) {
// 创建实例
instance = new Singleton();
}
}
}
return instance;
}

// 示例方法
public void showMessage() {
System.out.println("这是一个线程安全的懒汉式单例模式实例。");
}
}

饿汉式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test1;

class SingletonEager {
// 1. 在类加载时就创建静态实例
private static final SingletonEager INSTANCE = new SingletonEager();

// 2. 私有化构造器,防止外部直接创建实例
private SingletonEager() {}

// 3. 提供一个公共的静态方法来获取唯一实例
public static SingletonEager getInstance() {
return INSTANCE;
}

public void showMessage() {
System.out.println("这是一个线程安全的饿汉式单例模式实例。");
}
}

2.继承与多态 (Inheritance and Polymorphism)

设计一个 Animal 抽象类,并创建 DogCat 类来展示继承和多态。=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1. Animal 抽象类,定义通用行为
abstract class Animal {
// 抽象方法,子类必须实现
public abstract void eat();
}

// 2. Dog 类继承 Animal,并重写 eat() 方法
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗正在吃骨头。");
}
}

// 3. Cat 类继承 Animal,并重写 eat() 方法
class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫正在吃鱼。");
}
}

// 4. 展示多态的类
class PolymorphismDemo {
public static void showPolymorphism() {
// 父类引用指向子类对象,这就是多态
Animal myDog = new Dog();
Animal myCat = new Cat();

// 调用相同的方法,但由于指向不同的子类实例,执行不同的行为
myDog.eat(); // 输出: 狗正在吃骨头。
myCat.eat(); // 输出: 猫正在吃鱼。
}
}

equals()hashCode()

编写 Student 类,并重写 equals()hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.Objects;

class Student {
private int id;
private String name;

public Student(int id, String name) {
this.id = id;
this.name = name;
}

// 重写 equals() 方法,根据 id 和 name 判断两个 Student 对象是否相等
@Override
public boolean equals(Object o) {
// 1. 检查是否为同一个对象的引用
if (this == o) return true;
// 2. 检查对象是否为 null 或类型是否匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
Student student = (Student) o;
// 4. 比较关键字段 (id 和 name)
return id == student.id &&
Objects.equals(name, student.name);
}

// 重写 hashCode() 方法,为相等的对象生成相同的哈希码
@Override
public int hashCode() {
// 使用 Objects.hash() 方法生成哈希码,它会为多个字段生成一个组合哈希值
return Objects.hash(id, name);
}
}

/*
* 为什么需要两者一起重写?
* 1. 它们之间存在约定:如果两个对象通过 equals() 方法比较是相等的,那么它们的 hashCode() 方法返回的值也必须相等。
* 反之则不一定,不相等的对象可以有相同的哈希码(哈希冲突)。
* 2. 哈希表类(如 HashSet, HashMap)依赖于这个约定:当将对象存入哈希表时,
* 它会先调用 hashCode() 确定存储位置,再调用 equals() 来确认是否存在相同的对象。
* 3. 如果只重写 equals() 但不重写 hashCode(),可能导致两个逻辑上相等的对象被存储在不同的哈希位置,
* 从而无法正确查找和去重。例如,在 HashSet 中,即使两个 Student 对象 id 和 name 相同,
* 也会被认为是不同的对象而重复添加。
*/

接口与实现 (Interfaces and Implementation)

设计 Drawable 接口,并由 CircleRectangle 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 定义 Drawable 接口,包含一个抽象方法
interface Drawable {
void draw();
}

// 2. Circle 类实现 Drawable 接口
class Circle implements Drawable {
@Override
public void draw() {
System.out.println("正在画一个圆形。");
}
}

// 3. Rectangle 类实现 Drawable 接口
class Rectangle implements Drawable {
@Override
public void draw() {
System.out.println("正在画一个矩形。");
}
}

异常处理 (Exception Handling)

使用 try-catch-finally 结构处理 FileNotFoundException 并确保资源关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class ExceptionDemo {
public static void processFile(String filePath) {
FileInputStream fileInputStream = null; // 在 try 块外部声明,以便在 finally 块中访问
try {
// 尝试打开文件
fileInputStream = new FileInputStream(filePath);
System.out.println("文件已成功打开。");
// 假设这里进行文件读取操作
} catch (FileNotFoundException e) {
// 捕获文件未找到异常
System.err.println("错误:指定的文件不存在!路径:" + filePath);
e.printStackTrace(); // 打印异常堆栈信息
} finally {
// 无论是否发生异常,finally 块都会执行
if (fileInputStream != null) {
try {
// 确保文件流被关闭,防止资源泄露
fileInputStream.close();
System.out.println("文件流已在 finally 块中关闭。");
} catch (IOException e) {
System.err.println("关闭文件流时发生异常:" + e.getMessage());
}
}
}
}
}

try-with-resources

使用 try-with-resources 重写上一个题目,展示其简化优势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class TryWithResourcesDemo {
public static void processFileWithResources(String filePath) {
// try-with-resources 语句,自动管理实现了 AutoCloseable 接口的资源
try (FileInputStream fileInputStream = new FileInputStream(filePath)) {
System.out.println("文件已成功打开。");
// 假设这里进行文件读取操作
} catch (FileNotFoundException e) {
// 捕获文件未找到异常
System.err.println("错误:指定的文件不存在!路径:" + filePath);
e.printStackTrace();
} catch (IOException e) {
// 捕获文件关闭时可能发生的异常
System.err.println("关闭文件流时发生异常:" + e.getMessage());
}
// 不需要单独的 finally 块来关闭资源,JVM 会自动完成
System.out.println("文件流已在 try-with-resources 语句中自动关闭。");
}
}

/*
* try-with-resources 的优势:
* 1. 简化代码:不再需要显式的 finally 块来关闭资源,代码更简洁。
* 2. 避免资源泄露:无论 try 块是否正常完成或抛出异常,资源都会被自动关闭,
* 有效防止了因忘记关闭资源而导致的内存和文件句柄泄露。
* 3. 更好的异常处理:如果 try 块和资源关闭时都抛出异常,try-with-resources 会
* 将资源关闭时抛出的异常作为被抑制(suppressed)的异常,主异常保持不变,
* 便于调试。
*/

线程创建

1. 继承 Thread

通过继承 java.lang.Thread 类并重写其 run() 方法来创建线程。

1
2
3
4
5
6
7
8
// 继承 Thread 类创建线程
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务
System.out.println("使用继承 Thread 类的方式创建的线程正在运行。");
}
}

2. 实现 Runnable 接口

通过实现 java.lang.Runnable 接口并将其作为参数传递给 Thread 类的构造函数来创建线程。这种方式更灵活,推荐使用。

1
2
3
4
5
6
7
8
// 实现 Runnable 接口创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的任务
System.out.println("使用实现 Runnable 接口的方式创建的线程正在运行。");
}
}

sleep()yield()

sleep()yield() 都是线程调度的方法,但它们的作用和效果不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SleepYieldDemo {
public static void main(String[] args) {
// 使用 sleep() 的线程
new Thread(() -> {
System.out.println("线程 A: 开始执行...");
try {
// sleep() 使线程暂停指定时间,进入 WAITING 状态,但不会释放锁
System.out.println("线程 A: 准备睡眠 2 秒...");
Thread.sleep(2000);
System.out.println("线程 A: 睡眠结束,继续执行。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

// 使用 yield() 的线程
new Thread(() -> {
System.out.println("线程 B: 开始执行...");
for (int i = 0; i < 5; i++) {
System.out.println("线程 B: 正在执行 " + i);
// yield() 提示线程调度器让出 CPU 时间,但不保证一定生效
Thread.yield();
}
System.out.println("线程 B: 执行完毕。");
}).start();
}
}
  • sleep(): 使当前线程暂停执行指定的时间,进入 TIMED_WAITING 状态。它会释放 CPU 资源,但不释放锁
  • yield(): 提示线程调度器,当前线程愿意让出当前 CPU 时间片。线程会从 RUNNING 状态转换为 RUNNABLE 状态,与其他线程竞争 CPU,但不保证其他线程能立即获得执行。它主要用于优化线程调度,通常在多线程程序中不应依赖其行为来保证正确性。

join() 方法

join() 方法允许一个线程等待另一个线程执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(() -> {
try {
System.out.println("子线程: 正在执行...");
Thread.sleep(3000); // 模拟耗时操作
System.out.println("子线程: 执行完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

workerThread.start();
System.out.println("主线程: 等待子线程执行完毕...");
// 调用 join() 方法,主线程进入等待状态,直到 workerThread 执行完毕
workerThread.join();
System.out.println("主线程: 子线程已执行完毕,主线程继续执行。");
}
}

非线程安全计数器

在多线程环境下,多个线程同时对共享资源进行读写操作,可能导致数据不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class UnsafeCounter {
private int count = 0;

public void increment() {
count++; // 这一行代码并非原子操作,它包含三个步骤:读、加 1、写
}

public int getCount() {
return count;
}
}

class ThreadSafetyDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果不一定是 100 * 10000 = 1000000
System.out.println("非线程安全计数器的最终结果: " + counter.getCount());
}
}

synchronized 关键字

使用 synchronized 关键字可以保证同一时刻只有一个线程访问共享资源,从而解决线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SynchronizedCounter {
private int count = 0;

// 使用 synchronized 关键字修饰方法,锁住整个对象实例
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果为 1000000
System.out.println("synchronized 计数器的最终结果: " + counter.getCount());
}
}

synchronized 块与方法

  • synchronized 方法: 锁定的是当前对象实例 (this)。
  • synchronized 块: 提供了更细粒度的控制,可以指定锁定的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class SynchronizedBlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();

// synchronized 方法,锁定当前对象实例
public synchronized void synchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " 进入 synchronized 方法。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开 synchronized 方法。");
}

// synchronized 块,锁定指定的对象
public void synchronizedBlock1() {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock1...");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 已获取 lock1。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 释放 lock1。");
}
}

public void synchronizedBlock2() {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock2...");
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 已获取 lock2。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 释放 lock2。");
}
}
}

/*
* 区别与适用场景:
* - synchronized 方法: 简单易用,但锁定范围大,可能导致性能问题。当需要同步整个方法时使用。
* - synchronized 块: 锁定范围小,可以只同步需要保护的代码段,提高并发性能。当只需要同步部分代码时使用。
* 同时,可以通过锁定不同的对象来避免不必要的阻塞,实现更高的并发度。
*/

volatile 关键字

volatile 关键字保证了变量在多线程间的可见性,但不保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class VolatileDemo {
// volatile 保证所有线程看到的是该变量的最新值
private static volatile boolean ready = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("线程 A: 正在等待标志位变为 true。");
while (!ready) {
// 空循环,等待 ready 变为 true
}
System.out.println("线程 A: 标志位已变为 true,循环结束。");
}).start();

Thread.sleep(1000); // 确保线程 A 先运行
System.out.println("主线程: 正在将标志位设置为 true。");
ready = true;
}
}

/*
* 为什么 volatile 不能保证原子性?
* - 原子性是指一个操作是不可中断的,要么全部执行,要么都不执行。
* - volatile 只能保证变量的读写操作是原子的,但像 `count++` 这样的复合操作(读、加、写)
* 依然不是原子的。多个线程可能同时读到旧值,导致最终结果不正确。
* - volatile 主要用于一写多读的场景,或者用于控制线程执行流程的标志位。
*/

AtomicInteger

AtomicInteger 是一个原子类,它使用 CAS (Compare-and-Swap) 机制来保证操作的原子性,从而实现线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
// 使用 AtomicInteger 替代 int
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet(); // 这是一个原子操作
}

public int getCount() {
return count.get();
}
}

class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果为 1000000
System.out.println("AtomicInteger 计数器的最终结果: " + counter.getCount());
}
}

线程通信

使用 wait()notifyAll() 实现两个线程交替打印奇数和偶数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class PrintNumbers {
private int count = 0;
private final Object lock = new Object();

public void printOdd() {
synchronized (lock) {
while (count < 10) {
while (count % 2 == 0) { // 如果是偶数,则等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notifyAll(); // 唤醒其他等待的线程
}
}
}

public void printEven() {
synchronized (lock) {
while (count < 10) {
while (count % 2 != 0) { // 如果是奇数,则等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notifyAll(); // 唤醒其他等待的线程
}
}
}

public static void main(String[] args) {
PrintNumbers pn = new PrintNumbers();
Thread oddThread = new Thread(pn::printOdd, "奇数线程");
Thread evenThread = new Thread(pn::printEven, "偶数线程");

oddThread.start();
evenThread.start();
}
}

好的,以下是 中高级并发编程的实现,并附带详细注释。

生产者-消费者模式 (使用 wait()notifyAll())

这是一个经典的线程协作问题。生产者生产数据,放入共享队列;消费者从队列中取出数据进行消费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.LinkedList;
import java.util.Queue;

class ProducerConsumerClassic {
// 共享队列,使用 LinkedList 实现
private final Queue<Integer> queue = new LinkedList<>();
// 队列最大容量
private final int MAX_SIZE = 5;
private final Object LOCK = new Object();

public void produce() throws InterruptedException {
int i = 0;
while (true) {
synchronized (LOCK) {
// 如果队列已满,生产者等待
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
LOCK.wait();
}
// 生产数据并放入队列
queue.offer(i);
System.out.println("生产者生产了: " + i);
i++;
// 唤醒所有等待的线程(包括消费者)
LOCK.notifyAll();
}
Thread.sleep(100); // 模拟生产时间
}
}

public void consume() throws InterruptedException {
while (true) {
synchronized (LOCK) {
// 如果队列为空,消费者等待
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
LOCK.wait();
}
// 消费数据
int data = queue.poll();
System.out.println("消费者消费了: " + data);
// 唤醒所有等待的线程(包括生产者)
LOCK.notifyAll();
}
Thread.sleep(500); // 模拟消费时间
}
}
}

生产者-消费者模式 (使用 BlockingQueue)

java.util.concurrent.BlockingQueue 接口提供了线程安全的队列操作,简化了生产者-消费者模型的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class ProducerConsumerBlockingQueue {
// 使用 BlockingQueue,它内部已经处理了线程同步
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

public void produce() throws InterruptedException {
int i = 0;
while (true) {
// put() 方法在队列满时会自动阻塞
queue.put(i);
System.out.println("生产者生产了: " + i);
i++;
Thread.sleep(100);
}
}

public void consume() throws InterruptedException {
while (true) {
// take() 方法在队列空时会自动阻塞
int data = queue.take();
System.out.println("消费者消费了: " + data);
Thread.sleep(500);
}
}
}

ReentrantLock

使用 ReentrantLock 实现线程安全计数器,并解释其与 synchronized 的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock(); // 获取锁
try {
count++; // 在 try 块中执行需要同步的代码
} finally {
lock.unlock(); // 在 finally 块中释放锁,确保锁总是被释放
}
}

public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}

/*
* ReentrantLock 与 synchronized 的区别:
* 1. 语法层面: `synchronized` 是 JVM 的内置关键字,而 `ReentrantLock` 是一个类。
* 2. 灵活性: `ReentrantLock` 提供了更灵活的锁定控制。例如,它支持公平锁(按请求顺序获取锁),
* 可以尝试非阻塞地获取锁(`tryLock()`),以及支持中断(`lockInterruptibly()`)。
* 3. 性能: 在早期版本中,`ReentrantLock` 通常性能更好。但随着 对 `synchronized` 优化(偏向锁、轻量级锁),
* 两者性能已非常接近。在简单场景下,`synchronized` 更简洁。
* 4. 协作: `ReentrantLock` 必须配合 `Condition` 接口才能实现线程间的等待/唤醒机制,而 `synchronized` 直接
* 使用 `Object` 的 `wait()` 和 `notify()`。
*/

ReentrantLockCondition

使用 ReentrantLockCondition 重新实现生产者-消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class ProducerConsumerCondition {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final ReentrantLock lock = new ReentrantLock();
// 创建两个 Condition 实例,分别用于生产者和消费者
private final Condition producerCondition = lock.newCondition();
private final Condition consumerCondition = lock.newCondition();

public void produce() throws InterruptedException {
int i = 0;
while (true) {
lock.lock();
try {
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
producerCondition.await(); // 生产者等待
}
queue.offer(i);
System.out.println("生产者生产了: " + i);
i++;
consumerCondition.signalAll(); // 唤醒所有消费者
} finally {
lock.unlock();
}
Thread.sleep(100);
}
}

public void consume() throws InterruptedException {
while (true) {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
consumerCondition.await(); // 消费者等待
}
int data = queue.poll();
System.out.println("消费者消费了: " + data);
producerCondition.signalAll(); // 唤醒所有生产者
} finally {
lock.unlock();
}
Thread.sleep(500);
}
}
}

ExecutorService

创建一个固定大小的线程池,并向其提交多个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ExecutorServiceDemo {
public static void main(String[] args) {
// 创建一个固定大小为 3 的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);

for (int i = 0; i < 10; i++) {
final int taskId = i;
executorService.execute(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行。");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池,不再接收新任务,已提交的任务会继续执行
executorService.shutdown();
}
}

CallableFuture

使用 Callable 提交任务,并使用 Future 获取返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.*;

class CallableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 创建一个 Callable 任务,它会返回一个字符串
Callable<String> task = () -> {
System.out.println("任务开始执行...");
Thread.sleep(2000); // 模拟耗时操作
return "任务执行完毕,返回结果!";
};

// 提交任务并获得 Future 对象
Future<String> future = executorService.submit(task);

System.out.println("主线程: 任务已提交,继续执行其他操作...");
// get() 方法会阻塞,直到任务完成并返回结果
String result = future.get();
System.out.println("主线程: 获得任务结果 -> " + result);

executorService.shutdown();
}
}

CountDownLatch

CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 5;
// 计数器,当计数器减到 0 时,主线程会被唤醒
CountDownLatch latch = new CountDownLatch(workerCount);
ExecutorService executor = Executors.newFixedThreadPool(workerCount);

for (int i = 0; i < workerCount; i++) {
final int workerId = i;
executor.execute(() -> {
System.out.println("工作线程 " + workerId + " 开始执行任务...");
try {
Thread.sleep((long) (Math.random() * 2000)); // 模拟任务执行
System.out.println("工作线程 " + workerId + " 任务完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 任务完成后,计数器减 1
}
});
}

System.out.println("主线程: 等待所有工作线程完成...");
latch.await(); // 阻塞主线程,直到计数器为 0
System.out.println("主线程: 所有工作线程已完成,继续执行下一步。");

executor.shutdown();
}
}

CyclicBarrier

CyclicBarrier 允许一组线程相互等待,直到所有线程都到达一个共同的屏障点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CyclicBarrierDemo {
public static void main(String[] args) {
int partySize = 3;
// 当 3 个线程都到达屏障时,执行一个屏障动作
CyclicBarrier barrier = new CyclicBarrier(partySize, () -> {
System.out.println("\n所有线程已到达屏障点!继续执行下一阶段。\n");
});
ExecutorService executor = Executors.newFixedThreadPool(partySize);

for (int i = 0; i < partySize; i++) {
final int threadId = i;
executor.execute(() -> {
try {
System.out.println("线程 " + threadId + " 正在执行第一阶段任务...");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + threadId + " 第一阶段任务完成,到达屏障。");
barrier.await(); // 线程在此处等待

System.out.println("线程 " + threadId + " 正在执行第二阶段任务...");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}

Semaphore

Semaphore(信号量)用来控制对资源的并发访问数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class SemaphoreDemo {
// 允许 3 个线程同时访问
private final Semaphore semaphore = new Semaphore(3);

public void accessResource(int threadId) {
try {
semaphore.acquire(); // 获取许可
System.out.println("线程 " + threadId + " 正在访问资源...");
Thread.sleep(2000); // 模拟资源访问时间
System.out.println("线程 " + threadId + " 访问资源完毕。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}

public static void main(String[] args) {
SemaphoreDemo demo = new SemaphoreDemo();
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.execute(() -> demo.accessResource(threadId));
}

executor.shutdown();
}
}

线程死锁

1. 死锁演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public void threadA() {
synchronized (lock1) {
System.out.println("线程A: 已获得 lock1,尝试获取 lock2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("线程A: 已获得 lock2。");
}
}
}

public void threadB() {
synchronized (lock2) {
System.out.println("线程B: 已获得 lock2,尝试获取 lock1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("线程B: 已获得 lock1。");
}
}
}

public static void main(String[] args) {
DeadlockDemo demo = new DeadlockDemo();
new Thread(demo::threadA, "Thread-A").start();
new Thread(demo::threadB, "Thread-B").start();
}
}

/*
* 死锁产生条件:
* 1. 互斥条件: 资源是独占的,一个线程在使用时,其他线程无法使用。
* 2. 请求与保持条件: 线程已经持有至少一个资源,但又请求其他资源,同时又保持对已有资源的占有。
* 3. 不可剥夺条件: 资源只能在持有它的线程完成任务后由它自己释放。
* 4. 循环等待条件: 存在一个线程资源的循环链,每个线程都在等待下一个线程所持有的资源。
*/

死锁预防

通过打破死锁的四个必要条件之一来预防死锁。这里通过资源有序分配来打破循环等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public void threadA() {
// 线程 A 按顺序先获取 lock1,再获取 lock2
synchronized (lock1) {
System.out.println("线程A: 已获得 lock1,尝试获取 lock2...");
synchronized (lock2) {
System.out.println("线程A: 已获得 lock2。");
}
}
}

public void threadB() {
// 线程 B 也按顺序先获取 lock1,再获取 lock2
synchronized (lock1) {
System.out.println("线程B: 已获得 lock1,尝试获取 lock2...");
synchronized (lock2) {
System.out.println("线程B: 已获得 lock2。");
}
}
}

public static void main(String[] args) {
DeadlockPrevention demo = new DeadlockPrevention();
new Thread(demo::threadA, "Thread-A").start();
new Thread(demo::threadB, "Thread-B").start();
// 两个线程都按相同的顺序获取锁,不会发生死锁
}
}

ThreadLocal

ThreadLocal 为每个线程提供了一个独立的变量副本,实现了线程间的数据隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public void setAndPrint(String value) {
// 设置线程本地变量
threadLocal.set(value);
try {
Thread.sleep(1000); // 模拟任务
// 获取并打印当前线程的变量值
System.out.println(Thread.currentThread().getName() + " 的变量值: " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 最佳实践:使用完后移除,防止内存泄漏
threadLocal.remove();
}
}

public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
new Thread(() -> demo.setAndPrint("线程 A 的数据"), "Thread-A").start();
new Thread(() -> demo.setAndPrint("线程 B 的数据"), "Thread-B").start();
}
}

/*
* 原理和作用:
* 原理: ThreadLocal 内部有一个 `ThreadLocalMap`,每个线程都有一个独立的 `ThreadLocalMap`。
* 当我们调用 `set()` 方法时,实际上是将值存储到了当前线程的 `ThreadLocalMap` 中,
* 键为 `ThreadLocal` 实例本身。
* 作用:
* 1. 数据隔离: 解决了多线程访问共享变量的线程安全问题,但其本质不是同步,而是通过“以空间换时间”的方式,
* 为每个线程提供独立副本,避免了竞争。
* 2. 传递参数: 在整个方法调用链中,无需层层传递参数,可以方便地在任何地方获取当前线程的上下文信息。
*/

线程安全单例 (静态内部类)

静态内部类方式是实现线程安全的懒汉式单例的最佳实践之一。它利用了 JVM 类加载机制的线程安全特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SingletonThreadSafe {
// 私有化构造器
private SingletonThreadSafe() {}

// 静态内部类,它只在 SingletonThreadSafe 被首次调用时才会被加载
private static class SingletonHolder {
// 静态成员变量,在类加载时初始化
private static final SingletonThreadSafe INSTANCE = new SingletonThreadSafe();
}

// 提供全局访问点
public static SingletonThreadSafe getInstance() {
return SingletonHolder.INSTANCE;
}
}

/*
* 优点:
* 1. 线程安全: 类的加载是线程安全的,因此 INSTANCE 的初始化是原子的。
* 2. 懒加载: 只有在调用 getInstance() 方法时,SingletonHolder 类才会被加载,
* 从而实现懒加载。
*/

ReadWriteLock

ReadWriteLock 适用于读多写少的场景,它允许多个线程同时进行读操作,但写操作必须是互斥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteLockDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Object sharedData = "初始数据";

public String readData() {
rwLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
Thread.sleep(1000);
return (String) sharedData;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}

public void writeData(String newData) {
rwLock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
Thread.sleep(2000);
// sharedData = newData; // 实际更新数据
System.out.println(Thread.currentThread().getName() + " 写入完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
}

线程池关闭

ExecutorService 的两种关闭方式:shutdown()shutdownNow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ShutdownDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交 5 个任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.execute(() -> {
try {
System.out.println("任务 " + taskId + " 正在执行...");
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("任务 " + taskId + " 被中断。");
}
});
}

// shutdown() vs shutdownNow()
// executor.shutdown(); // 优雅关闭
executor.shutdownNow(); // 暴力关闭

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程池已关闭。");
}
}

/*
* 区别:
* - shutdown(): 优雅关闭。不再接受新的任务,但会等待已提交的任务(包括正在执行和在队列中的)全部执行完毕。
* 执行后,isShutdown() 返回 true,isTerminated() 返回 false,直到所有任务完成。
* - shutdownNow(): 暴力关闭。立即停止所有正在执行的任务,并返回在队列中等待执行的任务列表。
* 它会向所有线程发送 interrupt() 中断信号。
* 执行后,isShutdown() 返回 true,isTerminated() 立即返回 true。
*/

中断机制

一个线程通过响应 interrupt() 调用来正确停止自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class InterruptedThreadDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 检查中断标志
try {
System.out.println("线程正在执行...");
// sleep()、wait() 等方法会抛出 InterruptedException 并清除中断标志
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程被中断!");
// 重新设置中断标志,以便外层循环能正确退出
Thread.currentThread().interrupt();
// 或者直接 return 退出
return;
}
}
System.out.println("线程已退出。");
});

worker.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发送中断信号
worker.interrupt();
}
}

死锁预防 (按锁顺序)

通过资源有序分配来打破死锁的循环等待条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DeadlockPrevention {
private static final Object lockA = new Object();
private static final Object lockB = new Object();

public void threadOne() {
synchronized (lockA) {
System.out.println("线程 1: 已获得 lockA,尝试获取 lockB...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lockB) {
System.out.println("线程 1: 已获得 lockB。");
}
}
}

public void threadTwo() {
// 两个线程都按相同的顺序(先 A 后 B)获取锁
synchronized (lockA) {
System.out.println("线程 2: 已获得 lockA,尝试获取 lockB...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lockB) {
System.out.println("线程 2: 已获得 lockB。");
}
}
}

public static void main(String[] args) {
DeadlockPrevention demo = new DeadlockPrevention();
new Thread(demo::threadOne, "Thread-1").start();
new Thread(demo::threadTwo, "Thread-2").start();
}
}

线程池异常

使用 Future.get() 或在任务中捕获异常来处理线程池中任务抛出的异常。execute() 方法无法直接捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

class ThreadPoolExceptionDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交一个会抛出运行时异常的任务
Future<?> future = executor.submit(() -> {
System.out.println("任务开始执行...");
throw new RuntimeException("这是一个模拟的任务执行异常。");
});

// 尝试获取任务结果来捕获异常
try {
future.get();
} catch (ExecutionException e) {
System.err.println("捕获到任务执行异常:" + e.getCause().getMessage());
}

// 使用 execute() 提交任务,异常会被吞掉,除非自定义 UncaughtExceptionHandler
executor.execute(() -> {
System.out.println("另一个任务开始执行...");
throw new IllegalArgumentException("这个异常会被吞掉。");
});

executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
}

ThreadLocal

ThreadLocal 为每个线程提供了独立的变量副本,实现了数据隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public void setAndPrint(String value) {
threadLocal.set(value); // 设置当前线程的变量副本
try {
Thread.sleep(1000); // 模拟任务
System.out.println(Thread.currentThread().getName() + " 的变量值: " + threadLocal.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
threadLocal.remove(); // 最佳实践:使用完后移除,防止内存泄漏
}
}

public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
new Thread(() -> demo.setAndPrint("线程 A 的数据"), "Thread-A").start();
new Thread(() -> demo.setAndPrint("线程 B 的数据"), "Thread-B").start();
}
}

LockSupport

LockSupport.park()LockSupport.unpark() 提供了更灵活的线程阻塞和唤醒机制,类似于 wait()notify(),但不需要依赖锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.locks.LockSupport;

class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(() -> {
System.out.println("工作线程: 任务准备就绪,即将阻塞...");
LockSupport.park(); // 阻塞当前线程
System.out.println("工作线程: 被唤醒,继续执行。");
});

workerThread.start();
Thread.sleep(2000); // 确保工作线程已执行 park()

System.out.println("主线程: 唤醒工作线程。");
LockSupport.unpark(workerThread); // 唤醒指定线程
}
}

线程通信 (按顺序打印)

使用 wait()notifyAll() 实现三个线程按顺序打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class SequentialPrinter {
private int state = 0; // 0: A, 1: B, 2: C
private final Object lock = new Object();

public void printA() {
printLetter("A", 0);
}

public void printB() {
printLetter("B", 1);
}

public void printC() {
printLetter("C", 2);
}

private void printLetter(String letter, int expectedState) {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (state != expectedState) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.print(letter);
state = (state + 1) % 3; // 切换到下一个状态
lock.notifyAll();
}
}
}

public static void main(String[] args) throws InterruptedException {
SequentialPrinter printer = new SequentialPrinter();
Thread threadA = new Thread(printer::printA);
Thread threadB = new Thread(printer::printB);
Thread threadC = new Thread(printer::printC);

threadA.start();
threadB.start();
threadC.start();

threadA.join();
threadB.join();
threadC.join();
System.out.println("\n打印完成。");
}
}

线程池

ThreadPoolExecutor 的七个参数

  1. corePoolSize: 核心线程数。线程池中常驻的线程数量,即使空闲也不会被销毁。
  2. maximumPoolSize: 最大线程数。当工作队列已满,且任务量继续增加时,线程池可以创建的最大线程数。
  3. keepAliveTime: 空闲线程存活时间。当线程数大于 corePoolSize 时,非核心线程的空闲存活时间。
  4. unit: keepAliveTime 的时间单位。
  5. workQueue: 工作队列。用于存放等待执行的任务,常用的有 ArrayBlockingQueueLinkedBlockingQueue 等。
  6. threadFactory: 线程工厂。用于创建新线程,可以自定义线程的名称、优先级等。
  7. handler: 拒绝策略。当线程池和工作队列都已满时,用于处理新来的任务,例如抛出异常、由调用者执行等。

手写一个自定义线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

public class CustomThreadPool {
public static void main(String[] args) {
// 自定义线程池
ExecutorService executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 工作队列容量为 10
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行
);

for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}

原子类

使用 AtomicBooleanAtomicReference 解决并发问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.atomic.AtomicBoolean;

class AtomicBooleanDemo {
// 确保只有一个线程执行初始化操作
private static final AtomicBoolean initialized = new AtomicBoolean(false);

public static void initialize() {
// 只有当 initialized 为 false 时,才将其设置为 true 并执行初始化
if (initialized.compareAndSet(false, true)) {
System.out.println(Thread.currentThread().getName() + ": 开始执行初始化操作...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + ": 初始化完成。");
} else {
System.out.println(Thread.currentThread().getName() + ": 初始化已被其他线程执行,跳过。");
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(AtomicBooleanDemo::initialize, "Thread-" + i).start();
}
}
}

volatile 内存语义

volatile 确保了可见性有序性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class VolatileMemorySemantics {
// 1. 可见性:当一个线程修改了 ready 的值,其他线程能立即看到最新值
private static volatile boolean ready = false;
private static int number = 0;

public static class WriterThread extends Thread {
@Override
public void run() {
number = 42; // 修改 number
ready = true; // 修改 ready
}
}

public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
// 等待 ready 变为 true
// 如果没有 volatile,这里可能会陷入死循环
}
// 2. 有序性:写 volatile 变量(ready = true)之前的操作(number = 42)
// 对其他线程都是可见的。保证了 number 的值是 42。
System.out.println("读取到的 number 值: " + number);
}
}

public static void main(String[] args) throws InterruptedException {
new WriterThread().start();
new ReaderThread().start();
}
}

中断机制

一个线程通过响应 interrupt() 调用来正确停止自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class InterruptibleThreadDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 检查中断标志
try {
System.out.println("线程正在执行...");
// sleep()、wait() 等方法会抛出 InterruptedException 并清除中断标志
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程被中断,即将退出...");
// 重新设置中断标志,以便外层循环能正确退出
Thread.currentThread().interrupt();
// 或者直接 break 或 return 退出
break;
}
}
System.out.println("线程已优雅地退出。");
});

worker.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("主线程: 发送中断信号。");
worker.interrupt();
}
}

ConcurrentHashMap

ConcurrentHashMap 的并发原理是**分段锁( 7)**或 CAS + Synchronized( 8),只对操作的桶进行锁定,大大提高了并发性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class ConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);

// 启动 10 个线程,每个线程向 map 中添加 1000 个键值对
for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.execute(() -> {
for (int j = 0; j < 1000; j++) {
String key = "key-" + (threadId * 1000 + j);
map.put(key, j);
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);

System.out.println("最终 map 的大小: " + map.size()); // 期望值为 10000
}
}

ForkJoinPool

ForkJoinPool 是一个用于分治任务的线程池,RecursiveTask 是可返回结果的分治任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class SumTask extends RecursiveTask<Long> {
private final long[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000; // 任务分解的阈值

public SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
// 如果任务规模小于等于阈值,则直接计算
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// 否则,将任务分解成两个子任务
int mid = start + (end - start) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);

leftTask.fork(); // 提交左子任务
long rightResult = rightTask.compute(); // 同步计算右子任务
long leftResult = leftTask.join(); // 等待左子任务结果

return leftResult + rightResult;
}
}

public static void main(String[] args) {
long[] array = new long[1000000];
for (int i = 0; i < array.length; i++) {
array[i] = i;
}

ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
long result = pool.invoke(task);
System.out.println("大数组的和为: " + result);

pool.shutdown();
}
}

1. 语言基础

变量与数据类型

的基本数据类型决定了变量可以存储的数据范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DataTypesDetailed {
public static void main(String[] args) {
// 整数类型:默认为 int
byte b = 100; // 占用 1 字节,-128 到 127
short s = 10000; // 占用 2 字节
int i = 100000; // 占用 4 字节
long l = 10000000000L; // 占用 8 字节,需要 L 或 l 后缀

// 浮点类型:默认为 double
float f = 3.14f; // 占用 4 字节,需要 f 或 F 后缀
double d = 3.1415926535; // 占用 8 字节

// 字符类型
char c1 = 'A'; // 单个字符,占用 2 字节
char c2 = 65; // 也可以使用 ASCII 码或 Unicode
System.out.println("c1 和 c2 是否相等? " + (c1 == c2));

// 布尔类型
boolean isFun = true;
System.out.println("学Java有趣吗?" + isFun);
}
}

类型转换

中,从小范围类型向大范围类型转换是自动的(隐式转换);从大范围向小范围转换需要强制转换(显式转换),可能造成数据丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TypeCasting {
public static void main(String[] args) {
// 隐式转换:int -> long
int myInt = 100;
long myLong = myInt;
System.out.println("隐式转换后的 long 类型: " + myLong);

// 显式转换:double -> int
double myDouble = 9.99;
int myInteger = (int) myDouble; // 强制转换,小数部分被丢弃
System.out.println("显式转换后的 int 类型: " + myInteger); // 输出 9
}
}

数组

数组是存储固定大小同类型元素的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayExample {
public static void main(String[] args) {
// 声明并初始化一个整数数组
int[] numbers = new int[5]; // 创建一个长度为 5 的数组
numbers[0] = 10;
numbers[1] = 20;

// 声明、初始化并赋值
String[] fruits = {"Apple", "Banana", "Cherry"};

// 遍历数组
System.out.println("所有水果:");
for (String fruit : fruits) { // 增强 for 循环
System.out.println(fruit);
}
}
}

2. 面向对象编程 (OOP)

构造方法与方法重载

构造方法是一种特殊方法,用于创建对象时初始化。方法重载是指在同一个类中,方法名相同但参数列表不同(参数类型、数量或顺序)的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Student {
String name;
int age;
String id;

// 无参构造方法
public Student() {
this("未知", 0, "000"); // 调用本类的三参构造方法
}

// 重载构造方法
public Student(String name, int age) {
this.name = name;
this.age = age;
this.id = "000";
}

// 重载构造方法
public Student(String name, int age, String id) {
this.name = name;
this.age = age;
this.id = id;
}

// 方法重载: 计算两个数的和
public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}
}

继承、多态与抽象

继承实现代码复用,多态实现行为多样化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 父类:抽象类
abstract class Vehicle {
protected String brand; // 子类可以访问

public Vehicle(String brand) {
this.brand = brand;
}

public abstract void run(); // 抽象方法,子类必须实现

public void displayBrand() {
System.out.println("品牌是: " + brand);
}
}

// 子类:继承 Vehicle
class Car extends Vehicle {
public Car(String brand) {
super(brand); // 调用父类的构造方法
}

@Override
public void run() {
System.out.println("汽车正在路上行驶...");
}
}

// 子类:继承 Vehicle
class Bicycle extends Vehicle {
public Bicycle(String brand) {
super(brand);
}

@Override
public void run() {
System.out.println("自行车正在骑行...");
}
}

public class PolymorphismDemo {
public static void main(String[] args) {
Vehicle myCar = new Car("BMW"); // 多态引用
Vehicle myBicycle = new Bicycle("Giant");

// 同一个方法调用,不同行为
myCar.run(); // 输出 "汽车正在路上行驶..."
myBicycle.run(); // 输出 "自行车正在骑行..."
}
}

3. 核心类库

好的,这次我们将把各种方法的使用代码直接嵌入到每个知识点的解释中,让您在学习概念的同时,就能看到具体的代码实现和效果。我们将专注于数组、字符串和集合这三大核心部分,把它们的创建、遍历、和各种常用方法的代码示例都清晰地展示出来。


1. 数组(Array)

数组是一种固定大小的、用于存储同类型元素的容器。

创建和遍历

这里展示两种最常见的创建数组的方式,并使用两种循环进行遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;

public class ArrayExample {
public static void main(String[] args) {
// 方式一:声明并分配空间,系统赋默认值
int[] intArray = new int[3]; // 创建一个包含3个整数的数组,默认值都是0
intArray[0] = 10;
intArray[1] = 20;
intArray[2] = 30;

System.out.println("数组 intArray 的第一个元素是: " + intArray[0]); // 输出: 10

// 方式二:声明并直接初始化
String[] stringArray = {"Hello", "World", "Java"};

// 遍历方式一:使用 for 循环
System.out.println("\n--- 使用 for 循环遍历 ---");
for (int i = 0; i < stringArray.length; i++) {
System.out.println("stringArray[" + i + "] = " + stringArray[i]);
}

// 遍历方式二:使用增强 for 循环(更简洁)
System.out.println("\n--- 使用增强 for 循环遍历 ---");
for (String element : stringArray) {
System.out.println("元素: " + element);
}
}
}

Arrays 类的常用方法

java.util.Arrays 类提供了很多静态方法,方便我们对数组进行操作,比如排序、查找、复制等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;

public class ArraysMethodExample {
public static void main(String[] args) {
int[] numbers = {4, 2, 8, 1, 6};

// 1. 排序:Arrays.sort()
Arrays.sort(numbers);
System.out.println("排序后: " + Arrays.toString(numbers)); // 输出: [1, 2, 4, 6, 8]

// 2. 查找:Arrays.binarySearch() (必须先排序)
int index = Arrays.binarySearch(numbers, 6);
System.out.println("元素 6 的索引是: " + index); // 输出: 3

// 3. 填充:Arrays.fill()
int[] newArray = new int[5];
Arrays.fill(newArray, 99);
System.out.println("填充后: " + Arrays.toString(newArray)); // 输出: [99, 99, 99, 99, 99]

// 4. 复制:Arrays.copyOf()
int[] copiedArray = Arrays.copyOf(numbers, 3); // 复制前3个元素
System.out.println("复制前3个元素: " + Arrays.toString(copiedArray)); // 输出: [1, 2, 4]

// 5. 比较:Arrays.equals()
int[] anotherArray = {1, 2, 4, 6, 8};
System.out.println("两个数组是否相等: " + Arrays.equals(numbers, anotherArray)); // 输出: true
}
}

2. 字符串(String)

String 是一个不可变的字符序列,这意味着一旦创建,它的内容就不能被修改。所有修改操作都会返回一个新的 String 对象。

创建和常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class StringExample {
public static void main(String[] args) {
// 创建方式
String str1 = " Java is a great language. "; // 字面量
String str2 = new String("Java is a great language."); // 使用 new 关键字

// 常用方法
// 1. 获取长度
int length = str1.length();
System.out.println("字符串长度: " + length); // 输出: 29

// 2. 访问字符
char firstChar = str1.charAt(2);
System.out.println("第3个字符是: " + firstChar); // 输出: J

// 3. 截取子串
String sub = str1.substring(5, 7);
System.out.println("截取子串: " + sub); // 输出: is

// 4. 查找位置
int index = str1.indexOf("great");
System.out.println("'great' 的索引: " + index); // 输出: 11

// 5. 判断
boolean contains = str1.contains("language");
System.out.println("是否包含 'language': " + contains); // 输出: true
boolean startsWith = str1.startsWith(" Java");
System.out.println("是否以 ' Java' 开头: " + startsWith); // 输出: true

// 6. 替换
String replacedStr = str1.replace("great", "wonderful");
System.out.println("替换后: " + replacedStr); // 输出: Java is a wonderful language.

// 7. 大小写转换和去空格
String trimmedStr = str1.trim();
System.out.println("去除首尾空格: '" + trimmedStr + "'"); // 输出: 'Java is a great language.'
System.out.println("转为大写: " + trimmedStr.toUpperCase());

// 8. 分割和连接
String data = "apple,banana,orange";
String[] fruits = data.split(",");
System.out.println("分割后: " + Arrays.toString(fruits)); // 输出: [apple, banana, orange]

String joinedString = String.join(" - ", fruits);
System.out.println("连接后: " + joinedString); // 输出: apple - banana - orange
}
}

StringBuilderStringBuffer

对于需要频繁修改字符串的场景,应使用 StringBuilderStringBuffer 以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StringBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");

// 1. 追加内容
sb.append(" World");
System.out.println("追加后: " + sb); // 输出: Hello World

// 2. 插入内容
sb.insert(6, "Beautiful ");
System.out.println("插入后: " + sb); // 输出: Hello Beautiful World

// 3. 删除内容
sb.delete(6, 15);
System.out.println("删除后: " + sb); // 输出: Hello World
}
}

3. 集合(Collections)

Java 集合框架提供了强大的数据结构来存储和操作对象。

List(列表)

List 是一种有序、可重复的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListExample {
public static void main(String[] args) {
// 创建 ArrayList(查询快)
List<String> fruits = new ArrayList<>();
fruits.add("Apple"); // 添加元素
fruits.add("Banana");
fruits.add("Apple"); // 允许重复

System.out.println("列表元素: " + fruits); // 输出: [Apple, Banana, Apple]
System.out.println("第一个元素: " + fruits.get(0)); // 获取元素
System.out.println("列表大小: " + fruits.size());

fruits.set(1, "Grape"); // 替换第二个元素
System.out.println("替换后: " + fruits); // 输出: [Apple, Grape, Apple]

fruits.remove(1); // 删除第二个元素
System.out.println("删除后: " + fruits); // 输出: [Apple, Apple]
}
}

Set(集)

Set 是一种无序、不可重复的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.HashSet;
import java.util.Set;

public class SetExample {
public static void main(String[] args) {
// 创建 HashSet
Set<String> uniqueFruits = new HashSet<>();
uniqueFruits.add("Apple");
uniqueFruits.add("Banana");
uniqueFruits.add("Apple"); // 添加重复元素,会失败

System.out.println("集合元素: " + uniqueFruits); // 输出: [Apple, Banana] (顺序不定)

boolean containsBanana = uniqueFruits.contains("Banana");
System.out.println("是否包含 'Banana': " + containsBanana); // 输出: true

uniqueFruits.remove("Banana");
System.out.println("删除后: " + uniqueFruits); // 输出: [Apple]
}
}

Map(映射)

Map 存储键值对,键是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.HashMap;
import java.util.Map;

public class MapExample {
public static void main(String[] args) {
// 创建 HashMap
Map<String, Integer> studentScores = new HashMap<>();
studentScores.put("Alice", 95); // 添加键值对
studentScores.put("Bob", 88);
studentScores.put("Alice", 100); // 键已存在,会覆盖旧值

System.out.println("Alice 的分数: " + studentScores.get("Alice")); // 输出: 100

// 遍历 Map 的三种方式
// 方式一:遍历键集
System.out.println("\n--- 遍历键集 ---");
for (String name : studentScores.keySet()) {
System.out.println("姓名: " + name + ", 分数: " + studentScores.get(name));
}

// 方式二:遍历值集
System.out.println("\n--- 遍历值集 ---");
for (Integer score : studentScores.values()) {
System.out.println("分数: " + score);
}

// 方式三:遍历键值对集 (Entry Set),最常用且高效
System.out.println("\n--- 遍历键值对集 ---");
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
System.out.println("姓名: " + entry.getKey() + ", 分数: " + entry.getValue());
}
}
}

web

好的,让我们深入到 HTTP 协议的底层,全面解剖它的工作原理、请求-响应模型、以及各个组成部分。


一、HTTP 协议概述

HTTP(Hypertext Transfer Protocol,超文本传输协议)是应用层协议,它定义了 Web 客户端(如浏览器)和 Web 服务器之间交换数据的规则。它的核心特点是无状态(Stateless),这意味着服务器不会保存客户端的任何信息,每次请求都是独立的。


二、HTTP 请求(Request)

一个完整的 HTTP 请求由四部分组成:

1. 请求行(Request Line)

这是请求的第一行,它定义了请求的基本信息。

  • 请求方法(Method):如 GET, POST, PUT, DELETE 等,表示对资源的操作类型。
  • 请求 URL (URI):资源的地址。
  • HTTP 协议版本:如 HTTP/1.1, HTTP/2.0

示例:

GET /index.html HTTP/1.1

2. 请求头(Request Headers)

请求头提供了关于客户端、请求体和一些其他元数据的信息。它们以键值对的形式存在。

  • Host:指定请求的目标服务器的域名和端口号。
  • User-Agent:客户端的软件类型和版本,如浏览器信息。
  • Accept:客户端能够处理的媒体类型,告诉服务器希望返回什么格式的数据(如 application/json, text/html)。
  • Content-Type请求体中的数据类型,只在有请求体时使用(如 POST)。常见的有:
    • application/x-www-form-urlencoded:默认编码,用于提交表单数据。
    • multipart/form-data:用于上传文件。
    • application/json:用于前后端分离,提交 JSON 数据。
  • Content-Length:请求体的长度(字节)。
  • Cookie:客户端存储的 Cookie 信息,用于会话管理。
  • Authorization:用于身份认证,如携带 Bearer Token。
  • Referer:请求从哪个页面跳转而来。

3. 空行

一个空行用于分隔请求头和请求体。

4. 请求体(Request Body)

请求体包含客户端提交给服务器的数据,通常用于 POSTPUT 等请求。GET 请求通常没有请求体。


三、HTTP 响应(Response)

一个完整的 HTTP 响应由三部分组成:

1. 状态行(Status Line)

这是响应的第一行,定义了响应的基本信息。

  • HTTP 协议版本:如 HTTP/1.1
  • 状态码(Status Code):三位数字,表示请求结果。
  • 状态信息(Reason Phrase):对状态码的简短描述。

示例:

HTTP/1.1 200 OK

2. 响应头(Response Headers)

响应头提供了关于服务器、响应体和会话的元数据信息。

  • Content-Type响应体的数据类型。
  • Content-Length:响应体的长度。
  • Set-Cookie:服务器向客户端发送 Cookie,用于存储会话 ID 等信息。
  • Location:用于重定向,指定新的 URL。
  • Cache-Control:控制浏览器缓存行为。

3. 空行

一个空行用于分隔响应头和响应体。

4. 响应体(Response Body)

响应体包含了服务器返回给客户端的实际数据,如 HTML 网页内容、JSON 数据或图片等。


四、HTTP 方法的幂等性与安全性

  • 安全性:指请求方法不会对服务器上的资源状态产生修改。GETHEAD 方法是安全的。
  • 幂等性:指请求方法重复执行多次,对服务器上的资源状态产生的影响与执行一次的影响相同。
    • 幂等方法GET, HEAD, PUT, DELETE
    • 非幂等方法POST
方法 安全性 幂等性 典型用途
GET 获取资源
POST 创建资源
PUT 更新或替换资源
DELETE 删除资源

五、HTTP 的会话管理:Cookie

HTTP 本身是无状态的,但为了跟踪用户,引入了 Cookie。

  1. 创建:服务器通过响应头 Set-Cookie 向客户端发送一个 Cookie。
  2. 存储:浏览器接收到 Set-Cookie 后,会将其存储在本地。
  3. 携带:之后,每次对同一域名发起请求时,浏览器都会自动在请求头 Cookie 中带上这个 Cookie。

通过在 Cookie 中存储一个 SessionID,服务器就可以在后续请求中找到对应的会话数据,从而实现状态管理。


六、HTTP/1.1 与 HTTP/2 的区别

  • 多路复用(Multiplexing):HTTP/2 允许在一个 TCP 连接上同时发送多个请求和响应,解决了 HTTP/1.1 队头阻塞的问题,显著提高了性能。
  • 头部压缩(Header Compression):HTTP/2 使用 HPACK 算法压缩请求和响应头,减少了数据传输量。
  • 服务器推送(Server Push):服务器可以在客户端请求一个资源时,主动推送其他它认为客户端可能需要的资源,减少了客户端的请求次数。
  • 二进制分帧(Binary Framing):HTTP/2 将所有传输信息分割成更小的消息和帧,并采用二进制编码,使得解析更高效。

好的,我们来详细补充 HTTP/1.0 的核心概念,并对整个 HTTP 协议族进行全面的对比和深入解析。


一、HTTP 协议族演变:从 1.0 到 2.0

HTTP/1.0

HTTP/1.0 是 HTTP 协议的早期版本,它的设计相对简单,主要用于满足基本的网页浏览需求。

  • 核心特点:
    • 非持久连接(Non-persistent Connection):这是 HTTP/1.0 最显著的特点。每进行一次 HTTP 请求-响应,客户端和服务器之间就会建立一个新的 TCP 连接,并在请求完成后立即断开。
      • 缺点:每次请求都需要经过 TCP 三次握手和四次挥手的过程,这带来了巨大的性能开销。如果一个网页包含多个图片、CSS 或 JavaScript 文件,浏览器需要为每个文件单独建立和断开连接,导致页面加载速度慢。
    • 无主机头(No Host Header):请求头中没有 Host 字段。这意味着一个 IP 地址只能对应一个域名。如果服务器上有多个网站,就无法通过 IP 地址来区分它们,这在虚拟主机时代是个大问题。
    • 不支持管线化(Pipelining):客户端发送一个请求后,必须等待服务器的响应,才能发送下一个请求。

HTTP/1.1

HTTP/1.1 协议是对 1.0 的重大改进,解决了其大部分性能瓶颈。

  • 核心特点:
    • 持久连接(Persistent Connection):默认情况下,HTTP/1.1 会在一次请求-响应之后保持 TCP 连接不断开。客户端可以继续在这个连接上发送后续请求。
      • 优点:显著减少了 TCP 连接的建立和断开开销,提高了页面加载速度。这个特性也称为“Keep-Alive”。
    • 支持主机头(Host Header):请求头中引入了 Host 字段,允许在同一个 IP 地址上部署多个虚拟主机(域名)。
    • 支持管线化(Pipelining):客户端可以在收到上一个响应之前,连续发送多个请求。
      • 缺点:虽然提高了效率,但存在**队头阻塞(Head-of-Line Blocking)**问题。如果第一个请求处理时间很长,后面的请求即使已经处理完成,也必须等待它的响应,导致整个连接的效率降低。
    • 引入缓存机制:通过 Cache-Control, ETag, If-None-Match 等请求头,HTTP/1.1 提供了更完善的缓存机制,减少了不必要的请求。

HTTP/2.0

HTTP/2.0 是为了解决 HTTP/1.1 在移动互联网和高并发场景下的性能问题而设计的。它不是对 HTTP/1.1 的简单升级,而是对协议底层进行了重构。

  • 核心特点:
    • 多路复用(Multiplexing):这是 HTTP/2 的核心。它允许在一个 TCP 连接上同时处理多个 HTTP 请求和响应。
      • 如何实现?:HTTP/2 将所有数据流(Stream)分割成更小的二进制帧(Frame),每个帧都带有唯一的标识符。这样,客户端和服务器可以在同一个连接上交错发送和接收帧,然后根据标识符重新组装,从而彻底解决了 HTTP/1.1 的队头阻塞问题。
    • 头部压缩(Header Compression):HTTP/2 使用 HPACK 算法来压缩请求和响应头,尤其是对于重复发送的字段,大大减少了数据传输量。
    • 服务器推送(Server Push):服务器可以在客户端请求一个资源(如 HTML 页面)时,主动推送其他它认为客户端可能需要的资源(如 CSS 和 JS 文件),而无需客户端显式请求,进一步提高了加载速度。
    • 二进制协议:HTTP/2 是一个二进制协议,而不是文本协议,解析更高效、更不容易出错。

二、HTTP 协议版本对比总结

理解 HTTP 协议的演变历程,能让你更深刻地体会到 Web 性能优化的方向,以及现代 Web 框架如何利用这些底层协议特性来提供更高效的服务。

特性 HTTP/1.0 HTTP/1.1 HTTP/2.0
TCP 连接 非持久连接 默认持久连接 单个 TCP 连接多路复用
性能瓶颈 多次握手挥手开销 队头阻塞
主机头 不支持 支持 (Host Header) 支持
并发请求 串行(一个请求一个连接) 串行(一个连接一个请求) 并行(一个连接多个请求)
数据格式 文本协议 文本协议 二进制协议
头部 无压缩 无压缩 HPACK 算法压缩
服务器推送 不支持 不支持 支持

好的,让我们把所有关于 Servlet 的知识点整合在一起,进行一次最全面、最深入的剖析。我们将从基础概念开始,逐步深入到它的生命周期、配置、核心方法以及与 Servlet 规范相关的其他重要组件。


Servlet 概述:Java Web 的核心基石

Servlet 是 Java EE 规范中的一个核心组件,它是一个运行在服务器端的 Java 程序,用于处理 HTTP 请求和生成动态响应。你可以把它看作是所有 Java Web 框架(如 Spring MVC)的底层引擎。

它的核心作用是作为客户端(浏览器)和 Java 应用程序之间的“桥梁”。Servlet 容器(如 Tomcat、Jetty)负责监听网络请求,然后将请求分发给相应的 Servlet 进行处理。

Servlet 的生命周期:由容器严格管理

一个 Servlet 的生命周期由 Servlet 容器严格管理,通常分为三个阶段:

  1. 初始化(Initialization)
    • 时机:当 Servlet 容器第一次加载 Servlet 类并创建其实例后,立即调用 init() 方法。
    • 特性:这个方法只执行一次,通常用于加载配置文件、建立数据库连接池等一次性、耗时的任务。如果 init() 方法抛出异常,Servlet 将无法提供服务。
  2. 服务(Servicing)
    • 时机:每当一个客户端请求到达时,容器会为该请求创建一个新的线程,并调用 service() 方法。
    • 特性service() 方法是 Servlet 的核心,它会根据请求类型(如 GET、POST)自动调用相应的 doGet()doPost() 等方法。由于此方法会被多个线程并发调用,因此访问共享资源时必须注意线程安全
  3. 销毁(Destruction)
    • 时机:当 Servlet 容器关闭或决定卸载某个 Servlet 时,会调用其 destroy() 方法。
    • 特性:这个方法也只执行一次,用于释放资源,如关闭数据库连接或文件流。

Servlet 的配置和部署:两种主要方式

为了让容器知道如何加载和映射 Servlet,你需要进行配置。

  1. web.xml 配置(传统方式)
    • web.xml 文件中,使用 <servlet> 标签定义 Servlet 类,并使用 <servlet-mapping> 标签将它映射到一个 URL 路径。
    • 优点:配置集中,易于管理。
    • 缺点:如果 Servlet 很多,web.xml 文件会变得非常庞大。
  2. 注解配置(现代方式)
    • 从 Servlet 3.0 规范开始,你可以使用 @WebServlet 注解来简化配置。只需在你的 Servlet 类上添加 @WebServlet("/path") 注解,容器就会自动识别和配置它。
    • 优点:配置简单,代码和配置在一起,提高了可读性。

Servlet 的核心方法:处理请求和响应

所有 Servlet 都应该实现 javax.servlet.Servlet 接口。通常,我们更常继承 javax.servlet.http.HttpServlet,因为它提供了更方便的 HTTP 请求处理方法。

  • service(HttpServletRequest req, HttpServletResponse res):这是核心服务方法,由容器调用。它会根据请求的 HTTP 方法(GET、POST、PUT 等)来调用相应的 doGet()doPost() 等方法。
  • doGet(HttpServletRequest req, HttpServletResponse res):处理所有 GET 请求。
  • doPost(HttpServletRequest req, HttpServletResponse res):处理所有 POST 请求。
  • doPut(HttpServletRequest req, HttpServletResponse res):处理所有 PUT 请求。
  • doDelete(HttpServletRequest req, HttpServletResponse res):处理所有 DELETE 请求。

在这些方法中,你可以通过 req (请求) 和 res (响应) 对象来与客户端进行交互。

Servlet 的请求和响应处理:与客户端交互

  • 获取请求参数
    • req.getParameter("paramName"):获取单个参数值。
    • req.getParameterValues("paramName"):获取具有相同名称的多个参数值(如复选框)。
  • 设置响应头
    • res.setContentType("text/html;charset=UTF-8"):设置响应的内容类型和字符编码。
    • res.setHeader("HeaderName", "HeaderValue"):设置自定义响应头。
  • 写入响应内容
    • res.getWriter():获取一个 PrintWriter,用于向客户端发送文本响应。
    • res.getOutputStream():获取一个 ServletOutputStream,用于向客户端发送二进制响应(如图片、文件)。

Servlet 的核心组件与作用域

理解 Servlet 规范中的这些组件,能帮助你更合理地管理数据和资源。

  1. ServletContext (应用上下文)
    • 作用:代表整个 Web 应用,它的生命周期与应用相同。
    • 作用域:数据在所有 Servlet、JSP 和 Filter 之间共享
    • 用途:存储全局配置和共享数据。
  2. ServletConfig (配置对象)
    • 作用:代表一个 Servlet 独有的配置。
    • 作用域:仅在它所关联的 Servlet 内部有效。
    • 用途:获取特定 Servlet 的初始化参数。
  3. 四大作用域
    • PageContext (页面作用域):仅在 JSP 页面内有效。
    • HttpServletRequest (请求作用域):在一次完整的请求-响应周期内有效,即使请求被转发,数据依然可见。
    • HttpSession (会话作用域):在同一个浏览器会话中有效,用于存储用户特定数据。
    • ServletContext (应用作用域):在整个 Web 应用中都有效,用于存储全局数据。

好的,我们来详细、深入地聊聊 Web 开发中至关重要的两个概念:Cookie 和 Session。它们是解决 HTTP 无状态问题的核心方案,但工作机制和应用场景却大不相同。


Cookie:客户端的“小纸条”

Cookie 是服务器发送给浏览器并存储在客户端的一小段文本信息。浏览器在下次访问同一服务器时,会自动将该 Cookie 包含在请求中发送回去。

核心工作机制

  1. 服务器创建 Cookie:

    当用户首次访问网站时,服务器在响应头(Response Header)中添加一个 Set-Cookie 字段。

    比如:Set-Cookie: JSESSIONID=abcde12345; Path=/; HttpOnly

  2. 浏览器保存 Cookie:

    浏览器接收到响应后,会解析 Set-Cookie 头,并将该信息以键值对的形式存储在本地。

  3. 浏览器发送 Cookie:

    在后续的请求中,只要请求的域名和路径与 Cookie 的设置相符,浏览器就会自动在请求头(Request Header)中添加 Cookie 字段,将存储的 Cookie 信息发送给服务器。

    比如:Cookie: JSESSIONID=abcde12345

  • 优点
    • 减轻服务器压力:数据存储在客户端,服务器不需要为每个用户维护状态,适用于大规模访问。
    • 可扩展性强:无状态,天然适合分布式架构。
  • 缺点
    • 安全性差:数据以明文形式存储,容易被窃取和篡改。
    • 容量限制:单个 Cookie 的大小通常不超过 4KB,且一个域名下的 Cookie 总数也有限制。
    • 用户可禁用:如果用户禁用了浏览器 Cookie 功能,相关功能将无法使用。

Session:服务器的“个人档案”

Session 是服务器为每个用户创建的一个对象,用于存储特定用户的会话数据。它将用户的状态信息保存在服务器端,并通过一个唯一的 Session ID 来识别不同的用户。

核心工作机制

  1. 服务器创建 Session:

    当用户首次访问 Web 应用时,服务器会创建一个 HttpSession 对象,并为其分配一个唯一的 Session ID。

  2. 传递 Session ID:

    服务器将这个 Session ID 以 Cookie 的形式发送给浏览器(这个 Cookie 通常叫 JSESSIONID)。

    • 如果浏览器禁用了 Cookie,服务器可以通过 URL 重写的方式,将 Session ID 附加到每个 URL 的末尾,例如:.../index.jsp;jsessionid=abcde12345
  3. 服务器维护 Session:

    Session ID 传递到客户端后,服务器会在内存中或持久化存储中保存这个 HttpSession 对象及其数据。

  4. 识别用户:

    后续的请求中,浏览器都会带着包含 Session ID 的 Cookie。服务器通过这个 ID 就能从内存中找到对应的 HttpSession 对象,从而获取该用户的状态信息。

Session 的优缺点

  • 优点
    • 安全性高:核心数据存储在服务器,客户端只传递一个无法猜解的 ID。
    • 容量大:存储在服务器,理论上没有大小限制。
  • 缺点
    • 占用服务器资源:每个活跃的 Session 都会占用服务器内存,在高并发场景下可能成为瓶颈。
    • 分布式挑战:在多台服务器组成的集群环境中,需要额外的机制(如 Session 共享或粘性会话)来确保用户的请求总是被转发到同一个 Session 所在的服务器,或者所有服务器都能访问到 Session 数据。

特性 Cookie Session
数据存储位置 客户端(浏览器) 服务器端
安全性 较低,易被篡改 较高,数据安全
数据容量 较小(~4KB) 较大,无明显限制
性能 轻量,不占用服务器资源 占用服务器内存,可能影响性能
可扩展性 天然无状态,易于扩展 分布式环境下需要额外配置
主要用途 购物车、用户偏好、轻量级状态 登录状态、权限验证、敏感数据

好的,我已将 GET 和 POST 请求的所有核心区别整理成一个简洁明了的表格,并补充了更多细节,使其更全面。

GET 和 POST 请求的全面对比

特性 GET 请求 POST 请求 备注
基本作用 从服务器获取资源 向服务器提交数据,通常用于创建资源。 语义不同是所有区别的根本。
传参方式 参数附加在 URL 中 参数放在请求体(Request Body)中 GET 参数可见,POST 参数隐藏。
安全性 不安全,URL 暴露 相对安全,参数隐藏在请求体中 这里的“安全”指数据不被暴露。
幂等性 (多次执行结果相同) (多次执行可能产生新资源) 幂等性是 RESTful 设计的关键原则。
浏览器行为 回退无害,可被记录在历史、收藏为书签。 回退时通常会提示重新提交,不记录在历史、不可收藏。 防止意外提交导致数据重复。
缓存 可以被缓存 不会被缓存 缓存能提高性能,但仅限于只读操作。
传输数据量 有长度限制(URL长度限制) 无长度限制(取决于服务器配置) GET 不适合传输大量数据。
数据编码 只支持 URL 编码 支持多种编码(如application/json POST 灵活性高,适合各种数据类型。
发送文件 不支持 支持 (multipart/form-data) 文件必须通过请求体传输。
HTTP 报文 无请求体 有请求体 GET 请求报文更小,更轻量。
TCP/IP 协议 一次性发送所有数据包 浏览器通常会先发送请求头,服务器响应 100-continue 后再发送请求体 POST 的分步发送机制可以避免发送不必要的数据。

好的,我们将以 Servlet 的详细剖析方式,全面、深入地介绍 WebSocket。我们将从它的概念、生命周期、核心 API,到它与 HTTP 的区别以及在现代 Web 中的应用,进行系统性的梳理。


WebSocket 概述:Web 通信的革命

WebSocket 协议是 HTML5 开始提供的一种在单个 TCP 连接上进行全双工通信的协议。与传统的 HTTP 协议不同,它打破了请求-响应的单向模式,使得客户端和服务器可以实时地双向自由传输数据

它的核心作用是解决 Web 应用中实时通信的需求,例如:聊天应用、在线游戏、股票行情、实时协作工具等。它极大地减少了网络开销,提高了通信效率。

WebSocket 的核心工作原理

要理解 WebSocket,必须首先理解它与 HTTP 的关系。

  1. 基于 HTTP 的握手(Handshake)
    • WebSocket 的连接建立过程是基于 HTTP 协议的。
    • 客户端发送一个特殊的 HTTP 请求,其中包含 Upgrade: websocketConnection: Upgrade 请求头,向服务器请求将 HTTP 协议升级到 WebSocket 协议。
    • 这是一个**“握手”**过程。
  2. 协议升级与持久连接
    • 如果服务器支持 WebSocket,它会返回一个特殊的 HTTP 响应(状态码 101 Switching Protocols)。
    • 握手成功后,客户端和服务器之间的 TCP 连接将保持打开状态
    • 这时,协议就从 HTTP 升级到了 WebSocket,双方可以在这个持久的 TCP 连接上自由地双向发送数据,而无需再进行 HTTP 头部开销。
  3. 数据帧传输
    • WebSocket 协议定义了**数据帧(Data Frames)**的概念。它将数据分割成更小、更轻量级的帧,而不是像 HTTP 那样发送整个报文。
    • 这样,即使传输少量数据,也不会有很大的协议开销,非常适合实时通信。

WebSocket 的生命周期:事件驱动模型

WebSocket 的生命周期由客户端和服务器共同维护,由一系列事件驱动:

1. 连接建立(Connection Establishment)

  • 客户端:通过 new WebSocket(url) 建立连接。
  • 服务器:当客户端发送握手请求后,服务器处理并接受连接,此时触发 onopen 事件。

2. 数据传输(Data Transmission)

  • 发送:客户端和服务器都可以通过各自的 send() 方法向对方发送数据。
  • 接收:当一方接收到数据时,会触发 onmessage 事件。

3. 连接关闭(Connection Close)

  • 主动关闭:客户端或服务器可以调用 close() 方法主动关闭连接。
  • 异常关闭:连接可能因网络故障、心跳超时等原因意外关闭。
  • 事件:无论是主动还是被动,连接关闭时都会触发 onclose 事件。

WebSocket 的核心 API(以 JavaScript 为例)

WebSocket 的 API 设计非常简洁,主要基于事件监听和方法调用。

  • new WebSocket(url):创建一个 WebSocket 客户端实例。
  • websocket.onopen:连接成功建立时触发。
  • websocket.onmessage = function(event):接收到服务器数据时触发,数据在 event.data 中。
  • websocket.onerror:连接发生错误时触发。
  • websocket.onclose:连接关闭时触发。
  • websocket.send(data):向服务器发送数据。
  • websocket.close():关闭连接。

WebSocket 与 HTTP 的本质区别

特性 HTTP WebSocket 备注
通信模式 单向(请求-响应) 双向(全双工) HTTP 客户端必须先发请求,服务器才能响应。
连接状态 无状态,短连接 有状态,长连接 HTTP 每次请求都需要重新建立连接。
协议开销 (每次请求都携带头部) (握手后只传输数据帧) HTTP 适合传输大文件,WebSocket 适合小数据频繁传输。
服务器主动性 被动(无法主动推送) 主动(可随时推送数据) WebSocket 解决了 HTTP 的“服务器推”难题。
应用场景 网页浏览、API 调用、文件下载 聊天、游戏、实时数据更新

WebSocket 在现代 Java Web 中的应用

在 Java 后端,WebSocket 通常由专门的框架或容器来实现,如:

  • JavaEE 7+ 的 WebSocket API:提供了 javax.websocket 包,可以直接在 Servlet 容器中开发 WebSocket 应用。
  • Spring Framework:提供了强大的 WebSocket 支持,集成了 STOMP(Simple Text Oriented Messaging Protocol)协议,简化了消息路由和管理。

在这些框架中,你通常会定义一个 WebSocket 端点(Endpoint),类似于 Servlet,它负责处理连接的建立、消息的接收和发送、以及连接的关闭。

总而言之,WebSocket 是 HTTP 协议在实时通信领域的有力补充。它通过一次性的握手建立一个持久的双向通道,极大地提高了 Web 应用的交互性和效率,是现代 Web 架构中不可或缺的一部分。

好的,为了更好地理解 WebSocket 的工作原理,我将提供几个具体的应用场景和相应的代码示例。这些示例将涵盖客户端(JavaScript)和服务器端(Java/Spring Boot)的代码,以便你能够完整地看到双向通信是如何实现的。


示例一:实时聊天应用

这是 WebSocket 最经典的用例。

应用场景

多个用户连接到聊天室,当一个用户发送消息时,服务器将该消息实时广播给所有其他在线用户。

核心逻辑

  • 客户端:当用户在输入框中按下回车,JavaScript 将消息通过 WebSocket 连接发送给服务器。
  • 服务器:接收到消息后,遍历所有已连接的 WebSocket 会话,将消息逐一发送给每个会话。

代码示例

1. 客户端 (JavaScript)

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
// 连接 WebSocket 服务器
const socket = new WebSocket('ws://localhost:8080/chat');

// 监听连接成功事件
socket.onopen = function(event) {
console.log("WebSocket连接已建立");
};

// 监听接收消息事件
socket.onmessage = function(event) {
const message = event.data;
const chatBox = document.getElementById('chat-box');
chatBox.innerHTML += `<p>${message}</p>`; // 将消息添加到聊天框
};

// 监听连接关闭事件
socket.onclose = function(event) {
console.log("WebSocket连接已关闭");
};

// 监听错误事件
socket.onerror = function(event) {
console.log("WebSocket发生错误:", event);
};

// 发送消息函数
function sendMessage() {
const input = document.getElementById('message-input');
const message = input.value;
if (message) {
socket.send(message); // 通过 WebSocket 发送消息
input.value = ''; // 清空输入框
}
}

// 绑定回车事件
document.getElementById('message-input').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
sendMessage();
}
});

2. 服务器端 (Java/Spring Boot)

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/chat")
@Component
public class ChatWebSocketEndpoint {

// 存储所有连接的客户端会话
private static final CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();

/**
* 当客户端连接成功时调用
*/
@OnOpen
public void onOpen(Session session) {
sessions.add(session); // 添加新会话
System.out.println("新连接:" + session.getId());
}

/**
* 当客户端发送消息时调用
*/
@OnMessage
public void onMessage(String message, Session session) {
System.out.println("收到消息:" + message + " 来自:" + session.getId());
// 遍历所有会话,广播消息
sessions.forEach(s -> {
try {
s.getBasicRemote().sendText(message);
} catch (IOException e) {
e.printStackTrace();
}
});
}

/**
* 当连接关闭时调用
*/
@OnClose
public void onClose(Session session) {
sessions.remove(session); // 移除会话
System.out.println("连接关闭:" + session.getId());
}

/**
* 当发生错误时调用
*/
@OnError
public void onError(Session session, Throwable throwable) {
System.out.println("连接发生错误:" + session.getId());
throwable.printStackTrace();
}
}

示例二:实时股票行情或数据看板

应用场景

客户端连接到服务器,服务器定期或在数据更新时,向所有连接的客户端推送最新的股票价格或监控数据。

核心逻辑

  • 客户端:只监听 onmessage 事件,被动接收服务器推送的数据。
  • 服务器:在后台启动一个定时任务或数据监听器。当数据变化时,主动通过 WebSocket 连接将新数据发送给客户端。

代码示例

1. 客户端 (JavaScript)

JavaScript

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const socket = new WebSocket('ws://localhost:8080/stock');

socket.onopen = function(event) {
console.log("股票行情连接已建立");
};

socket.onmessage = function(event) {
// 假设服务器发送的是JSON格式的股票数据
const stockData = JSON.parse(event.data);
const stockPriceElement = document.getElementById('stock-price');
stockPriceElement.innerText = `当前价格: ${stockData.price}`;
};

// 无需 sendMessage 函数,因为是单向被动接收

2. 服务器端 (Java/Spring Boot)

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.ServerEndpoint;
import java.io.IOException;
import java.util.concurrent.CopyOnWriteArraySet;

@ServerEndpoint("/stock")
@Component
public class StockWebSocketEndpoint {

private static final CopyOnWriteArraySet<Session> sessions = new CopyOnWriteArraySet<>();

@OnOpen
public void onOpen(Session session) {
sessions.add(session);
System.out.println("新股票行情订阅者:" + session.getId());
}

@OnClose
public void onClose(Session session) {
sessions.remove(session);
}

// 假设这是一个定时任务,每隔5秒向所有客户端推送数据
@Scheduled(fixedRate = 5000)
public void pushStockData() {
// 模拟获取最新股票价格
double price = Math.random() * 100 + 100;
String json = "{\"price\": " + String.format("%.2f", price) + "}";

// 遍历所有会话,推送数据
sessions.forEach(session -> {
try {
if (session.isOpen()) {
session.getBasicRemote().sendText(json);
}
} catch (IOException e) {
e.printStackTrace();
}
});
}
}

这两个例子清楚地展示了 WebSocket 如何通过持久连接双向通信来解决实时应用中的核心痛点。

好的,让我们详细、清晰地解释静态资源动态资源的概念,以及为什么某些服务器或工具只能处理静态资源。


一、静态资源 (Static Resources)

概念

静态资源是指在服务器端不需要经过任何处理或计算,就能直接返回给客户端的资源。它们的内容是固定不变的,无论何时、被谁请求,服务器返回的都是同一个文件。

常见类型

  • HTML 文件 (.html):纯 HTML 代码,浏览器直接渲染。
  • CSS 样式表 (.css):定义网页样式。
  • JavaScript 脚本 (.js):用于网页交互。
  • 图片 (.jpg, .png, .gif):图像文件。
  • 字体文件 (.woff, .ttf):网页字体。
  • 视频 (.mp4, .webm):视频文件。

工作流程

  1. 客户端向服务器请求一个 URL,例如 http://example.com/styles.css
  2. 服务器接收到请求,直接从文件系统中找到名为 styles.css 的文件。
  3. 服务器将 styles.css 的内容原封不动地返回给客户端。

特点

  • 高效:无需服务器计算,直接读取文件,响应速度极快。
  • 可缓存:由于内容不变,浏览器、代理服务器都可以对静态资源进行缓存,进一步减少网络传输。
  • 部署简单:只需将文件放在服务器的特定目录下即可。

二、动态资源 (Dynamic Resources)

概念

动态资源是指服务器端需要经过处理、计算或查询数据库,才能生成并返回给客户端的资源。它们的内容是可变的,不同的请求、不同的时间,返回的内容可能不同。

常见类型

  • Servlet/JSP:Java 后端通过 Servlet 或 JSP 动态生成 HTML。
  • PHP 脚本 (.php):PHP 后端生成 HTML。
  • Python/Django 视图:Python 后端处理请求并返回数据。
  • RESTful API:返回 JSON、XML 等格式的数据。

工作流程

  1. 客户端向服务器请求一个 URL,例如 http://example.com/user/profile?id=123
  2. 服务器接收到请求,将请求转发给后端程序(如 Servlet 容器)。
  3. 后端程序接收请求,根据请求参数 id=123 查询数据库,获取该用户的信息。
  4. 后端程序将数据渲染到 HTML 模板中,或者将数据封装成 JSON 格式。
  5. 后端程序将动态生成的内容作为响应返回给服务器。
  6. 服务器将该响应发送给客户端。

特点

  • 灵活:可以根据业务逻辑生成个性化内容。
  • 消耗资源:需要消耗 CPU、内存、数据库连接等资源进行计算。
  • 不可直接缓存:由于内容可变,通常无法像静态资源那样直接缓存。

三、是什么东西只能解析静态资源?

这个问题的核心在于**“解析”这个词。能“解析”静态资源的,通常指的是Web 服务器**。

  • Web 服务器:一种软件,它的主要职责是处理 HTTP 请求并返回文件。

1. 纯静态 Web 服务器 (Static Web Server)

这类服务器的核心功能就是提供静态文件服务。 它们只知道如何根据 URL 路径,去文件系统找到对应的文件,然后返回。

  • 工作原理:它们没有内置的后端语言解析器(如 Java 虚拟机、PHP 解释器)。当它们收到一个请求时,只会将 URL 映射到文件目录。如果文件存在,就返回;如果不存在,就返回 404 错误。
  • 代表Nginx(作为反向代理或静态文件服务器时)、Apache HTTP Server(作为静态文件服务器时)。

为什么它们不能解析动态资源?

因为它们不具备执行后端代码的能力。例如,当 Nginx 收到一个对 .jsp 文件的请求时,它不知道如何运行 Java 代码来生成 HTML。它只会把 .jsp 文件当作一个普通的文本文件返回给浏览器,浏览器也无法正确地解析。

2. 动态 Web 服务器 (Dynamic Web Server)

这类服务器通常被称为 “应用服务器”,它们在 Web 服务器的功能上,内置了后端语言的运行环境

  • 工作原理
    • 当它们收到静态资源请求时,行为与静态服务器相同:直接返回文件。
    • 当它们收到动态资源请求时,会将请求转发给内置的后端程序运行环境。例如,Tomcat 会将 .jsp 文件交给 JSP 引擎来编译和执行。
  • 代表Apache TomcatJetty

因此,在实际生产环境中,我们经常采用动静分离的架构:

  • 静态资源index.htmlstyle.css 等)交给 Nginx 这样的高效静态服务器处理。
  • 动态资源api/user, .jsp 等)交给 Tomcat 这样的应用服务器处理。

这样做的目的是,让专业的工具做专业的事:Nginx 擅长高并发的静态文件服务,而 Tomcat 则专注于复杂的动态业务逻辑。

好的,让我们来系统、全面地梳理前端的核心知识,这不仅是前端工程师的必备技能,也是后端开发者理解整个 Web 应用架构的关键。

一、基础三剑客:构建网页的基石

这三者是所有前端技术的基础,就像建筑的钢筋、水泥和水电。

1. HTML (HyperText Markup Language)

  • 是什么:超文本标记语言,用于定义网页的结构和内容。它由一系列标签(tag)组成,这些标签告诉浏览器如何组织页面内容,如段落、标题、图片、链接等。
  • 核心概念
    • 标签(Tags):如 <p> (段落), <h1> (一级标题), <img> (图片), <a> (超链接)。
    • 元素(Elements):由开始标签、内容和结束标签组成,例如 <p>这是一个段落。</p>
    • 属性(Attributes):提供关于元素的额外信息,如 <img src="image.jpg" alt="描述"> 中的 srcalt
    • 语义化(Semantic HTML):使用恰当的标签来表达内容的含义,如 <header>, <nav>, <article>, <footer>,这有助于搜索引擎优化(SEO)和无障碍访问。

2. CSS (Cascading Style Sheets)

  • 是什么:层叠样式表,用于定义网页的样式和表现。它告诉浏览器如何显示 HTML 元素,如颜色、字体、布局、大小等。
  • 核心概念
    • 选择器(Selectors):用于选中要应用样式的 HTML 元素,如 h1 (标签选择器), .my-class (类选择器), #my-id (ID 选择器)。
    • 盒模型(Box Model):每个 HTML 元素都被视为一个矩形盒子,包含 content(内容)、padding(内边距)、border(边框)和 margin(外边距)。
    • 布局(Layout)
      • 传统布局float, position, display
      • 现代布局Flexbox (弹性盒子,用于一维布局) 和 Grid (网格系统,用于二维布局)。这两种是目前最主流的布局方式。
    • 响应式设计(Responsive Design):使用 @media 查询来根据设备屏幕大小调整样式,使网页在不同设备上都能良好显示。

3. JavaScript (JS)

  • 是什么:一种高级编程语言,用于实现网页的动态行为和交互。它可以操作 HTML 和 CSS,处理用户事件,并与服务器进行通信。
  • 核心概念
    • DOM (Document Object Model) 操作:通过 document 对象,JS 可以获取、修改、添加或删除页面上的 HTML 元素。例如 document.getElementById('myId')
    • 事件处理(Event Handling):响应用户的行为,如点击、输入、鼠标移动等。例如 element.addEventListener('click', function() { ... })
    • 异步编程(Asynchronous Programming):处理耗时操作,如网络请求。
      • Callback (回调函数):传统方式。
      • Promise:ES6 引入,解决了回调地狱问题。
      • async/await:基于 Promise 的语法糖,使异步代码看起来像同步代码,更易读。
    • AJAX (Asynchronous JavaScript and XML):在不重新加载整个页面的情况下,与服务器进行异步通信。现代应用中通常使用 fetch() API 或 Axios 库来发送 JSON 数据。

二、现代前端框架:提升开发效率

为了处理日益复杂的 Web 应用,开发者通常会使用以下框架。

  • React:由 Facebook 开发,组件化声明式的 UI 库。
    • 核心组件(Components),将 UI 拆分成独立、可复用的部分。
    • 工作原理:使用虚拟 DOM (Virtual DOM),在内存中进行计算,然后只更新实际改变的部分,提高了性能。
    • 生态:庞大而活跃,有成熟的状态管理(Redux, Zustand)、路由(React Router)等解决方案。
  • Vue.js:由尤雨溪开发,易学易用,渐进式框架。
    • 核心响应式数据绑定,数据改变,视图自动更新。
    • 特点:上手快,文档友好,社区生态完善。
  • Angular:由 Google 开发,全能型框架。
    • 核心:提供了完整的解决方案,包括路由、依赖注入、状态管理等。
    • 特点:适合大型企业级应用,学习曲线相对陡峭。

三、工程化:高效协作和自动化

现代前端开发已经不仅仅是写代码,还需要使用一系列工具来提高效率。

  • 包管理器npm, yarn, pnpm。用于管理项目依赖。
  • 构建工具
    • Webpack:将多个模块打包成一个或多个文件,并能处理资源依赖、代码压缩等。
    • Vite:基于 ES Modules 的新一代构建工具,开发模式下速度极快。
  • 转译工具Babel。将 ES6+ 的代码转译为浏览器兼容的 ES5 代码。
  • 代码规范ESLint (代码风格检查), Prettier (代码格式化)。
  • 版本控制Git。用于团队协作和代码版本管理。

好的,让我们详细、全面地介绍 CSS 选择器。选择器是 CSS 的核心,掌握它才能精确地控制网页样式。我们将从基础到高级,系统地梳理不同类型的选择器及其用法。


一、基础选择器 (Basic Selectors)

这些是 CSS 中最简单、最常用的选择器,用于直接选中元素。

1. 元素选择器 (Type Selector)

  • 作用:根据元素的标签名来选择元素。
  • 语法element_name { ... }
  • 示例
    • p { color: blue; }:选中所有 <p> 元素。
    • h1 { font-size: 24px; }:选中所有 <h1> 元素。

2. 类选择器 (Class Selector)

  • 作用:根据元素的 class 属性来选择元素。
  • 语法.class_name { ... }
  • 特点:一个元素可以有多个类名,类名可以重复使用,非常灵活。
  • 示例
    • .highlight { background-color: yellow; }:选中所有 class 属性中包含 highlight 的元素。

3. ID 选择器 (ID Selector)

  • 作用:根据元素的 id 属性来选择元素。
  • 语法#id_name { ... }
  • 特点:在 HTML 文档中,一个 id 属性的值必须是唯一的
  • 示例
    • #header { height: 100px; }:选中 idheader 的唯一元素。

4. 通用选择器 (Universal Selector)

  • 作用:选择页面上的所有元素。
  • 语法* { ... }
  • 示例
    • * { margin: 0; padding: 0; }:清除所有元素的默认外边距和内边距,常用于 CSS 重置。

二、组合选择器 (Combinators)

这些选择器用于根据元素之间的关系来选择元素,如父子关系、兄弟关系等。

1. 后代选择器 (Descendant Selector)

  • 作用:选择某个元素内部的所有后代元素(包括子元素、孙子元素等)。
  • 语法ancestor descendant { ... },用空格分隔。
  • 示例
    • ul li { list-style-type: none; }:选中所有 <ul> 内部的 <li> 元素。

2. 子选择器 (Child Selector)

  • 作用:选择某个元素的直接子元素
  • 语法parent > child { ... },用 > 分隔。
  • 示例
    • ul > li { list-style-type: none; }:只选中 <ul> 的直接子元素 <li>
    • 区别:与后代选择器相比,更加精确,性能也更好。

3. 相邻兄弟选择器 (Adjacent Sibling Selector)

  • 作用:选择紧接在另一个元素后面的兄弟元素
  • 语法element + adjacent_element { ... },用 + 分隔。
  • 示例
    • h1 + p { margin-top: 0; }:选中紧跟在 <h1> 后的第一个 <p> 元素。

4. 通用兄弟选择器 (General Sibling Selector)

  • 作用:选择某个元素后面所有的兄弟元素(不限于紧邻的)。
  • 语法element ~ sibling { ... },用 ~ 分隔。
  • 示例
    • h1 ~ p { margin-top: 0; }:选中 <h1> 后的所有 <p> 元素。

三、属性选择器 (Attribute Selectors)

这些选择器根据元素的属性及其值来选择元素。

  • [attribute]:选择具有该属性的元素。
    • [title] { ... }:选中所有具有 title 属性的元素。
  • [attribute=”value”]:选择具有特定属性和值的元素。
    • input[type="text"] { ... }:选中所有 type 属性值为 text<input> 元素。
  • [attribute~=”value”]:选择属性值中包含特定独立单词的元素。
    • [class~="box"] { ... }:选中所有 class 属性中包含 box 这个独立单词的元素(如 class="card box")。
  • [attribute|=”value”]:选择属性值以特定字符串开头(后跟连字符 -)的元素。
    • [lang|="en"] { ... }:选中 lang 属性值为 enen-us 等的元素。
  • [attribute^=”value”]:选择属性值以特定字符串开头的元素。
    • a[href^="https"] { ... }:选中所有 href 属性以 https 开头的 <a> 元素。
  • [attribute$=”value”]:选择属性值以特定字符串结尾的元素。
    • img[src$=".png"] { ... }:选中所有 src 属性以 .png 结尾的 <img> 元素。
  • [attribute*=”value”]:选择属性值中包含特定字符串的元素。
    • [title*="hello"] { ... }:选中所有 title 属性值中包含 hello 的元素。

四、伪类选择器 (Pseudo-class Selectors)

伪类用于选择元素的特定状态

  • a:link:未访问的链接。
  • a:visited:已访问的链接。
  • a:hover:鼠标悬停在元素上。
  • a:active:元素被点击时。
  • element:focus:元素获得焦点时(常用于表单)。
  • :nth-child(n):选择属于其父元素的第 n 个子元素。
  • :first-child:选择属于其父元素的第一个子元素。
  • :last-child:选择属于其父元素的最后一个子元素。
  • :not(selector):选择不匹配指定选择器的元素。
  • :empty:选择没有子元素或文本内容的元素。

五、伪元素选择器 (Pseudo-element Selectors)

伪元素用于选择元素的特定部分

  • ::first-line:选择元素的第一行。
  • ::first-letter:选择元素的首个字母。
  • ::before:在元素的内容前面插入生成的内容。
  • ::after:在元素的内容后面插入生成的内容。

优先级与层叠

当多个选择器选中同一个元素并应用不同的样式时,浏览器会根据**优先级(Specificity)**来决定最终样式。

  • 优先级计算
    • !important:最高优先级。
    • 行内样式:1000
    • ID 选择器:100
    • 类、属性、伪类选择器:10
    • 元素、伪元素选择器:1
    • 通用选择器:0
  • 举例
    • p { color: red; }(优先级 1)
    • .highlight { color: blue; }(优先级 10)
    • #main p { color: green; }(优先级 101)

在这个例子中,即使 p 的样式最先声明,但 main p 的优先级最高,因此 <p id="main"> 的字体颜色最终会是绿色。

好的,让我们来全面、深入地介绍 Maven,并剖析一些关键细节以及面试中可能遇到的问题。


一、Maven 核心概念

1. 什么是 Maven?

Maven 是一个项目管理和构建自动化工具。它提供了一套标准化的项目结构、统一的构建生命周期,并依赖于一个中央仓库来管理项目所需的依赖。

它的核心思想是**“约定优于配置”(Convention over Configuration)**,这意味着它有一套默认的项目目录结构和构建流程。只要你遵循这些约定,就可以用很少的配置完成复杂的构建任务。

2. 为什么需要 Maven?

在 Maven 出现之前,Java 项目的构建非常混乱:

  • 项目结构不统一:每个项目都有自己的目录结构,新成员需要花时间熟悉。
  • 依赖管理混乱:需要手动下载所有 JAR 包,并添加到项目中,非常容易出错。
  • 构建过程不统一:编译、测试、打包等操作需要手动执行脚本,效率低下。

Maven 通过标准化解决了这些问题:

  • 标准化的项目结构:所有 Maven 项目都遵循相同的目录结构,如 src/main/javasrc/test/java 等。
  • 统一的依赖管理:通过在 pom.xml 中声明依赖,Maven 会自动从仓库下载并管理。
  • 标准化的构建生命周期:定义了一系列标准的构建阶段(如 compile, test, package),可以一键执行。

二、Maven 的核心组成部分

1. POM (Project Object Model)

  • 概念pom.xml 文件是 Maven 项目的核心配置文件。它定义了项目的元数据、依赖、插件、构建配置等所有信息。
  • 重要标签
    • <project>:根元素。
    • <groupId>, <artifactId>, <version>:项目的唯一标识符,通常称为 GAV 坐标
    • <packaging>:打包类型,如 jar, war, pom
    • <dependencies>:定义项目所需的依赖。
    • <parent>:继承父 POM,实现依赖的统一管理。
    • <build>:定义构建过程,如插件配置。

2. 依赖管理 (Dependency Management)

  • 概念:通过在 <dependencies> 中定义 GAV 坐标,Maven 会自动从本地或远程仓库下载依赖。
  • 传递性依赖:如果你的项目依赖 A,A 又依赖 B,Maven 会自动将 B 也下载下来。这是其强大的地方,但有时也可能导致版本冲突。
  • :这是一个非常重要的标签,通常在父 POM 中使用。它只定义依赖的版本,但不实际引入。子项目继承后,只需声明 <artifactId><groupId>,版本号会自动继承,这能确保整个项目所有模块的依赖版本一致。

3. 仓库 (Repositories)

Maven 仓库是用来存放所有依赖 JAR 包的地方。它分为三种:

  • 本地仓库 (Local Repository)~/.m2/repository,首次下载的依赖会存放在这里,后续构建时会优先从这里读取。
  • 远程仓库 (Remote Repository)
    • 中央仓库 (Central Repository):Maven 官方维护的公共仓库,包含了绝大多数常用的开源库。
    • 私服 (Private Repository):企业内部搭建的仓库,用于存放公司内部的 JAR 包或作为中央仓库的代理,加快下载速度。

4. 生命周期与阶段 (Lifecycle & Phases)

Maven 有一套标准的构建生命周期,分为三个:

  • clean:清理项目。
    • clean:删除 target 目录。
  • default:构建项目。
    • validate
    • compile:编译源代码。
    • test:运行测试。
    • package:打包。
    • install:安装到本地仓库。
    • deploy:部署到远程仓库。
  • site:生成项目站点。

重要规则:执行某个阶段时,它之前的所有阶段都会按顺序执行。例如,mvn package 会自动执行 compiletest


三、面试常见问题与回答技巧

1. Maven 的 GAV 坐标是什么?有什么作用?

  • 回答:GAV 坐标是 Maven 项目的唯一标识符,由 <groupId>, <artifactId>, 和 <version> 三个元素组成。
  • 作用
    • 唯一性:确保每个项目和每个版本的依赖在仓库中都是唯一的。
    • 定位:Maven 通过 GAV 坐标来查找和下载依赖。
    • 传递性:在传递性依赖中,通过 GAV 坐标来识别和处理依赖关系。

2. <dependencyManagement><dependencies> 有什么区别?

  • 回答:这是面试高频问题,需要清晰地说明两者的职责。
  • 实际引入依赖。它用于在当前项目中添加一个具体的依赖,Maven 会立即下载并使用它。
  • 只定义版本。它只声明依赖的版本,但不实际引入。其主要目的是统一管理子模块的依赖版本。子模块继承父 POM 后,只需在自己的 <dependencies> 中声明 <groupId><artifactId>,版本号会自动从父 POM 中继承,这避免了版本不一致的问题。

3. 什么是 Maven 的生命周期?mvn installmvn deploy 有什么区别?

  • 回答
  • 生命周期:Maven 有三个标准的生命周期:cleandefaultsite。其中 default 包含了从编译到部署的所有阶段。
  • mvn install:执行 default 生命周期到 install 阶段。它会将项目打包,并安装到本地仓库。这样,其他本地项目就可以依赖这个包。
  • mvn deploy:执行 default 生命周期到 deploy 阶段。它会将项目打包,并部署到远程仓库(私服或中央仓库)。这使得其他团队成员或项目可以从远程仓库获取并使用这个包。

4. Maven 依赖冲突如何解决?

  • 回答:当多个依赖间接引入了同一个库的不同版本时,就会发生依赖冲突。
  • 解决策略
    • 依赖调解(Dependency Mediation):Maven 的默认规则是“路径最短者优先”。即在依赖树中,路径最短的那个版本会被选中。
    • 手动排除(Exclusion):如果默认规则不能解决问题,可以在 <dependency> 标签内使用 <exclusions> 标签手动排除有问题的传递性依赖。
    • 手动引入(Declaration):在 <dependencies> 中明确声明需要使用的版本。Maven 的另一个规则是“最近声明者优先”,即如果两个依赖路径长度相同,先声明的那个会被使用。但更好的做法是直接在父 POM 的 <dependencyManagement> 中统一版本。

好的,我们来详细梳理你提供的这份关于会话技术的笔记,并按照你要求的逻辑,以一种更清晰、更专业的面试回答或技术讲解的方式进行重新组织和补充。


一、会话技术概述:解决 HTTP 无状态问题

HTTP 协议本身是无状态的,它不记得上一次请求的任何信息。为了在多次请求之间共享数据并识别用户,引入了两种核心的会话技术:客户端会话(Cookie)和服务器端会话(Session)。

二、客户端会话:Cookie

核心概念:Cookie 是服务器发送给浏览器并存储在客户端的一小段文本信息。浏览器在下次访问同一服务器时会自动将该 Cookie 携带在请求中。

  • 一次请求响应可以发送多个 Cookie 吗?
    • 回答可以。服务器可以在一个响应中通过多个 Set-Cookie 响应头来设置多个 Cookie。浏览器会分别存储这些 Cookie,并在后续请求中将它们全部发送给服务器。
  • Cookie 支持中文传输吗?
    • 回答在大多数现代服务器和浏览器中都支持。 在早期的 Servlet 容器(如 Tomcat 8.0 之前),Cookie 不能直接存储中文,需要手动进行 URL 编码(URLEncoder)和解码(URLDecoder)。在 Tomcat 8.0 及以后版本,容器默认支持 UTF-8 编码,可以直接存储中文。
  • Cookie 的过期时间如何设置?
    • 回答:通过 response.addCookie(cookie) 方法,并调用 cookie.setMaxAge(int expiry) 方法来设置。
      • expiry > 0:表示 Cookie 将被持久化到客户端硬盘,有效期为 expiry 秒。即使浏览器关闭,Cookie 依然存在,直到过期。
      • expiry = 0:表示立即删除该 Cookie。常用于退出登录功能。
      • expiry < 0默认值。表示 Cookie 只在内存中存在,当浏览器关闭时,该 Cookie 就会被删除。
  • 存储位置:存储在客户端(浏览器)。
  • 大小限制:单个 Cookie 最大约 4KB。
  • 数量限制:一个服务器最多可以向一个浏览器保存 20 个 Cookie,一个浏览器最多可以保存 300 个 Cookie(这些是早期的规范,现代浏览器已放宽,但仍有限制)。
  • 安全性:数据以明文形式存储,且容易被用户修改,安全性较差。

这个案例是经典的 Cookie 用法。

  • 首次访问:服务器接收请求,判断 Cookie 中没有记录上次访问时间。服务器创建一个新的 Cookie,存储当前时间,并将其发送给浏览器。
  • 再次访问:浏览器自动将上次存储的 Cookie 发送给服务器。服务器读取 Cookie,获取上次访问时间,并将其显示给用户。然后,服务器可以更新 Cookie 的时间,再次发送给浏览器。

三、服务器端会话:HttpSession

核心概念:Session 是服务器端为每个客户端创建的一个对象,用于在一次会话的多个请求之间存储和共享数据。

1. Session 的快速使用

  • 获取对象:通过 HttpServletRequest 对象的 getSession() 方法来获取 HttpSession 对象。
  • 使用对象(域对象):Session 是一个域对象,提供了以下方法:
    • void setAttribute(String name, Object value):将数据以键值对形式存储到 Session 中。
    • Object getAttribute(String name):根据键名获取存储的数据。
    • void removeAttribute(String name):根据键名删除存储的数据。

2. Session 的失效时间

  • 服务器关闭:当服务器关闭时,所有 Session 对象都会被销毁。

  • 手动失效:调用 session.invalidate() 方法,可以立即强制 Session 失效。常用于用户退出登录。

  • 默认失效时间:默认情况下,Session 有一个超时时间,通常是 30 分钟。如果客户端在 30 分钟内没有向服务器发送任何请求,Session 就会自动失效。这个时间可以在 web.xml 中配置。

  • 对的 👍,在 **Java Web 应用(Servlet 容器,比如 Tomcat)**里,Session 的默认失效时间通常是 30 分钟

    要修改它,可以在 web.xml 中配置 <session-config>

    1
    2
    3
    4
    5
    6
    7
    <web-app ...>
    <!-- session 过期时间设置(单位:分钟) -->
    <session-config>
    <session-timeout>60</session-timeout>
    </session-config>
    </web-app>

📌 说明:

  • 60 表示 Session 在 60 分钟无请求时会自动失效。
  • 默认值是 30 分钟,如果不配置,就走默认。
  • 这个配置对所有用户 Session 都生效。

🔧 在代码里动态设置(单个 Session)

除了在 web.xml 里全局配置,还可以针对某个用户的 Session 动态修改:

1
2
3
HttpSession session = request.getSession();
session.setMaxInactiveInterval(120 * 60); // 单位:秒,这里是 2 小时


🔧 特殊情况

  • 如果 session-timeout 设置为 0 → Session 永不超时(危险 ⚠️,容易导致内存泄露)。
  • 如果设置为 负数(如 -1) → 表示 Session 仅在浏览器关闭时销毁(有些容器支持,有些不支持。

3. Session 的特点

  • 存储位置:存储在服务器端。
  • 大小限制:可以存储任意类型、任意大小的数据(受限于服务器内存)。
  • 安全性:由于数据存储在服务器,客户端只传递一个 Session ID,安全性相对较高。

4. 面试题:Cookie 和 Session 的区别?

  • 存储位置:Cookie 存储在客户端,Session 存储在服务器端
  • 数据大小:Cookie 的大小有限制(约 4KB),Session 的大小没有限制
  • 安全性:Session 相对安全,Cookie 相对不安全。因为 Session ID 难以被猜测,而 Cookie 存储的数据是明文。
  • 服务器开销:Session 会占用服务器内存,在高并发场景下可能成为性能瓶颈;而 Cookie 不占用服务器资源。

四、Servlet 三大域对象的关系(按照你的逻辑)

域对象(Scope Object)是指那些可以用来在不同组件之间共享数据的对象。Servlet 规范提供了三个这样的对象,它们的生命周期不同,从而决定了数据的共享范围。

  • request 域:生命周期最短。数据只能在一次请求-响应的周期内共享。即使请求被转发(forward),数据也依然可见。
  • session 域:生命周期中等。数据可以在一次会话(多个请求)之间共享。只要浏览器不关闭,且 Session 未超时或失效,数据就一直存在。
  • servletContext 域:生命周期最长。数据可以在整个 Web 应用中共享。从 Web 应用启动到关闭,数据都一直存在。

三大域对象之间的关系是包含关系,即 request < session < servletContext。

  • request 包含于 session:一个 Session 中可以有多个请求。
  • session 包含于 servletContext:一个 Web 应用(ServletContext)中可以有多个 Session。

这个关系图清晰地展示了它们各自的生命周期和数据共享范围,是理解 Servlet 域对象的关键。

啊哈~你这个场景就是 同一个库的两个版本必须共存,典型的 “深度 JAR hell” 🚨。
比如:

  • 你的系统用到 老版本 log4j
  • 但某个第三方库硬性依赖 新版本 log4j

这时候如果放到同一个 classpath 里,肯定会冲突。解决思路主要有三类:


✅ 两个版本 JAR 共存的解决方案

1. ClassLoader 隔离(推荐方式)

利用 不同的类加载器 来隔离两个版本的依赖,让它们互不干扰。

  • Web 容器隔离

    • 把一个版本放在 WEB-INF/lib,另一个放在全局 lib/ext
    • 通过 parent-first / parent-last 策略控制加载。
  • 自定义 ClassLoader

    • URLClassLoader 指定不同版本 JAR 的路径。

    • 哪个模块需要哪个版本,就用哪个 ClassLoader。

    • 示例:

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      URLClassLoader loaderV1 = new URLClassLoader(
      new URL[]{ new File("lib/lib-old.jar").toURI().toURL() },
      null // 不委托父加载器
      );

      URLClassLoader loaderV2 = new URLClassLoader(
      new URL[]{ new File("lib/lib-new.jar").toURI().toURL() },
      null
      );

      Class<?> clazz1 = loaderV1.loadClass("com.example.MyClass");
      Class<?> clazz2 = loaderV2.loadClass("com.example.MyClass");

👉 OSGi、Spring Boot 的 ClassLoader 层级就是用这种方法解决多版本依赖的。


2. 包重定位(Shading / Relocation)

把其中一个版本的包路径 改名,让 JVM 认为它是完全不同的类库。

  • 工具:

    • jarjar
    • Maven Shade(即使你不用 Maven,也可以单独跑插件)
    • Gradle Shadow
  • 原理:自动把 org.apache.commons 改成 myproject.shaded.org.apache.commons

  • 示例(JarJar 规则):

    1
    rule org.apache.log4j.** com.myapp.shaded.log4j.@1

这样,项目就能同时用 log4j-1.2log4j-2.x


3. 服务解耦 / 模块化

如果两个版本都要在 同一个 JVM 里用,但 ClassLoader 太麻烦:

  • 拆分成微服务 / 子进程,让不同进程使用不同版本的依赖。
  • 或者在 Java 9+ 模块系统 (JPMS) 下,给不同模块声明不同依赖,避免直接冲突。

🚀 实际建议

  • 如果你是 自己开发的项目 → 用 包重定位 比较简单。
  • 如果是 插件式系统 / 容器运行 → 用 ClassLoader 隔离
  • 如果是 重量级系统(比如需要不同版本的 JDBC 驱动) → 拆进程更干净。

要不要我帮你写一个 最小可运行的 Java Demo(两个版本 JAR 同时运行,通过 ClassLoader 隔离)?这样你可以直接跑起来看看效果。

好的,我将根据您提供的这份详尽的笔记,以更专业、更系统的方式,对其进行重新组织、补充和深入解析,使其成为一份高质量的技术讲解或面试回答。


一、CSS 选择器概述:精准定位网页元素

选择器是 CSS 的核心,它用于精确地选择 HTML 文档中的元素,并为其应用样式。掌握不同类型的选择器及其组合,是高效编写 CSS 代码的关键。

二、基础选择器 (Basic Selectors)

基础选择器用于根据最基本的属性(标签、类、ID)来选择元素。

1. 标签选择器 (Type Selector)

  • 原理:根据元素的标签名称来匹配。

  • 优点:简单、直接,可以一次性选中所有同类型的元素。

  • 缺点:过于笼统,无法对个别元素进行精确控制。

  • 示例

    CSS

    1
    div { color: red; } /* 所有<div>标签的字体颜色都为红色 */

2. 类选择器 (Class Selector)

  • 原理:根据元素的 class 属性值来匹配。

  • 优点

    • 高度复用:可以在多个元素上使用同一个类名。
    • 灵活:一个元素可以拥有多个类名,用空格分隔。
  • 示例

    CSS

    1
    .cl1 { color: red; } /* 所有class属性包含cl1的元素,字体颜色为红色 */

3. ID 选择器 (ID Selector)

  • 原理:根据元素的 id 属性值来匹配。

  • 优点唯一性,精确地选中文档中唯一的元素。

  • 缺点:不具复用性,一个 ID 在文档中只能出现一次。

  • 示例

    CSS

    1
    #p1 { color: red; } /* id为p1的唯一元素,字体颜色为红色 */

4. 通配符选择器 (Universal Selector)

  • 原理:匹配文档中所有的元素。

  • 优点:可以快速设置全局样式,常用于重置浏览器默认样式。

  • 缺点:性能开销大,因为浏览器需要遍历所有元素,应谨慎使用。

  • 示例

    CSS

    1
    * { margin: 0; padding: 0; } /* 清除所有元素的内外边距 */

三、组合选择器 (Combinators)

组合选择器通过符号连接多个选择器,根据元素之间的关系来匹配。

1. 交集选择器 (Intersection Selector)

  • 原理:同时满足多个选择器条件的元素。

  • 语法selector1selector2 { ... },中间没有空格

  • 示例

    CSS

    1
    div.cl1 { color: red; } /* 既是<div>标签,又拥有cl1这个class的元素 */

2. 并集选择器 (Union Selector)

  • 原理:匹配所有满足其中任意一个选择器条件的元素。

  • 语法selector1, selector2 { ... },以逗号分隔。

  • 示例

    CSS

    1
    a, p { color: red; } /* 所有<a>标签和所有<p>标签的字体颜色都为红色 */

3. 子选择器 (Child Selector)

  • 原理:匹配直接子元素。

  • 语法parent > child { ... },以 > 符号分隔。

  • 示例

    CSS

    1
    #outer > span { color: red; } /* 只选中id为outer的<div>的直接子元素<span> */

4. 后代选择器 (Descendant Selector)

  • 原理:匹配所有位于祖先元素内部的所有后代元素。

  • 语法ancestor descendant { ... },以空格分隔。

  • 示例

    CSS

    1
    #outer span { color: red; } /* 选中id为outer的<div>内部的所有<span>,无论嵌套多深 */

四、属性选择器 (Attribute Selectors)

属性选择器根据元素的属性及其值来匹配。

  • [attribute]:匹配具有该属性的元素。
    • 示例:[class] 匹配所有具有 class 属性的元素。
  • [attribute=”value”]:匹配属性值完全相等的元素。
    • 示例:[type="text"] 匹配 type 属性值恰好为 "text" 的元素。
  • [attribute~=”value”]:匹配属性值中包含该独立单词的元素。
  • [attribute^=”value”]:匹配属性值以该字符串开头的元素。
  • [attribute$=”value”]:匹配属性值以该字符串结尾的元素。
  • [attribute*=”value”]:匹配属性值中包含该字符串的任意位置的元素。

五、伪类与伪元素选择器 (Pseudo-class & Pseudo-element Selectors)

  • 伪类:用一个冒号 :,表示元素在特定状态下的样式。
    • :link:未访问的链接。
    • :visited:已访问的链接。
    • :hover:鼠标悬停状态。
    • :active:被点击状态。
    • :focus:获得焦点状态(常用于表单)。
  • 伪元素:用两个冒号 ::,表示元素的特定部分
    • ::before::after:在元素内容的前后插入内容。
    • ::first-line:元素的文本第一行。
    • ::first-letter:元素的文本第一个字母。

六、选择器优先级 (Specificity)

这是 CSS 的核心机制,决定了当多个规则应用到同一个元素时,哪一个会生效。

  • 计算规则:优先级由选择器的类型数量决定。
    1. 行内样式:优先级最高,为 1000
    2. ID 选择器:优先级为 100
    3. 类、属性、伪类选择器:优先级为 10
    4. 元素、伪元素选择器:优先级为 1
    5. 通用选择器 *:优先级为 0
  • !important:最高优先级,但会破坏层叠机制,应避免使用

如何计算:

将每个选择器的优先级数字相加,得到一个最终的权重值。权重值越高的规则越优先。

  • 示例:
    • p:权重 1
    • .cl1:权重 10
    • div.cl1:权重 1 + 10 = 11
    • #p1:权重 100
    • #outer span:权重 100 + 1 = 101

!important 会凌驾于所有这些规则之上。当你遇到样式不生效的问题时,首先要检查优先级,其次是是否有 !important 的存在。

好的,让我们来详细、深入地解释 BOMDOM 这两个 JavaScript 在浏览器中操作网页的核心概念。


BOM (Browser Object Model)

1. 概念

BOM,即浏览器对象模型,是 JavaScript 用于操作浏览器窗口的一套 API。它没有统一的标准,而是由各个浏览器厂商各自实现的。BOM 的核心是 window 对象,它既代表了浏览器窗口本身,也是 JavaScript 全局作用域的唯一对象。

2. BOM 的核心对象与功能

BOM 提供了以下关键对象,用于与浏览器进行交互:

  • window 对象
    • 全局对象window 是 JavaScript 的全局对象,所有全局变量和函数都是 window 对象的属性和方法。
    • 窗口控制window.open() (打开新窗口), window.close() (关闭当前窗口)。
    • 定时器setTimeout()setInterval(),用于延迟或重复执行代码。
    • 导航与位置window.location,用于获取和修改当前页面的 URL。
  • location 对象
    • URL 信息:提供了当前 URL 的详细信息,如 location.href (完整 URL), location.protocol (协议), location.hostname (主机名), location.pathname (路径)。
    • 页面跳转location.href = 'new_url'location.assign('new_url') (有历史记录), location.replace('new_url') (不留历史记录), location.reload() (刷新页面)。
  • history 对象
    • 历史记录:提供了对浏览器历史记录的访问。
    • 导航history.back() (后退), history.forward() (前进), history.go(n) (前进或后退 n 页)。
  • navigator 对象
    • 浏览器信息:提供了关于浏览器本身的信息,如 navigator.userAgent (用户代理字符串), navigator.platform (操作系统), navigator.onLine (是否在线)。
  • screen 对象
    • 屏幕信息:提供了关于用户屏幕的信息,如 screen.width (屏幕宽度), screen.height (屏幕高度)。

3. BOM 的特点

  • 没有标准:BOM 没有 W3C 的正式标准,不同浏览器可能在实现上存在差异。
  • 以 window 为核心:所有 BOM 对象都是 window 对象的属性。

DOM (Document Object Model)

1. 概念

DOM,即文档对象模型,是 JavaScript 用于操作 HTML 或 XML 文档的一套 API。它将整个 HTML 文档解析为一个树形结构,每个 HTML 标签、属性和文本都成为了树中的一个节点(Node)。DOM 是一个由 W3C 制定的标准。

2. DOM 的核心对象与功能

DOM 的核心是 document 对象,它是整个文档的入口。

  • document 对象
    • DOM 树的根节点:代表整个 HTML 文档。
    • 元素获取:提供了多种方法来获取页面上的元素:
      • document.getElementById('id'):通过 ID 获取单个元素。
      • document.getElementsByClassName('class'):通过类名获取元素集合。
      • document.getElementsByTagName('tag'):通过标签名获取元素集合。
      • document.querySelector('css_selector'):通过 CSS 选择器获取第一个匹配的元素。
      • document.querySelectorAll('css_selector'):通过 CSS 选择器获取所有匹配的元素。
  • 元素节点(Element Nodes)
    • 操作元素:提供了操作 HTML 元素的方法和属性:
      • element.innerHTML:获取或设置元素的 HTML 内容。
      • element.style.color:修改元素的行内样式。
      • element.setAttribute('attr', 'val'):设置元素的属性。
      • element.addEventListener('event', handler):添加事件监听器。
  • DOM 树操作
    • 创建document.createElement('tag') (创建新元素)。
    • 添加parent.appendChild(child) (添加子元素)。
    • 删除parent.removeChild(child) (删除子元素)。
    • 替换parent.replaceChild(newChild, oldChild) (替换子元素)。

3. DOM 的特点

  • 有标准:DOM 是由 W3C 制定的标准,因此在不同浏览器中的实现差异较小。
  • 树形结构:将 HTML 文档抽象为树形结构,使得我们可以通过编程的方式像操作树一样操作网页。

BOM 和 DOM 的核心区别与联系

特性 BOM (Browser Object Model) DOM (Document Object Model)
主要功能 操作浏览器窗口 操作网页文档内容
核心对象 window document
标准化 无统一标准(各浏览器实现不同) 有 W3C 标准
层级关系 window 包含了 location, history, navigator 等,也包含了 document 对象 document 包含了 HTML 元素的树形结构。

它们的关系:

BOM 是 DOM 的超集。window 对象是所有 JavaScript 对象的全局对象,它包含了 document 对象。换句话说,window.document 就是 document。因此,当我们使用 document 对象来操作网页时,实际上是通过 window 对象来访问的。

这个关系可以理解为:

window -> 浏览器窗口

document -> 窗口中加载的文档

掌握 BOM 和 DOM 是成为一个合格前端工程师的基础,它们是 JavaScript 驱动 Web 页面交互的两个最重要的工具。

好的,我将根据您提供的这份 JS 学习笔记,以一种更深入、更条理化的方式,逐一进行详细的展开和补充。这不仅是对知识点的梳理,也包含了其背后的原理和一些进阶用法,使其更具面试和实战价值。


一、JavaScript 中的数据类型

1. 基础数据类型(原始数据类型)

  • number 类型

    • 深入:JS 采用 IEEE 754 标准的双精度 64 位浮点数来表示所有数字。这意味着它没有单独的整数类型,所有数字都是浮点数。
    • NaN (Not a Number):表示非数字值。需要注意的是,typeof NaN 结果是 numberNaN 不等于自身,NaN === NaN 结果为 false
    • Infinity:表示正无穷大,+Infinity-Infinity 分别表示正负无穷大。
    • 0.1 + 0.2 !== 0.3:由于浮点数表示的精度问题,这个经典问题需要注意。
  • boolean 类型

    • 深入truefalse。在条件判断中,所有数据类型都会被隐式转换为布尔值。
  • undefined 类型

    • 深入:表示一个未定义的变量或变量未被赋值typeof undefined 结果为 undefined。它是一个值也是一个类型。
    • null:与 undefined 的区别在于,null 是一种意图,表示变量被显式地赋予了“空”值。typeof null 结果为 object,这是一个历史遗留的 bug。
  • string 类型

    • 深入:JS 中没有字符类型。字符串是不可变的,一旦创建就不能修改。

    • 反引号( ):ES6 引入,支持模板字面量。它允许在字符串中嵌入变量和表达式,并支持多行书写,极大提升了字符串拼接的便利性。

      JavaScript

      1
      2
      const name = "张三";
      console.log(`你好,我的名字是${name}。`);

2. 数据类型转换

  • 转换为 numberNumber(value)

    • 深入:会尝试将参数转换为数字。非数字字符串会返回 NaNnull 转为 0undefined 转为 NaN
  • 转换为 booleanBoolean(value)

    • 深入:所有能被转换为 false 的值被称为假值(Falsy Value)
    • 假值列表0, -0, null, false, NaN, undefined, '' (空字符串)。
    • 真值(Truthy Value):除上述假值外,所有值都是真值,包括空数组 [] 和空对象 {}
  • 转换为 stringString(value)value + ''

    • 深入value + '' 是最常用的技巧,利用了 JS 的隐式类型转换。
  • parseInt 和 parseFloat

    • 深入:这两个函数专门用于从字符串开头解析出数字。

    • parseInt:解析整数。遇到第一个非数字字符就停止解析。

      JavaScript

      1
      2
      parseInt("100px"); // 100
      parseInt("a100"); // NaN
    • parseFloat:解析浮点数。遇到第一个非数字字符(除了小数点)就停止解析。

二、JavaScript 中的弹出框

这些是浏览器提供的 BOM API。

  • alert(message)警告框。阻塞式,显示一条消息,不返回任何值。
  • prompt(message, default_value)询问框。返回用户输入的字符串,如果点击“取消”则返回 null
  • confirm(message)确认框。返回一个布尔值,用户点击“确定”返回 true,点击“取消”返回 false

三、条件运算符:=====

  • ==(宽松相等):只比较,不比较类型。它在比较前会进行隐式类型转换

    JavaScript

    1
    2
    5 == "5";     // true
    null == undefined; // true
  • ===(严格相等):既比较,也比较类型。不会进行类型转换。

    JavaScript

    1
    2
    5 === "5";    // false
    null === undefined; // false
  • 面试建议:在实际开发中,强烈建议使用 ===,以避免不必要的类型转换带来的 bug。

四、字符串和数组

  • 字符串
    • 深入string 是一个对象,拥有 length 属性和许多方法(如 split(), slice(), indexOf() 等),但它不可变
  • 数组
    • 特点
      1. 动态长度:JS 数组的长度是可变的,你可以随时添加或删除元素。
      2. 异构性:一个数组可以存放不同数据类型的元素,如 [1, "hello", true]
    • 方法
      • push(), pop():在数组末尾添加和删除。
      • unshift(), shift():在数组开头添加和删除。
      • splice():功能强大的方法,用于删除、替换或添加元素。
      • forEach(), map(), filter():常用的遍历方法。

五、函数和对象

  • 函数
    • 深入:在 JS 中,函数是一等公民(First-Class Citizens)。这意味着函数可以作为参数传递、作为返回值,也可以赋值给变量。
  • 对象
    • 深入:JS 对象是键值对的集合。键是字符串,值可以是任意类型。
    • 访问属性
      • 点语法:obj.prop
      • 中括号语法:obj['prop']。后者适用于键名包含特殊字符或动态键名的情况。

六、定时器

  • setTimeout(callback, delay)
    • 用途:只执行一次
    • 深入delay 参数是最小延迟时间。由于 JS 是单线程的,如果主线程被阻塞,callback 的执行会延迟。
  • setInterval(callback, delay)
    • 用途重复执行。
    • 深入:每次执行完回调函数后,setInterval 都会将下一个回调任务放入队列。

七、ES6 新特性(重要)

  • let 和 const
    • var 的问题:全局作用域,可以被重复声明,有变量提升。
    • let块级作用域{} 内),不能重复声明,没有变量提升。
    • const块级作用域,用于声明常量。一旦声明,其引用地址不能改变。
  • 字符串模板字面量:使用反引号( )创建多行字符串和嵌入变量。
  • 箭头函数
    • 语法const func = (param) => { ... }
    • this 指向:箭头函数没有自己的 this,它会捕获其所在上下文的 this 值。这解决了传统函数中 this 绑定复杂的问题。
  • 可变参数...args,用于函数接收不确定数量的参数。

八、DOM

  • 什么是 DOM
    • 深入:DOM 是浏览器将 HTML 文档解析后生成的树形结构,是 JavaScript 操作网页的接口。HTML 文件是文本,DOM 是一个对象,可以被 JS 编程控制。
  • DOM 操作
    • 获取标签对象
      • document.getElementById()
      • document.getElementsByClassName()
      • document.querySelector() (更常用)
    • 操作属性
      • element.attribute = '...' (例如 element.style.color = 'red')
      • element.setAttribute('attr', 'value')
    • 操作内容
      • element.innerHTML:获取或设置元素的 HTML 内容(包括子标签)。
      • element.textContent:获取或设置元素的文本内容(不含 HTML)。

好的,我将根据您提供的这份详尽的 Web 技术笔记,以一种更专业、更系统的方式,对其进行重新组织、补充和深入解析,使其成为一份高质量的技术讲解或面试回答。


一、Web 架构与网络基础

1. 常见的软件架构

  • B/S 架构 (Browser/Server)
    • 概念:浏览器和服务器架构。客户端只需安装一个通用的浏览器,业务逻辑和数据都存储在服务器端。
    • 优点:易于维护和升级,跨平台性好。
    • 缺点:对网络依赖性强,用户体验可能不如 C/S 架构。
  • C/S 架构 (Client/Server)
    • 概念:客户端和服务器架构。客户端需要安装专用的应用程序,例如桌面 QQ、微信等。
    • 优点:用户体验好,响应速度快,可以离线使用。
    • 缺点:维护和升级复杂,需要为不同平台开发不同版本。

2. 网络编程三要素

这是所有网络通信的基础,理解这三点至关重要。

  • IP 地址:设备在网络上的唯一标识,类似于你的家庭住址。它用于在网络中定位到一台具体的计算机。
  • 端口号:应用程序在计算机上的唯一标识,类似于你家里的电话号码或门牌号。一台计算机上可以运行多个应用程序,端口号用于区分它们。
  • 协议:通信规则。就像人与人交流需要遵循共同的语言一样,网络设备之间通信也需要遵循特定的协议,如 HTTP、FTP、TCP 等。

3. 资源的分类

  • 静态资源
    • 特点:内容固定,无需服务器端处理,可由浏览器直接解析。
    • 原理:当浏览器请求静态资源时,Web 服务器直接从文件系统中读取文件并返回。
    • 示例:HTML、CSS、JS、图片等。
  • 动态资源
    • 特点:内容动态生成,需要服务器端处理后才能返回给浏览器。
    • 原理:当浏览器请求动态资源时,Web 服务器将请求交给后台程序(如 Servlet 容器),后台程序执行业务逻辑,生成静态内容(如 HTML、JSON 等),然后返回给服务器,服务器再返回给浏览器。
    • 示例:Servlet、JSP、PHP、ASP 等。

二、Web 服务器

1. 作用

Web 服务器(也称为 Web 容器)是一个软件,它负责处理 HTTP 请求,并提供静态资源和动态资源。它为动态资源(如 Servlet)提供了一个运行环境。

2. 常见的 Web 服务器

  • Tomcat:Apache 基金组织开发,中小型的 JavaEE 服务器免费且开源。它是一个Servlet 容器,能够运行 Servlet 和 JSP。
  • WebSphere:IBM 公司开发,大型的 JavaEE 服务器收费。功能强大,支持完整的 JavaEE 规范。
  • WebLogic:Oracle 公司开发,大型的 JavaEE 服务器收费
  • JBoss/WildFly:开源,但其商业版收费。

三、Tomcat 的使用与配置

  • 安装与启动:Tomcat 是免安装的,解压即可。启动前需要配置 JAVA_HOME 环境变量。启动后,默认监听 8080 端口。
  • 端口号修改
    • 面试题:修改 Tomcat 端口号在哪个文件?
    • 回答:在 Tomcat 的 conf 目录下的 server.xml 文件中,修改 <Connector> 标签的 port 属性。
    • 注意:HTTP 协议的默认端口号是 80。如果将 Tomcat 端口号改为 80,那么访问时就可以省略端口号,例如 http://localhost/
  • 项目部署
    • 静态项目:将 HTML、CSS、JS 等文件直接放到 webapps 目录下的文件夹中。
    • 动态项目:将包含 WEB-INF 文件夹的整个项目目录放到 webapps 目录下。
    • WEB-INF:这是动态项目的核心目录,具有特殊作用,外部无法直接通过 URL 访问该目录下的资源,保证了项目的安全性。
      • classes:存放所有编译后的 .class 字节码文件。
      • lib:存放项目依赖的第三方 .jar 包。
      • web.xml:Web 项目的核心配置文件,用于配置 Servlet、监听器、过滤器等。

四、Servlet 深入解析

1. Servlet 的概念和本质

  • 概念:Servlet 是运行在服务器端的 Java 程序,用于处理客户端请求并生成动态响应。它不是一个独立的程序,必须部署到支持 Servlet 的容器中(如 Tomcat)才能运行。
  • 本质:Servlet 的本质是一个接口。所有自定义的 Servlet 类都必须实现 javax.servlet.Servlet 接口

2. Servlet 的执行原理(详细解释)

这是一个非常重要的面试点,需要从请求-响应的整个流程来详细阐述。

  1. 客户端请求:用户在浏览器中输入 URL,向服务器发送一个 HTTP 请求。
  2. Web 服务器接收请求:Web 服务器(Tomcat)接收到这个请求。
  3. Servlet 容器处理:Tomcat 会根据请求 URL,在 web.xml 或通过注解(如 @WebServlet)查找匹配的 Servlet。
  4. Servlet 实例创建
    • 如果是第一次访问该 Servlet,Servlet 容器会创建一个该 Servlet 的实例。
    • 面试点:Servlet 是单例的,一个 Servlet 在容器中只会被创建一次。
  5. init() 方法执行
    • 在 Servlet 实例创建后,容器会立即调用它的 init() 方法。
    • init() 方法只在 Servlet 的生命周期中执行一次,用于完成一些初始化工作,如加载配置文件、数据库连接等。
  6. service() 方法执行
    • 每次客户端请求该 Servlet 时,容器都会调用它的 service() 方法。
    • service() 方法根据请求的 HTTP 方法(GET、POST 等),将请求分发给相应的 doGet()doPost() 方法。
    • 面试点service() 方法是处理请求的核心方法,它是多线程的,每个请求都会在新线程中执行 service() 方法。
  7. destroy() 方法执行
    • 当 Servlet 容器关闭,或者决定卸载该 Servlet 时,会调用其 destroy() 方法。
    • destroy() 方法也只执行一次,用于释放资源,如关闭数据库连接池。

总结:Servlet 的生命周期是:创建实例 -> 调用 init() (只一次) -> 调用 service() (多次) -> 调用 destroy() (只一次) -> 销毁实例

好的,我们来将“网络编程三要素”这一部分进行更深入、更详细的展开,并补充常见应用的默认端口号,使其更具实用性和面试价值。


一、网络编程三要素:深入解析

网络编程的本质就是让不同的计算机上的应用程序能够进行通信。要实现这一点,必须解决三个核心问题:

  1. 找到对方计算机:IP 地址
  2. 找到对方计算机上的应用程序:端口号
  3. 以什么样的规则进行通信:协议

这三者缺一不可。

1. IP 地址 (Internet Protocol Address)

  • 概念:IP 地址是分配给连接到网络中的设备(如计算机、手机、服务器)的一串数字标识。
  • 作用:它用于唯一地标识网络上的一个设备。数据包在网络中传输时,就是根据 IP 地址来路由和寻址的。
  • 版本
    • IPv4:由 32 位二进制数组成,通常表示为四个十进制数,用点分隔(例如 192.168.1.1)。由于地址资源枯竭,现在已经不够用。
    • IPv6:由 128 位二进制数组成,地址空间巨大,足以满足未来需求。
  • 类型
    • 公网 IP:在互联网上是唯一的,可以直接访问。
    • 内网 IP:在局域网内是唯一的,不能直接在互联网上访问。例如 192.168.x.x10.x.x.x

2. 端口号 (Port Number)

  • 概念:端口号是用于区分一台计算机上不同应用程序的数字标识。它的范围是从 065535
  • 作用:当一个数据包到达一台计算机时,操作系统会检查其目的端口号,然后将数据包交给监听该端口号的相应应用程序。
  • 与 IP 地址的关系:IP 地址解决了“数据包发送到哪台计算机”的问题,而端口号则解决了“数据包发送到这台计算机上的哪个应用程序”的问题。两者结合起来才能唯一确定一个网络连接的端点。

3. 协议 (Protocol)

  • 概念:协议是网络通信中数据传输的规则和约定。它定义了数据如何打包、传输、路由和接收。
  • 作用:确保通信双方能够理解彼此发送的数据。没有协议,数据包就是一堆无意义的字节。
  • 分层:网络协议通常是分层的,最经典的是 TCP/IP 协议栈
    • 应用层决定数据内容,如 HTTP、FTP、SMTP。
    • 传输层决定数据如何传输,如 TCP 和 UDP。
      • TCP (Transmission Control Protocol):面向连接、可靠、有序。适用于对数据完整性要求高的场景,如文件传输、网页浏览。
      • UDP (User Datagram Protocol):无连接、不可靠、速度快。适用于对实时性要求高的场景,如在线视频、游戏。
    • 网络层决定数据如何路由,如 IP 协议。
    • 数据链路层/物理层:负责物理设备的通信。

二、常见应用的默认端口号

了解这些默认端口号,可以帮助你更好地理解网络协议和服务。

  • Web 服务
    • HTTP (HyperText Transfer Protocol):80
    • HTTPS (HTTP Secure):443
    • Tomcat (默认):8080
    • WebLogic (默认):7001
  • 文件传输
    • FTP (File Transfer Protocol):21 (控制连接)
    • SFTP (SSH File Transfer Protocol):22
  • 远程登录与管理
    • SSH (Secure Shell):22
    • Telnet23
    • RDP (Remote Desktop Protocol):3389
  • 数据库服务
    • MySQL3306
    • PostgreSQL5432
    • SQL Server1433
    • Oracle1521
  • 邮件服务
    • SMTP (Simple Mail Transfer Protocol):25 (发送邮件)
    • POP3 (Post Office Protocol 3):110 (接收邮件)
    • IMAP (Internet Message Access Protocol):143 (接收邮件)
  • 其他常见服务
    • DNS (Domain Name System):53
    • Redis6379
    • MongoDB27017
    • Kafka9092

为什么需要默认端口号?

为了方便用户。当你在浏览器中访问 www.example.com 时,你不需要手动输入 www.example.com:80,因为浏览器知道 HTTP 协议的默认端口就是 80。如果服务器的端口不是默认端口,你就必须手动指定,比如 www.example.com:8080。

好的,我将根据您提供的这份详尽的 Web 技术笔记,以一种更专业、更系统的方式,对其进行重新组织、补充和深入解析,使其成为一份高质量的技术讲解或面试回答。


一、Servlet 剩余部分

1. Servlet 的生命周期方法

这是理解 Servlet 工作原理的核心。

  • init() 方法
    • 特点:只执行一次,用于初始化 Servlet 实例。
    • 执行时机
      • 默认(懒加载):第一次被访问时执行。这是一种“按需加载”的策略,节省了服务器启动时的资源。
      • 预加载:可以在 web.xml 中通过 <load-on-startup> 标签来设置。如果值为非负整数(0或正数),Servlet 容器将在服务器启动时立即创建并初始化该 Servlet。这适用于需要立即提供服务、启动耗时较长的 Servlet。
  • service() 方法
    • 特点:每次客户端请求该 Servlet 时,都会执行一次。
    • 原理:它是 Servlet 接口的核心方法,用于处理请求。对于 HttpServlet 来说,它会根据 HTTP 请求方法(GET、POST 等)来分发请求给相应的 doGet()doPost() 方法。
    • 重要性service() 方法是多线程的。Servlet 容器会为每个请求创建一个新线程来执行 service() 方法,确保并发访问时互不影响。
  • destroy() 方法
    • 特点:只执行一次,在 Servlet 实例正常销毁时调用。
    • 执行时机:通常在 Web 应用关闭或 Servlet 容器关闭时。用于释放资源,如关闭数据库连接、文件句柄等。

2. Servlet 的实现方式:XML vs. 注解

  • XML 配置方式:在 web.xml 文件中,通过 <servlet><servlet-mapping> 标签来配置 Servlet 的名称、类和访问路径。这是 Servlet 2.x 及以前版本的主流方式。

  • 注解方式:自 Servlet 3.0 开始引入,可以使用 @WebServlet 注解来代替 XML 配置。

    • 优点:简化了配置,代码和配置更集中,提高了开发效率。
    • 示例

    Java

    1
    2
    @WebServlet("/demo")
    public class MyServlet extends HttpServlet { ... }

3. Servlet 的体系结构

  • Servlet 接口:所有 Servlet 的顶层接口,定义了 init(), service(), destroy() 等核心方法。
  • GenericServlet 抽象类:实现了 Servlet 接口,并提供了 init(), destroy() 的空实现,以及一些通用方法。开发者可以继承它来编写协议无关的 Servlet。
  • HttpServlet 抽象类:继承自 GenericServlet,专门用于处理 HTTP 请求。它重写了 service() 方法,并根据请求方法分发给 doGet(), doPost() 等具体方法。
  • 总结:在 Web 项目中,我们几乎总是处理 HTTP 请求,因此继承 HttpServlet 是最常用、最推荐的方式

二、HTTP 协议

1. 概念与特点

  • 概念超文本传输协议,是 Web 应用层协议,基于 TCP/IP。它规定了客户端和服务器之间的通信格式。
  • 特点
    • 基于 TCP/IP:它位于 TCP/IP 协议栈的应用层,利用了 TCP 的可靠传输特性。
    • 请求/响应模型:客户端发送请求,服务器返回响应,一次请求只对应一次响应。
    • 无状态:这是 HTTP 的核心特征。服务器不保留任何关于客户端过去请求的信息。每个请求都是独立的。
    • 无状态带来的问题:服务器无法识别多个请求是否来自同一个用户,因此需要引入会话技术(如 Cookie 和 Session)来解决。

2. HTTP 协议的数据格式

HTTP 协议是文本格式,由请求格式响应格式两部分组成。

  • 请求格式
    1. 请求行请求方式 虚拟路径/资源路径[?参数] 请求协议/版本
    2. 请求头:键值对形式,提供额外信息。
      • Host:目标主机名。
      • User-Agent:浏览器类型和版本。
      • Referer:请求来源地址,常用于防盗链。
    3. 请求空行:一个空行,用于分隔请求头和请求体。
    4. 请求体只有 POST 请求才有,用于封装请求参数。

3. ServletRequest 对象

ServletRequest 是 Servlet 容器在接收到请求后,封装请求信息的对象。

  • 获取请求行信息
    • getMethod():获取请求方式(GET/POST)。
    • getContextPath():获取虚拟路径。
    • getRequestURI():获取 URI,如 /web02/demo4
    • getRequestURL():获取 URL,如 http://localhost:8080/web02/demo4
  • 获取请求头信息
    • getHeader(name):根据请求头名获取值。
    • getHeaderNames():获取所有请求头名。
  • 获取请求体信息
    • getReader() (字符流) 和 getInputStream() (字节流),用于读取 POST 请求的请求体内容。
  • 获取请求参数(通用)
    • getParameter(name):获取单个参数值。
    • getParameterValues(name):获取参数值数组,用于复选框等。
  • 中文乱码
    • GET 请求:在 Tomcat 8.x 及以后版本,默认已解决。
    • POST 请求:需要手动设置编码,request.setCharacterEncoding("UTF-8");

4. request 请求转发

  • 概念:是一种服务器内部的资源跳转方式。请求从一个 Servlet 转发到另一个 Servlet 或 JSP,浏览器地址栏不会发生改变
  • 特点
    1. 地址栏不变:用户不知道发生了跳转。
    2. 一次请求/响应:整个转发过程发生在一次 HTTP 请求和一次 HTTP 响应中。
    3. 共享数据:由于是同一次请求,request 对象中的数据在转发前后是共享的。

5. request 作为域对象

  • 概念request 是一种域对象,其作用范围是一次请求-响应的生命周期
  • 共享数据:可以使用 setAttribute(), getAttribute(), removeAttribute() 方法在这次请求的生命周期内共享数据。
  • 应用场景:常用于在 Servlet 和 JSP 之间传递数据。
  • 与会话技术的联系request 是三大域对象(request, session, servletContext)中作用范围最小的一个。

好的,我将根据您提供的这份详尽的 Web 技术笔记,以一种更专业、更系统的方式,对其进行重新组织、补充和深入解析,使其成为一份高质量的技术讲解或面试回答。


一、HTTP 协议的响应格式

1. 响应格式组成

HTTP 响应格式由四部分组成:响应行响应头响应空行响应体。这与 HTTP 请求格式相呼应。

2. 响应行

  • 格式协议/版本号 状态码 状态描述。例如:HTTP/1.1 200 OK
  • 状态码:一个三位数的数字,服务器用于告诉浏览器本次响应的状态。
    • 2xx 成功
      • 200 OK:请求成功,一切正常。
    • 3xx 重定向
      • 302 Found:重定向。服务器告诉浏览器,资源临时移动到另一个位置,请重新发起请求。
      • 304 Not Modified:访问缓存。服务器告诉浏览器,请求的资源没有更新,可以使用浏览器本地的缓存副本。
    • 4xx 客户端错误
      • 404 Not Found:找不到资源。请求的路径没有对应的资源。
      • 405 Method Not Allowed:请求方法不被允许。例如,客户端用 POST 请求访问了只支持 GET 方法的 Servlet。
    • 5xx 服务器端错误
      • 500 Internal Server Error:服务器内部错误。通常是服务器端的代码出现了异常。
  • 面试题:列举 5 个常见的状态码:200、302、304、404、405、500。

3. 响应头

  • 作用:以键值对形式提供关于响应的额外信息。
  • 常见响应头
    • Content-Type:告诉浏览器响应体的数据类型和字符编码,例如 text/html;charset=utf-8
    • Content-disposition
      • inline (默认):浏览器在当前页面内直接打开。
      • attachment;filename=xxx:浏览器将数据作为附件下载。
    • Location:与 3xx 状态码配合使用,用于告诉浏览器重定向到哪个 URL。

二、ServletResponse (response)

  • 简介ServletResponse 对象由 Tomcat 创建,封装了服务器发送给客户端的响应消息。
  • 相关方法
    • 设置响应行setStatus(int sc)
    • 设置响应头setHeader(String name, String value)
    • 设置响应体
      • getWriter():获取字符输出流,用于发送文本数据。
      • getOutputStream():获取字节输出流,用于发送字节数据(如图片、视频)。

三、转发(Forward)和重定向(Redirect)的区别

这是一个非常重要的面试题,需要从多个角度进行对比。

特性 转发(Forward) 重定向(Redirect)
发生方 服务器内部 浏览器
地址栏 不变 会改变
请求次数 一次请求,一次响应 两次请求,两次响应
共享数据 request 对象共享数据 request 对象不共享数据
跳转范围 只能在当前 Web 项目内部 可以跳出项目,访问外部资源
调用方 RequestDispatcher (request) ServletResponse (response)
底层实现 request.getRequestDispatcher().forward() response.sendRedirect()
  • 重定向的实现原理
    1. 服务器收到请求,执行 response.sendRedirect(),并发送 302 状态码和 Location 响应头(值为新的 URL)。
    2. 浏览器收到响应,解析到 302Location 头后,会自动向新的 URL 发起第二次请求。

四、ServletContext

1. 简介与特点

  • 作用:代表整个 Web 应用。可以用来和服务器容器进行通信。
  • 特点
    • 单例:一个 Web 应用只有一个 ServletContext 对象。
    • 作用域:是最大的域对象,其作用范围是整个 Web 应用,从应用启动到关闭。

2. 获取 ServletContext 对象

  • 通过 requestrequest.getServletContext()
  • 通过 HttpServletgetServletContext()

3. ServletContext 的方法

  • 获取文件 MIME 类型getMimeType(String file),返回文件的类型,如 image/jpeg
  • 作为域对象:提供了 setAttribute(), getAttribute(), removeAttribute() 方法,用于在整个 Web 应用范围内共享数据。
    • 示例:您提供的代码片段完美地展示了如何使用 ServletContext 来统计服务器的访问次数。这是 ServletContext 作为域对象最经典的用法。
  • 获取文件真实路径getRealPath(String path),将相对路径转换为服务器上的绝对路径。

五、会话(Session)

1. 概念与作用

  • 概念:在 HTTP 协议的无状态特性基础上,通过会话技术将多次请求和响应联系起来,形成一个完整的会话。
  • 作用:在一次会话范围内的多次请求之间共享数据,从而识别用户。
  • 实现方式
    • 客户端会话(Cookie):数据存储在客户端浏览器。
    • 服务器端会话(Session):数据存储在服务器端。

2. 客户端会话:Cookie

  • 快速开始
    1. 创建new Cookie(name, value)
    2. 发送response.addCookie(cookie),将 Cookie 发送给浏览器。
    3. 获取request.getCookies(),从请求中获取所有 Cookie。
  • 特点:数据存储在客户端,有大小限制,安全性低。

这是一个非常好的问题,它触及了 Web 开发中一个容易混淆但至关重要的概念。enctypeContent-Type 确实有相似之处,但它们的作用域和角色是完全不同的。

让我来详细解释一下它们的区别:


一、enctype

  • 作用域:HTML <form> 标签的属性。
  • 角色:客户端表单的编码方式
  • 概念enctype (Encoding Type) 告诉浏览器如何将 <form> 表单中的数据编码成 HTTP 请求体中的数据。它只作用于表单提交这个行为。
  • 常见值
    • application/x-www-form-urlencoded:默认值,用于文本数据。
    • multipart/form-data:用于文件上传。
    • text/plain:用于纯文本提交。

简单来说,enctype 是“表单数据在浏览器端如何打包”的规则。

二、Content-Type

  • 作用域:HTTP 请求头或响应头。
  • 角色通信数据的媒体类型
  • 概念Content-Type 告诉接收方(服务器或浏览器)请求体或响应体中的数据是什么格式。它是一个通用的 HTTP 头部,不仅限于表单提交。
  • 常见值
    • text/html:HTML 文档。
    • application/json:JSON 格式数据。
    • image/jpeg:JPEG 图片。
    • application/xml:XML 文档。
    • text/plain:纯文本。

简单来说,Content-Type 是“我发送给你的数据是什么类型”的声明。

三、核心区别与联系

特性 enctype Content-Type
位置 HTML <form> 标签的属性 HTTP 请求头或响应头
谁定义 开发者在 HTML 中设置 发送方(浏览器或服务器)在 HTTP 头部中设置
作用 告诉浏览器如何打包表单数据 告诉接收方如何解析数据
关系 enctype 的值决定了 HTTP 请求头中 Content-Type 的值。 Content-Type 是 HTTP 协议的一部分,enctype 是 HTML 的一部分。

举例说明

假设你有一个包含用户名和头像上传的表单:

HTML

1
2
3
4
5
<form action="/profile" method="post" enctype="multipart/form-data">
<input type="text" name="username">
<input type="file" name="avatar">
<button type="submit">提交</button>
</form>

当用户点击提交时,浏览器会做两件事:

  1. 根据 enctype=”multipart/form-data” 规则,浏览器会生成一个**多部分(multipart)**的请求体,包含用户名和头像文件的二进制数据。

  2. 在发送 HTTP 请求时,浏览器会在请求头中自动添加一个 Content-Type 字段,其值为与 enctype 匹配的 multipart/form-data

    HTTP

    1
    2
    3
    4
    POST /profile HTTP/1.1
    Host: example.com
    Content-Type: multipart/form-data; boundary=----WebKitFormBoundary...
    ...

总结

  • enctype 是一个前端概念,它是 HTML 表单的属性,用于指导浏览器如何构建请求体。
  • Content-Type 是一个 HTTP 协议概念,它是 HTTP 头部的一个字段,用于告知数据格式。
  • 两者之间存在联动关系:你在 HTML 中设置的 enctype 值,会最终影响浏览器在 HTTP 请求中设置的 Content-Type 头的值。

好的,我来为你分别列出 enctypeContent-Type 各自常见的属性值,并简要说明其用途。


一、enctype 的常见值

enctype 是 HTML <form> 标签的属性,用于定义表单数据提交的编码方式。

  1. application/x-www-form-urlencoded
    • 用途:这是 enctype默认值。它将表单数据编码为键值对字符串,其中所有特殊字符都会被 URL 编码(例如空格被 %20 替代)。
    • 适用场景:只包含文本数据的普通表单提交。
  2. multipart/form-data
    • 用途:将表单数据分割成多个部分,每个部分都有独立的 Content-TypeContent-Disposition 头部。这种格式能够同时处理文本和二进制数据
    • 适用场景文件上传
  3. text/plain
    • 用途:将表单数据以纯文本格式发送,不进行任何编码。数据以键值对的形式,用换行符分隔。
    • 适用场景:不常用,主要用于调试目的。

二、Content-Type 的常见值

Content-Type 是 HTTP 头部字段,用于指定消息体的媒体类型(MIME 类型)。

1. 文本类型 (text/)

  • text/plain:纯文本。
  • text/html:HTML 文档。这是浏览器渲染网页的默认类型。
  • text/css:CSS 样式表。
  • text/javascript:JavaScript 代码。

2. 应用类型 (application/)

  • application/json:JSON 格式的数据。目前最常用的前后端数据交互格式。
  • application/xml:XML 格式的数据。
  • application/pdf:PDF 文档。
  • application/octet-stream:通用的二进制流数据。通常用于强制浏览器下载未知类型的文件,因为它告诉浏览器“这是一个原始的字节流,请不要尝试解析它”。
  • application/x-www-form-urlencoded:与 enctype 的默认值对应,表明请求体是 URL 编码的键值对。
  • application/javascript:与 text/javascript 类似,更推荐使用。

3. 图片类型 (image/)

  • image/jpeg:JPEG/JPG 格式的图片。
  • image/png:PNG 格式的图片。
  • image/gif:GIF 格式的图片。
  • image/svg+xml:SVG 矢量图。

4. 音视频类型

  • audio/mpeg:MP3 音频文件。
  • video/mp4:MP4 视频文件。

5. 多部分类型 (multipart/)

  • multipart/form-data:用于文件上传,与 enctype 的值对应。
  • multipart/byteranges:用于分块下载,支持断点续传。

好的,这是一个非常经典的面试题,也是理解 Web 基础知识的关键。我会详细、清晰地解释 URL 和 URI,并用通俗易懂的方式区分它们。


一、核心概念

  • URI (Uniform Resource Identifier) - 统一资源标识符
    • 概念:URI 是一个用于标识互联网上任何资源的字符串。它不仅仅能标识网页,还可以标识文件、服务、电子邮箱等。
    • 作用标识。URI 就像一个资源的“身份证号”,它能唯一地识别一个资源,但不一定告诉我们如何访问它。
  • URL (Uniform Resource Locator) - 统一资源定位符
    • 概念:URL 是一个用于定位互联网上资源的字符串。它是 URI 的一个子集
    • 作用定位。URL 就像一个资源的“详细地址”,它不仅标识了资源,还提供了如何访问该资源的完整信息,包括协议、主机名、端口号和路径等。

二、URL 和 URI 的关系

用一个比喻来理解:

  • URI 就像一个人的名字张三这个名字可以标识这个人,但你不知道他在哪里、怎么找到他。
  • URL 就像一个人的家庭住址北京市海淀区中关村大街1号。这个地址不仅标识了这个人,还告诉了你如何定位他。

因此,所有的 URL 都是 URI,但并非所有的 URI 都是 URL。

三、URL 和 URI 的具体结构

URI 的结构:

一个 URI 通常由两部分组成:

scheme:[//authority][path][?query][#fragment]

  • scheme:协议,如 http, ftp, mailto
  • path:资源路径。

URL 的结构:

URL 包含了 URI 的所有组成部分,并加入了定位信息。

scheme://host:port/path?query#fragment

  • scheme:协议。如 http, https, ftp
  • host:主机名或 IP 地址。
  • port:端口号(可选,如果使用默认端口则可以省略)。
  • path:资源路径。
  • query:查询参数,以 ? 开始。
  • fragment:片段标识符,以 # 开始。

四、举例说明

字符串 类别 解释
https://www.google.com/search?q=url+uri URLURI 既是 URI(标识),也是 URL(定位),提供了完整的访问信息。
urn:isbn:0451450523 URI不是 URL urn:isbn 是一个 URN (Uniform Resource Name),它是 URI 的另一种类型,用于标识一本书。它标识了资源,但没有提供如何访问它的位置信息。
/images/logo.png URI不是 URL 这是一个相对路径,它标识了资源,但没有完整的定位信息(如协议、主机名),需要结合当前页面的 URL 才能确定完整位置。
mailto:test@example.com URI不是 URL 标识了一个邮箱资源,但无法通过它来定位到具体的文件或服务器。

五、面试回答总结

  1. 开门见山:URL 是 URI 的子集。

  2. 核心区别

    • URI 是标识符,它只负责标识一个资源,不提供如何访问它的信息。
    • URL 是定位符,它在标识资源的同时,还提供了定位该资源的完整信息,如协议、主机名等。
  3. 举例

    • URL:https://www.example.com/index.html,包含了协议、主机、路径,可以明确地定位一个资源。
    • URI:urn:isbn:0451450523,它标识了一本书,但你无法通过它来访问这本书的电子版。
  4. 关系图

    1
    2
    3
    4
    5
    6
    7
    8
    9
    +-------------+
    | URI |
    | |
    | +----------+---------+
    | | URL | URN |
    | | (定位) | (命名) |
    | +----------+---------+
    | |
    +-------------+

    用这个图能非常清晰地展现它们之间的关系。

分布式

1. Spring Cloud 体系

核心思想

Spring Cloud 并非一个全新的框架,而是一套用于构建微服务架构的规范和解决方案的集合。它巧妙地利用了 Spring Boot 的自动配置和快速开发特性,将业界经过广泛验证的优秀微服务组件(如 Netflix OSS、Alibaba Nacos、HashiCorp Consul 等)进行封装和集成,为开发者提供了一站式的分布式系统开发工具箱,涵盖了服务治理、配置管理、熔断降级、智能路由、服务调用等方方面面。

Netflix OSS 常用组件(部分进入维护模式)
  • 服务注册与发现 (Service Discovery): Eureka
    • 作用: 提供一个服务注册中心。每个微服务启动时,将自己的网络地址等信息“注册”到 Eureka Server。其他服务(消费者)则从 Eureka Server “发现”并拉取所需服务提供者的地址列表,从而实现服务间的动态寻址和调用。
    • 使用: 服务端添加 spring-cloud-starter-netflix-eureka-server 依赖,并使用 @EnableEurekaServer 注解。客户端添加 spring-cloud-starter-netflix-eureka-client 依赖,并配置 Eureka Server 地址。
  • 服务调用与负载均衡 (RPC & Load Balancing): OpenFeign + Ribbon
    • 作用: Feign 让远程服务调用变得像调用本地方法一样简单。开发者只需定义一个接口,并使用 @FeignClient 注解,即可完成对远程服务的调用。Ribbon(现已被 Spring Cloud LoadBalancer 替代)则提供了客户端负载均衡能力,当从 Eureka 获取到多个服务实例地址时,Ribbon 会根据配置的策略(如轮询、随机)选择一个实例进行调用。
    • 使用: 添加 spring-cloud-starter-openfeign 依赖,在启动类上加 @EnableFeignClients,创建接口并使用 @FeignClient("service-name") 注解。
  • 熔断与降级 (Circuit Breaker): Hystrix
    • 作用: 当某个下游服务出现故障或响应缓慢时,为了防止故障在系统中蔓延(即“服务雪崩”),熔断器会快速失败,暂时切断对该服务的调用。同时,可以执行一个预定义的降级逻辑(Fallback),例如返回一个缓存的、默认的或友好的提示信息。
    • 状态: Hystrix 已进入维护模式,官方推荐使用 Resilience4j 或其他替代方案。
  • API 网关 (API Gateway): Zuul
    • 作用: 作为系统的统一入口,API 网关负责请求路由、协议转换、权限校验、流量控制、日志监控等。所有外部请求都先经过网关,再由网关分发到后端的各个微服务。
    • 状态: Zuul 1.x 已进入维护模式,官方推荐使用 Spring Cloud Gateway。

Spring Cloud Alibaba 详解

Spring Cloud Alibaba 致力于提供微服务开发的一站式解决方案,是 Spring Cloud 体系的重要实现。它集成了阿里巴巴开源的优秀组件,为开发者提供了更符合国内技术生态的选择。

  • 服务注册与发现 & 分布式配置中心: Nacos
    • 作用: Nacos (Naming and Configuration Service) 是一个功能丰富的平台,完美整合了服务注册发现配置管理两大核心功能。
      • 服务发现: 与 Eureka 类似,提供服务注册、发现和健康检查。但 Nacos 支持基于 DNS 和 RPC 的服务发现,并提供更实时的健康检查机制。
      • 配置管理: 可以作为分布式配置中心,对所有微服务的配置进行集中化管理。支持配置的热更新,即修改配置后无需重启服务即可生效。还支持配置的版本管理、灰度发布等高级功能。
    • 使用:
      1. 引入 spring-cloud-starter-alibaba-nacos-discoveryspring-cloud-starter-alibaba-nacos-config 依赖。
      2. bootstrap.properties (或 .yml) 文件中配置 Nacos 服务器地址和应用名。
      3. 使用 @Value@ConfigurationProperties 注解即可动态获取和刷新配置。
  • 熔断、降级与流量控制: Sentinel
    • 作用: Sentinel 是面向分布式服务架构的“流量的守护者”,以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度保护服务的稳定性。相较于 Hystrix,Sentinel 功能更强大,提供了可视化的监控和配置平台,并且支持热点参数限流等精细化控制。
    • 核心概念:
      • 资源 (Resource): Sentinel 中一切皆资源,可以是一个方法、一段代码或一个服务 URL。
      • 规则 (Rule): 定义如何保护资源,包括流控规则、降级规则、系统保护规则等。
    • 使用:
      1. 引入 spring-cloud-starter-alibaba-sentinel 依赖。
      2. 配置 Sentinel 控制台地址。
      3. 通过 @SentinelResource 注解来定义资源,并指定 Fallback (降级逻辑) 和 BlockHandler (流控/熔断逻辑)。
  • 分布式事务解决方案: Seata
    • 作用: Seata 是一个开源的分布式事务解决方案,提供了高性能且易于使用的分布式事务服务。它支持多种事务模式,包括 AT(自动补偿)、TCC、Saga 和 XA 模式,旨在解决微服务架构下的数据一致性问题。
    • 使用: 引入 spring-cloud-starter-alibaba-seata 依赖,配置 Seata Server 地址,并使用 @GlobalTransactional 注解开启全局事务。

2. 分布式ID:雪花算法(Snowflake)

原理

Snowflake 是 Twitter 开源的一种分布式 ID 生成算法,它能生成一个 64 位的 long 型数字作为全局唯一 ID。这个 64 位的 ID 由四部分构成:

  • 1位符号位: 最高位,固定为0,表示正数,无实际意义。
  • 41位时间戳 (Timestamp): 精确到毫秒级,是 (当前时间戳 - 起始时间戳) 的差值。41位可以表示 (241−1) 毫秒,大约可以使用 69 年。
  • 10位工作机器ID (Worker ID): 这 10 位可以被灵活划分,例如前 5 位代表数据中心 ID (Datacenter ID),后 5 位代表机器 ID (Machine ID)。这样总共可以支持 210=1024 台机器。
  • 12位序列号 (Sequence): 表示在同一毫秒内,同一台机器上生成的 ID 序列号。12位意味着每台机器每毫秒可以生成 212=4096 个不同的 ID。
优点
  • 全局唯一: 通过时间戳、机器 ID 和序列号的组合,可以保证在分布式环境下的 ID 唯一性。
  • 趋势递增: 由于时间戳在高位,所以生成的 ID 整体上是按时间趋势递增的,这对于数据库索引(特别是 B+树)非常友好,可以减少页分裂,提高插入性能。
  • 高性能: ID 在本地生成,不依赖任何外部服务(如数据库或 Redis),生成效率极高。
  • 高可用: 算法本身不依赖网络,部署简单,具有很高的可用性。
面试题:“雪花算法有时钟回拨问题,如何解决?”

回答要点:

时钟回拨是指服务器时间被同步到一个过去的时间点。如果算法不做处理,可能会生成重复的 ID。解决方案通常是:在生成 ID 时,记录上一次生成 ID 时所使用的时间戳。当发现当前时间戳小于上次记录的时间戳时,就意味着发生了时钟回拨。

  • 方案一(拒绝服务): 直接抛出异常,拒绝生成 ID,等待时钟恢复正常。这种方案简单,但会暂时影响可用性,适合对 ID 连续性要求不高的场景。
  • 方案二(等待追赶): 如果回拨幅度很小(比如几毫秒),程序可以 while(currentTime < lastTimestamp) 这样自旋等待,直到当前时间追赶上上次的时间戳。这会造成短暂的线程阻塞。
  • 方案三(使用备用位): 一些改进版的雪花算法会预留几位作为扩展位,当发生时钟回拨时,在这几位上做自增,从而在短时间回拨内仍能生成不同的 ID。(这种方案实现较为复杂)
  • 业界实践(美团 Leaf): 在发生时钟回拨时,切换到另一种备用 ID 生成策略(如号段模式),或者直接报错。

3. RBAC(基于角色的访问控制)

原理

RBAC (Role-Based Access Control) 是一种主流且灵活的权限管理模型。它的核心思想是在 用户 (User)权限 (Permission) 之间引入一个中间层——角色 (Role)。权限不再直接授予用户,而是授予角色;然后将角色分配给用户。这样,用户与权限实现了解耦,当需要修改大量用户的权限时,只需修改他们共同拥有的角色的权限即可,极大地简化了权限管理和维护。

核心三要素
  • 用户 (User): 系统操作的主体。
  • 角色 (Role): 权限的集合,代表了一组特定的职责或身份,如“管理员”、“文章编辑”、“普通会员”。
  • 权限 (Permission): 对系统中特定资源进行特定操作的许可。通常用一个字符串表示,如 user:createorder:deletearticle:publish
面试题:“请设计一个 RBAC 权限管理系统的数据库表结构。”

回答要点:

一个基础的 RBAC 模型至少需要五张表:

  • 用户表 (t_user): 存储用户信息。
    • user_id (主键), username, password, …
  • 角色表 (t_role): 存储角色信息。
    • role_id (主键), role_name (角色名, 如”管理员”), role_key (角色标识, 如”admin”), …
  • 权限表 (t_permission): 存储具体的权限点信息。
    • permission_id (主键), permission_name (权限名称, 如“新增用户”), permission_code (权限标识, 如 user:add), parent_id (用于菜单层级), …
  • 用户-角色关联表 (t_user_role): 存储用户和角色的多对多关系。
    • user_id (外键), role_id (外键)。(主键是 user_idrole_id 的联合主键)
  • 角色-权限关联表 (t_role_permission): 存储角色和权限的多对多关系。
    • role_id (外键), permission_id (外键)。(主键是 role_idpermission_id 的联合主键)

4. Redis 进阶详解

核心特点
  • 基于内存: Redis 是一个内存数据结构存储系统,所有数据都存放在内存中,因此读写速度极快。
  • 单线程模型: Redis 的核心网络模型处理客户端请求是单线程的。这避免了多线程环境下的上下文切换和锁竞争带来的开销。(注意:Redis 6.0 之后引入了多线程来处理 I/O,但执行命令的核心仍然是单线程)。
  • IO多路复用: 单线程能实现高性能的关键在于它使用了 IO 多路复用技术(如 Linux 下的 epoll)。该技术允许单个线程同时监听多个网络连接上的 IO 事件,当某个连接准备好读或写时,才去处理它,极大地提高了并发处理能力。
  • 丰富的数据类型: 支持 String, Hash, List, Set, Sorted Set, Bitmap, HyperLogLog, GEO 等多种数据结构。
缓存三大问题与解决方案
  • 缓存穿透:
    • 问题: 客户端查询一个数据库和缓存中不存在的数据。这导致每次请求都会绕过缓存,直接打到数据库上,当有大量此类请求时,会给数据库带来巨大压力。
    • 解决:
      1. 缓存空对象: 当从数据库查询不到数据时,也在缓存中存入一个特殊的空值(如 null 或特定字符串),并设置一个较短的过期时间。
      2. 布隆过滤器 (Bloom Filter): 在访问缓存之前,先通过布隆过滤器判断 key 是否可能存在。布隆过滤器可以高效地判断一个元素一定不存在,从而在第一层就拦截掉大量无效请求。
  • 缓存击穿:
    • 问题: 某个热点 Key 在某一时刻突然失效,而此时恰好有大量的并发请求访问这个 Key,这些请求会同时穿透缓存,直接打到数据库上,可能导致数据库瞬间崩溃。
    • 解决:
      1. 设置热点 Key 永不过期: 对于一些访问极其频繁且数据相对固定的热点数据,可以考虑不设置过期时间,通过后台任务定时更新。
      2. 使用分布式锁: 在查询数据库之前,先获取一个该 Key 对应的分布式锁。只有第一个获取到锁的线程才能去查询数据库并回写缓存,其他线程则等待或直接返回。
  • 缓存雪崩:
    • 问题: 大量的缓存 Key 在同一时间集中失效(例如,在应用启动时缓存了大量数据,并设置了相同的过期时间),导致所有请求都瞬间涌向数据库,造成数据库压力剧增甚至宕机。
    • 解决:
      1. 过期时间加随机值: 在设置缓存的过期时间时,在一个基础时间上增加一个随机数,使得 Key 的失效时间点尽可能分散。
      2. 多级缓存: 建立多级缓存体系,如 Nginx 缓存 + Redis 缓存 + JVM 本地缓存(Caffeine/Guava Cache)。
      3. 熔断降级: 使用 Hystrix 或 Sentinel 等组件,当检测到数据库压力过大时,进行熔断或降级处理,暂时不访问数据库,返回一个默认值或提示信息。

5. 消息队列(MQ)

核心作用
  • 异步 (Asynchronous): 将耗时的操作(如发送邮件、生成报表)作为消息放入 MQ,主流程可以立即返回,无需等待这些操作完成,从而提高系统的响应速度和吞吐量。
  • 解耦 (Decoupling): 生产者和消费者之间通过 MQ 进行通信,无需直接相互依赖。任何一方的修改、宕机或升级都不会影响到另一方,增强了系统的灵活性和可维护性。
  • 削峰 (Peak Shaving): 在秒杀、大促等高并发场景下,将瞬时涌入的大量请求暂存在 MQ 中,下游的消费者系统可以按照自己的处理能力,平稳地从 MQ 中拉取并处理请求,避免了流量洪峰直接冲垮下游服务。
面试题:“请列举你使用消息队列时可能遇到的问题,并给出解决方案。”

回答要点:

  • 消息丢失 (Message Loss):
    • 问题: 消息从生产者发出后,由于网络或 MQ 服务故障,未能成功到达消费者。
    • 解决:
      1. 生产者端: 开启生产者的 confirmack 机制,确保消息被 MQ 成功接收。如果发送失败,可以进行重试。
      2. MQ 服务端: 对消息进行持久化,防止 MQ 宕机导致消息丢失(如 RabbitMQ 的持久化队列和消息,Kafka 的磁盘存储)。
      3. 消费者端: 消费者在处理完业务逻辑后,再向 MQ 发送确认应答(ack)。如果处理过程中消费者宕机,MQ 没有收到 ack,会将该消息重新投递给其他消费者。
  • 重复消费 (Duplicate Consumption):
    • 问题: 由于网络抖动、消费者 ack 超时等原因,MQ 可能会重复投递同一条消息。
    • 解决: 核心是保证消费逻辑的幂等性 (Idempotence)。即多次执行同一个操作,结果都是相同的。实现方式有:
      1. 在数据库中为业务操作创建一个唯一键,每次操作前先检查该唯一键是否存在。
      2. 使用一个全局唯一的业务 ID(如订单号),在处理消息前,先查询这个 ID 是否已经被处理过(例如,存入 Redis Set 或数据库)。
  • 消息堆积 (Message Backlog):
    • 问题: 生产者的生产速度远大于消费者的消费速度,导致大量消息在 MQ 中积压,占用资源并可能导致消息超时丢失。
    • 解决:
      1. 水平扩展消费者: 增加消费者实例的数量,并行处理消息。这是最常用的方法。
      2. 优化消费逻辑: 检查消费者代码,看是否有可以优化的慢操作(如 I/O 密集型操作)。
      3. 消息分片/分区: 对 Topic 进行分区(Partitioning),让不同的消费者组处理不同的分区,提高并发度。
      4. 增加预警监控: 对消息堆积数量设置阈值,达到阈值时及时告警,人工介入。

6. 分布式锁详解

作用

在分布式系统环境下,多个进程或服务器上的多个线程需要访问同一个共享资源时,为了保证数据的一致性和操作的原子性,需要一种机制来确保在同一时刻,只有一个客户端能够持有锁并访问该资源。

实现方案对比
实现方式 优点 缺点 适用场景
基于数据库 实现简单,直接利用数据库(如唯一索引、悲观锁 for update)。 性能开销大,有锁库锁表的风险,不可重入,不是阻塞锁,数据库单点故障问题。 并发度不高的简单场景。
基于 ZooKeeper 可靠性高,天然支持阻塞锁和可重入,解决死锁问题(临时节点),无锁过期问题,支持公平锁。 性能不如 Redis,实现复杂,依赖 ZK 集群。 对可靠性要求极高,且能容忍一定性能损耗的场景,如分布式协调。
基于 Redis 性能极高,实现相对简单,有成熟的框架 (Redisson) 可用。 可靠性相对 ZK 稍差,需要处理锁过期和业务未执行完的问题,非公平锁。 互联网高并发、对性能要求高的绝大多数场景。
基于 Redis 的实现进阶
  • 加锁的正确姿势:

    使用 SET key value NX EX time 命令。

    • SET key value: 设置键值。value 通常是一个唯一的随机字符串(如 UUID),用于标识锁的持有者。

    • NX: (if Not eXists),确保只有在 key 不存在时才能设置成功,实现了“加锁”的原子性。

    • EX time: 设置一个自动过期时间(秒),防止因客户端宕机而导致死锁。

      这三个参数必须在一个命令中执行,保证原子性。

  • 解锁的正确姿势:Lua 脚本

    • 为什么需要 Lua: 解锁操作包含“判断”和“删除”两步:1. GET 锁的 value,判断是否与自己加锁时设置的随机字符串相等。2. 如果相等,则 DEL 锁。如果不用 Lua 脚本,在执行完第一步后,若该线程阻塞,此时锁恰好过期,另一个线程获取了锁,那么当原线程恢复执行第二步时,就会误删掉新线程的锁

    • Lua 脚本示例:

      Lua

      1
      2
      3
      4
      5
      6
      -- 脚本接收两个参数:KEYS[1] 是锁的 key,ARGV[1] 是加锁时设置的唯一值
      if redis.call('get', KEYS[1]) == ARGV[1] then
      return redis.call('del', KEYS[1])
      else
      return 0
      end

    Lua 脚本可以确保多个命令在 Redis 服务端被原子性地执行,杜绝了上述问题。

  • Redis 乐观锁:WATCH 命令

    • 作用: WATCH 命令可以监视一个或多个 key,如果在事务 EXEC 执行之前,任何一个被监视的 key 被其他命令修改了,那么整个事务将被取消,EXEC 返回 nil

    • 原理: 这是一种检查后设置 (Check-And-Set, CAS) 的实现。它不是真正的加锁,而是在更新数据时检查数据是否被修改过。

    • 使用场景: 适用于读多写少的并发场景,可以减少锁的开销。例如,更新商品库存。

      1. WATCH stock_key // 监视库存

      2. current_stock = GET stock_key // 获取当前库存

      3. (在客户端代码中判断 current_stock 是否足够)

      4. MULTI // 开启事务

      5. SET stock_key new_stock // 准备更新库存

      6. EXEC // 执行事务

        如果从 WATCH 到 EXEC 之间 stock_key 被其他客户端修改,EXEC 将失败,此时客户端需要重试整个操作。

面试题:“Redis 分布式锁锁过期了但业务没执行完怎么办?”

回答要点:

这是分布式锁的一个经典问题,被称为锁的超时续期问题。

  • 问题根源: 我们给锁设置了一个过期时间,比如 30 秒,但业务执行了 35 秒。在第 30 秒时锁会自动释放,其他线程就能获取到锁,导致并发问题。
  • 解决方案:“看门狗”(Watchdog)机制
    • 原理: 比如 Java 中的 Redisson 框架就内置了看门狗。当一个线程获取锁成功后,Redisson 会启动一个后台线程(看门狗),定期(例如每 10 秒)检查该线程是否还持有锁。如果持有,并且业务仍在执行,看门狗就会自动为这个锁延长过期时间(续期),比如再续 30 秒。这个过程会一直持续,直到业务执行完毕,线程主动释放锁,看门狗才会停止。
    • 总结: 看门狗机制通过后台线程自动续期,确保了在业务执行完成之前,锁不会因为超时而提前释放,从而保证了锁的可靠性。

7. 分布式事务详解

作用

在微服务架构中,一个业务操作可能需要调用多个独立的服务来共同完成(例如,电商下单操作需要调用订单服务、库存服务、积分服务)。分布式事务旨在保证这些跨服务的数据库操作能够遵循 ACID 原则,要么全部成功,要么全部回滚,以确保数据的最终一致性

解决方案深入分析
  • XA (2PC/3PC): 两阶段/三阶段提交协议。
    • 角色: 事务管理器 (Transaction Manager, TM) 和 资源管理器 (Resource Manager, RM)。
    • 流程 (2PC):
      1. 准备阶段 (Prepare): TM 通知所有 RM 准备提交,RM 执行本地事务并锁定资源,但不提交,然后向 TM 报告准备就绪。
      2. 提交/回滚阶段 (Commit/Rollback): 如果所有 RM 都准备就绪,TM 通知所有 RM 提交;否则,通知所有 RM 回滚。
    • 评价: 是一种强一致性的方案,但协议复杂,性能差,同步阻塞模型会长时间锁定资源,且协调器存在单点故障风险,在互联网高并发场景下很少使用
  • TCC (Try-Confirm-Cancel): 补偿型事务。
    • 核心: 是一种业务层面的柔性事务方案,对代码侵入性强。
    • 流程:
      1. Try: 对业务资源进行检查和预留。例如,扣减库存操作,Try 阶段是冻结库存。
      2. Confirm: 如果所有服务的 Try 阶段都成功,则执行所有服务的 Confirm 操作,真正完成业务。例如,将冻结的库存真实扣减。
      3. Cancel: 如果任何一个服务的 Try 阶段失败,则执行所有已成功服务的 Cancel 操作,释放预留的资源。例如,解冻之前被冻结的库存。
    • 评价: 性能较好,数据一致性高于可靠消息方案。但开发成本极高,需要为每个操作都编写 Try, Confirm, Cancel 三个幂等的方法。
  • Saga 模式: 长事务解决方案。
    • 核心: 将一个大的分布式事务拆分成多个本地事务,每个本地事务都有一个对应的补偿操作
    • 流程:
      • 正向执行: Saga 协调器按顺序调用 T1, T2, T3…Tn。
      • 反向补偿: 如果 Ti 失败,Saga 会按相反顺序调用前面已成功事务的补偿操作 C(i-1)…C2, C1,进行回滚。
    • 与 TCC 对比:
      • TCC 有资源预留阶段,锁资源时间长;Saga 没有预留,直接提交本地事务,锁资源时间短。
      • TCC 的补偿是逆向操作 (Cancel);Saga 的补偿是反向操作
    • 评价: 适合于业务流程长、需要保证最终一致性的场景。同样对代码有侵入性,需要设计好每个子事务的补偿逻辑。
  • 基于可靠消息的最终一致性 (常用):
    • 核心: 这是微服务架构中最常用的柔性事务方案。
    • 原理: 服务 A 在执行完本地事务后,发送一条消息到 MQ。服务 B 订阅该消息,消费消息并执行自己的本地事务。
    • 关键问题: 如何保证本地事务执行消息发送的原子性?
      • 事务消息 (RocketMQ 支持): 生产者先发送一条“半消息”到 MQ,MQ 收到后不投递。然后生产者执行本地事务。如果事务成功,则向 MQ 发送确认,MQ 投递该消息;如果事务失败,则通知 MQ 删除该半消息。
      • 本地消息表: 业务操作和“待发送消息”记录在同一本地事务中。一个后台任务定时扫描这张表,将消息发送到 MQ,发送成功后更新表状态。
    • 评价: 实现了服务间的解耦,性能高,吞吐量大。但它不保证数据的强一致性,只保证最终一致性,存在一个短暂的数据不一致状态窗口。需要处理好消息的可靠投递和幂等消费问题。

Lua 脚本详解 (在 Redis 中的应用)

1. Lua 是什么?

Lua 是一种轻量级、可扩展的脚本语言,被设计用于嵌入到其他应用程序中,从而为应用程序提供灵活的扩展和定制功能。它以其简洁的语法、高效的执行性能和极小的内存占用而闻名。

在 Redis 的上下文中,Lua 脚本提供了一种在 Redis 服务器端执行复杂逻辑的强大能力。

2. 为什么 Redis 要支持 Lua 脚本?

  • 原子性 (Atomicity): 这是在 Redis 中使用 Lua 最核心的原因。Redis 会将整个 Lua 脚本作为一个单独的命令来执行,在脚本执行期间,不会有其他客户端的命令被插入执行。这完美地解决了需要组合多个 Redis 命令才能完成一个业务逻辑时,可能出现的竞态条件问题。例如前面提到的“判断锁并删除锁”的操作,如果分两步执行,就不是原子的,而封装在 Lua 脚本中就是原子的。
  • 减少网络开销: 对于需要多次与 Redis 交互的复杂操作,可以将所有逻辑封装在一个 Lua 脚本中,一次性发送给 Redis 服务器。客户端只需发送一次请求,而不是多次,这显著减少了客户端与服务器之间的网络往返时间(RTT),提升了性能。
  • 代码复用: 编写好的 Lua 脚本可以被缓存(通过 SCRIPT LOAD 命令生成一个 SHA1 校验和),之后客户端可以通过这个简短的 SHA1 校验和(使用 EVALSHA 命令)来调用脚本,避免了每次都发送完整的脚本内容。

3. 如何在 Redis 中使用 Lua 脚本?

通过 EVAL 或 EVALSHA 命令来执行。

EVAL script numkeys key [key …] arg [arg …]

  • script: 要执行的 Lua 脚本字符串。
  • numkeys: 后面跟的 key 参数的数量。这有助于 Redis 正确地将参数区分为键名(KEYS)和普通参数(ARGV),这对于 Redis Cluster 模式下的路由至关重要。
  • key [key ...]:脚本中要操作的 Redis 键,在 Lua 脚本中可以通过全局变量 KEYS table(例如 KEYS[1])来访问。
  • arg [arg ...]:传递给脚本的额外参数,在 Lua 脚本中可以通过全局变量 ARGV table(例如 ARGV[1])来访问。

示例:实现一个安全的库存扣减

Lua

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 脚本逻辑:检查库存是否充足,如果充足则扣减并返回1,否则返回0
-- KEYS[1]: 库存的 key,例如 "product:1001:stock"
-- ARGV[1]: 本次要扣减的数量

local stock = tonumber(redis.call('get', KEYS[1]))
local quantity = tonumber(ARGV[1])

if stock >= quantity then
redis.call('decrby', KEYS[1], quantity)
return 1
else
return 0
end

这个脚本保证了“读取库存”和“扣减库存”两个操作的原子性,避免了在高并发下超卖的问题。


Token 认证机制详解

1. Token 是什么?

Token(令牌)是在服务端生成的一串加密字符串,作为客户端进行请求的一个“凭证”。当用户第一次登录成功后,服务端会生成一个 Token 并返回给客户端。之后,客户端在每次请求需要身份认证的接口时,都需要在请求头(通常是 Authorization 字段)中携带这个 Token。服务端接收到请求后,会验证 Token 的有效性,如果验证通过,则处理该请求;否则,拒绝该请求。

一个典型的 Token 是 JWT (JSON Web Token),它由三部分组成,用 . 分隔:

  • Header (头部): 包含了令牌的类型(typ,即 JWT)和所使用的签名算法(alg,如 HMAC SHA256 或 RSA)。
  • Payload (负载): 包含了“声明 (claims)”,是存放实际需要传递的数据的地方。例如用户ID(sub)、签发时间(iat)、过期时间(exp)以及其他自定义的用户信息。注意:Payload 部分是 Base64 编码的,并非加密,因此不应存放敏感信息。
  • Signature (签名): 对前两部分(Header 和 Payload)使用指定的算法和存储在服务端的密钥(secret)进行签名。这个签名的作用是防止数据被篡改。服务端收到 Token 后,会用同样的算法和密钥重新计算签名,并与 Token 中的签名进行比对,若一致,则说明 Token 未被篡改且是可信的。

2. Token 认证原理(工作流程)

  1. 登录: 用户使用用户名和密码发起登录请求。
  2. 验证: 服务端验证用户的凭据是否正确。
  3. 签发 Token: 验证成功后,服务端根据用户ID、角色等信息,结合密钥(secret),生成一个 Token。
  4. 返回 Token: 服务端将生成的 Token 返回给客户端。
  5. 存储 Token: 客户端(浏览器、App)将 Token 存储起来,通常放在 localStoragesessionStorageHttpOnly 的 Cookie 中。
  6. 携带 Token 请求: 客户端在后续每次请求受保护的 API 时,都会在 HTTP 请求头的 Authorization 字段中附上 Token,格式通常为 Bearer <token>
  7. 验证 Token: 服务端收到请求后,从请求头中解析出 Token,然后:
    • 验证签名是否正确,确保 Token 未被篡改。
    • 检查 Token 是否在有效期内(exp 声明)。
    • 如果验证通过,则从 Payload 中获取用户信息,执行业务逻辑并返回结果。
    • 如果验证失败,则返回 401 Unauthorized 错误。

3. 为什么使用 Token?(与 Session 的区别)

在 Web 开发早期,Session-Cookie 机制是主流。服务端在用户登录后创建一个 Session 对象存储在内存或 Redis 中,并生成一个 Session ID,通过 Cookie 返回给浏览器。浏览器后续请求会自动带上这个 Session ID,服务端根据 ID 找到对应的 Session 信息来识别用户。

Token 机制相比 Session 机制,核心优势在于“无状态性 (Statelessness)”,这带来了以下好处:

特性对比 Session 机制 Token 机制 优势说明
状态存储 有状态 (Stateful)。Session 信息需存储在服务端。 无状态 (Stateless)。用户信息包含在 Token 的 Payload 中,服务端无需存储。 减轻服务端压力。服务端不需要为每个在线用户维护一个 Session 对象。
可扩展性 。在分布式或集群环境下,需要解决 Session 共享问题(如 Session Sticky、Session Replication、集中存储)。 。由于服务端不存储状态,任何一台服务器只要有相同的密钥,就能验证 Token,天然适合分布式和微服务架构。 轻松实现水平扩展。增加服务器节点无需额外配置 Session 共享。
跨域支持 有限。基于 Cookie 的 Session 机制在跨域(CORS)场景下处理起来比较麻烦。 优秀。Token 可以通过 HTTP 请求头发送,不受同源策略限制,非常适合前后端分离和跨域 API 调用。 适应现代架构。完美支持 SPA(单页应用)、移动 App 等多种客户端。
安全性 依赖 Cookie 机制,可能遭受 CSRF 攻击。 如果 Token 存储在 localStorage,可能遭受 XSS 攻击。需要综合考虑存储方式。 两者各有安全风险点,需配合其他安全策略。Token 机制不依赖 Cookie,更灵活。
适用性 适合传统的、一体化的 Web 应用。 适合现代的、分布式的、跨终端的(Web, Mobile, IoT)应用架构。 Token 更具通用性和前瞻性。

4. 双令牌策略 (Access Token + Refresh Token)

  • Q: 为什么不用单个 Token?
    • 如果 Token 有效期很长(如一个月): 安全风险高。一旦 Token 在此期间被窃取,攻击者可以长时间冒充用户身份进行操作。
    • 如果 Token 有效期很短(如 15 分钟): 用户体验差。用户需要频繁地重新登录,这是无法接受的。
  • A: 双令牌策略应运而生,完美平衡了安全性和用户体验。
    • Access Token (访问令牌): 它的有效期非常短(如 15 分钟到 1 小时)。它被用于访问受保护的资源,由于其生命周期短,即使被窃取,攻击者能造成的危害也有限。
    • Refresh Token (刷新令牌): 它的有效期很长(如 7 天或 30 天)。它的唯一作用是用来获取新的 Access Token。Refresh Token 本身不包含任何权限信息,不能用于直接访问 API。
  • 双令牌工作流程(静默刷新)
    1. 首次登录: 用户登录成功,服务端返回一个短期的 Access Token 和一个长期的 Refresh Token。客户端将两者都存储起来。
    2. 正常访问: 客户端使用 Access Token 访问 API。服务端验证 Access Token 通过,返回数据。
    3. Access Token 过期: 客户端再次使用过期的 Access Token 访问 API,服务端返回 401 Unauthorized 错误,并可能带上一个特定错误码,告知客户端是“令牌过期”而非“无效令牌”。
    4. 静默刷新: 客户端的请求拦截器捕获到这个 401 错误后,不会立即跳转到登录页。而是自动发起一个特殊的请求,携带那个长期的 Refresh Token 去访问一个专门的刷新接口(如 /api/token/refresh)。
    5. 签发新令牌: 服务端验证 Refresh Token 的有效性(通常会将其存储在 Redis 或数据库中进行比对,以实现强制下线功能)。如果验证通过,就生成一个新的 Access Token(有时也会生成一个新的 Refresh Token,这被称为刷新令牌滚动策略)并返回给客户端。
    6. 重试请求: 客户端收到新的 Access Token 后,用它替换掉本地旧的 Access Token,然后自动重新发送刚才因令牌过期而失败的那个请求
    7. 无感体验: 整个过程对用户是透明的,用户感觉不到令牌已经过期并被刷新,实现了“静默刷新”,体验非常流畅。
    8. Refresh Token 过期: 如果 Refresh Token 也过期了,那么刷新接口会返回错误,此时客户端才会真正清除用户凭证并引导用户重新登录。

5. Token 相关场景与面试题

  • 面试题 1:“Token 应该存储在哪里?localStorage、sessionStorage 还是 Cookie?”
    • 回答要点:
      • localStorage/sessionStorage:
        • 优点: 方便 JavaScript 直接读写,容量较大(5MB)。
        • 缺点: 容易受到 XSS (跨站脚本攻击)。如果网站存在 XSS 漏洞,攻击者可以执行 JS 代码直接窃取存储在其中的 Token。
      • Cookie (HttpOnly):
        • 优点: 设置为 HttpOnly 后,JavaScript 将无法读写该 Cookie,可以有效防御 XSS 攻击。浏览器会自动在同域请求中携带它。
        • 缺点: 容易受到 CSRF (跨站请求伪造) 攻击。攻击者可以诱导用户点击恶意链接,浏览器会自动带上用户的 Cookie 去请求你的网站,执行非用户本意的操作。需要配合 Anti-CSRF Token 等机制来防御。容量较小(4KB)。
      • 最佳实践/结论: 没有绝对完美的选择,需要权衡。
        • 高安全性方案: 将 Refresh Token 存储在 HttpOnly 的 Cookie 中(防止 XSS),将 Access Token 存储在内存中(变量里,页面刷新丢失)或 sessionStorage 中。同时,后端接口必须实施 CSRF 防御策略。
        • 主流实践方案 (前后端分离): 将 Token 存储在 localStorage 中,并在 Authorization 请求头中携带。同时,前端必须严格做好输入过滤和内容转义,尽最大努力防止 XSS 漏洞的出现。
  • 面试题 2:“用户点击“退出登录”时,Token 如何失效?”
    • 回答要点:
      • 对于无状态的 JWT: 由于所有信息都在 Token 自身,服务端无法主动让其失效。因此,“退出登录”主要是一个客户端行为
        • 客户端: 只需从 localStorage 或其他存储位置清除 Token 即可。用户将无法再发起认证请求。
      • 如何实现服务端强制下线?: 如果需要实现“踢人下线”或“修改密码后所有设备强制下线”的功能,就必须打破纯粹的无状态。
        • 黑名单机制: 服务端可以建立一个 Token 黑名单(例如,使用 Redis Set)。当用户退出登录时,将该 Token 的 jti (JWT ID) 或整个 Token 放入黑名单,并设置与 Token 剩余有效期相同的过期时间。在每次验证 Token 时,除了常规验证,还需检查该 Token 是否在黑名单中。
        • 基于 Refresh Token: 在双令牌模式下,退出登录时只需让服务端的 Refresh Token 失效(例如,从 Redis 中删除)。这样用户就无法再获取新的 Access Token,当旧的 Access Token 过期后,自然就下线了。
  • 面试题 3:“请你设计一个支持 Web 端和 App 端统一登录的认证系统。”
    • 回答要点:
      • 这正是 Token 认证机制的典型应用场景。我会采用基于 OAuth 2.0/OIDC 或自定义的**双令牌(Access/Refresh Token)**方案。
      • 统一认证中心 (UAC): 建立一个独立的认证服务,负责处理所有客户端(Web, iOS, Android)的登录、注册、Token 签发和刷新。
      • API 网关: 所有业务请求都通过 API 网关。网关的核心职责之一就是统一鉴权。它会拦截所有请求,解析 Authorization 头中的 Access Token,调用认证中心或自行验证 Token 的有效性。验证通过后,可以将解析出的用户信息(如用户ID)附加到请求头中,再转发给后端的业务微服务。
      • 业务微服务: 业务微服务本身不再关心 Token 的验证细节,它们信任来自网关的请求,直接从请求头中获取用户信息进行业务处理,实现了业务与认证的解耦。
      • 流程:
        1. Web/App 客户端引导用户到认证中心进行登录。
        2. 登录成功后,认证中心返回 Access TokenRefresh Token
        3. 客户端保存令牌,后续访问业务 API 时,在请求头携带 Access Token
        4. API 网关拦截请求,验证 Access Token
        5. Access Token 过期后,客户端使用 Refresh Token 向认证中心申请新令牌。
      • 这个架构具有高内聚、低耦合、可扩展性强、安全性高的优点。

Redis 详细技术解析

Redis 核心架构与原理

内存模型与数据结构

Redis采用基于内存的存储架构,所有数据都保存在RAM中,这是其高性能的根本原因。Redis使用了多种底层数据结构来实现上层的抽象数据类型:

SDS(Simple Dynamic String) Redis没有直接使用C语言的字符串,而是构建了SDS。SDS在字符串头部记录了长度信息,避免了strlen的O(n)复杂度,同时预分配空间减少内存重分配次数。

跳跃表(Skip List) 有序集合的底层实现之一,是一种随机化的数据结构,通过多层链表实现O(log N)的查找复杂度。相比红黑树,跳跃表实现更简单,且支持范围查询。

压缩列表(Ziplist) 当哈希、列表、有序集合元素较少时使用的紧凑存储结构,所有元素存储在一块连续内存中,节省内存但插入删除效率较低。

字典(Dict) Redis的核心数据结构,使用开放寻址法解决哈希冲突,支持渐进式rehash。当负载因子过高时,会创建新的哈希表并逐步迁移数据。

单线程模型与事件循环

Redis 6.0之前采用单线程模型处理客户端请求,通过I/O多路复用(epoll/kqueue)实现高并发。单线程避免了线程切换开销和并发控制问题,但也限制了CPU利用率。

事件循环机制 Redis使用Reactor模式的事件循环,分为文件事件和时间事件:

  • 文件事件:处理客户端连接、读写请求
  • 时间事件:处理定时任务,如过期键删除、持久化等

Redis 6.0引入了多线程I/O,但命令执行仍是单线程,多线程只用于网络I/O操作的读写,这样既提高了网络处理能力,又保持了数据操作的原子性。

数据类型详解与应用场景

String类型

String是Redis最基础的数据类型,底层可以是SDS、整数或浮点数。

常用命令:SET、GET、INCR、DECR、APPEND、GETRANGE 应用场景

  • 缓存:存储用户会话、配置信息
  • 计数器:网站访问量、点赞数(INCR原子性保证)
  • 分布式锁:SET key value NX EX seconds
  • 限流:结合EXPIRE实现滑动窗口限流

Hash类型

Hash类型适合存储对象,避免了将对象序列化为JSON字符串的开销。

底层实现:元素较少时使用ziplist,元素较多时使用hashtable 应用场景

  • 存储用户信息:HSET user:1001 name “张三” age 25
  • 购物车:HSET cart:1001 product:123 2
  • 配置管理:分类存储不同模块的配置

List类型

List是双向链表实现,支持在两端进行O(1)的插入和删除操作。

常用命令:LPUSH、RPUSH、LPOP、RPOP、LRANGE、BLPOP 应用场景

  • 消息队列:生产者LPUSH,消费者BRPOP实现阻塞队列
  • 最新列表:朋友圈动态、商品评论
  • 栈和队列:LPUSH+LPOP实现栈,LPUSH+RPOP实现队列

Set类型

Set是无序集合,元素唯一,底层使用hashtable或intset实现。

集合运算:SINTER(交集)、SUNION(并集)、SDIFF(差集) 应用场景

  • 去重:统计网站独立访客
  • 社交关系:共同好友、推荐用户
  • 标签系统:用户标签、文章分类
  • 抽奖系统:SRANDMEMBER随机抽取

Sorted Set类型

有序集合,每个元素关联一个分数,按分数排序。底层使用跳跃表和哈希表。

应用场景

  • 排行榜:游戏积分、热搜榜
  • 延时队列:分数为执行时间戳
  • 范围查询:按时间、按分数范围获取数据

持久化机制深度解析

RDB持久化

RDB通过fork子进程,将内存数据快照写入磁盘。

优点

  • 文件紧凑,适合备份和灾难恢复
  • 恢复速度快
  • 对Redis性能影响小(子进程操作)

缺点

  • 数据丢失风险:两次快照间的数据可能丢失
  • fork过程会阻塞主进程
  • 大数据集fork耗时较长

触发条件

  • 手动执行SAVE或BGSAVE命令
  • 配置自动触发:save 900 1(900秒内至少1个键改变)
  • 主从复制时自动生成RDB

AOF持久化

AOF记录每个写命令,通过重放命令恢复数据。

写入时机

  • always:每个写命令立即同步,安全但性能低
  • everysec:每秒同步一次,平衡安全性和性能
  • no:由操作系统决定同步时机,性能高但安全性低

AOF重写: AOF文件会越来越大,Redis提供重写机制优化:

  • 遍历内存数据,用最少命令重建AOF文件
  • 重写期间的新命令写入AOF重写缓冲区
  • 原子性替换旧AOF文件

混合持久化: Redis 4.0引入RDB+AOF混合模式,重写时以RDB格式写入基础数据,增量命令以AOF格式追加,兼顾了恢复速度和数据安全。

缓存问题与解决方案

缓存穿透

查询不存在的数据,缓存无法生效,请求直达数据库。

解决方案

  1. 空值缓存:查询结果为空时也缓存,设置较短过期时间
1
2
3
if (data == null) {
redis.setex(key, 60, "null"); // 缓存空值60秒
}
  1. 布隆过滤器:预先将所有可能存在的数据哈希到位数组
1
2
3
4
5
BloomFilter<String> filter = BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()), 1000000, 0.01);
// 查询前先检查布隆过滤器
if (!filter.mightContain(key)) {
return null; // 一定不存在
}
  1. 参数校验:在API层面进行参数合法性校验

缓存击穿

热点数据过期瞬间,大量并发请求击穿缓存。

解决方案

  1. 互斥锁:只允许一个线程查询数据库并重建缓存
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public String getData(String key) {
String data = redis.get(key);
if (data == null) {
String lockKey = "lock:" + key;
if (redis.setnx(lockKey, "1", 10)) { // 获取锁
try {
data = database.query(key); // 查询数据库
redis.setex(key, 3600, data); // 重建缓存
} finally {
redis.del(lockKey); // 释放锁
}
} else {
Thread.sleep(50); // 等待其他线程重建缓存
return getData(key); // 递归重试
}
}
return data;
}
  1. 热点数据永不过期:逻辑上设置过期时间,物理上不过期,异步更新
  2. 预热机制:系统启动时预先加载热点数据
  3. 二级缓存:L1缓存过期后,先返回L2缓存数据,异步更新L1

缓存雪崩

大量缓存同时过期或Redis宕机,请求涌向数据库。

解决方案

  1. 过期时间随机化:避免同时过期
1
2
int randomExpire = baseExpire + new Random().nextInt(300);  // 基础时间+随机时间
redis.setex(key, randomExpire, data);
  1. 多级缓存架构
    • L1:本地缓存(如Caffeine)
    • L2:Redis分布式缓存
    • L3:数据库
  2. 限流降级:使用Sentinel、Hystrix等组件
  3. Redis高可用:主从复制、哨兵模式、集群部署

缓存预热

系统启动时预先加载热点数据到缓存。

实现方式

  1. 定时任务预热:凌晨低峰期执行
  2. 手动预热:管理后台触发预热任务
  3. 实时预热:监控系统发现热点数据自动预热

数据一致性保证

Cache Aside模式(旁路缓存)

应用程序负责维护缓存和数据库的一致性。

读操作

  1. 先读缓存,命中则返回
  2. 缓存不命中,查询数据库
  3. 将数据写入缓存并返回

写操作

  1. 先更新数据库
  2. 删除缓存(让下次读取时重新加载)

为什么是删除而不是更新缓存?

  • 更新缓存可能存在并发问题
  • 复杂查询的缓存更新成本高
  • 删除缓存更简单可靠

延时双删策略

解决读写并发导致的数据不一致问题。

实现步骤

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public void updateData(String key, Object data) {
// 1. 删除缓存
redis.del(key);

// 2. 更新数据库
database.update(data);

// 3. 延时删除缓存(异步)
CompletableFuture.runAsync(() -> {
try {
Thread.sleep(500); // 延时500ms
redis.del(key); // 再次删除缓存
} catch (InterruptedException e) {
log.error("延时删除缓存失败", e);
}
});
}

延时时间设置:通常为主从同步时间 + 读数据库时间 + 几十毫秒

基于消息队列的最终一致性

使用消息队列异步处理缓存更新,保证最终一致性。

实现流程

  1. 更新数据库,发送消息到队列
  2. 消息消费者删除相关缓存
  3. 消费失败时重试,保证最终一致性
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 发送缓存删除消息
@Transactional
public void updateUser(User user) {
userDao.update(user); // 更新数据库

// 发送缓存删除消息(事务提交后发送)
TransactionSynchronizationManager.registerSynchronization(
new TransactionSynchronizationAdapter() {
@Override
public void afterCommit() {
cacheDeleteMQ.send("user:" + user.getId());
}
}
);
}

分布式事务方案

对于强一致性要求高的场景,可以使用分布式事务。

2PC(两阶段提交)

  • 准备阶段:协调者询问参与者是否准备好
  • 提交阶段:所有参与者都准备好则提交,否则回滚

TCC(Try-Confirm-Cancel)

  • Try:尝试执行,预留资源
  • Confirm:确认提交
  • Cancel:取消执行,释放资源

分布式锁实现

基于SET命令的分布式锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class RedisDistributedLock {
private Jedis jedis;

public boolean tryLock(String lockKey, String requestId, int expireTime) {
String result = jedis.set(lockKey, requestId, "NX", "PX", expireTime);
return "OK".equals(result);
}

public boolean releaseLock(String lockKey, String requestId) {
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('del', KEYS[1]) else return 0 end";
Object result = jedis.eval(script, Collections.singletonList(lockKey),
Collections.singletonList(requestId));
return "1".equals(result.toString());
}
}

使用方式

1
2
3
4
5
6
7
8
9
10
11
String lockKey = "lock:user:1001";
String requestId = UUID.randomUUID().toString();

if (tryLock(lockKey, requestId, 30000)) {
try {
// 执行业务逻辑
doSomething();
} finally {
releaseLock(lockKey, requestId);
}
}

Redlock算法

为了解决单点故障问题,Redis官方提出了Redlock算法。

算法步骤

  1. 获取当前时间戳
  2. 依次向N个Redis实例申请锁
  3. 如果在大多数实例(N/2+1)上获取锁成功,且总耗时小于锁超时时间,则认为获取锁成功
  4. 锁的有效时间 = 初始有效时间 - 获取锁消耗的时间
  5. 释放锁时,向所有Redis实例发送释放命令
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class Redlock {
private List<Jedis> jedisInstances;

public boolean lock(String resource, int ttl) {
int quorum = jedisInstances.size() / 2 + 1;
int successCount = 0;
long startTime = System.currentTimeMillis();

for (Jedis jedis : jedisInstances) {
if (lockInstance(jedis, resource, ttl)) {
successCount++;
}
}

long elapsedTime = System.currentTimeMillis() - startTime;
if (successCount >= quorum && elapsedTime < ttl) {
return true;
} else {
unlock(resource); // 释放已获取的锁
return false;
}
}
}

锁的问题与优化

锁超时问题: 业务执行时间超过锁超时时间,锁自动释放,可能导致并发问题。

解决方案

  1. 看门狗机制:定时续期锁的过期时间
1
2
3
4
5
6
7
8
9
10
11
12
13
public class WatchDog {
private ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);

public void startWatchDog(String lockKey, String requestId) {
scheduler.scheduleAtFixedRate(() -> {
// 续期锁
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('expire', KEYS[1], ARGV[2]) else return 0 end";
jedis.eval(script, Arrays.asList(lockKey),
Arrays.asList(requestId, "30"));
}, 10, 10, TimeUnit.SECONDS);
}
}
  1. 合理评估业务执行时间:设置足够的锁超时时间

锁竞争激烈问题: 大量线程竞争同一把锁,导致性能下降。

解决方案

  1. 分段锁:将资源分段,减少锁竞争
  2. 队列锁:使用List实现公平锁
  3. 自旋锁优化:适当的退避算法

悲观锁与乐观锁

悲观锁

假设会发生并发冲突,在操作数据前先获取锁。

Redis实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// 使用Redis分布式锁实现悲观锁
public void updateWithPessimisticLock(String userId, int amount) {
String lockKey = "lock:account:" + userId;
String requestId = UUID.randomUUID().toString();

if (tryLock(lockKey, requestId, 30000)) {
try {
// 查询账户余额
int balance = getBalance(userId);
if (balance >= amount) {
// 扣减余额
updateBalance(userId, balance - amount);
} else {
throw new InsufficientBalanceException();
}
} finally {
releaseLock(lockKey, requestId);
}
} else {
throw new LockAcquisitionException();
}
}

乐观锁

假设不会发生冲突,在更新时检查数据是否被修改。

基于版本号的乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public boolean updateWithOptimisticLock(String userId, int amount, int expectedVersion) {
// 使用Lua脚本保证原子性
String script =
"local current = redis.call('hmget', KEYS[1], 'balance', 'version') " +
"if current[2] == ARGV[2] then " +
" if tonumber(current[1]) >= tonumber(ARGV[1]) then " +
" redis.call('hmset', KEYS[1], 'balance', current[1] - ARGV[1], 'version', current[2] + 1) " +
" return 1 " +
" else " +
" return -1 " + // 余额不足
" end " +
"else " +
" return 0 " + // 版本号不匹配
"end";

String key = "account:" + userId;
Object result = jedis.eval(script, Arrays.asList(key),
Arrays.asList(String.valueOf(amount), String.valueOf(expectedVersion)));

int code = ((Long) result).intValue();
if (code == 1) {
return true; // 更新成功
} else if (code == 0) {
throw new OptimisticLockException("数据已被修改");
} else {
throw new InsufficientBalanceException("余额不足");
}
}

基于CAS的乐观锁

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public void updateWithCAS(String key, Function<String, String> updater) {
while (true) {
// 1. 获取当前值
String currentValue = redis.get(key);

// 2. 计算新值
String newValue = updater.apply(currentValue);

// 3. CAS更新
String script = "if redis.call('get', KEYS[1]) == ARGV[1] then " +
"return redis.call('set', KEYS[1], ARGV[2]) else return nil end";
Object result = redis.eval(script, Arrays.asList(key),
Arrays.asList(currentValue, newValue));

if (result != null) {
break; // 更新成功
}
// 更新失败,重试
}
}

悲观锁 vs 乐观锁选择

悲观锁适用场景

  • 写操作频繁,冲突概率高
  • 业务逻辑复杂,重试成本高
  • 对数据一致性要求严格

乐观锁适用场景

  • 读多写少,冲突概率低
  • 业务逻辑简单,重试成本低
  • 对性能要求高

高可用架构

主从复制

Master负责写操作,Slave负责读操作,通过复制实现数据同步。

复制原理

  1. Slave向Master发送PSYNC命令
  2. Master执行BGSAVE生成RDB文件
  3. Master将RDB文件发送给Slave
  4. Slave载入RDB文件
  5. Master将缓冲区的写命令发送给Slave
  6. 后续写命令实时同步

部分重同步: 网络断连后,Slave只需要同步断连期间的命令,而不是完整重同步。

哨兵模式(Sentinel)

哨兵负责监控Master状态,在Master故障时自动进行故障转移。

哨兵职责

  • 监控:定期ping Master和Slave
  • 通知:故障时通知管理员和客户端
  • 故障转移:自动将Slave提升为新Master
  • 配置管理:为客户端提供服务发现

故障转移流程

  1. 哨兵发现Master下线(主观下线)
  2. 多个哨兵确认Master下线(客观下线)
  3. 选举领导哨兵执行故障转移
  4. 选择合适的Slave作为新Master
  5. 修改其他Slave的配置指向新Master
  6. 通知客户端Master地址变更

集群模式(Cluster)

Redis Cluster通过分片实现横向扩展和高可用。

分片算法: 使用CRC16算法计算key的哈希值,然后对16384取模得到槽位号。

节点通信: 使用Gossip协议在节点间交换状态信息,包括节点上线/下线、槽位分配等。

故障转移: 当Master节点故障时,其Slave自动提升为新Master,过程对客户端透明。

数据迁移: 集群扩容时,需要将部分槽位从现有节点迁移到新节点。

性能监控与优化

慢查询日志

Redis提供慢查询日志功能,记录执行时间超过阈值的命令。

配置参数

1
2
slowlog-log-slower-than 10000  # 超过10毫秒记录
slowlog-max-len 128 # 最多保存128条记录

查看慢查询

1
SLOWLOG GET 10  # 获取最近10条慢查询

内存分析

使用MEMORY命令分析内存使用情况。

1
2
3
MEMORY USAGE key         # 查看key占用内存
MEMORY STATS # 查看内存统计信息
MEMORY DOCTOR # 内存使用建议

性能优化建议

避免大key

  • 单个key的value不要超过10KB
  • 集合类型元素数量控制在合理范围
  • 使用SCAN代替KEYS命令

合理使用数据结构

  • 小对象使用Hash而不是多个String
  • 合理设置ziplist等压缩结构的阈值
  • 使用位图(bitmap)存储布尔类型大数据集

网络优化

  • 使用Pipeline批量操作
  • 合理设置客户端连接池
  • 启用TCP_NODELAY选项

持久化优化

  • 根据业务需求选择RDB或AOF
  • 合理配置自动保存条件
  • 在从节点上进行持久化操作

这些详细的技术点涵盖了Redis的核心概念、常见问题解决方案和实际应用场景,是Redis技术面试的重要考查内容。掌握这些知识点并能结合实际项目经验进行说明,将大大提高面试通过率。

内存管理与淘汰机制

内存淘汰策略详解

当Redis内存使用达到maxmemory限制时,会根据配置的策略淘汰数据。

8种淘汰策略

1
2
3
4
5
6
7
8
9
10
11
# 针对所有key
noeviction # 不淘汰,写入返回错误
allkeys-lru # 所有key中淘汰最近最少使用
allkeys-lfu # 所有key中淘汰最少频率使用
allkeys-random # 所有key中随机淘汰

# 针对设置了过期时间的key
volatile-lru # 过期key中淘汰最近最少使用
volatile-lfu # 过期key中淘汰最少频率使用
volatile-random # 过期key中随机淘汰
volatile-ttl # 过期key中淘汰即将过期的

LRU vs LFU 实现细节: Redis的LRU并非严格的LRU,而是近似LRU算法:

  • 每个key都有24位的时钟字段记录访问时间
  • 淘汰时随机采样5个key(可配置),选择时钟值最小的

LFU算法维护访问频率:

  • 高16位存储上次访问时间
  • 低8位存储访问频率计数器
  • 计数器采用概率性递增,避免频率无限增长

内存碎片问题

产生原因

  • 频繁的数据更新导致内存分配/释放
  • Redis使用jemalloc内存分配器,存在内存对齐
  • 删除大key后留下内存空洞

检测方法

1
2
3
4
INFO memory
# 关注 mem_fragmentation_ratio 指标
# 比值 > 1.5 表示碎片较多
# 比值 < 1 表示使用了swap,性能严重下降

解决方案

  1. 内存整理(Redis 4.0+):
1
2
3
CONFIG SET activedefrag yes        # 开启自动整理
CONFIG SET active-defrag-threshold-lower 10 # 碎片率超过10%启动
MEMORY PURGE # 手动触发整理
  1. 重启Redis:最彻底但影响服务可用性
  2. 优化数据结构:减少小对象,使用Hash存储相关数据

过期策略与删除机制

三种过期删除策略

定时删除:设置过期时间时创建定时器,到期立即删除

  • 优点:及时释放内存
  • 缺点:消耗CPU资源创建和管理定时器

惰性删除:访问key时检查是否过期,过期则删除

  • 优点:CPU友好,只在必要时删除
  • 缺点:内存不友好,过期key可能长期占用内存

定期删除:定期随机检查部分key,删除过期的

  • Redis的实际策略,平衡CPU和内存使用

Redis过期删除实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
// 简化的过期删除逻辑
void activeExpireCycle(int type) {
static int current_db = 0;
static int timelimit_exit = 0;

for (int i = 0; i < server.dbnum; i++) {
redisDb *db = server.db + current_db % server.dbnum;

int iteration = 0;
while (iteration < 20) { // 最多检查20个key
long sampled = 0, expired = 0;

// 随机选择key检查过期
for (int i = 0; i < 20; i++) {
dictEntry *de = dictGetRandomKey(db->expires);
if (de == NULL) break;

sampled++;
if (keyIsExpired(db, de)) {
deleteExpiredKey(db, de);
expired++;
}
}

// 如果过期比例超过25%,继续下轮检查
if (expired > 5) iteration++;
else break;
}
current_db++;
}
}

数据结构底层实现深度解析

压缩列表(ZipList)演进

Redis 7.0用ListPack替代了ZipList,解决了级联更新问题。

ZipList问题

1
2
3
// ZipList结构导致的级联更新
// 当插入元素导致某个entry的长度改变时
// 可能引起后续所有entry的重新编码

ListPack优势

  • 每个元素独立编码,避免级联更新
  • 支持从尾部遍历,提高某些操作效率

字典扩容与rehash

Redis字典使用增量式rehash避免阻塞:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 渐进式rehash实现
int dictRehash(dict *d, int n) {
int empty_visits = n * 10;

if (!dictIsRehashing(d)) return 0;

while (n-- && d->ht[0].used != 0) {
// 跳过空桶
while (d->ht[0].table[d->rehashidx] == NULL) {
d->rehashidx++;
if (--empty_visits == 0) return 1;
}

// 迁移桶中所有元素
dictEntry *de = d->ht[0].table[d->rehashidx];
while (de) {
dictEntry *nextde = de->next;
unsigned int h = dictHashKey(d, de->key) & d->ht[1].sizemask;
de->next = d->ht[1].table[h];
d->ht[1].table[h] = de;
d->ht[0].used--;
d->ht[1].used++;
de = nextde;
}
d->ht[0].table[d->rehashidx] = NULL;
d->rehashidx++;
}

return 0;
}

网络模型与性能优化

Redis 6.0 多线程I/O

多线程只用于网络I/O,命令执行仍是单线程:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
// 多线程I/O处理流程
void *IOThreadMain(void *myid) {
while (1) {
// 等待主线程分配任务
pthread_mutex_lock(&io_threads_mutex[id]);

while (io_threads_list[id] && io_threads_pending[id] == 0) {
pthread_cond_wait(&io_threads_cond[id], &io_threads_mutex[id]);
}

// 处理读写任务
listNode *ln;
listIter li;
listRewind(io_threads_list[id], &li);

while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (io_threads_op == IO_THREADS_OP_WRITE) {
writeToClient(c, 0);
} else if (io_threads_op == IO_THREADS_OP_READ) {
readQueryFromClient(c->conn);
}
}

io_threads_pending[id] = 0;
pthread_mutex_unlock(&io_threads_mutex[id]);
}
}

客户端连接管理

连接池配置优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// Jedis连接池配置
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(200); // 最大连接数
config.setMaxIdle(50); // 最大空闲连接
config.setMinIdle(10); // 最小空闲连接
config.setTestOnBorrow(true); // 获取连接时测试
config.setTestOnReturn(true); // 归还连接时测试
config.setTestWhileIdle(true); // 空闲时测试
config.setTimeBetweenEvictionRunsMillis(30000); // 检查间隔
config.setNumTestsPerEvictionRun(10); // 每次检查连接数
config.setMinEvictableIdleTimeMillis(60000); // 最小空闲时间

JedisPool pool = new JedisPool(config, "localhost", 6379);

Pipeline优化

1
2
3
4
5
6
7
8
9
10
11
12
13
// Pipeline批量操作
public void batchSet(Map<String, String> data) {
try (Jedis jedis = pool.getResource()) {
Pipeline pipeline = jedis.pipelined();

for (Map.Entry<String, String> entry : data.entrySet()) {
pipeline.set(entry.getKey(), entry.getValue());
}

List<Object> results = pipeline.syncAndReturnAll();
// 处理结果
}
}

高级数据类型与应用

HyperLogLog

用于基数统计,占用内存固定(12KB),误差率0.81%。

实现原理

  • 基于概率算法,通过观察随机数的最大前导零个数估算基数
  • 使用调和平均数减少误差
  • 适用于UV统计等场景
1
2
3
4
# 网站UV统计
PFADD uv:20230815 user1 user2 user3
PFCOUNT uv:20230815 # 获取UV数量
PFMERGE uv:week uv:20230815 uv:20230816 # 合并多日数据

布隆过滤器(Redis Module)

1
2
3
4
5
6
# Redis布隆过滤器模块
BF.RESERVE myfilter 0.01 1000000 # 创建过滤器,误报率0.01%,预期元素100万
BF.ADD myfilter "user123" # 添加元素
BF.EXISTS myfilter "user123" # 检查元素是否存在
BF.MADD myfilter item1 item2 item3 # 批量添加
BF.MEXISTS myfilter item1 item2 item3 # 批量检查

Geo地理位置

基于Sorted Set实现,使用GeoHash算法。

1
2
3
4
5
6
7
8
9
10
11
# 添加地理位置
GEOADD cities 116.397128 39.916527 "北京" 121.473701 31.230416 "上海"

# 计算距离
GEODIST cities "北京" "上海" km

# 范围查询
GEORADIUS cities 116.397128 39.916527 1000 km WITHDIST WITHCOORD

# 根据成员查询
GEORADIUSBYMEMBER cities "北京" 1000 km

安全性问题与防护

常见安全漏洞

命令注入

1
2
3
4
5
6
// 错误做法:直接拼接用户输入
String key = "user:" + userInput; // userInput可能包含恶意命令
jedis.eval("return redis.call('get', '" + key + "')", 0);

// 正确做法:使用参数化查询
jedis.eval("return redis.call('get', KEYS[1])", 1, key);

未授权访问

1
2
3
4
5
6
# redis.conf安全配置
bind 127.0.0.1 # 绑定指定IP
requirepass your_password # 设置密码
protected-mode yes # 开启保护模式
port 0 # 禁用默认端口
port 6380 # 使用非标准端口

危险命令禁用

1
2
3
4
5
# 重命名危险命令
rename-command FLUSHDB "" # 禁用FLUSHDB
rename-command FLUSHALL "" # 禁用FLUSHALL
rename-command SHUTDOWN "REDIS_SHUTDOWN" # 重命名SHUTDOWN
rename-command CONFIG "REDIS_CONFIG" # 重命名CONFIG

ACL访问控制(Redis 6.0+)

1
2
3
4
5
6
7
8
9
10
11
# 创建用户
ACL SETUSER alice on >password123 ~cached:* +get +set

# 查看用户权限
ACL LIST

# 删除用户
ACL DELUSER alice

# 检查权限
AUTH alice password123

监控与运维

关键监控指标

性能指标

1
2
3
4
5
6
7
8
# 通过INFO命令获取
INFO stats
# 关注指标:
# - instantaneous_ops_per_sec: QPS
# - keyspace_hits/keyspace_misses: 命中率
# - used_cpu_sys/used_cpu_user: CPU使用率
# - connected_clients: 连接数
# - blocked_clients: 阻塞连接数

内存指标

1
2
3
4
5
6
INFO memory
# 关注指标:
# - used_memory: 已使用内存
# - used_memory_rss: 物理内存占用
# - mem_fragmentation_ratio: 内存碎片率
# - used_memory_peak: 内存使用峰值

持久化指标

1
2
3
4
5
INFO persistence
# 关注指标:
# - rdb_last_save_time: 最后RDB保存时间
# - aof_last_rewrite_time_sec: AOF重写耗时
# - aof_current_size: AOF文件大小

故障排查常用命令

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 客户端连接信息
CLIENT LIST
CLIENT INFO

# 慢查询分析
SLOWLOG GET 10
CONFIG GET slowlog-log-slower-than

# 大key查找
MEMORY USAGE keyname
redis-cli --bigkeys

# 热点key分析
redis-cli --hotkeys

# 延迟监控
LATENCY HISTORY command
CONFIG SET latency-monitor-threshold 100

特殊应用场景

分布式限流

固定窗口限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class RedisRateLimiter {
public boolean isAllowed(String key, int limit, int window) {
long current = System.currentTimeMillis() / 1000 / window;
String windowKey = key + ":" + current;

String script =
"local count = redis.call('incr', KEYS[1]) " +
"if count == 1 then " +
" redis.call('expire', KEYS[1], ARGV[1]) " +
"end " +
"return count";

Long count = (Long) jedis.eval(script, Arrays.asList(windowKey),
Arrays.asList(String.valueOf(window)));

return count <= limit;
}
}

滑动窗口限流

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public boolean slidingWindowLimit(String key, int limit, int window) {
long now = System.currentTimeMillis();
long windowStart = now - window * 1000;

String script =
"redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"local count = redis.call('zcard', KEYS[1]) " +
"if count < tonumber(ARGV[2]) then " +
" redis.call('zadd', KEYS[1], ARGV[3], ARGV[3]) " +
" redis.call('expire', KEYS[1], ARGV[4]) " +
" return 1 " +
"else " +
" return 0 " +
"end";

Long result = (Long) jedis.eval(script, Arrays.asList(key),
Arrays.asList(String.valueOf(windowStart), String.valueOf(limit),
String.valueOf(now), String.valueOf(window)));

return result == 1;
}

分布式Session

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
public class RedisSessionManager {

@Autowired
private RedisTemplate<String, Object> redisTemplate;

public void createSession(String sessionId, String userId, int timeout) {
String key = "session:" + sessionId;
Map<String, Object> sessionData = new HashMap<>();
sessionData.put("userId", userId);
sessionData.put("createTime", System.currentTimeMillis());

redisTemplate.opsForHash().putAll(key, sessionData);
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}

public boolean isValidSession(String sessionId) {
String key = "session:" + sessionId;
return redisTemplate.hasKey(key);
}

public void renewSession(String sessionId, int timeout) {
String key = "session:" + sessionId;
if (redisTemplate.hasKey(key)) {
redisTemplate.expire(key, timeout, TimeUnit.SECONDS);
}
}
}

消息队列实现

基于List的简单队列

1
2
3
4
5
6
7
8
9
10
11
12
public class RedisQueue {
// 生产者
public void produce(String queue, String message) {
jedis.lpush(queue, message);
}

// 消费者(阻塞式)
public String consume(String queue, int timeout) {
List<String> result = jedis.brpop(timeout, queue);
return result != null ? result.get(1) : null;
}
}

基于Stream的消息队列(Redis 5.0+):

1
2
3
4
5
6
7
8
9
10
11
# 生产消息
XADD mystream * field1 value1 field2 value2

# 创建消费者组
XGROUP CREATE mystream mygroup $ MKSTREAM

# 消费消息
XREADGROUP GROUP mygroup consumer1 COUNT 10 STREAMS mystream >

# 确认消息
XACK mystream mygroup message_id

延时队列

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class RedisDelayQueue {

public void addDelayTask(String task, long delayTime) {
long executeTime = System.currentTimeMillis() + delayTime;
jedis.zadd("delay_queue", executeTime, task);
}

public List<String> getReadyTasks() {
long now = System.currentTimeMillis();
Set<String> tasks = jedis.zrangeByScore("delay_queue", 0, now);

if (!tasks.isEmpty()) {
// 原子性删除已获取的任务
String script =
"local tasks = redis.call('zrangebyscore', KEYS[1], 0, ARGV[1]) " +
"if #tasks > 0 then " +
" redis.call('zremrangebyscore', KEYS[1], 0, ARGV[1]) " +
"end " +
"return tasks";

@SuppressWarnings("unchecked")
List<String> result = (List<String>) jedis.eval(script,
Arrays.asList("delay_queue"), Arrays.asList(String.valueOf(now)));
return result;
}

return Collections.emptyList();
}
}

性能调优技巧

内存优化

使用Hash存储小对象

1
2
3
4
5
6
7
8
9
// 不推荐:为每个用户字段单独设置key
jedis.set("user:1001:name", "张三");
jedis.set("user:1001:age", "25");
jedis.set("user:1001:email", "zhang@example.com");

// 推荐:使用Hash存储
jedis.hset("user:1001", "name", "张三");
jedis.hset("user:1001", "age", "25");
jedis.hset("user:1001", "email", "zhang@example.com");

合理设置过期时间

1
2
3
4
// 避免同时过期导致雪崩
int baseExpire = 3600; // 1小时
int randomExpire = baseExpire + new Random().nextInt(300); // 增加0-5分钟随机时间
jedis.setex(key, randomExpire, value);

网络优化

批量操作优化

1
2
3
4
5
6
7
8
9
10
11
// 使用Pipeline
Pipeline pipeline = jedis.pipelined();
for (int i = 0; i < 1000; i++) {
pipeline.set("key" + i, "value" + i);
}
pipeline.sync();

// 或使用原生批量命令
Map<String, String> batch = new HashMap<>();
// ... 填充数据
jedis.mset(flattenMap(batch));

CPU优化

Lua脚本减少网络往返

1
2
3
4
5
6
7
8
9
10
11
12
13
-- 原子性增加库存
local current = redis.call('get', KEYS[1])
if current == false then
return -1 -- key不存在
end

current = tonumber(current)
if current >= tonumber(ARGV[1]) then
redis.call('decrby', KEYS[1], ARGV[1])
return current - tonumber(ARGV[1])
else
return -2 -- 库存不足
end

你好,我很乐意为你详细讲解 Redis。

Redis 是一个开源的、基于内存的、高性能的键值存储系统。它通常被用作数据库、缓存和消息中间件。Redis 支持多种数据结构,如字符串、哈希、列表、集合和有序集合,这使得它在各种应用场景中都非常灵活。

Redis 的核心特点


  • 高性能: Redis 的所有数据都存储在内存中,因此读写速度非常快。通常,单个 Redis 实例每秒能处理超过 10 万次读写操作。
  • 丰富的数据结构: Redis 不仅仅是简单的键值存储,它支持多种复杂的数据结构,这让它能满足更广泛的应用需求。
  • 持久化: 虽然 Redis 是基于内存的,但它提供了两种不同的持久化方式,可以将数据从内存中保存到磁盘上,以防止服务器重启后数据丢失。
  • 原子性: Redis 的所有操作都是原子性的。这意味着一个命令要么完全执行,要么根本不执行,中间不会被打断,这在多客户端并发访问时尤其重要。
  • 主从复制: Redis 支持主从复制,可以将数据从一个主节点(Master)同步到一个或多个从节点(Slave),从而实现数据的备份和读写分离,提高系统的可用性和扩展性。
  • 高可用性与集群: Redis 提供了 Sentinel(哨兵)和 Cluster(集群)两种方案,来保证系统的高可用性和扩展性。

Redis 的数据结构详解


这是 Redis 最有特色的地方,理解这些数据结构是掌握 Redis 的关键。

1. String (字符串)

  • 最基本的数据结构,可以存储任何类型的数据,比如字符串、数字或二进制数据。一个键最多可以存储 512MB 的值。
  • 常用命令:
    • SET key value: 设置键值对。
    • GET key: 获取键对应的值。
    • INCR key: 将键对应的值加 1,常用于计数器。
    • MSET key1 value1 key2 value2: 同时设置多个键值对。
    • MGET key1 key2: 同时获取多个键的值。
  • 应用场景: 缓存、计数器、分布式锁等。

2. Hash (哈希)

  • 类似于一个键值对的集合。一个哈希键可以存储多个字段(field)和对应的值(value),非常适合存储对象。
  • 常用命令:
    • HSET key field value: 设置哈希键中一个字段的值。
    • HGET key field: 获取哈希键中一个字段的值。
    • HGETALL key: 获取哈希键中所有的字段和值。
    • HMSET key field1 value1 field2 value2: 同时设置多个字段。
    • HDEL key field1 field2: 删除一个或多个字段。
  • 应用场景: 存储用户信息(如user:100作为键,nameagecity作为字段),商品信息等。

3. List (列表)

  • 一个有序的、可以重复的字符串集合。可以从列表的头部或尾部添加或删除元素。
  • 常用命令:
    • LPUSH key value1 value2: 从列表的头部插入一个或多个值。
    • RPUSH key value1 value2: 从列表的尾部插入一个或多个值。
    • LPOP key: 从列表的头部移除并返回一个值。
    • RPOP key: 从列表的尾部移除并返回一个值。
    • LRANGE key start stop: 获取列表中指定范围的元素。
  • 应用场景: 消息队列(队列、栈)、最新文章列表、历史记录等。

4. Set (集合)

  • 一个无序的、不重复的字符串集合。集合提供了交集、并集和差集等操作。
  • 常用命令:
    • SADD key member1 member2: 向集合中添加一个或多个成员。
    • SMEMBERS key: 获取集合中的所有成员。
    • SISMEMBER key member: 判断一个成员是否在集合中。
    • SINTER key1 key2: 求两个集合的交集。
    • SUNION key1 key2: 求两个集合的并集。
  • 应用场景: 社交网络中的共同好友、标签系统、去重等。

5. Sorted Set (有序集合)

  • 和集合类似,但每个成员都会关联一个分数(score)。通过这个分数,可以对集合中的成员进行排序。分数可以重复,但成员必须唯一。
  • 常用命令:
    • ZADD key score1 member1 score2 member2: 向有序集合中添加一个或多个带分数的成员。
    • ZRANGE key start stop: 按分数从小到大排序,获取指定范围的成员。
    • ZREVRANGE key start stop: 按分数从大到小排序,获取指定范围的成员。
    • ZSCORE key member: 获取一个成员的分数。
    • ZINCRBY key increment member: 给指定成员的分数增加指定值。
  • 应用场景: 排行榜(游戏分数、热点新闻)、带权重的任务队列等。

Redis 的持久化机制


Redis 提供了两种持久化方式,可以将数据从内存保存到硬盘,确保数据不会因为服务重启而丢失。

1. RDB (Redis Database)

  • 快照方式。它会在指定的时间间隔内,将内存中的数据快照写入到磁盘上一个二进制文件中(dump.rdb)。
  • 优点: RDB 文件是一个非常紧凑的二进制文件,非常适合备份和灾难恢复。
  • 缺点: 每次保存都会丢失从上次快照到现在之间的数据。

2. AOF (Append Only File)

  • 日志方式。它会记录每一次对 Redis 数据库的写操作命令,并以追加(append)的方式写入到文件中。当 Redis 重启时,会重新执行 AOF 文件中的命令来恢复数据。
  • 优点: 数据丢失风险低,可以配置为每秒同步一次,或者每执行一个命令就同步一次。
  • 缺点: AOF 文件通常比 RDB 文件大,且恢复速度可能较慢。

在实际应用中,通常会同时使用 RDB 和 AOF,以获得更高的可靠性。

Redis 的高可用性


1. 主从复制 (Replication)

  • 基本原理: 将一个 Redis 实例(主节点)的数据复制到一个或多个其他实例(从节点)。从节点的数据是主节点的完整副本。
  • 作用:
    • 数据备份: 从节点可以作为数据的热备份。
    • 读写分离: 大多数读操作可以分流到从节点上,减轻主节点的压力。

2. Sentinel (哨兵)

  • 作用: 自动化管理主从复制集群。它是一个监控系统,可以监控主节点和从节点是否正常运行。
  • 核心功能:
    • 监控: 不断检查主从节点是否正常工作。
    • 通知: 当某个 Redis 实例出现问题时,会发送通知。
    • 故障转移: 如果主节点发生故障,Sentinel 会自动从剩下的从节点中选举一个新的主节点,并让其他从节点切换到这个新的主节点上,从而实现高可用。

3. Redis Cluster (集群)

  • 作用: 解决 Redis 单机内存容量和并发量的瓶颈问题。它将数据分散到多个节点上,每个节点只负责存储部分数据。
  • 核心功能:
    • 数据分片: 自动将数据分布在多个节点上。
    • 高可用性: 即使部分节点宕机,集群也能继续正常工作。

框架背诵

Java框架核心知识详解

一、Spring框架深入解析

1.1 Spring核心概念与原理

Spring框架是一个分层的企业级应用开发框架,其核心是控制反转(IoC)和面向切面编程(AOP)。

IoC容器原理深入分析

IoC(控制反转)的本质: 传统开发中,对象的创建和依赖关系由程序代码直接控制,而IoC将这个控制权交给了外部容器。Spring通过依赖注入(DI)来实现IoC。

IoC容器的实现机制:

  1. BeanFactory: 基础容器,提供基本的IoC功能
  2. ApplicationContext: 高级容器,继承BeanFactory,提供更多企业级功能

Bean的生命周期详解:

1
实例化 → 属性赋值 → 初始化前处理 → 初始化 → 初始化后处理 → 使用 → 销毁前处理 → 销毁

详细生命周期步骤:

  1. Bean元数据解析(XML、注解、Java配置)
  2. 调用Bean构造函数实例化
  3. 依赖注入(setter方法、构造函数、字段注入)
  4. 如果实现了BeanNameAware,调用setBeanName()
  5. 如果实现了BeanFactoryAware,调用setBeanFactory()
  6. 如果实现了ApplicationContextAware,调用setApplicationContext()
  7. 如果有BeanPostProcessor,调用postProcessBeforeInitialization()
  8. 如果实现了InitializingBean,调用afterPropertiesSet()
  9. 如果配置了init-method,调用自定义初始化方法
  10. 如果有BeanPostProcessor,调用postProcessAfterInitialization()
  11. Bean可以被使用
  12. 容器关闭时,如果实现了DisposableBean,调用destroy()
  13. 如果配置了destroy-method,调用自定义销毁方法

DI的三种注入方式对比

  • 构造器注入(Constructor Injection)

    • 优点:强制依赖、依赖清晰、不可变性
    • 缺点:当依赖项过多时构造函数臃肿
    • 最佳实践:官方推荐,特别是当依赖是必需的、不可选的时候
  • Setter注入(Setter Injection)

    • 优点:可选依赖,灵活性高
    • 缺点:对象可能处于不完整状态,无法使用final修饰
    • 最佳实践:用于注入可选的依赖项
  • 字段注入(Field Injection)

    • 优点:代码简洁
    • 缺点:不推荐使用,隐藏依赖、测试困难、无法使用final修饰

AOP原理深入分析

AOP实现机制: Spring AOP基于代理模式实现,支持两种代理方式:

  1. JDK动态代理: 针对实现了接口的类
  2. CGLIB代理: 针对没有实现接口的类

我们来系统性地讲解一下 Spring AOP 的实现机制,包括:

  1. JDK 动态代理
  2. CGLIB 动态代理
  3. 静态代理(对比说明)

🌟 一、Spring AOP 实现机制概述

Spring AOP(面向切面编程)是基于 代理模式 实现的,它通过在目标方法执行前后织入逻辑(增强),实现横切关注点(如日志、安全、事务等)的分离。

Spring AOP 仅支持方法级别的代理(即对方法进行增强),不支持字段、构造器等底层字节码增强(这要用 AspectJ)。


🧩 二、Spring AOP 的两种动态代理方式

1️⃣ JDK 动态代理(基于接口)

✅ 原理:

  • 基于 Java 的 java.lang.reflect.Proxy 类实现。
  • 必须要有接口,Spring 会为接口生成代理类。
  • 代理类在运行时实现接口,并将调用委托给 InvocationHandler。

📦 示例:

1
2
3
4
5
6
7
8
9
public interface UserService {
void addUser();
}

public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println("添加用户");
}
}

Spring AOP 为 UserService 创建代理:

1
2
3
4
5
6
7
8
9
10
11
UserService proxy = (UserService) Proxy.newProxyInstance(
userService.getClass().getClassLoader(),
userService.getClass().getInterfaces(),
new InvocationHandler() {
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("前置增强");
Object result = method.invoke(userService, args);
System.out.println("后置增强");
return result;
}
});

🎯 特点:

特性 描述
是否依赖接口 ✅ 是
性能 较高(比 CGLIB 略快)
生成的代理类 实现接口

2️⃣ CGLIB 动态代理(基于子类)

✅ 原理:

  • 使用 CGLIB 库在运行时 生成目标类的子类,并重写其方法来实现增强。
  • 适用于 没有接口 的类,或者想对类本身增强而非接口。

🧪 示例:

1
2
3
4
5
public class UserService {
public void addUser() {
System.out.println("添加用户");
}
}

Spring 通过 CGLIB 生成子类:

1
2
3
4
5
6
7
8
9
10
11
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback(new MethodInterceptor() {
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("前置增强");
Object result = proxy.invokeSuper(obj, args);
System.out.println("后置增强");
return result;
}
});
UserService proxy = (UserService) enhancer.create();

🎯 特点:

特性 描述
是否依赖接口 ❌ 否
是否可代理 final 类/方法 ❌ 否(final 无法被继承/重写)
生成的代理类 子类

🏗 三、静态代理(对比动态代理)

✅ 原理:

  • 自己写一个代理类,实现相同接口,将方法调用委托给目标对象,并在调用前后手动添加增强逻辑。

🧪 示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public interface UserService {
void addUser();
}

public class UserServiceImpl implements UserService {
public void addUser() {
System.out.println("添加用户");
}
}

// 静态代理类
public class UserServiceProxy implements UserService {
private UserService target;

public UserServiceProxy(UserService target) {
this.target = target;
}

public void addUser() {
System.out.println("前置增强");
target.addUser();
System.out.println("后置增强");
}
}

🎯 特点:

特性 描述
是否自动生成 ❌ 需手动编码
灵活性 差(每个类都要写一个代理类)
是否支持 AOP ❌ 无法动态织入多个切面

🔚 四、三者对比总结

特性 静态代理 JDK 动态代理 CGLIB 动态代理
是否依赖接口 ✅ 是 ✅ 是 ❌ 否
是否自动生成代理类 ❌ 手动编写 ✅ 运行时生成 ✅ 运行时生成
是否可代理类 ❌ 否(接口为主) ❌ 否(接口为主) ✅ 是(类的子类)
性能 一般 较高 略慢(但可接受)
是否支持 final 方法 ❌ 否

✅ Spring 如何选择代理方式?

  • 默认使用 JDK 动态代理(如果目标类实现了接口)。
  • 如果目标类没有接口,Spring 自动切换为 CGLIB。
  • 可以强制使用 CGLIB:
1
@EnableAspectJAutoProxy(proxyTargetClass = true)

AOP核心概念:

  • 切面(Aspect): 横切关注点的模块化
  • 连接点(Joinpoint): 程序执行中的特定点
  • 切点(Pointcut): 连接点的集合
  • 通知(Advice): 切面在特定连接点执行的代码
  • 目标对象(Target): 被代理的对象
  • 代理对象(Proxy): AOP框架创建的对象

JDK动态代理 vs. CGLIB动态代理

  • JDK动态代理

    • 基于接口实现
    • 使用Proxy.newProxyInstance()创建代理
    • 只能代理实现了接口的类
  • CGLIB动态代理

    • 基于继承实现
    • 使用Enhancer类创建代理
    • 可以代理普通类
    • 不能代理final类和方法

1.2 Spring核心注解详解

基础配置注解

1
2
3
4
5
6
@Configuration  // 表示这是一个配置类
@ComponentScan(basePackages = "com.example") // 组件扫描
@EnableAutoConfiguration // 启用自动配置
@SpringBootApplication // SpringBoot主类注解,包含上述三个
@Import(OtherConfig.class) // 导入其他配置类
@PropertySource("classpath:application.properties") // 加载属性文件

Bean定义注解

1
2
3
4
5
6
7
8
9
10
@Component  // 通用组件
@Service // 业务层组件
@Repository // 数据访问层组件
@Controller // 控制层组件
@RestController // RESTful控制器,相当于@Controller + @ResponseBody
@Bean // 方法级别,定义Bean
@Scope("singleton/prototype/request/session") // Bean作用域
@Lazy // 延迟初始化
@Primary // 优先注入
@Qualifier("beanName") // 指定注入的Bean名称

依赖注入注解

1
2
3
4
5
@Autowired  // 自动装配,可用于构造函数、方法、字段
@Resource // JSR-250标准,按名称注入
@Inject // JSR-330标准
@Value("${property.name}") // 注入配置值
@ConfigurationProperties(prefix = "app") // 绑定配置属性

生命周期注解

1
2
@PostConstruct  // 初始化方法
@PreDestroy // 销毁方法

AOP相关注解

1
2
3
4
5
6
7
@Aspect         // 声明切面
@Pointcut // 定义切点
@Before // 前置通知
@After // 后置通知
@AfterReturning // 返回后通知
@AfterThrowing // 异常通知
@Around // 环绕通知

1.3 Spring常见使用场景

场景1:服务层事务管理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Service
@Transactional
public class UserService {

@Autowired
private UserRepository userRepository;

@Transactional(propagation = Propagation.REQUIRED, rollbackFor = Exception.class)
public User createUser(User user) {
// 业务逻辑
return userRepository.save(user);
}

@Transactional(readOnly = true)
public User findById(Long id) {
return userRepository.findById(id);
}
}

场景2:缓存管理

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class ProductService {

@Cacheable(value = "products", key = "#id")
public Product getProduct(Long id) {
return productRepository.findById(id);
}

@CacheEvict(value = "products", key = "#product.id")
public Product updateProduct(Product product) {
return productRepository.save(product);
}
}

1.4 Spring常见面试问题

Q1: Spring IoC容器的初始化过程是怎样的?

A: Spring IoC容器初始化分为三个阶段:

  1. Resource定位: 定位配置文件
  2. BeanDefinition载入: 将配置信息转换为Spring内部数据结构
  3. BeanDefinition注册: 将BeanDefinition注册到IoC容器的HashMap中

具体流程:

  • 创建ApplicationContext
  • 加载配置元数据(XML、注解、Java配置)
  • 解析配置,创建BeanDefinition
  • 注册BeanDefinition到BeanDefinitionRegistry
  • 实例化非懒加载的单例Bean

Q2: Spring中的循环依赖是如何解决的?

A: Spring通过三级缓存解决循环依赖:

  1. singletonObjects: 一级缓存,存放完整的Bean实例
  2. earlySingletonObjects: 二级缓存,存放早期的Bean实例
  3. singletonFactories: 三级缓存,存放Bean工厂

解决过程:

  • A依赖B,B依赖A
  • 创建A时,将A的工厂放入三级缓存
  • A需要注入B,开始创建B
  • B需要注入A,从缓存中获取A的早期实例
  • B创建完成,A继续创建完成

Q3: Spring AOP的实现原理?

A: Spring AOP基于代理模式实现:

  1. JDK动态代理: 目标类实现接口时使用,基于反射机制
  2. CGLIB代理: 目标类没有接口时使用,基于字节码技术

代理创建过程:

  • Spring在Bean初始化后,检查是否需要AOP
  • 如果需要,创建代理对象替换原始Bean
  • 代理对象拦截方法调用,执行切面逻辑

二、Spring Boot深入解析

2.1 Spring Boot核心原理

自动配置原理: Spring Boot通过@EnableAutoConfiguration注解启用自动配置机制。

核心类分析:

  1. SpringBootApplication: 组合注解,包含@Configuration、@EnableAutoConfiguration、@ComponentScan
  2. AutoConfigurationImportSelector: 负责导入自动配置类
  3. spring.factories: META-INF/spring.factories文件定义自动配置类

自动配置流程:

  1. SpringBoot启动时扫描所有jar包下的META-INF/spring.factories文件
  2. 加载文件中定义的自动配置类
  3. 根据条件注解(@ConditionalOnClass等)判断是否生效
  4. 生效的配置类会创建相应的Bean

2.2 Spring Boot启动流程详解

SpringApplication.run()方法执行流程:

  1. 准备阶段
    • 创建SpringApplication实例
    • 确定应用类型(SERVLET、REACTIVE、NONE)
    • 加载ApplicationContextInitializer
    • 加载ApplicationListener
  2. 启动阶段
    • 启动计时器
    • 配置Headless模式
    • 获取并启动监听器
    • 准备环境(Environment)
    • 打印Banner
  3. 容器创建阶段
    • 创建ApplicationContext
    • 准备ApplicationContext
    • 刷新ApplicationContext
    • 刷新后处理
  4. 完成阶段
    • 停止计时器
    • 发布启动完成事件
    • 调用Runners

2.3 Spring Boot核心注解

启动类注解

1
2
3
4
5
6
7
8
9
10
@SpringBootApplication
// 等价于以下三个注解的组合
@Configuration
@EnableAutoConfiguration
@ComponentScan

@SpringBootConfiguration // Spring Boot配置类
@EnableScheduling // 启用定时任务
@EnableAsync // 启用异步处理
@EnableCaching // 启用缓存

条件注解

1
2
3
4
5
6
@ConditionalOnClass(DataSource.class)      // 类路径下存在指定类
@ConditionalOnMissingBean(DataSource.class) // 容器中不存在指定Bean
@ConditionalOnProperty(name = "app.enabled", havingValue = "true") // 属性匹配
@ConditionalOnWebApplication // Web应用环境
@ConditionalOnNotWebApplication // 非Web应用环境
@Profile("dev") // 激活的profile

配置属性注解

1
2
3
4
5
6
7
@ConfigurationProperties(prefix = "app.datasource")
public class DataSourceProperties {
private String url;
private String username;
private String password;
// getters and setters
}

2.4 Spring Boot实际应用场景

场景1:微服务架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@SpringBootApplication
@EnableEurekaClient
@EnableFeignClients
public class UserServiceApplication {
public static void main(String[] args) {
SpringApplication.run(UserServiceApplication.class, args);
}
}

@FeignClient(name = "order-service")
public interface OrderServiceClient {
@GetMapping("/orders/{userId}")
List<Order> getOrdersByUserId(@PathVariable Long userId);
}

场景2:数据访问层整合

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SpringBootApplication
@EnableJpaRepositories
public class Application {

@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}

@Bean
@ConfigurationProperties("spring.datasource.secondary")
public DataSourceProperties secondaryDataSourceProperties() {
return new DataSourceProperties();
}
}

2.5 Spring Boot面试问题

Q1: Spring Boot的启动原理是什么?

A: Spring Boot启动原理核心在于自动配置:

  1. @SpringBootApplication组合了三个注解,其中@EnableAutoConfiguration是关键
  2. AutoConfigurationImportSelector会扫描所有引入的jar包,查找其META-INF/spring.factories文件中org.springframework.boot.autoconfigure.EnableAutoConfiguration键所对应的配置类全限定名列表
  3. 根据条件注解判断哪些自动配置类生效
  4. 生效的配置类会向容器中注册相应的Bean

Q2: Spring Boot如何实现自动配置?

A: 自动配置通过以下机制实现:

  1. 条件注解: @ConditionalOnClass、@ConditionalOnBean等判断配置是否生效
  2. 配置文件: spring.factories定义自动配置类列表
  3. 配置属性: @ConfigurationProperties绑定配置文件中的属性
  4. 默认配置: 提供合理的默认值,用户可覆盖

Q3: Spring Boot Starter的工作原理?

A: Starter是Spring Boot自动配置的载体:

  1. 依赖管理: 通过Maven/Gradle引入相关依赖
  2. 自动配置: 包含AutoConfiguration类
  3. 属性绑定: 提供ConfigurationProperties类
  4. 条件装配: 使用条件注解控制Bean的创建

三、Spring MVC深入解析

3.1 Spring MVC核心组件

DispatcherServlet处理流程:

  1. 接收请求: DispatcherServlet接收HTTP请求
  2. 查找Handler: HandlerMapping查找处理请求的Handler
  3. 获取HandlerAdapter: 获取能够执行Handler的HandlerAdapter
  4. 执行Handler: HandlerAdapter执行Handler(Controller方法)
  5. 处理结果: 返回ModelAndView
  6. 视图解析: ViewResolver解析视图名称
  7. 渲染视图: View渲染模型数据
  8. 返回响应: 响应结果返回给客户端

核心组件详解:

  • DispatcherServlet: 前端控制器,统一处理请求
  • HandlerMapping: 处理器映射器,URL与Handler的映射
  • HandlerAdapter: 处理器适配器,执行Handler
  • Handler: 处理器,即Controller
  • ViewResolver: 视图解析器
  • View: 视图

3.2 Spring MVC核心注解

控制器注解

1
2
3
4
5
6
7
8
@Controller             // 标记控制器类
@RestController // RESTful控制器
@RequestMapping("/api") // 请求映射
@GetMapping("/users") // GET请求映射
@PostMapping("/users") // POST请求映射
@PutMapping("/users/{id}") // PUT请求映射
@DeleteMapping("/users/{id}") // DELETE请求映射
@PatchMapping("/users/{id}") // PATCH请求映射

参数绑定注解

1
2
3
4
5
6
7
8
@RequestParam("name")           // 请求参数
@PathVariable("id") // 路径变量
@RequestBody // 请求体
@RequestHeader("Content-Type") // 请求头
@CookieValue("sessionId") // Cookie值
@ModelAttribute // 模型属性
@SessionAttribute // Session属性
@RequestPart // 文件上传

响应处理注解

1
2
3
4
5
@ResponseBody              // 响应体
@ResponseStatus(HttpStatus.CREATED) // 响应状态码
@ExceptionHandler // 异常处理
@ControllerAdvice // 全局控制器增强
@RestControllerAdvice // RESTful全局异常处理

3.3 Spring MVC实际应用场景

场景1:RESTful API开发

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
@RestController
@RequestMapping("/api/users")
@Validated
public class UserController {

@Autowired
private UserService userService;

@GetMapping
public ResponseEntity<PageResult<User>> getUsers(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int size,
@RequestParam(required = false) String keyword) {

PageResult<User> result = userService.findUsers(page, size, keyword);
return ResponseEntity.ok(result);
}

@PostMapping
public ResponseEntity<User> createUser(@Valid @RequestBody UserCreateRequest request) {
User user = userService.createUser(request);
return ResponseEntity.status(HttpStatus.CREATED).body(user);
}

@PutMapping("/{id}")
public ResponseEntity<User> updateUser(
@PathVariable Long id,
@Valid @RequestBody UserUpdateRequest request) {
User user = userService.updateUser(id, request);
return ResponseEntity.ok(user);
}

@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteUser(@PathVariable Long id) {
userService.deleteUser(id);
return ResponseEntity.noContent().build();
}
}

场景2:全局异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@RestControllerAdvice
public class GlobalExceptionHandler {

@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse handleValidationException(ValidationException e) {
return ErrorResponse.builder()
.code("VALIDATION_ERROR")
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
}

@ExceptionHandler(EntityNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
public ErrorResponse handleEntityNotFoundException(EntityNotFoundException e) {
return ErrorResponse.builder()
.code("ENTITY_NOT_FOUND")
.message(e.getMessage())
.timestamp(LocalDateTime.now())
.build();
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ErrorResponse handleGenericException(Exception e) {
log.error("Unexpected error occurred", e);
return ErrorResponse.builder()
.code("INTERNAL_ERROR")
.message("An unexpected error occurred")
.timestamp(LocalDateTime.now())
.build();
}
}

场景3:文件上传处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
@RestController
@RequestMapping("/api/files")
public class FileController {

@PostMapping("/upload")
public ResponseEntity<FileUploadResponse> uploadFile(
@RequestPart("file") MultipartFile file,
@RequestParam(required = false) String description) {

// 文件类型验证
if (!isValidFileType(file.getContentType())) {
throw new InvalidFileTypeException("Invalid file type");
}

// 文件大小验证
if (file.getSize() > MAX_FILE_SIZE) {
throw new FileSizeExceededException("File size exceeds limit");
}

String fileName = fileService.saveFile(file, description);

FileUploadResponse response = FileUploadResponse.builder()
.fileName(fileName)
.originalName(file.getOriginalFilename())
.size(file.getSize())
.contentType(file.getContentType())
.uploadTime(LocalDateTime.now())
.build();

return ResponseEntity.ok(response);
}
}

3.4 Spring MVC面试问题

Q1: Spring MVC的执行流程是怎样的?

A: Spring MVC的执行流程如下:

  1. 用户发送请求到DispatcherServlet
  2. DispatcherServlet调用HandlerMapping查找Handler
  3. HandlerMapping返回HandlerExecutionChain(包含Handler和拦截器)
  4. DispatcherServlet调用HandlerAdapter执行Handler
  5. Handler执行完成后返回ModelAndView
  6. DispatcherServlet调用ViewResolver解析视图名称
  7. ViewResolver返回View对象
  8. DispatcherServlet调用View的render方法渲染视图
  9. 响应结果返回给用户

Q2: Spring MVC中的拦截器是如何工作的?

A: 拦截器基于AOP思想,在Handler执行前后进行处理:

  1. HandlerInterceptor接口提供三个方法:
    • preHandle(): 前置处理,返回false则中断请求
    • postHandle(): 后置处理,Handler执行后调用
    • afterCompletion(): 完成处理,视图渲染后调用
  2. 执行顺序
    • 多个拦截器按配置顺序执行preHandle()
    • 按相反顺序执行postHandle()和afterCompletion()

Q3: @RequestBody和@ResponseBody的工作原理?

A: 这两个注解基于HttpMessageConverter工作:

  1. @RequestBody:
    • 使用HttpMessageConverter将HTTP请求体转换为Java对象
    • 常用的转换器:MappingJackson2HttpMessageConverter处理JSON
  2. @ResponseBody:
    • 使用HttpMessageConverter将Java对象转换为HTTP响应体
    • 根据Accept头选择合适的转换器

四、MyBatis深入解析

4.1 MyBatis核心原理

MyBatis架构分析: MyBatis采用分层架构设计:

  1. API接口层: 提供给外部使用的接口API
  2. 数据处理层: 参数映射、SQL解析、结果映射
  3. 基础支撑层: 连接管理、事务管理、配置加载、缓存处理

MyBatis核心组件:

  • SqlSessionFactory: 会话工厂,负责创建SqlSession
  • SqlSession: 会话,执行SQL的核心接口
  • Executor: 执行器,实际执行SQL
  • StatementHandler: 语句处理器,处理SQL语句
  • ParameterHandler: 参数处理器,处理SQL参数
  • ResultSetHandler: 结果集处理器,处理查询结果
  • MappedStatement: 映射语句,封装SQL配置信息

4.2 MyBatis执行流程详解

SQL执行流程:

  1. 解析配置: 解析mybatis-config.xml和Mapper XML文件
  2. 创建会话: 通过SqlSessionFactory创建SqlSession
  3. 获取Mapper: 通过动态代理创建Mapper接口实例
  4. 执行SQL: 调用Mapper方法,转换为MappedStatement执行
  5. 参数处理: ParameterHandler处理输入参数
  6. 执行查询: StatementHandler执行SQL语句
  7. 结果映射: ResultSetHandler处理结果集
  8. 返回结果: 将结果返回给调用方

动态代理原理: MyBatis使用JDK动态代理为Mapper接口创建代理对象:

1
2
3
4
5
6
7
8
// MapperProxy实现InvocationHandler
public class MapperProxy<T> implements InvocationHandler {
@Override
public Object invoke(Object proxy, Method method, Object[] args) {
// 根据方法签名找到对应的MappedStatement
// 执行SQL并返回结果
}
}

4.3 MyBatis核心注解

基础映射注解

1
2
3
4
5
6
7
8
9
10
11
12
@Select("SELECT * FROM users WHERE id = #{id}")
User findById(@Param("id") Long id);

@Insert("INSERT INTO users(name, email) VALUES(#{name}, #{email})")
@Options(useGeneratedKeys = true, keyProperty = "id")
int insert(User user);

@Update("UPDATE users SET name = #{name} WHERE id = #{id}")
int update(User user);

@Delete("DELETE FROM users WHERE id = #{id}")
int delete(@Param("id") Long id);

高级映射注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
// 结果映射
@Results({
@Result(column = "user_id", property = "id"),
@Result(column = "user_name", property = "name"),
@Result(column = "create_time", property = "createTime")
})
@Select("SELECT user_id, user_name, create_time FROM users")
List<User> findAll();

// 一对一映射
@One(select = "findUserById")
@Result(column = "user_id", property = "user")
Order findOrderById(@Param("id") Long id);

// 一对多映射
@Many(select = "findOrdersByUserId")
@Result(column = "id", property = "orders")
User findUserWithOrders(@Param("id") Long id);

动态SQL注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@SelectProvider(type = UserSqlProvider.class, method = "findUsers")
List<User> findUsers(@Param("name") String name, @Param("email") String email);

public class UserSqlProvider {
public String findUsers(Map<String, Object> params) {
return new SQL() {{
SELECT("*");
FROM("users");
if (params.get("name") != null) {
WHERE("name LIKE CONCAT('%', #{name}, '%')");
}
if (params.get("email") != null) {
WHERE("email = #{email}");
}
}}.toString();
}
}

4.4 MyBatis缓存机制

一级缓存(默认开启):

  • 作用域:SqlSession级别
  • 生命周期:与SqlSession相同
  • 存储:HashMap结构,key为CacheKey

二级缓存(需要配置):

  • 作用域:Mapper级别
  • 生命周期:与应用程序相同
  • 配置:@CacheNamespace注解或标签
1
2
3
4
5
6
7
8
9
@CacheNamespace(
eviction = LRU.class,
flushInterval = 60000,
size = 1024,
readWrite = true
)
public interface UserMapper {
// mapper methods
}

4.5 MyBatis实际应用场景

场景1:复杂查询场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Mapper
public interface OrderMapper {

// 分页查询订单
@Select("""
SELECT o.*, u.name as user_name, u.email as user_email
FROM orders o
LEFT JOIN users u ON o.user_id = u.id
WHERE o.status = #{status}
AND o.create_time BETWEEN #{startTime} AND #{endTime}
ORDER BY o.create_time DESC
LIMIT #{offset}, #{limit}
""")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "user_id", property = "userId"),
@Result(column = "user_name", property = "user.name"),
@Result(column = "user_email", property = "user.email")
})
List<OrderVO> findOrdersWithUser(
@Param("status") String status,
@Param("startTime") LocalDateTime startTime,
@Param("endTime") LocalDateTime endTime,
@Param("offset") int offset,
@Param("limit") int limit
);

// 统计查询
@Select("""
SELECT
COUNT(*) as total_count,
SUM(amount) as total_amount,
AVG(amount) as avg_amount
FROM orders
WHERE status = #{status}
AND create_time >= #{startTime}
""")
OrderStatistics getOrderStatistics(
@Param("status") String status,
@Param("startTime") LocalDateTime startTime
);
}

场景2:批量操作场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Mapper
public interface BatchMapper {

// 批量插入
@Insert("""
<script>
INSERT INTO users (name, email, create_time) VALUES
<foreach collection="users" item="user" separator=",">
(#{user.name}, #{user.email}, #{user.createTime})
</foreach>
</script>
""")
int batchInsertUsers(@Param("users") List<User> users);

// 批量更新
@Update("""
<script>
<foreach collection="users" item="user" separator=";">
UPDATE users SET
name = #{user.name},
email = #{user.email},
update_time = NOW()
WHERE id = #{user.id}
</foreach>
</script>
""")
int batchUpdateUsers(@Param("users") List<User> users);
}

场景3:动态SQL场景

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
@SelectProvider(type = UserSqlProvider.class, method = "searchUsers")
List<User> searchUsers(UserSearchCriteria criteria);

public class UserSqlProvider {

public String searchUsers(UserSearchCriteria criteria) {
return new SQL() {{
SELECT("u.*, p.name as profile_name");
FROM("users u");
LEFT_OUTER_JOIN("user_profiles p ON u.id = p.user_id");

if (StringUtils.hasText(criteria.getName())) {
WHERE("u.name LIKE CONCAT('%', #{name}, '%')");
}

if (StringUtils.hasText(criteria.getEmail())) {
WHERE("u.email = #{email}");
}

if (criteria.getMinAge() != null) {
WHERE("u.age >= #{minAge}");
}

if (criteria.getMaxAge() != null) {
WHERE("u.age <= #{maxAge}");
}

if (criteria.getCreateTimeStart() != null) {
WHERE("u.create_time >= #{createTimeStart}");
}

if (criteria.getCreateTimeEnd() != null) {
WHERE("u.create_time <= #{createTimeEnd}");
}

if (CollectionUtils.isNotEmpty(criteria.getStatuses())) {
WHERE("u.status IN (" +
criteria.getStatuses().stream()
.map(s -> "'" + s + "'")
.collect(Collectors.joining(",")) + ")");
}

// 排序
if (StringUtils.hasText(criteria.getSortField())) {
if ("desc".equalsIgnoreCase(criteria.getSortDirection())) {
ORDER_BY("u." + criteria.getSortField() + " DESC");
} else {
ORDER_BY("u." + criteria.getSortField() + " ASC");
}
} else {
ORDER_BY("u.create_time DESC");
}

}}.toString();
}
}

4.6 MyBatis性能优化

优化策略1:合理使用缓存

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 开启二级缓存,设置合理的缓存策略
@CacheNamespace(
eviction = LRU.class, // 缓存回收策略
flushInterval = 300000, // 缓存刷新间隔(5分钟)
size = 1024, // 缓存大小
readWrite = true, // 读写缓存
blocking = false // 非阻塞缓存
)
public interface ProductMapper {

// 对于频繁查询且变化不大的数据使用缓存
@Select("SELECT * FROM products WHERE category_id = #{categoryId}")
@Options(useCache = true)
List<Product> findByCategory(@Param("categoryId") Long categoryId);

// 对于实时性要求高的数据禁用缓存
@Select("SELECT * FROM products WHERE id = #{id}")
@Options(useCache = false)
Product findRealTimeById(@Param("id") Long id);
}

优化策略2:批量操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 使用批量插入替代循环插入
@Insert("""
<script>
INSERT INTO order_items (order_id, product_id, quantity, price) VALUES
<foreach collection="items" item="item" separator=",">
(#{item.orderId}, #{item.productId}, #{item.quantity}, #{item.price})
</foreach>
</script>
""")
int batchInsertOrderItems(@Param("items") List<OrderItem> items);

// 批量更新优化
public void batchUpdateProducts(List<Product> products) {
try (SqlSession sqlSession = sqlSessionFactory.openSession(ExecutorType.BATCH)) {
ProductMapper mapper = sqlSession.getMapper(ProductMapper.class);
for (Product product : products) {
mapper.updateProduct(product);
}
sqlSession.commit();
}
}

优化策略3:延迟加载

1
2
3
4
5
6
7
8
9
10
11
12
13
@ResultMap("userResultMap")
@Select("SELECT * FROM users WHERE id = #{id}")
User findUserById(@Param("id") Long id);

// 在ResultMap中配置延迟加载
<resultMap id="userResultMap" type="User">
<id column="id" property="id"/>
<result column="name" property="name"/>
<collection property="orders"
select="findOrdersByUserId"
column="id"
fetchType="lazy"/>
</resultMap>

4.7 MyBatis常见问题与解决方案

问题1:N+1查询问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// 问题:会产生N+1次查询
@Select("SELECT * FROM users")
List<User> findAllUsers();

@Select("SELECT * FROM orders WHERE user_id = #{userId}")
List<Order> findOrdersByUserId(@Param("userId") Long userId);

// 解决方案1:使用联表查询
@Select("""
SELECT u.*, o.id as order_id, o.amount, o.status as order_status
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
""")
@Results({
@Result(column = "id", property = "id"),
@Result(column = "name", property = "name"),
@Result(column = "order_id", property = "orders.id"),
@Result(column = "amount", property = "orders.amount"),
@Result(column = "order_status", property = "orders.status")
})
List<User> findUsersWithOrders();

// 解决方案2:使用批量查询
public List<User> findUsersWithOrdersOptimized(List<Long> userIds) {
List<User> users = userMapper.findUsersByIds(userIds);
if (!users.isEmpty()) {
List<Long> ids = users.stream().map(User::getId).collect(Collectors.toList());
List<Order> orders = orderMapper.findOrdersByUserIds(ids);

// 手动组装数据
Map<Long, List<Order>> orderMap = orders.stream()
.collect(Collectors.groupingBy(Order::getUserId));

users.forEach(user -> user.setOrders(orderMap.get(user.getId())));
}
return users;
}

问题2:大数据量查询内存溢出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 问题:一次性加载大量数据导致内存溢出
@Select("SELECT * FROM large_table")
List<LargeData> findAllData(); // 可能导致OOM

// 解决方案1:分页查询
public List<LargeData> findAllDataWithPaging() {
List<LargeData> allData = new ArrayList<>();
int pageSize = 1000;
int offset = 0;

List<LargeData> pageData;
do {
pageData = mapper.findDataWithLimit(offset, pageSize);
allData.addAll(pageData);
offset += pageSize;
} while (pageData.size() == pageSize);

return allData;
}

// 解决方案2:使用游标查询
@Select("SELECT * FROM large_table WHERE process_status = 'PENDING'")
@Options(resultSetType = ResultSetType.FORWARD_ONLY, fetchSize = 1000)
Cursor<LargeData> findPendingDataCursor();

public void processLargeData() {
try (Cursor<LargeData> cursor = mapper.findPendingDataCursor()) {
cursor.forEach(this::processData);
}
}

问题3:SQL注入防护

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 危险:直接拼接SQL,容易SQL注入
@Select("SELECT * FROM users WHERE name = '${name}'") // 错误示例
List<User> findByNameUnsafe(@Param("name") String name);

// 安全:使用参数绑定
@Select("SELECT * FROM users WHERE name = #{name}")
List<User> findByNameSafe(@Param("name") String name);

// 动态排序的安全处理
@SelectProvider(type = UserSqlProvider.class, method = "findUsersWithSort")
List<User> findUsersWithSort(@Param("sortField") String sortField,
@Param("sortDirection") String sortDirection);

public class UserSqlProvider {
private static final Set<String> ALLOWED_SORT_FIELDS =
Set.of("id", "name", "email", "create_time", "update_time");

public String findUsersWithSort(Map<String, Object> params) {
String sortField = (String) params.get("sortField");
String sortDirection = (String) params.get("sortDirection");

// 白名单验证
if (!ALLOWED_SORT_FIELDS.contains(sortField)) {
sortField = "id";
}

if (!"DESC".equalsIgnoreCase(sortDirection)) {
sortDirection = "ASC";
}

return "SELECT * FROM users ORDER BY " + sortField + " " + sortDirection;
}
}

4.8 MyBatis面试高频问题

Q1: MyBatis的执行流程是怎样的?

A: MyBatis的执行流程包括以下步骤:

  1. 配置解析: 解析mybatis-config.xml配置文件和Mapper XML文件,创建Configuration对象
  2. SqlSessionFactory创建: 根据Configuration创建SqlSessionFactory
  3. SqlSession创建: 通过SqlSessionFactory.openSession()创建SqlSession
  4. Mapper获取: 通过SqlSession.getMapper()获取Mapper接口的代理对象
  5. 方法调用: 调用Mapper接口方法,通过动态代理转换为SQL执行
  6. SQL执行: 通过Executor执行SQL,包括参数处理、语句执行、结果映射
  7. 结果返回: 将执行结果返回给调用方

Q2: MyBatis的一级缓存和二级缓存有什么区别?

A: 两级缓存的主要区别:

一级缓存(默认开启):

  • 作用域:SqlSession级别
  • 生命周期:与SqlSession相同,SqlSession关闭时缓存清空
  • 存储结构:HashMap,key为CacheKey(由SQL、参数、分页等组成)
  • 失效条件:执行update、insert、delete操作或手动清空

二级缓存(需要配置):

  • 作用域:Mapper级别,多个SqlSession可以共享
  • 生命周期:与应用程序相同
  • 存储结构:可配置(HashMap、LRU、FIFO等)
  • 配置方式:@CacheNamespace注解或标签
  • 注意事项:需要序列化,可能存在脏读问题

Q3: MyBatis如何防止SQL注入?

A: MyBatis通过以下方式防止SQL注入:

  1. 参数绑定: 使用#{}而不是${}
    • #{}:预编译处理,参数作为占位符传递
    • ${}:字符串替换,直接拼接到SQL中(危险)
  2. 类型检查: MyBatis会对参数类型进行检查
  3. 白名单验证: 对于动态排序等场景,使用白名单验证
1
2
3
4
5
6
7
// 安全的写法
@Select("SELECT * FROM users WHERE name = #{name} AND age > #{age}")
List<User> findUsers(@Param("name") String name, @Param("age") Integer age);

// 危险的写法(避免使用)
@Select("SELECT * FROM users WHERE name = '${name}'")
List<User> findUsersUnsafe(@Param("name") String name);

Q4: MyBatis中#{}和${}的区别?

A: 两者的主要区别:

#{}(推荐使用):

  • 预编译处理,生成PreparedStatement
  • 参数会被处理为占位符?
  • 可以防止SQL注入
  • 会进行类型转换
  • 适用于参数值传递

${}(谨慎使用):

  • 字符串替换,直接拼接到SQL中
  • 不会进行预编译
  • 存在SQL注入风险
  • 不会进行类型转换
  • 适用于动态表名、列名等场景

Q5: MyBatis的动态SQL是如何实现的?

A: MyBatis的动态SQL通过以下标签实现:

  1. if标签: 条件判断
  2. choose/when/otherwise: 类似switch-case
  3. where标签: 智能处理WHERE条件
  4. set标签: 智能处理SET语句
  5. foreach标签: 循环处理
  6. trim标签: 去除多余的字符

实现原理:

  • 使用OGNL表达式进行条件判断
  • 在SQL解析阶段根据参数值动态生成SQL
  • 通过SqlNode树结构表示动态SQL
  • 在执行时遍历SqlNode树生成最终SQL

五、框架整合与最佳实践

5.1 Spring Boot + MyBatis整合

完整配置示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
// 主启动类
@SpringBootApplication
@MapperScan("com.example.mapper")
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

// 数据源配置
@Configuration
public class DataSourceConfig {

@Bean
@Primary
@ConfigurationProperties("spring.datasource.primary")
public DataSourceProperties primaryDataSourceProperties() {
return new DataSourceProperties();
}

@Bean
@Primary
public DataSource primaryDataSource() {
return primaryDataSourceProperties()
.initializeDataSourceBuilder()
.type(HikariDataSource.class)
.build();
}

@Bean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);

// MyBatis配置
org.apache.ibatis.session.Configuration config =
new org.apache.ibatis.session.Configuration();
config.setMapUnderscoreToCamelCase(true);
config.setLogImpl(Slf4jImpl.class);
config.setCacheEnabled(true);
config.setLazyLoadingEnabled(true);
config.setAggressiveLazyLoading(false);

factory.setConfiguration(config);
return factory.getObject();
}
}

事务管理配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
@Service
@Transactional
public class OrderService {

@Autowired
private OrderMapper orderMapper;

@Autowired
private OrderItemMapper orderItemMapper;

@Autowired
private ProductMapper productMapper;

@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
// 1. 创建订单
Order order = new Order();
order.setUserId(request.getUserId());
order.setStatus("PENDING");
order.setCreateTime(LocalDateTime.now());
orderMapper.insert(order);

// 2. 创建订单项
BigDecimal totalAmount = BigDecimal.ZERO;
List<OrderItem> orderItems = new ArrayList<>();

for (CreateOrderItemRequest itemRequest : request.getItems()) {
// 检查库存
Product product = productMapper.findById(itemRequest.getProductId());
if (product == null) {
throw new ProductNotFoundException("Product not found: " + itemRequest.getProductId());
}

if (product.getStock() < itemRequest.getQuantity()) {
throw new InsufficientStockException("Insufficient stock for product: " + product.getName());
}

// 减库存
productMapper.decreaseStock(itemRequest.getProductId(), itemRequest.getQuantity());

// 创建订单项
OrderItem orderItem = new OrderItem();
orderItem.setOrderId(order.getId());
orderItem.setProductId(itemRequest.getProductId());
orderItem.setQuantity(itemRequest.getQuantity());
orderItem.setPrice(product.getPrice());
orderItems.add(orderItem);

totalAmount = totalAmount.add(
product.getPrice().multiply(BigDecimal.valueOf(itemRequest.getQuantity()))
);
}

// 3. 批量插入订单项
if (!orderItems.isEmpty()) {
orderItemMapper.batchInsert(orderItems);
}

// 4. 更新订单总金额
order.setTotalAmount(totalAmount);
orderMapper.updateAmount(order.getId(), totalAmount);

return order;
}

@Transactional(readOnly = true)
public OrderDetailVO getOrderDetail(Long orderId) {
Order order = orderMapper.findById(orderId);
if (order == null) {
throw new OrderNotFoundException("Order not found: " + orderId);
}

List<OrderItem> orderItems = orderItemMapper.findByOrderId(orderId);

return OrderDetailVO.builder()
.order(order)
.items(orderItems)
.build();
}
}

5.2 统一异常处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

@ExceptionHandler(ValidationException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleValidationException(ValidationException e) {
log.warn("Validation error: {}", e.getMessage());
return ApiResponse.error("VALIDATION_ERROR", e.getMessage());
}

@ExceptionHandler(MethodArgumentNotValidException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ApiResponse<Void> handleMethodArgumentNotValid(MethodArgumentNotValidException e) {
List<String> errors = e.getBindingResult()
.getFieldErrors()
.stream()
.map(error -> error.getField() + ": " + error.getDefaultMessage())
.collect(Collectors.toList());

return ApiResponse.error("VALIDATION_ERROR", String.join(", ", errors));
}

@ExceptionHandler(DataIntegrityViolationException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiResponse<Void> handleDataIntegrityViolation(DataIntegrityViolationException e) {
log.error("Data integrity violation", e);
return ApiResponse.error("DATA_CONFLICT", "Data conflict occurred");
}

@ExceptionHandler(OptimisticLockingFailureException.class)
@ResponseStatus(HttpStatus.CONFLICT)
public ApiResponse<Void> handleOptimisticLockingFailure(OptimisticLockingFailureException e) {
log.warn("Optimistic locking failure: {}", e.getMessage());
return ApiResponse.error("OPTIMISTIC_LOCK_ERROR", "Resource has been modified by another user");
}

@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ApiResponse<Void> handleGenericException(Exception e) {
log.error("Unexpected error occurred", e);
return ApiResponse.error("INTERNAL_ERROR", "An unexpected error occurred");
}
}

5.3 接口文档与验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
// 统一响应格式
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class ApiResponse<T> {
private boolean success;
private String code;
private String message;
private T data;
private Long timestamp;

public static <T> ApiResponse<T> success(T data) {
return ApiResponse.<T>builder()
.success(true)
.code("SUCCESS")
.data(data)
.timestamp(System.currentTimeMillis())
.build();
}

public static <T> ApiResponse<T> error(String code, String message) {
return ApiResponse.<T>builder()
.success(false)
.code(code)
.message(message)
.timestamp(System.currentTimeMillis())
.build();
}
}

// 请求参数验证
@Data
@Valid
public class CreateUserRequest {

@NotBlank(message = "用户名不能为空")
@Size(min = 2, max = 20, message = "用户名长度必须在2-20个字符之间")
@Pattern(regexp = "^[a-zA-Z0-9_\\u4e00-\\u9fa5]+$", message = "用户名只能包含字母、数字、下划线和中文")
private String username;

@NotBlank(message = "邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;

@NotBlank(message = "密码不能为空")
@Size(min = 8, max = 20, message = "密码长度必须在8-20个字符之间")
@Pattern(
regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]+$",
message = "密码必须包含大小写字母、数字和特殊字符"
)
private String password;

@NotNull(message = "年龄不能为空")
@Min(value = 1, message = "年龄必须大于0")
@Max(value = 150, message = "年龄不能超过150")
private Integer age;

@NotEmpty(message = "角色不能为空")
private List<@NotBlank(message = "角色名称不能为空") String> roles;
}

5.4 性能监控与优化

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
// 性能监控切面
@Aspect
@Component
@Slf4j
public class PerformanceMonitorAspect {

@Around("@annotation(org.springframework.web.bind.annotation.RequestMapping) || " +
"@annotation(org.springframework.web.bind.annotation.GetMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PostMapping) || " +
"@annotation(org.springframework.web.bind.annotation.PutMapping) || " +
"@annotation(org.springframework.web.bind.annotation.DeleteMapping)")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
long startTime = System.currentTimeMillis();
String methodName = joinPoint.getSignature().toShortString();

try {
Object result = joinPoint.proceed();
long endTime = System.currentTimeMillis();
long executionTime = endTime - startTime;

if (executionTime > 1000) { // 超过1秒记录警告
log.warn("Slow API detected: {} took {}ms", methodName, executionTime);
} else {
log.info("API performance: {} took {}ms", methodName, executionTime);
}

return result;
} catch (Exception e) {
long endTime = System.currentTimeMillis();
log.error("API error: {} took {}ms, error: {}",
methodName, endTime - startTime, e.getMessage());
throw e;
}
}
}

// 数据库连接池监控
@Component
public class DataSourceHealthIndicator implements HealthIndicator {

@Autowired
private DataSource dataSource;

@Override
public Health health() {
try {
if (dataSource instanceof HikariDataSource) {
HikariDataSource hikariDataSource = (HikariDataSource) dataSource;
HikariPoolMXBean poolBean = hikariDataSource.getHikariPoolMXBean();

return Health.up()
.withDetail("database", "MySQL")
.withDetail("activeConnections", poolBean.getActiveConnections())
.withDetail("idleConnections", poolBean.getIdleConnections())
.withDetail("totalConnections", poolBean.getTotalConnections())
.withDetail("threadsAwaitingConnection", poolBean.getThreadsAwaitingConnection())
.build();
}

try (Connection connection = dataSource.getConnection()) {
return Health.up()
.withDetail("database", connection.getMetaData().getDatabaseProductName())
.build();
}
} catch (Exception e) {
return Health.down()
.withDetail("error", e.getMessage())
.build();
}
}
}

5.5 高频面试综合题

Q1: 在微服务架构中,如何设计一个高并发的订单系统?

A: 设计高并发订单系统需要考虑以下几个方面:

1. 架构设计:

  • 使用分布式架构,订单服务、库存服务、支付服务分离
  • 引入消息队列处理异步任务
  • 使用Redis做缓存和分布式锁
  • 数据库读写分离,分库分表

2. 并发控制:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
public class OrderService {

@Autowired
private RedisTemplate<String, String> redisTemplate;

@Transactional(rollbackFor = Exception.class)
public Order createOrder(CreateOrderRequest request) {
String lockKey = "order:lock:" + request.getUserId();

// 分布式锁防止重复下单
Boolean locked = redisTemplate.opsForValue()
.setIfAbsent(lockKey, "1", Duration.ofSeconds(30));

if (!locked) {
throw new OrderCreationException("Order creation in progress");
}

try {
// 订单创建逻辑
return processOrder(request);
} finally {
redisTemplate.delete(lockKey);
}
}
}

3. 性能优化:

  • 使用批量操作减少数据库交互
  • 合理使用缓存策略
  • 异步处理非核心业务逻辑
  • 数据库连接池优化

Q2: 如何处理分布式事务?

A: 分布式事务处理方案:

1. 2PC/3PC协议:

  • 强一致性,但性能较差
  • 适用于对一致性要求极高的场景

2. TCC模式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
@TccTransaction
public class OrderTccService {

public void tryCreateOrder(CreateOrderRequest request) {
// Try阶段:预留资源
orderService.reserveOrder(request);
stockService.reserveStock(request.getItems());
paymentService.reservePayment(request.getPaymentInfo());
}

public void confirmCreateOrder(CreateOrderRequest request) {
// Confirm阶段:确认操作
orderService.confirmOrder(request);
stockService.confirmStock(request.getItems());
paymentService.confirmPayment(request.getPaymentInfo());
}

public void cancelCreateOrder(CreateOrderRequest request) {
// Cancel阶段:回滚操作
orderService.cancelOrder(request);
stockService.cancelStock(request.getItems());
paymentService.cancelPayment(request.getPaymentInfo());
}
}

3. 消息队列最终一致性:

1
2
3
4
5
6
7
8
9
10
11
12
@Service
public class OrderEventService {

@EventListener
@Async
public void handleOrderCreated(OrderCreatedEvent event) {
// 异步处理订单相关业务
notificationService.sendOrderNotification(event.getOrder());
inventoryService.updateInventory(event.getOrderItems());
pointsService.addPoints(event.getUserId(), event.getAmount());
}
}

Q3: Spring Boot应用如何优化启动速度?

A: Spring Boot启动优化策略:

1. 依赖优化:

  • 移除不必要的依赖
  • 使用spring-boot-starter-web替代完整的spring-web
  • 避免引入大量自动配置类

2. 配置优化:

1
2
3
4
5
6
7
8
9
10
spring:
main:
lazy-initialization: true # 启用懒加载
jpa:
hibernate:
ddl-auto: none # 禁用DDL自动生成
show-sql: false # 禁用SQL日志
devtools:
restart:
enabled: false # 生产环境禁用热重启

3. JVM优化:

1
2
3
4
5
6
java -XX:+UnlockExperimentalVMOptions 
-XX:+UseZGC
-XX:+UseTransparentHugePages
-XX:MaxMetaspaceSize=256m
-Xms512m -Xmx1024m
-jar application.jar

4. 代码优化:

  • 使用@Lazy注解延迟Bean初始化
  • 避免在@PostConstruct中执行耗时操作
  • 合理使用@ConditionalOn*注解

好的,这是一份详细的解释,我们来逐个分析这些 Java 虚拟机(JVM)参数:

Bash

1
java -XX:+UnlockExperimentalVMOptions -XX:+UseZGC -XX:+UseTransparentHugePages -XX:MaxMetaspaceSize=256m -Xms512m -Xmx1024m -jar application.jar
  • java: 这是启动 Java 程序的命令。

内存管理和垃圾回收

这部分参数主要用于优化 JVM 的内存使用和垃圾回收机制,以获得更好的性能。

  • -XX:+UnlockExperimentalVMOptions: 这个参数是用来解锁实验性(experimental)的 JVM 选项。ZGC 曾经是实验性功能,所以需要这个参数才能使用。
  • -XX:+UseZGC: 这条命令是启用 Z 垃圾回收器(Z Garbage Collector)。ZGC 是一种为超大堆内存(从几GB到几十TB)设计的、低延迟的垃圾回收器。它的主要目标是在垃圾回收时,尽量减少应用程序的停顿时间,这对于需要极低延迟的应用程序非常重要。
  • -XX:+UseTransparentHugePages: 这个参数开启了透明大页(Transparent Huge Pages)的支持。在操作系统层面,通常内存是以 4KB 的小页来管理的。而大页(通常是 2MB 或 1GB)可以减少 CPU 在内存管理上的开销,从而提升性能,尤其对于内存占用较大的应用效果更明显。
  • -XX:MaxMetaspaceSize=256m: 这个参数设置了元空间(Metaspace)的最大大小为 256MB。元空间是 JVM 存储类的元数据(如类的名称、方法、字段等信息)的地方。在 Java 8 之后,它取代了之前的“永久代”(PermGen)。
  • -Xms512m: 这条命令设定了 JVM 堆的初始内存为 512MB。堆是用来存放 Java 对象的地方。-Xms 决定了 JVM 启动时会申请的最小内存。
  • -Xmx1024m: 这条命令设定了 JVM 堆的最大内存为 1024MB(即 1GB)。这是 JVM 运行时可以使用的最大内存。当堆内存用尽时,JVM 会触发垃圾回收,如果依然不足,就会抛出 OutOfMemoryError

好的,我们来更详细地聊聊 JVM 堆内存的结构,特别是分代设计和具体的划分比例。


堆内存的传统分代结构

在绝大多数的垃圾回收器中(例如 CMS、G1 之前的串行和并行 GC),堆内存被分为以下几个代(Generation):

1. 年轻代(Young Generation)

年轻代是新对象的诞生地。 绝大多数对象,比如局部变量创建的临时对象,都会在年轻代被创建。年轻代被设计成较小且垃圾回收频率高的区域。

  • Eden 区:这是年轻代的主要部分,新创建的对象首先被分配到这里。
  • Survivor 区(幸存者区):有两个大小相等的 Survivor 区,通常命名为 S0S1。它们的作用是保存每次垃圾回收后,还存活的对象。

划分比例

在默认情况下,年轻代内部的划分比例通常是:

  • Eden 区 : S0 区 : S1 区 = 8 : 1 : 1

这个比例可以通过 JVM 参数进行调整,比如使用 -XX:SurvivorRatio=8。这意味着 Eden 区的大小是单个 Survivor 区的 8 倍。

2. 老年代(Old Generation)

老年代用于存放生命周期较长的对象。 当一个对象在年轻代经过多次垃圾回收(通常是 15 次,这个次数也可以通过 -XX:MaxTenuringThreshold 参数调整)后仍然存活,或者年轻代放不下的大对象,就会被“晋升”到老年代。

老年代的垃圾回收频率远低于年轻代,但每次回收的开销更大。

划分比例

堆内存中,年轻代和老年代的默认划分比例通常是:

  • 年轻代 : 老年代 = 1 : 2

例如,如果你设置 -Xms1200m -Xmx1200m,那么年轻代大约是 400MB,老年代大约是 800MB。这个比例可以通过 -XX:NewRatio 参数来调整。例如,-XX:NewRatio=2 表示老年代与年轻代的大小比值为 2:1。


垃圾回收过程(以 Minor GC 为例)

理解了这些区域,我们再来看看垃圾回收是怎么进行的:

  1. Eden 区满了,会触发一次 Minor GC
  2. Minor GC 会检查 Eden 区和其中一个 Survivor 区(比如 S0)。
  3. 它会将所有还存活的对象复制到另一个空的 Survivor 区(比如 S1)。
  4. 同时,那些不再被引用的对象则会被清理。
  5. 所有被复制到 S1 区的对象,它们的年龄(age)会加一。
  6. 下一次 Minor GC 发生时,同样会扫描 Eden 区和 S1 区,将存活对象复制到 S0 区,清空 Eden 和 S1 区,并增加对象的年龄。
  7. 当对象的年龄达到某个阈值时,它就会被晋升到老年代。

现代垃圾回收器(如 G1 和 ZGC)

值得注意的是,像 G1 垃圾回收器已经打破了这种严格的分代比例。它将堆划分为一个个大小相等的区域(Region),每个区域都可以是 Eden、Survivor 或者老年代。G1 能够更智能地选择要回收的区域,从而在保证低停顿的同时,提高吞吐量。

ZGC 则更进一步,如我之前所说,它完全没有年轻代、老年代的概念,而是通过着色指针和读屏障技术,在不中断应用程序的情况下并发地进行垃圾回收,实现了更低的停顿。

因此,当你使用 ZGC 时,传统的分代比例就不适用了。不过,了解传统的分代结构对于理解 Java 内存管理的基础依然非常重要。

以下是针对Spring Cloud Alibaba及其他分布式技术的深度补充,包含实现原理和核心组件的详细说明:


六、Spring Cloud与分布式技术详解

6.1 Spring Cloud Alibaba核心组件

Nacos深度解析

服务发现原理

  1. 注册流程

    • 服务启动时向Nacos Server发送注册请求(HTTP/GRPC)
    • 注册信息包含元数据、健康检查方式(TCP/HTTP/MySQL)
    • 客户端本地缓存服务列表(故障转移)
  2. 健康检查机制

    1
    2
    3
    4
    5
    6
    7
    8
    // Nacos健康检查配置示例
    spring:
    cloud:
    nacos:
    discovery:
    health-check-enabled: true
    health-check-interval: 10s
    health-check-timeout: 5s

配置中心实现

  • 长轮询机制(Push+Pull混合模式):
    • 客户端发起长轮询请求(默认30秒超时)
    • 服务端配置变更时立即响应
    • 客户端收到变更后拉取最新配置
1
2
3
4
5
6
7
// 动态配置刷新示例
@RefreshScope
@RestController
public class ConfigController {
@Value("${app.config.item}")
private String configItem;
}

Sentinel核心原理

流量控制规则

  • 滑动窗口算法
    • 统计周期(1秒)分为多个格子(如20个50ms)
    • 实时淘汰过期格子数据
    • QPS计算基于当前窗口总请求数

熔断降级策略

策略类型 计算公式 适用场景
慢调用比例 响应时间 > RT阈值 && 比例 > 阈值 接口性能波动
异常比例 异常数 / 请求数 > 阈值 依赖服务不稳定
异常数 异常数 > 阈值(时间窗口内) 关键业务熔断
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// Sentinel规则持久化到Nacos示例
@PostConstruct
public void initRules() {
FlowRuleManager.register2Property(
new NacosDataSourceWrapper(
"nacos-server:8848", "sentinel-flow-rules",
new Converter<String, List<FlowRule>>() {
@Override
public List<FlowRule> convert(String source) {
return JSON.parseArray(source, FlowRule.class);
}
}
)
);
}

RocketMQ集成

消息轨迹追踪

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// 生产者配置
@Bean
public RocketMQTemplate rocketMQTemplate() {
RocketMQTemplate template = new RocketMQTemplate();
template.setProducer(new DefaultMQProducer("producer_group") {{
setVipChannelEnabled(false);
setTraceDispatcher(new AsyncTraceDispatcher("trace_topic"));
}});
return template;
}

// 消费者轨迹追踪
@RocketMQMessageListener(
topic = "order_topic",
consumerGroup = "order_consumer",
enableMsgTrace = true
)
public class OrderConsumer implements RocketMQListener<String> {
@Override
public void onMessage(String message) {
// 处理逻辑
}
}

6.2 分布式事务增强(Seata原理)

AT模式工作流程

  1. 一阶段

    • 解析SQL生成前后镜像
    • 注册分支事务到TC(Transaction Coordinator)
    • 本地事务提交前记录undo_log
  2. 二阶段

    • 成功:异步删除undo_log
    • 失败:根据undo_log补偿(反向SQL)

undo_log表示例

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE `undo_log` (
`id` bigint NOT NULL AUTO_INCREMENT,
`branch_id` bigint NOT NULL,
`xid` varchar(100) NOT NULL,
`context` varchar(128) NOT NULL,
`rollback_info` longblob NOT NULL,
`log_status` int NOT NULL,
`log_created` datetime NOT NULL,
`log_modified` datetime NOT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `ux_undo_log` (`xid`,`branch_id`)
);

TCC模式最佳实践

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 账户服务TCC接口
public interface AccountTccService {
@TwoPhaseBusinessAction(
name = "deduct",
commitMethod = "confirmDeduct",
rollbackMethod = "cancelDeduct"
)
boolean prepareDeduct(
@BusinessActionContextParameter(paramName = "userId") String userId,
@BusinessActionContextParameter(paramName = "amount") BigDecimal amount
);

boolean confirmDeduct(BusinessActionContext context);

boolean cancelDeduct(BusinessActionContext context);
}

// 业务调用方
@GlobalTransactional
public void placeOrder(Order order) {
// 1. 冻结库存
inventoryTccService.prepare(null, order.getProductId(), order.getCount());

// 2. 预扣款
accountTccService.prepareDeduct(order.getUserId(), order.getAmount());

// 3. 创建订单
orderMapper.insert(order);
}

6.3 分布式缓存深度优化

Redis多级缓存架构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// 本地缓存+Caffeine+Redis实现
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
return new CaffeineRedisCacheManager(
Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.maximumSize(1000),
RedisCacheWriter.nonLockingRedisCacheWriter(factory),
RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1))
);
}

// 热点Key探测
public <T> T getWithHotspotProtection(String key, Class<T> type) {
// 1. 本地缓存查询
T value = localCache.getIfPresent(key);
if (value != null) return value;

// 2. Redis集群查询(带随机过期时间)
value = redisTemplate.opsForValue().get(key);
if (value != null) {
localCache.put(key, value);
return value;
}

// 3. 分布式锁防击穿
String lockKey = "lock:" + key;
try {
if (redisLock.tryLock(lockKey, 3, 30)) {
// 4. 数据库查询
value = databaseLoader.load(key);
redisTemplate.opsForValue().set(
key, value,
ThreadLocalRandom.current().nextInt(30) + 30,
TimeUnit.MINUTES
);
localCache.put(key, value);
}
} finally {
redisLock.unlock(lockKey);
}
return value;
}

6.4 消息队列高级特性

RocketMQ事务消息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
// 事务消息生产者
public void sendTransactionMessage(Order order) {
TransactionSendResult result = rocketMQTemplate.sendMessageInTransaction(
"order-tx-group",
MessageBuilder.withPayload(order)
.setHeader(RocketMQHeaders.TRANSACTION_ID, order.getId())
.build(),
order
);
}

// 本地事务执行器
@RocketMQTransactionListener(txProducerGroup = "order-tx-group")
public class OrderTransactionListenerImpl implements RocketMQLocalTransactionListener {
@Override
public RocketMQLocalTransactionState executeLocalTransaction(Message msg, Object arg) {
try {
Order order = (Order) arg;
orderService.createOrder(order); // 本地事务
return RocketMQLocalTransactionState.COMMIT;
} catch (Exception e) {
return RocketMQLocalTransactionState.ROLLBACK;
}
}

@Override
public RocketMQLocalTransactionState checkLocalTransaction(Message msg) {
String orderId = msg.getHeaders().get("TRANSACTION_ID").toString();
return orderService.exists(orderId) ?
RocketMQLocalTransactionState.COMMIT :
RocketMQLocalTransactionState.ROLLBACK;
}
}

Kafka精确一次语义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 生产者配置
@Bean
public ProducerFactory<String, String> producerFactory() {
Map<String, Object> configs = new HashMap<>();
configs.put(ProducerConfig.ENABLE_IDEMPOTENCE_CONFIG, true);
configs.put(ProducerConfig.TRANSACTIONAL_ID_CONFIG, "tx-producer-1");
return new DefaultKafkaProducerFactory<>(configs);
}

// 消费者配置
@Bean
public ConsumerFactory<String, String> consumerFactory() {
Map<String, Object> configs = new HashMap<>();
configs.put(ConsumerConfig.ISOLATION_LEVEL_CONFIG, "read_committed");
return new DefaultKafkaConsumerFactory<>(configs);
}

// 事务性消费
@KafkaListener(topics = "order-topic")
@Transactional
public void processOrder(ConsumerRecord<String, String> record) {
OrderEvent event = parseEvent(record.value());
orderService.process(event); // 数据库操作
}

七、云原生支持(新增)

7.1 Kubernetes集成方案

服务发现适配

1
2
3
4
5
6
7
8
9
10
# Nacos与K8s Service集成
spring:
cloud:
kubernetes:
discovery:
all-namespaces: true
nacos:
discovery:
server-addr: ${NACOS_HOST:nacos-headless}:${NACOS_PORT:8848}
namespace: ${POD_NAMESPACE:default}

配置管理方案

1
2
3
4
5
6
7
8
9
10
11
// 多配置源加载(K8s ConfigMap + Nacos)
@Configuration
@ConfigurationProperties(prefix = "app")
@RefreshScope
public class AppConfig {
@Value("${configFromNacos}")
private String nacosConfig;

@Value("${configFromK8s}")
private String k8sConfig;
}

7.2 Service Mesh整合

Istio流量治理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 金丝雀发布策略
apiVersion: networking.istio.io/v1alpha3
kind: VirtualService
metadata:
name: order-service
spec:
hosts:
- order-service
http:
- route:
- destination:
host: order-service
subset: v1
weight: 90
- destination:
host: order-service
subset: v2
weight: 10

八、性能优化深度实践

8.1 全链路压测方案

影子库表配置

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# ShardingSphere影子库配置
spring:
shardingsphere:
datasource:
names: ds-real,ds-shadow
rules:
shadow:
enable: true
data-sources:
shadow-data-source:
source-data-source-name: ds-real
shadow-data-source-name: ds-shadow
tables:
t_order:
shadow-algorithm-names: [simple-hint-algorithm]

流量染色标记

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// 通过ThreadLocal传递压测标记
public class PressureTestContext {
private static final ThreadLocal<Boolean> FLAG = ThreadLocal.withInitial(() -> false);

public static void markPressureTest() {
FLAG.set(true);
}

public static boolean isPressureTest() {
return FLAG.get();
}
}

// MyBatis拦截器自动路由
@Intercepts(@Signature(type=Executor.class, method="update", args={MappedStatement.class,Object.class}))
public class ShadowDbInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
if (PressureTestContext.isPressureTest()) {
RoutingContext.setShadowDataSource();
}
return invocation.proceed();
}
}

手写答案

1.实现单例模式 (Singleton Pattern)

懒汉式:

使用双重检查锁定 (DCL) 实现线程安全的懒汉式单例模式。这种模式在第一次调用时才创建实例,并且通过两次检查和同步块确保了线程安全和性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class Singleton {
// 1. volatile 关键字确保多线程环境下,instance 变量的修改能够立即被其他线程看到
private static volatile Singleton instance;

// 2. 私有化构造器,防止外部直接 new 实例
private Singleton() {}

// 3. 提供一个全局访问点,使用 DCL 确保线程安全
public static Singleton getInstance() {
// 第一次检查:如果实例已经存在,直接返回,避免不必要的同步
if (instance == null) {
// 第一次检查失败,进入同步块
synchronized (Singleton.class) {
// 第二次检查:在同步块内部再次检查,防止多个线程同时通过第一次检查,
// 导致创建多个实例
if (instance == null) {
// 创建实例
instance = new Singleton();
}
}
}
return instance;
}


public void showMessage() {
System.out.println("这是一个线程安全的懒汉式单例模式实例。");
}
}

饿汉式:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package test1;

class SingletonEager {
// 1. 在类加载时就创建静态实例
private static final SingletonEager INSTANCE = new SingletonEager();

// 2. 私有化构造器,防止外部直接创建实例
private SingletonEager() {}

// 3. 提供一个公共的静态方法来获取唯一实例
public static SingletonEager getInstance() {
return INSTANCE;
}

public void showMessage() {
System.out.println("这是一个线程安全的饿汉式单例模式实例。");
}
}

2.继承与多态 (Inheritance and Polymorphism)

设计一个 Animal 抽象类,并创建 DogCat 类来展示继承和多态。=

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
// 1. Animal 抽象类,定义通用行为
abstract class Animal {
// 抽象方法,子类必须实现
public abstract void eat();
}

// 2. Dog 类继承 Animal,并重写 eat() 方法
class Dog extends Animal {
@Override
public void eat() {
System.out.println("狗正在吃骨头。");
}
}

// 3. Cat 类继承 Animal,并重写 eat() 方法
class Cat extends Animal {
@Override
public void eat() {
System.out.println("猫正在吃鱼。");
}
}

// 4. 展示多态的类
class PolymorphismDemo {
public static void showPolymorphism() {
// 父类引用指向子类对象,这就是多态
Animal myDog = new Dog();
Animal myCat = new Cat();

// 调用相同的方法,但由于指向不同的子类实例,执行不同的行为
myDog.eat(); // 输出: 狗正在吃骨头。
myCat.eat(); // 输出: 猫正在吃鱼。
}
}

equals()hashCode()

编写 Student 类,并重写 equals()hashCode()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.Objects;

class Student {
private int id;
private String name;

public Student(int id, String name) {
this.id = id;
this.name = name;
}

// 重写 equals() 方法,根据 id 和 name 判断两个 Student 对象是否相等
@Override
public boolean equals(Object o) {
// 1. 检查是否为同一个对象的引用
if (this == o) return true;
// 2. 检查对象是否为 null 或类型是否匹配
if (o == null || getClass() != o.getClass()) return false;
// 3. 类型转换
Student student = (Student) o;
// 4. 比较关键字段 (id 和 name)
return id == student.id &&
Objects.equals(name, student.name);
}

// 重写 hashCode() 方法,为相等的对象生成相同的哈希码
@Override
public int hashCode() {
// 使用 Objects.hash() 方法生成哈希码,它会为多个字段生成一个组合哈希值
return Objects.hash(id, name);
}
}

/*
* 为什么需要两者一起重写?
* 1. 它们之间存在约定:如果两个对象通过 equals() 方法比较是相等的,那么它们的 hashCode() 方法返回的值也必须相等。
* 反之则不一定,不相等的对象可以有相同的哈希码(哈希冲突)。
* 2. 哈希表类(如 HashSet, HashMap)依赖于这个约定:当将对象存入哈希表时,
* 它会先调用 hashCode() 确定存储位置,再调用 equals() 来确认是否存在相同的对象。
* 3. 如果只重写 equals() 但不重写 hashCode(),可能导致两个逻辑上相等的对象被存储在不同的哈希位置,
* 从而无法正确查找和去重。例如,在 HashSet 中,即使两个 Student 对象 id 和 name 相同,
* 也会被认为是不同的对象而重复添加。
*/

接口与实现 (Interfaces and Implementation)

设计 Drawable 接口,并由 CircleRectangle 实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 1. 定义 Drawable 接口,包含一个抽象方法
interface Drawable {
void draw();
}

// 2. Circle 类实现 Drawable 接口
class Circle implements Drawable {
@Override
public void draw() {
System.out.println("正在画一个圆形。");
}
}

// 3. Rectangle 类实现 Drawable 接口
class Rectangle implements Drawable {
@Override
public void draw() {
System.out.println("正在画一个矩形。");
}
}

异常处理 (Exception Handling)

使用 try-catch-finally 结构处理 FileNotFoundException 并确保资源关闭。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class ExceptionDemo {
public static void processFile(String filePath) {
FileInputStream fileInputStream = null; // 在 try 块外部声明,以便在 finally 块中访问
try {
// 尝试打开文件
fileInputStream = new FileInputStream(filePath);
System.out.println("文件已成功打开。");
// 假设这里进行文件读取操作
} catch (FileNotFoundException e) {
// 捕获文件未找到异常
System.err.println("错误:指定的文件不存在!路径:" + filePath);
e.printStackTrace(); // 打印异常堆栈信息
} finally {
// 无论是否发生异常,finally 块都会执行
if (fileInputStream != null) {
try {
// 确保文件流被关闭,防止资源泄露
fileInputStream.close();
System.out.println("文件流已在 finally 块中关闭。");
} catch (IOException e) {
System.err.println("关闭文件流时发生异常:" + e.getMessage());
}
}
}
}
}

try-with-resources

使用 try-with-resources 重写上一个题目,展示其简化优势。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;

class TryWithResourcesDemo {
public static void processFileWithResources(String filePath) {
// try-with-resources 语句,自动管理实现了 AutoCloseable 接口的资源
try (FileInputStream fileInputStream = new FileInputStream(filePath)) {
System.out.println("文件已成功打开。");
// 假设这里进行文件读取操作
} catch (FileNotFoundException e) {
// 捕获文件未找到异常
System.err.println("错误:指定的文件不存在!路径:" + filePath);
e.printStackTrace();
} catch (IOException e) {
// 捕获文件关闭时可能发生的异常
System.err.println("关闭文件流时发生异常:" + e.getMessage());
}
// 不需要单独的 finally 块来关闭资源,JVM 会自动完成
System.out.println("文件流已在 try-with-resources 语句中自动关闭。");
}
}

/*
* try-with-resources 的优势:
* 1. 简化代码:不再需要显式的 finally 块来关闭资源,代码更简洁。
* 2. 避免资源泄露:无论 try 块是否正常完成或抛出异常,资源都会被自动关闭,
* 有效防止了因忘记关闭资源而导致的内存和文件句柄泄露。
* 3. 更好的异常处理:如果 try 块和资源关闭时都抛出异常,try-with-resources 会
* 将资源关闭时抛出的异常作为被抑制(suppressed)的异常,主异常保持不变,
* 便于调试。
*/

冒泡排序 (Bubble Sort)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class SortingAlgorithms {
public static void bubbleSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}

int n = arr.length;
// 外层循环控制比较轮数,共进行 n-1 轮
for (int i = 0; i < n - 1; i++) {
boolean swapped = false; // 优化:如果在某一轮没有发生交换,说明数组已经有序
// 内层循环负责每轮比较和交换,将最大元素“冒”到数组末尾
for (int j = 0; j < n - 1 - i; j++) {
if (arr[j] > arr[j + 1]) {
// 交换元素
int temp = arr[j];
arr[j] = arr[j + 1];
arr[j + 1] = temp;
swapped = true;
}
}
if (!swapped) {
break; // 如果没有发生交换,提前结束排序
}
}
}
}

快速排序 (Quick Sort)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class SortingAlgorithms {
public static void quickSort(int[] arr) {
if (arr == null || arr.length <= 1) {
return;
}
quickSort(arr, 0, arr.length - 1);
}

private static void quickSort(int[] arr, int low, int high) {
if (low < high) {
// 找到基准元素的正确位置
int pivotIndex = partition(arr, low, high);
// 递归地对左子数组进行排序
quickSort(arr, low, pivotIndex - 1);
// 递归地对右子数组进行排序
quickSort(arr, pivotIndex + 1, high);
}
}

private static int partition(int[] arr, int low, int high) {
// 选择最右边的元素作为基准
int pivot = arr[high];
int i = (low - 1); // i 指向小于等于基准的元素的最后一个位置

for (int j = low; j < high; j++) {
// 如果当前元素小于等于基准
if (arr[j] <= pivot) {
i++;
// 交换 arr[i] 和 arr[j]
int temp = arr[i];
arr[i] = arr[j];
arr[j] = temp;
}
}
// 将基准元素放到正确的位置
int temp = arr[i + 1];
arr[i + 1] = arr[high];
arr[high] = temp;

return i + 1;
}
}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class SearchAlgorithms {
public static int binarySearch(int[] arr, int target) {
if (arr == null || arr.length == 0) {
return -1;
}

int left = 0;
int right = arr.length - 1;

while (left <= right) {
int mid = left + (right - left) / 2; // 防止整数溢出

if (arr[mid] == target) {
return mid; // 找到目标,返回索引
} else if (arr[mid] < target) {
left = mid + 1; // 目标在右半部分,更新左边界
} else {
right = mid - 1; // 目标在左半部分,更新右边界
}
}
return -1; // 没有找到目标
}
}

链表反转 (Reverse Linked List)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class LinkedListProblems {
public static class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}

public static ListNode reverseList(ListNode head) {
ListNode prev = null; // 前一个节点
ListNode current = head; // 当前节点

while (current != null) {
ListNode nextTemp = current.next; // 暂存下一个节点
current.next = prev; // 将当前节点的指针指向前一个节点
prev = current; // 前一个节点向前移动
current = nextTemp; // 当前节点向前移动
}
return prev; // prev 最终会是新的头节点
}
}

链表中间节点 (Middle of Linked List)

使用快慢指针,快指针每次走两步,慢指针每次走一步。当快指针到达链表末尾时,慢指针正好在中间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LinkedListProblems {
public static class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}

public static ListNode findMiddleNode(ListNode head) {
if (head == null) {
return null;
}

ListNode slow = head; // 慢指针,每次走一步
ListNode fast = head; // 快指针,每次走两步

// 当快指针到达末尾时,慢指针正好在中间
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}
return slow;
}
}

移除重复元素 (Remove Duplicates)

不使用 Set,通过双层循环或排序后遍历实现。这里使用排序后遍历的方法,因为它更高效。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.Collections;

public class ListProblems {
public static void removeDuplicates(ArrayList<String> list) {
if (list == null || list.size() <= 1) {
return;
}

Collections.sort(list); // 先排序,使得重复元素相邻

// 倒序遍历,移除重复元素
for (int i = list.size() - 2; i >= 0; i--) {
if (list.get(i).equals(list.get(i + 1))) {
list.remove(i + 1);
}
}
}
}

字符串反转 (Reverse String)

不使用 StringBuilderStringBufferreverse() 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringProblems {
public static String reverseString(String str) {
if (str == null || str.isEmpty()) {
return str;
}

char[] charArray = str.toCharArray();
int left = 0;
int right = charArray.length - 1;

while (left < right) {
// 交换首尾字符
char temp = charArray[left];
charArray[left] = charArray[right];
charArray[right] = temp;
left++;
right--;
}
return new String(charArray);
}
}

回文字符串 (Palindrome String)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public class StringProblems {
public static boolean isPalindrome(String str) {
if (str == null) {
return false;
}
// 将字符串转换为小写并移除非字母数字字符
String cleanedStr = str.toLowerCase().replaceAll("[^a-z0-9]", "");

int left = 0;
int right = cleanedStr.length() - 1;

while (left < right) {
if (cleanedStr.charAt(left) != cleanedStr.charAt(right)) {
return false;
}
left++;
right--;
}
return true;
}
}

计算阶乘 (Factorial Calculation)

递归方式

1
2
3
4
5
6
7
8
9
10
11
public class MathProblems {
public static long factorialRecursive(int n) {
if (n < 0) {
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
if (n == 0 || n == 1) {
return 1;
}
return n * factorialRecursive(n - 1);
}
}

非递归方式

1
2
3
4
5
6
7
8
9
10
11
12
public class MathProblems {
public static long factorialIterative(int n) {
if (n < 0) {
throw new IllegalArgumentException("Factorial is not defined for negative numbers.");
}
long result = 1;
for (int i = 2; i <= n; i++) {
result *= i;
}
return result;
}
}

斐波那契数列 (Fibonacci Sequence)

递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MathProblems {
public static long fibonacciRecursive(int n) {
if (n < 0) {
throw new IllegalArgumentException("Invalid input for Fibonacci sequence.");
}
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}
return fibonacciRecursive(n - 1) + fibonacciRecursive(n - 2);
}
}

非递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class MathProblems {
public static long fibonacciIterative(int n) {
if (n < 0) {
throw new IllegalArgumentException("Invalid input for Fibonacci sequence.");
}
if (n == 0) {
return 0;
}
if (n == 1) {
return 1;
}

long a = 0;
long b = 1;
for (int i = 2; i <= n; i++) {
long sum = a + b;
a = b;
b = sum;
}
return b;
}
}

字符串中字符计数 (Character Count)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashMap;
import java.util.Map;

public class StringProblems {
public static Map<Character, Integer> countCharacters(String str) {
Map<Character, Integer> charCountMap = new HashMap<>();
if (str == null || str.isEmpty()) {
return charCountMap;
}

for (char c : str.toCharArray()) {
charCountMap.put(c, charCountMap.getOrDefault(c, 0) + 1);
}
return charCountMap;
}
}

两数之和 (Two Sum)

使用 HashMap 将值和索引存储起来,实现一次遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import java.util.HashMap;
import java.util.Map;

public class ArrayProblems {
public static int[] twoSum(int[] nums, int target) {
Map<Integer, Integer> numMap = new HashMap<>();
for (int i = 0; i < nums.length; i++) {
int complement = target - nums[i];
if (numMap.containsKey(complement)) {
return new int[]{numMap.get(complement), i};
}
numMap.put(nums[i], i);
}
return new int[]{}; // 未找到
}
}

数组中最大/小值 (Max/Min in Array)

一次遍历找到最大值和最小值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ArrayProblems {
public static int[] findMaxMin(int[] arr) {
if (arr == null || arr.length == 0) {
throw new IllegalArgumentException("Array cannot be null or empty.");
}

int min = arr[0];
int max = arr[0];

for (int i = 1; i < arr.length; i++) {
if (arr[i] < min) {
min = arr[i];
}
if (arr[i] > max) {
max = arr[i];
}
}
return new int[]{max, min};
}
}

质数判断 (Prime Number)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MathProblems {
public static boolean isPrime(int n) {
if (n <= 1) {
return false; // 1 和更小的数都不是质数
}
// 只需要检查到 n 的平方根
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) {
return false;
}
}
return true;
}
}

集合交集 (List Intersection)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class ListProblems {
public static <T> List<T> intersection(List<T> list1, List<T> list2) {
List<T> result = new ArrayList<>();
if (list1 == null || list2 == null) {
return result;
}

// 遍历第一个列表,检查元素是否在第二个列表中
for (T element : list1) {
if (list2.contains(element) && !result.contains(element)) {
result.add(element);
}
}
return result;
}
}

数组合并 (Merge Sorted Arrays)

将两个已排序的数组合并为一个新的已排序数组,使用双指针方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class ArrayProblems {
public static int[] mergeSortedArrays(int[] arr1, int[] arr2) {
int[] merged = new int[arr1.length + arr2.length];
int i = 0, j = 0, k = 0;

while (i < arr1.length && j < arr2.length) {
if (arr1[i] < arr2[j]) {
merged[k++] = arr1[i++];
} else {
merged[k++] = arr2[j++];
}
}

while (i < arr1.length) {
merged[k++] = arr1[i++];
}

while (j < arr2.length) {
merged[k++] = arr2[j++];
}

return merged;
}
}

判断回文数 (Palindrome Number)

不使用 String 转换,通过数学方法反转一半数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class MathProblems {
public static boolean isPalindromeNumber(int x) {
// 负数和以 0 结尾的非 0 数字都不是回文
if (x < 0 || (x % 10 == 0 && x != 0)) {
return false;
}

int reversedNumber = 0;
while (x > reversedNumber) {
int lastDigit = x % 10;
reversedNumber = reversedNumber * 10 + lastDigit;
x /= 10;
}

// 偶数位数字时,x 等于 reversedNumber
// 奇数位数字时,x 等于 reversedNumber 除以 10
return x == reversedNumber || x == reversedNumber / 10;
}
}

罗马数字转整数 (Roman to Integer)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.HashMap;
import java.util.Map;

public class StringProblems {
public static int romanToInt(String s) {
Map<Character, Integer> romanMap = new HashMap<>();
romanMap.put('I', 1);
romanMap.put('V', 5);
romanMap.put('X', 10);
romanMap.put('L', 50);
romanMap.put('C', 100);
romanMap.put('D', 500);
romanMap.put('M', 1000);

int result = 0;
for (int i = 0; i < s.length(); i++) {
int currentVal = romanMap.get(s.charAt(i));
// 如果当前字符的值小于下一个字符的值,则需要减去当前值
if (i < s.length() - 1 && currentVal < romanMap.get(s.charAt(i + 1))) {
result -= currentVal;
} else {
result += currentVal;
}
}
return result;
}
}

爬楼梯 (Climbing Stairs)

这是经典的动态规划问题。

动态规划方式 (非递归)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class MathProblems {
public static int climbStairs(int n) {
if (n <= 2) {
return n;
}

int[] dp = new int[n + 1];
dp[1] = 1; // 爬 1 级台阶,有 1 种方法
dp[2] = 2; // 爬 2 级台阶,有 2 种方法 (1+1, 2)

for (int i = 3; i <= n; i++) {
// 到达第 i 级台阶的方法数 = (从 i-1 级爬 1 级的方法数) + (从 i-2 级爬 2 级的方法数)
dp[i] = dp[i - 1] + dp[i - 2];
}

return dp[n];
}
}

好的,以下是基于 语言的线程基础与同步的实现,并附带详细注释。

线程创建

1. 继承 Thread

通过继承 java.lang.Thread 类并重写其 run() 方法来创建线程。

1
2
3
4
5
6
7
8
// 继承 Thread 类创建线程
class MyThread extends Thread {
@Override
public void run() {
// 线程执行的任务
System.out.println("使用继承 Thread 类的方式创建的线程正在运行。");
}
}

2. 实现 Runnable 接口

通过实现 java.lang.Runnable 接口并将其作为参数传递给 Thread 类的构造函数来创建线程。这种方式更灵活,推荐使用。

1
2
3
4
5
6
7
8
// 实现 Runnable 接口创建线程
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程执行的任务
System.out.println("使用实现 Runnable 接口的方式创建的线程正在运行。");
}
}

sleep()yield()

sleep()yield() 都是线程调度的方法,但它们的作用和效果不同。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class SleepYieldDemo {
public static void main(String[] args) {
// 使用 sleep() 的线程
new Thread(() -> {
System.out.println("线程 A: 开始执行...");
try {
// sleep() 使线程暂停指定时间,进入 WAITING 状态,但不会释放锁
System.out.println("线程 A: 准备睡眠 2 秒...");
Thread.sleep(2000);
System.out.println("线程 A: 睡眠结束,继续执行。");
} catch (InterruptedException e) {
e.printStackTrace();
}
}).start();

// 使用 yield() 的线程
new Thread(() -> {
System.out.println("线程 B: 开始执行...");
for (int i = 0; i < 5; i++) {
System.out.println("线程 B: 正在执行 " + i);
// yield() 提示线程调度器让出 CPU 时间,但不保证一定生效
Thread.yield();
}
System.out.println("线程 B: 执行完毕。");
}).start();
}
}
  • sleep(): 使当前线程暂停执行指定的时间,进入 TIMED_WAITING 状态。它会释放 CPU 资源,但不释放锁
  • yield(): 提示线程调度器,当前线程愿意让出当前 CPU 时间片。线程会从 RUNNING 状态转换为 RUNNABLE 状态,与其他线程竞争 CPU,但不保证其他线程能立即获得执行。它主要用于优化线程调度,通常在多线程程序中不应依赖其行为来保证正确性。

join() 方法

join() 方法允许一个线程等待另一个线程执行完毕。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(() -> {
try {
System.out.println("子线程: 正在执行...");
Thread.sleep(3000); // 模拟耗时操作
System.out.println("子线程: 执行完毕。");
} catch (InterruptedException e) {
e.printStackTrace();
}
});

workerThread.start();
System.out.println("主线程: 等待子线程执行完毕...");
// 调用 join() 方法,主线程进入等待状态,直到 workerThread 执行完毕
workerThread.join();
System.out.println("主线程: 子线程已执行完毕,主线程继续执行。");
}
}

非线程安全计数器

在多线程环境下,多个线程同时对共享资源进行读写操作,可能导致数据不一致。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
class UnsafeCounter {
private int count = 0;

public void increment() {
count++; // 这一行代码并非原子操作,它包含三个步骤:读、加 1、写
}

public int getCount() {
return count;
}
}

class ThreadSafetyDemo {
public static void main(String[] args) throws InterruptedException {
UnsafeCounter counter = new UnsafeCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果不一定是 100 * 10000 = 1000000
System.out.println("非线程安全计数器的最终结果: " + counter.getCount());
}
}

synchronized 关键字

使用 synchronized 关键字可以保证同一时刻只有一个线程访问共享资源,从而解决线程安全问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class SynchronizedCounter {
private int count = 0;

// 使用 synchronized 关键字修饰方法,锁住整个对象实例
public synchronized void increment() {
count++;
}

public int getCount() {
return count;
}
}

class SynchronizedDemo {
public static void main(String[] args) throws InterruptedException {
SynchronizedCounter counter = new SynchronizedCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果为 1000000
System.out.println("synchronized 计数器的最终结果: " + counter.getCount());
}
}

synchronized 块与方法

  • synchronized 方法: 锁定的是当前对象实例 (this)。
  • synchronized 块: 提供了更细粒度的控制,可以指定锁定的对象。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
class SynchronizedBlockDemo {
private final Object lock1 = new Object();
private final Object lock2 = new Object();

// synchronized 方法,锁定当前对象实例
public synchronized void synchronizedMethod() {
System.out.println(Thread.currentThread().getName() + " 进入 synchronized 方法。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 离开 synchronized 方法。");
}

// synchronized 块,锁定指定的对象
public void synchronizedBlock1() {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock1...");
synchronized (lock1) {
System.out.println(Thread.currentThread().getName() + " 已获取 lock1。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 释放 lock1。");
}
}

public void synchronizedBlock2() {
System.out.println(Thread.currentThread().getName() + " 尝试获取 lock2...");
synchronized (lock2) {
System.out.println(Thread.currentThread().getName() + " 已获取 lock2。");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 释放 lock2。");
}
}
}

/*
* 区别与适用场景:
* - synchronized 方法: 简单易用,但锁定范围大,可能导致性能问题。当需要同步整个方法时使用。
* - synchronized 块: 锁定范围小,可以只同步需要保护的代码段,提高并发性能。当只需要同步部分代码时使用。
* 同时,可以通过锁定不同的对象来避免不必要的阻塞,实现更高的并发度。
*/

volatile 关键字

volatile 关键字保证了变量在多线程间的可见性,但不保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class VolatileDemo {
// volatile 保证所有线程看到的是该变量的最新值
private static volatile boolean ready = false;

public static void main(String[] args) throws InterruptedException {
new Thread(() -> {
System.out.println("线程 A: 正在等待标志位变为 true。");
while (!ready) {
// 空循环,等待 ready 变为 true
}
System.out.println("线程 A: 标志位已变为 true,循环结束。");
}).start();

Thread.sleep(1000); // 确保线程 A 先运行
System.out.println("主线程: 正在将标志位设置为 true。");
ready = true;
}
}

/*
* 为什么 volatile 不能保证原子性?
* - 原子性是指一个操作是不可中断的,要么全部执行,要么都不执行。
* - volatile 只能保证变量的读写操作是原子的,但像 `count++` 这样的复合操作(读、加、写)
* 依然不是原子的。多个线程可能同时读到旧值,导致最终结果不正确。
* - volatile 主要用于一写多读的场景,或者用于控制线程执行流程的标志位。
*/

AtomicInteger

AtomicInteger 是一个原子类,它使用 CAS (Compare-and-Swap) 机制来保证操作的原子性,从而实现线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.atomic.AtomicInteger;

class AtomicCounter {
// 使用 AtomicInteger 替代 int
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet(); // 这是一个原子操作
}

public int getCount() {
return count.get();
}
}

class AtomicDemo {
public static void main(String[] args) throws InterruptedException {
AtomicCounter counter = new AtomicCounter();
Thread[] threads = new Thread[100];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 10000; j++) {
counter.increment();
}
});
threads[i].start();
}

for (Thread thread : threads) {
thread.join();
}
// 最终结果为 1000000
System.out.println("AtomicInteger 计数器的最终结果: " + counter.getCount());
}
}

线程通信

使用 wait()notifyAll() 实现两个线程交替打印奇数和偶数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
class PrintNumbers {
private int count = 0;
private final Object lock = new Object();

public void printOdd() {
synchronized (lock) {
while (count < 10) {
while (count % 2 == 0) { // 如果是偶数,则等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notifyAll(); // 唤醒其他等待的线程
}
}
}

public void printEven() {
synchronized (lock) {
while (count < 10) {
while (count % 2 != 0) { // 如果是奇数,则等待
try {
lock.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println(Thread.currentThread().getName() + ": " + count++);
lock.notifyAll(); // 唤醒其他等待的线程
}
}
}

public static void main(String[] args) {
PrintNumbers pn = new PrintNumbers();
Thread oddThread = new Thread(pn::printOdd, "奇数线程");
Thread evenThread = new Thread(pn::printEven, "偶数线程");

oddThread.start();
evenThread.start();
}
}

好的,以下是 中高级并发编程的实现,并附带详细注释。

生产者-消费者模式 (使用 wait()notifyAll())

这是一个经典的线程协作问题。生产者生产数据,放入共享队列;消费者从队列中取出数据进行消费。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
import java.util.LinkedList;
import java.util.Queue;

class ProducerConsumerClassic {
// 共享队列,使用 LinkedList 实现
private final Queue<Integer> queue = new LinkedList<>();
// 队列最大容量
private final int MAX_SIZE = 5;
private final Object LOCK = new Object();

public void produce() throws InterruptedException {
int i = 0;
while (true) {
synchronized (LOCK) {
// 如果队列已满,生产者等待
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
LOCK.wait();
}
// 生产数据并放入队列
queue.offer(i);
System.out.println("生产者生产了: " + i);
i++;
// 唤醒所有等待的线程(包括消费者)
LOCK.notifyAll();
}
Thread.sleep(100); // 模拟生产时间
}
}

public void consume() throws InterruptedException {
while (true) {
synchronized (LOCK) {
// 如果队列为空,消费者等待
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
LOCK.wait();
}
// 消费数据
int data = queue.poll();
System.out.println("消费者消费了: " + data);
// 唤醒所有等待的线程(包括生产者)
LOCK.notifyAll();
}
Thread.sleep(500); // 模拟消费时间
}
}
}

生产者-消费者模式 (使用 BlockingQueue)

java.util.concurrent.BlockingQueue 接口提供了线程安全的队列操作,简化了生产者-消费者模型的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

class ProducerConsumerBlockingQueue {
// 使用 BlockingQueue,它内部已经处理了线程同步
private final BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(5);

public void produce() throws InterruptedException {
int i = 0;
while (true) {
// put() 方法在队列满时会自动阻塞
queue.put(i);
System.out.println("生产者生产了: " + i);
i++;
Thread.sleep(100);
}
}

public void consume() throws InterruptedException {
while (true) {
// take() 方法在队列空时会自动阻塞
int data = queue.take();
System.out.println("消费者消费了: " + data);
Thread.sleep(500);
}
}
}

ReentrantLock

使用 ReentrantLock 实现线程安全计数器,并解释其与 synchronized 的区别。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.locks.ReentrantLock;

class ReentrantLockCounter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();

public void increment() {
lock.lock(); // 获取锁
try {
count++; // 在 try 块中执行需要同步的代码
} finally {
lock.unlock(); // 在 finally 块中释放锁,确保锁总是被释放
}
}

public int getCount() {
lock.lock();
try {
return count;
} finally {
lock.unlock();
}
}
}

/*
* ReentrantLock 与 synchronized 的区别:
* 1. 语法层面: `synchronized` 是 JVM 的内置关键字,而 `ReentrantLock` 是一个类。
* 2. 灵活性: `ReentrantLock` 提供了更灵活的锁定控制。例如,它支持公平锁(按请求顺序获取锁),
* 可以尝试非阻塞地获取锁(`tryLock()`),以及支持中断(`lockInterruptibly()`)。
* 3. 性能: 在早期版本中,`ReentrantLock` 通常性能更好。但随着 对 `synchronized` 优化(偏向锁、轻量级锁),
* 两者性能已非常接近。在简单场景下,`synchronized` 更简洁。
* 4. 协作: `ReentrantLock` 必须配合 `Condition` 接口才能实现线程间的等待/唤醒机制,而 `synchronized` 直接
* 使用 `Object` 的 `wait()` 和 `notify()`。
*/

ReentrantLockCondition

使用 ReentrantLockCondition 重新实现生产者-消费者模型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import java.util.LinkedList;
import java.util.Queue;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

class ProducerConsumerCondition {
private final Queue<Integer> queue = new LinkedList<>();
private final int MAX_SIZE = 5;
private final ReentrantLock lock = new ReentrantLock();
// 创建两个 Condition 实例,分别用于生产者和消费者
private final Condition producerCondition = lock.newCondition();
private final Condition consumerCondition = lock.newCondition();

public void produce() throws InterruptedException {
int i = 0;
while (true) {
lock.lock();
try {
while (queue.size() == MAX_SIZE) {
System.out.println("队列已满,生产者等待...");
producerCondition.await(); // 生产者等待
}
queue.offer(i);
System.out.println("生产者生产了: " + i);
i++;
consumerCondition.signalAll(); // 唤醒所有消费者
} finally {
lock.unlock();
}
Thread.sleep(100);
}
}

public void consume() throws InterruptedException {
while (true) {
lock.lock();
try {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
consumerCondition.await(); // 消费者等待
}
int data = queue.poll();
System.out.println("消费者消费了: " + data);
producerCondition.signalAll(); // 唤醒所有生产者
} finally {
lock.unlock();
}
Thread.sleep(500);
}
}
}

ExecutorService

创建一个固定大小的线程池,并向其提交多个任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ExecutorServiceDemo {
public static void main(String[] args) {
// 创建一个固定大小为 3 的线程池
ExecutorService executorService = Executors.newFixedThreadPool(3);

for (int i = 0; i < 10; i++) {
final int taskId = i;
executorService.execute(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行。");
try {
Thread.sleep(1000); // 模拟任务执行时间
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
// 关闭线程池,不再接收新任务,已提交的任务会继续执行
executorService.shutdown();
}
}

CallableFuture

使用 Callable 提交任务,并使用 Future 获取返回值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.concurrent.*;

class CallableFutureDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
ExecutorService executorService = Executors.newSingleThreadExecutor();

// 创建一个 Callable 任务,它会返回一个字符串
Callable<String> task = () -> {
System.out.println("任务开始执行...");
Thread.sleep(2000); // 模拟耗时操作
return "任务执行完毕,返回结果!";
};

// 提交任务并获得 Future 对象
Future<String> future = executorService.submit(task);

System.out.println("主线程: 任务已提交,继续执行其他操作...");
// get() 方法会阻塞,直到任务完成并返回结果
String result = future.get();
System.out.println("主线程: 获得任务结果 -> " + result);

executorService.shutdown();
}
}

CountDownLatch

CountDownLatch 允许一个或多个线程等待直到在其他线程中执行的一组操作完成。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 5;
// 计数器,当计数器减到 0 时,主线程会被唤醒
CountDownLatch latch = new CountDownLatch(workerCount);
ExecutorService executor = Executors.newFixedThreadPool(workerCount);

for (int i = 0; i < workerCount; i++) {
final int workerId = i;
executor.execute(() -> {
System.out.println("工作线程 " + workerId + " 开始执行任务...");
try {
Thread.sleep((long) (Math.random() * 2000)); // 模拟任务执行
System.out.println("工作线程 " + workerId + " 任务完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
latch.countDown(); // 任务完成后,计数器减 1
}
});
}

System.out.println("主线程: 等待所有工作线程完成...");
latch.await(); // 阻塞主线程,直到计数器为 0
System.out.println("主线程: 所有工作线程已完成,继续执行下一步。");

executor.shutdown();
}
}

CyclicBarrier

CyclicBarrier 允许一组线程相互等待,直到所有线程都到达一个共同的屏障点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CyclicBarrierDemo {
public static void main(String[] args) {
int partySize = 3;
// 当 3 个线程都到达屏障时,执行一个屏障动作
CyclicBarrier barrier = new CyclicBarrier(partySize, () -> {
System.out.println("\n所有线程已到达屏障点!继续执行下一阶段。\n");
});
ExecutorService executor = Executors.newFixedThreadPool(partySize);

for (int i = 0; i < partySize; i++) {
final int threadId = i;
executor.execute(() -> {
try {
System.out.println("线程 " + threadId + " 正在执行第一阶段任务...");
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + threadId + " 第一阶段任务完成,到达屏障。");
barrier.await(); // 线程在此处等待

System.out.println("线程 " + threadId + " 正在执行第二阶段任务...");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}

Semaphore

Semaphore(信号量)用来控制对资源的并发访问数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class SemaphoreDemo {
// 允许 3 个线程同时访问
private final Semaphore semaphore = new Semaphore(3);

public void accessResource(int threadId) {
try {
semaphore.acquire(); // 获取许可
System.out.println("线程 " + threadId + " 正在访问资源...");
Thread.sleep(2000); // 模拟资源访问时间
System.out.println("线程 " + threadId + " 访问资源完毕。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}

public static void main(String[] args) {
SemaphoreDemo demo = new SemaphoreDemo();
ExecutorService executor = Executors.newFixedThreadPool(10);

for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.execute(() -> demo.accessResource(threadId));
}

executor.shutdown();
}
}

线程死锁

1. 死锁演示

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
class DeadlockDemo {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public void threadA() {
synchronized (lock1) {
System.out.println("线程A: 已获得 lock1,尝试获取 lock2...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("线程A: 已获得 lock2。");
}
}
}

public void threadB() {
synchronized (lock2) {
System.out.println("线程B: 已获得 lock2,尝试获取 lock1...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("线程B: 已获得 lock1。");
}
}
}

public static void main(String[] args) {
DeadlockDemo demo = new DeadlockDemo();
new Thread(demo::threadA, "Thread-A").start();
new Thread(demo::threadB, "Thread-B").start();
}
}

/*
* 死锁产生条件:
* 1. 互斥条件: 资源是独占的,一个线程在使用时,其他线程无法使用。
* 2. 请求与保持条件: 线程已经持有至少一个资源,但又请求其他资源,同时又保持对已有资源的占有。
* 3. 不可剥夺条件: 资源只能在持有它的线程完成任务后由它自己释放。
* 4. 循环等待条件: 存在一个线程资源的循环链,每个线程都在等待下一个线程所持有的资源。
*/

死锁预防

通过打破死锁的四个必要条件之一来预防死锁。这里通过资源有序分配来打破循环等待

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class DeadlockPrevention {
private static final Object lock1 = new Object();
private static final Object lock2 = new Object();

public void threadA() {
// 线程 A 按顺序先获取 lock1,再获取 lock2
synchronized (lock1) {
System.out.println("线程A: 已获得 lock1,尝试获取 lock2...");
synchronized (lock2) {
System.out.println("线程A: 已获得 lock2。");
}
}
}

public void threadB() {
// 线程 B 也按顺序先获取 lock1,再获取 lock2
synchronized (lock1) {
System.out.println("线程B: 已获得 lock1,尝试获取 lock2...");
synchronized (lock2) {
System.out.println("线程B: 已获得 lock2。");
}
}
}

public static void main(String[] args) {
DeadlockPrevention demo = new DeadlockPrevention();
new Thread(demo::threadA, "Thread-A").start();
new Thread(demo::threadB, "Thread-B").start();
// 两个线程都按相同的顺序获取锁,不会发生死锁
}
}

ThreadLocal

ThreadLocal 为每个线程提供了一个独立的变量副本,实现了线程间的数据隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public void setAndPrint(String value) {
// 设置线程本地变量
threadLocal.set(value);
try {
Thread.sleep(1000); // 模拟任务
// 获取并打印当前线程的变量值
System.out.println(Thread.currentThread().getName() + " 的变量值: " + threadLocal.get());
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 最佳实践:使用完后移除,防止内存泄漏
threadLocal.remove();
}
}

public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
new Thread(() -> demo.setAndPrint("线程 A 的数据"), "Thread-A").start();
new Thread(() -> demo.setAndPrint("线程 B 的数据"), "Thread-B").start();
}
}

/*
* 原理和作用:
* 原理: ThreadLocal 内部有一个 `ThreadLocalMap`,每个线程都有一个独立的 `ThreadLocalMap`。
* 当我们调用 `set()` 方法时,实际上是将值存储到了当前线程的 `ThreadLocalMap` 中,
* 键为 `ThreadLocal` 实例本身。
* 作用:
* 1. 数据隔离: 解决了多线程访问共享变量的线程安全问题,但其本质不是同步,而是通过“以空间换时间”的方式,
* 为每个线程提供独立副本,避免了竞争。
* 2. 传递参数: 在整个方法调用链中,无需层层传递参数,可以方便地在任何地方获取当前线程的上下文信息。
*/

线程安全单例 (静态内部类)

静态内部类方式是实现线程安全的懒汉式单例的最佳实践之一。它利用了 JVM 类加载机制的线程安全特性。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class SingletonThreadSafe {
// 私有化构造器
private SingletonThreadSafe() {}

// 静态内部类,它只在 SingletonThreadSafe 被首次调用时才会被加载
private static class SingletonHolder {
// 静态成员变量,在类加载时初始化
private static final SingletonThreadSafe INSTANCE = new SingletonThreadSafe();
}

// 提供全局访问点
public static SingletonThreadSafe getInstance() {
return SingletonHolder.INSTANCE;
}
}

/*
* 优点:
* 1. 线程安全: 类的加载是线程安全的,因此 INSTANCE 的初始化是原子的。
* 2. 懒加载: 只有在调用 getInstance() 方法时,SingletonHolder 类才会被加载,
* 从而实现懒加载。
*/

ReadWriteLock

ReadWriteLock 适用于读多写少的场景,它允许多个线程同时进行读操作,但写操作必须是互斥的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class ReadWriteLockDemo {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private final Object sharedData = "初始数据";

public String readData() {
rwLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
Thread.sleep(1000);
return (String) sharedData;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}

public void writeData(String newData) {
rwLock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
Thread.sleep(2000);
// sharedData = newData; // 实际更新数据
System.out.println(Thread.currentThread().getName() + " 写入完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
}

线程池关闭

ExecutorService 的两种关闭方式:shutdown()shutdownNow()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class ShutdownDemo {
public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交 5 个任务
for (int i = 0; i < 5; i++) {
final int taskId = i;
executor.execute(() -> {
try {
System.out.println("任务 " + taskId + " 正在执行...");
Thread.sleep(2000);
} catch (InterruptedException e) {
System.out.println("任务 " + taskId + " 被中断。");
}
});
}

// shutdown() vs shutdownNow()
// executor.shutdown(); // 优雅关闭
executor.shutdownNow(); // 暴力关闭

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("线程池已关闭。");
}
}

/*
* 区别:
* - shutdown(): 优雅关闭。不再接受新的任务,但会等待已提交的任务(包括正在执行和在队列中的)全部执行完毕。
* 执行后,isShutdown() 返回 true,isTerminated() 返回 false,直到所有任务完成。
* - shutdownNow(): 暴力关闭。立即停止所有正在执行的任务,并返回在队列中等待执行的任务列表。
* 它会向所有线程发送 interrupt() 中断信号。
* 执行后,isShutdown() 返回 true,isTerminated() 立即返回 true。
*/

中断机制

一个线程通过响应 interrupt() 调用来正确停止自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class InterruptedThreadDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 检查中断标志
try {
System.out.println("线程正在执行...");
// sleep()、wait() 等方法会抛出 InterruptedException 并清除中断标志
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程被中断!");
// 重新设置中断标志,以便外层循环能正确退出
Thread.currentThread().interrupt();
// 或者直接 return 退出
return;
}
}
System.out.println("线程已退出。");
});

worker.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 发送中断信号
worker.interrupt();
}
}

子数组求和 (Subarray Sum)

使用滑动窗口前缀和 + 哈希表两种方式解决。这里演示前缀和 + 哈希表,它能处理负数情况且时间复杂度更优。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
import java.util.HashMap;
import java.util.Map;

class ArrayProblems {
/**
* 找出和等于目标值的连续子数组。
* 使用前缀和与哈希表的方式,时间复杂度为 O(n)。
*
* @param nums 整数数组
* @param target 目标值
* @return 如果找到,返回子数组的起始和结束索引;否则返回 null
*/
public static int[] findSubarraySum(int[] nums, int target) {
if (nums == null || nums.length == 0) {
return null;
}

// key: 前缀和, value: 出现该前缀和的索引
Map<Integer, Integer> prefixSumMap = new HashMap<>();
prefixSumMap.put(0, -1); // 初始化,处理从数组开头开始的子数组

int currentSum = 0;
for (int i = 0; i < nums.length; i++) {
currentSum += nums[i];

// 检查是否存在 (currentSum - target) 这样的前缀和
if (prefixSumMap.containsKey(currentSum - target)) {
int start = prefixSumMap.get(currentSum - target) + 1;
int end = i;
return new int[]{start, end};
}

prefixSumMap.put(currentSum, i);
}

return null;
}
}

字符串去重 (Remove Duplicates from String)

使用 LinkedHashSet 保持相对顺序或手动遍历。这里手动遍历实现,避免使用额外数据结构。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
class StringProblems {
/**
* 对一个字符串进行去重,并保持原有字符的相对顺序。
*
* @param str 待去重的字符串
* @return 去重后的字符串
*/
public static String removeDuplicates(String str) {
if (str == null || str.length() <= 1) {
return str;
}

StringBuilder sb = new StringBuilder();
boolean[] charSet = new boolean[256]; // 假设为 ASCII 字符集

for (int i = 0; i < str.length(); i++) {
char c = str.charAt(i);
if (!charSet[c]) {
sb.append(c);
charSet[c] = true;
}
}
return sb.toString();
}
}

最长不重复子串 (Longest Substring Without Repeating Characters)

使用滑动窗口哈希表数组来高效解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import java.util.HashMap;
import java.util.Map;

class StringProblems {
/**
* 找出最长不重复子串的长度。
* 使用滑动窗口(双指针)和哈希表
*
* @param s 输入字符串
* @return 最长不重复子串的长度
*/
public static int lengthOfLongestSubstring(String s) {
if (s == null || s.length() == 0) {
return 0;
}

// key: 字符, value: 字符的最新索引
Map<Character, Integer> charIndexMap = new HashMap<>();
int maxLength = 0;
int left = 0; // 滑动窗口的左边界

for (int right = 0; right < s.length(); right++) {
char currentChar = s.charAt(right);

// 如果当前字符已在窗口内,更新左边界
if (charIndexMap.containsKey(currentChar)) {
left = Math.max(left, charIndexMap.get(currentChar) + 1);
}

charIndexMap.put(currentChar, right);
maxLength = Math.max(maxLength, right - left + 1);
}
return maxLength;
}
}

字符串转整数 (String to Integer - atoi)

实现 atoi 函数,需要考虑各种边界情况。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
class StringProblems {
/**
* 实现 atoi 函数,将字符串转换为整数。
*
* @param s 字符串
* @return 转换后的整数
*/
public static int myAtoi(String s) {
if (s == null || s.isEmpty()) {
return 0;
}

s = s.trim(); // 1. 去掉前导空格

if (s.isEmpty()) {
return 0;
}

int index = 0;
int sign = 1; // 2. 处理正负号
if (s.charAt(index) == '-') {
sign = -1;
index++;
} else if (s.charAt(index) == '+') {
index++;
}

long result = 0;
while (index < s.length()) {
char c = s.charAt(index);
// 3. 检查是否为数字
if (!Character.isDigit(c)) {
break;
}

int digit = c - '0';
result = result * 10 + digit;

// 4. 处理溢出
if (sign == 1 && result > Integer.MAX_VALUE) {
return Integer.MAX_VALUE;
}
if (sign == -1 && -result < Integer.MIN_VALUE) {
return Integer.MIN_VALUE;
}
index++;
}

return (int) (result * sign);
}
}

判断子串 (Substring Check)

使用 String.indexOf() 是最直接的方式。如果不能使用内置方法,可以通过双指针或 KMP 算法实现。这里提供双指针的实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class StringProblems {
/**
* 判断一个字符串是否是另一个字符串的子串。
*
* @param mainStr 主字符串
* @param subStr 子字符串
* @return 如果 subStr 是 mainStr 的子串,返回 true;否则返回 false
*/
public static boolean isSubstring(String mainStr, String subStr) {
if (mainStr == null || subStr == null || mainStr.length() < subStr.length()) {
return false;
}
if (subStr.isEmpty()) {
return true;
}

for (int i = 0; i <= mainStr.length() - subStr.length(); i++) {
boolean match = true;
for (int j = 0; j < subStr.length(); j++) {
if (mainStr.charAt(i + j) != subStr.charAt(j)) {
match = false;
break;
}
}
if (match) {
return true;
}
}
return false;
}
}

数组旋转 (Array Rotation)

向右旋转 k 步,可以通过三次反转或使用额外数组实现。这里使用空间复杂度为 O(1) 的三次反转方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
class ArrayProblems {
/**
* 将一个整数数组向右旋转 k 步。
* 使用三次反转的方法
*
* @param nums 整数数组
* @param k 旋转步数
*/
public static void rotate(int[] nums, int k) {
if (nums == null || nums.length <= 1) {
return;
}

k %= nums.length; // 处理 k 大于数组长度的情况

// 1. 反转整个数组
reverse(nums, 0, nums.length - 1);
// 2. 反转前 k 个元素
reverse(nums, 0, k - 1);
// 3. 反转剩下的元素
reverse(nums, k, nums.length - 1);
}

private static void reverse(int[] nums, int start, int end) {
while (start < end) {
int temp = nums[start];
nums[start] = nums[end];
nums[end] = temp;
start++;
end--;
}
}
}

交错字符串 (Interleaving String)

判断 s3 是否由 s1s2 交错而成,可以使用动态规划解决。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class StringProblems {
/**
* 判断两个字符串交错形成的字符串是否等于第三个字符串。
*
* @param s1 字符串1
* @param s2 字符串2
* @param s3 字符串3
* @return 如果是交错字符串,返回 true;否则返回 false
*/
public static boolean isInterleave(String s1, String s2, String s3) {
if (s1.length() + s2.length() != s3.length()) {
return false;
}

// dp[i][j] 表示 s1 的前 i 个字符和 s2 的前 j 个字符能否交错组成 s3 的前 i+j 个字符
boolean[][] dp = new boolean[s1.length() + 1][s2.length() + 1];
dp[0][0] = true;

// 初始化第一行 (s1 不取字符)
for (int j = 1; j <= s2.length(); j++) {
dp[0][j] = dp[0][j - 1] && s2.charAt(j - 1) == s3.charAt(j - 1);
}

// 初始化第一列 (s2 不取字符)
for (int i = 1; i <= s1.length(); i++) {
dp[i][0] = dp[i - 1][0] && s1.charAt(i - 1) == s3.charAt(i - 1);
}

// 填充 DP 表
for (int i = 1; i <= s1.length(); i++) {
for (int j = 1; j <= s2.length(); j++) {
dp[i][j] = (dp[i - 1][j] && s1.charAt(i - 1) == s3.charAt(i + j - 1)) ||
(dp[i][j - 1] && s2.charAt(j - 1) == s3.charAt(i + j - 1));
}
}
return dp[s1.length()][s2.length()];
}
}

最长公共前缀 (Longest Common Prefix)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class StringProblems {
/**
* 找出字符串数组中的最长公共前缀。
*
* @param strs 字符串数组
* @return 最长公共前缀
*/
public static String longestCommonPrefix(String[] strs) {
if (strs == null || strs.length == 0) {
return "";
}

String prefix = strs[0];
for (int i = 1; i < strs.length; i++) {
// 循环比较,直到找到子串
while (strs[i].indexOf(prefix) != 0) {
// 每次缩短 prefix
prefix = prefix.substring(0, prefix.length() - 1);
if (prefix.isEmpty()) {
return "";
}
}
}
return prefix;
}
}

字符串压缩 (String Compression)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class StringProblems {
/**
* 实现基本的字符串压缩,例如 aabcccccaaa 压缩为 a2b1c5a3。
* 如果压缩后的字符串没有变短,则返回原字符串。
*
* @param str 待压缩的字符串
* @return 压缩后的字符串
*/
public static String compressString(String str) {
if (str == null || str.isEmpty()) {
return "";
}

StringBuilder compressed = new StringBuilder();
int count = 1;

for (int i = 1; i <= str.length(); i++) {
if (i < str.length() && str.charAt(i) == str.charAt(i - 1)) {
count++;
} else {
compressed.append(str.charAt(i - 1)).append(count);
count = 1;
}
}

if (compressed.length() >= str.length()) {
return str; // 如果压缩后没有变短,返回原字符串
}
return compressed.toString();
}
}

寻找重复数 (Find the Duplicate Number)

给定一个包含 n+1 个整数的数组,数字都在 1n 的范围内。找出这个重复的数字。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
class ArrayProblems {
/**
* 使用**快慢指针**(类似于检测链表环)来寻找重复数。
* 假设数组是一个链表,索引 `i` 指向 `nums[i]`
*
* @param nums 包含 n+1 个整数的数组
* @return 重复的数字
*/
public static int findDuplicate(int[] nums) {
// 1. 寻找环的入口
int slow = nums[0];
int fast = nums[nums[0]];
while (slow != fast) {
slow = nums[slow];
fast = nums[nums[fast]];
}

// 2. 找到环后,从头开始,快慢指针以相同速度前进,相遇点即为环的入口(重复数)
int p1 = nums[0];
int p2 = slow;
while (p1 != p2) {
p1 = nums[p1];
p2 = nums[p2];
}
return p1;
}
}

链表、栈与队列

合并两个有序链表

使用双指针方法,创建一个新的链表,遍历两个输入链表,比较节点值并依次添加到新链表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public class ListNode {
int val;
ListNode next;
ListNode(int x) { val = x; }
}

public class LinkedListProblems {
public static ListNode mergeTwoLists(ListNode l1, ListNode l2) {
// 创建一个哑节点,简化操作
ListNode dummy = new ListNode(0);
ListNode current = dummy;

while (l1 != null && l2 != null) {
if (l1.val <= l2.val) {
current.next = l1;
l1 = l1.next;
} else {
current.next = l2;
l2 = l2.next;
}
current = current.next;
}

// 将剩余的节点添加到新链表末尾
if (l1 != null) {
current.next = l1;
}
if (l2 != null) {
current.next = l2;
}

return dummy.next;
}
}

链表倒数第 k 个节点

使用快慢指针,只遍历一次。快指针先走 k 步,然后快慢指针同时前进,当快指针到达末尾时,慢指针正好在倒数第 k 个位置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
public class LinkedListProblems {
public static ListNode findKthFromEnd(ListNode head, int k) {
if (head == null || k <= 0) {
return null;
}

ListNode fast = head;
ListNode slow = head;

// 快指针先走 k 步
for (int i = 0; i < k; i++) {
if (fast == null) {
return null; // 链表长度小于 k
}
fast = fast.next;
}

// 快慢指针同时前进,直到快指针到达末尾
while (fast != null) {
fast = fast.next;
slow = slow.next;
}

return slow;
}
}

删除重复节点

给定一个已排序的链表,删除所有重复的节点,使得每个元素只出现一次。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class LinkedListProblems {
public static ListNode deleteDuplicates(ListNode head) {
if (head == null) {
return null;
}

ListNode current = head;
while (current != null && current.next != null) {
if (current.val == current.next.val) {
current.next = current.next.next; // 删除下一个重复节点
} else {
current = current.next; // 前进到下一个不重复的节点
}
}
return head;
}
}

判断链表是否有环

使用快慢指针。如果链表有环,快指针最终会追上慢指针。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class LinkedListProblems {
public static boolean hasCycle(ListNode head) {
if (head == null || head.next == null) {
return false;
}

ListNode slow = head;
ListNode fast = head.next;

while (slow != fast) {
if (fast == null || fast.next == null) {
return false; // 已经到达末尾,没有环
}
slow = slow.next;
fast = fast.next.next;
}
return true;
}
}

链表求和

两个非负整数由链表表示,每个节点包含一个数字。计算它们的和。这里假设链表按逆序存储数字(个位在前)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class LinkedListProblems {
public static ListNode addTwoNumbers(ListNode l1, ListNode l2) {
ListNode dummy = new ListNode(0);
ListNode current = dummy;
int carry = 0; // 进位

while (l1 != null || l2 != null || carry > 0) {
int sum = carry;
if (l1 != null) {
sum += l1.val;
l1 = l1.next;
}
if (l2 != null) {
sum += l2.val;
l2 = l2.next;
}

carry = sum / 10;
current.next = new ListNode(sum % 10);
current = current.next;
}

return dummy.next;
}
}

栈实现队列

使用两个栈,一个用于入队 (inStack),一个用于出队 (outStack)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
import java.util.Stack;

class MyQueue {
private Stack<Integer> inStack;
private Stack<Integer> outStack;

public MyQueue() {
inStack = new Stack<>();
outStack = new Stack<>();
}

public void push(int x) {
inStack.push(x);
}

public int pop() {
if (outStack.isEmpty()) {
transfer();
}
return outStack.pop();
}

public int peek() {
if (outStack.isEmpty()) {
transfer();
}
return outStack.peek();
}

private void transfer() {
while (!inStack.isEmpty()) {
outStack.push(inStack.pop());
}
}

public boolean empty() {
return inStack.isEmpty() && outStack.isEmpty();
}
}

队列实现栈

使用两个队列,一个用于入栈 (q1),一个用于辅助 (q2)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.LinkedList;
import java.util.Queue;

class MyStack {
private Queue<Integer> q1;
private Queue<Integer> q2;

public MyStack() {
q1 = new LinkedList<>();
q2 = new LinkedList<>();
}

public void push(int x) {
q1.offer(x);
}

public int pop() {
while (q1.size() > 1) {
q2.offer(q1.poll());
}
int result = q1.poll();
Queue<Integer> temp = q1;
q1 = q2;
q2 = temp;
return result;
}

public int top() {
int result = pop();
q1.offer(result);
return result;
}

public boolean empty() {
return q1.isEmpty();
}
}

有效括号

使用一个来检查括号是否匹配。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.Stack;

public class StackProblems {
public static boolean isValid(String s) {
Stack<Character> stack = new Stack<>();
for (char c : s.toCharArray()) {
if (c == '(' || c == '[' || c == '{') {
stack.push(c);
} else {
if (stack.isEmpty()) {
return false;
}
char top = stack.pop();
if ((c == ')' && top != '(') ||
(c == ']' && top != '[') ||
(c == '}' && top != '{')) {
return false;
}
}
}
return stack.isEmpty();
}
}

树与递归

定义一个简单的二叉树节点类:

1
2
3
4
5
6
public class TreeNode {
int val;
TreeNode left;
TreeNode right;
TreeNode(int x) { val = x; }
}

二叉树前序遍历 (Preorder Traversal)

递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class TreeTraversal {
public static List<Integer> preorderTraversalRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
preorder(root, result);
return result;
}

private static void preorder(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
list.add(node.val);
preorder(node.left, list);
preorder(node.right, list);
}
}

非递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class TreeTraversal {
public static List<Integer> preorderTraversalIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);

while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val);
// 先压入右子节点,再压入左子节点,保证弹出时先处理左子节点
if (node.right != null) {
stack.push(node.right);
}
if (node.left != null) {
stack.push(node.left);
}
}
return result;
}
}

二叉树中序遍历 (Inorder Traversal)

递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class TreeTraversal {
public static List<Integer> inorderTraversalRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
inorder(root, result);
return result;
}

private static void inorder(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
inorder(node.left, list);
list.add(node.val);
inorder(node.right, list);
}
}

非递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import java.util.ArrayList;
import java.util.List;
import java.util.Stack;

public class TreeTraversal {
public static List<Integer> inorderTraversalIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
Stack<TreeNode> stack = new Stack<>();
TreeNode current = root;

while (current != null || !stack.isEmpty()) {
// 将所有左子节点压入栈
while (current != null) {
stack.push(current);
current = current.left;
}
// 弹出栈顶节点,并处理其右子树
current = stack.pop();
result.add(current.val);
current = current.right;
}
return result;
}
}

二叉树后序遍历 (Postorder Traversal)

递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class TreeTraversal {
public static List<Integer> postorderTraversalRecursive(TreeNode root) {
List<Integer> result = new ArrayList<>();
postorder(root, result);
return result;
}

private static void postorder(TreeNode node, List<Integer> list) {
if (node == null) {
return;
}
postorder(node.left, list);
postorder(node.right, list);
list.add(node.val);
}
}

非递归方式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.Stack;

public class TreeTraversal {
public static List<Integer> postorderTraversalIterative(TreeNode root) {
List<Integer> result = new ArrayList<>();
if (root == null) {
return result;
}
Stack<TreeNode> stack = new Stack<>();
stack.push(root);

while (!stack.isEmpty()) {
TreeNode node = stack.pop();
result.add(node.val); // 倒序插入
if (node.left != null) {
stack.push(node.left);
}
if (node.right != null) {
stack.push(node.right);
}
}
Collections.reverse(result); // 将结果反转
return result;
}
}

二叉树层序遍历 (Level Order Traversal)

使用队列实现。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;
import java.util.Queue;

public class TreeTraversal {
public static List<List<Integer>> levelOrder(TreeNode root) {
List<List<Integer>> result = new ArrayList<>();
if (root == null) {
return result;
}

Queue<TreeNode> queue = new LinkedList<>();
queue.offer(root);

while (!queue.isEmpty()) {
int levelSize = queue.size();
List<Integer> currentLevel = new ArrayList<>();
for (int i = 0; i < levelSize; i++) {
TreeNode node = queue.poll();
currentLevel.add(node.val);

if (node.left != null) {
queue.offer(node.left);
}
if (node.right != null) {
queue.offer(node.right);
}
}
result.add(currentLevel);
}
return result;
}
}

对称二叉树 (Symmetric Tree)

使用递归,检查根节点的左右子树是否是镜像对称的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class TreeProblems {
public static boolean isSymmetric(TreeNode root) {
if (root == null) {
return true;
}
return isMirror(root.left, root.right);
}

private static boolean isMirror(TreeNode t1, TreeNode t2) {
if (t1 == null && t2 == null) {
return true;
}
if (t1 == null || t2 == null) {
return false;
}
return (t1.val == t2.val) && isMirror(t1.right, t2.left) && isMirror(t1.left, t2.right);
}
}

最大深度 (Maximum Depth of Binary Tree)

使用递归(深度优先搜索)。

1
2
3
4
5
6
7
8
9
10
public class TreeProblems {
public static int maxDepth(TreeNode root) {
if (root == null) {
return 0;
}
int leftDepth = maxDepth(root.left);
int rightDepth = maxDepth(root.right);
return Math.max(leftDepth, rightDepth) + 1;
}
}

验证二叉搜索树 (Validate Binary Search Tree)

使用递归中序遍历。二叉搜索树的中序遍历结果是升序的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class TreeProblems {
public static boolean isValidBST(TreeNode root) {
return isValidBST(root, Long.MIN_VALUE, Long.MAX_VALUE);
}

private static boolean isValidBST(TreeNode node, long min, long max) {
if (node == null) {
return true;
}
// 检查当前节点的值是否在有效范围内
if (node.val <= min || node.val >= max) {
return false;
}
// 递归检查左右子树,并缩小有效范围
return isValidBST(node.left, min, node.val) && isValidBST(node.right, node.val, max);
}
}

其他

最大子数组和 (Maximum Subarray Sum)

使用动态规划 (Kadane’s algorithm)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayProblems {
public static int maxSubArray(int[] nums) {
if (nums == null || nums.length == 0) {
return 0;
}

int maxSoFar = nums[0]; // 迄今为止的最大和
int maxEndingHere = nums[0]; // 以当前位置结尾的最大和

for (int i = 1; i < nums.length; i++) {
// maxEndingHere 的值是:(当前元素) 或 (当前元素 + 之前的最大和)
maxEndingHere = Math.max(nums[i], maxEndingHere + nums[i]);
maxSoFar = Math.max(maxSoFar, maxEndingHere);
}
return maxSoFar;
}
}

两数相加 (Large Number Addition)

将两个超长正整数字符串相加,模拟小学加法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class StringProblems {
public static String addStrings(String num1, String num2) {
StringBuilder sb = new StringBuilder();
int i = num1.length() - 1;
int j = num2.length() - 1;
int carry = 0;

while (i >= 0 || j >= 0 || carry > 0) {
int digit1 = (i >= 0) ? num1.charAt(i--) - '0' : 0;
int digit2 = (j >= 0) ? num2.charAt(j--) - '0' : 0;

int sum = digit1 + digit2 + carry;
sb.append(sum % 10);
carry = sum / 10;
}
return sb.reverse().toString();
}
}

位运算实现加法 (Addition with Bitwise Operators)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class MathProblems {
public static int add(int a, int b) {
int sum;
int carry;

while (b != 0) {
// 计算无进位的和(异或)
sum = a ^ b;
// 计算进位(与&左移)
carry = (a & b) << 1;
a = sum;
b = carry;
}
return a;
}
}

全排列 (Permutations)

使用回溯算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.ArrayList;
import java.util.List;

public class BacktrackingProblems {
public static List<List<Integer>> permute(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrackPermute(result, new ArrayList<>(), nums, new boolean[nums.length]);
return result;
}

private static void backtrackPermute(List<List<Integer>> result, List<Integer> tempList, int[] nums, boolean[] used) {
if (tempList.size() == nums.length) {
result.add(new ArrayList<>(tempList));
return;
}
for (int i = 0; i < nums.length; i++) {
if (used[i]) {
continue;
}
used[i] = true;
tempList.add(nums[i]);
backtrackPermute(result, tempList, nums, used);
tempList.remove(tempList.size() - 1);
used[i] = false;
}
}
}

子集 (Subsets)

使用回溯算法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import java.util.ArrayList;
import java.util.List;

public class BacktrackingProblems {
public static List<List<Integer>> subsets(int[] nums) {
List<List<Integer>> result = new ArrayList<>();
backtrackSubsets(result, new ArrayList<>(), nums, 0);
return result;
}

private static void backtrackSubsets(List<List<Integer>> result, List<Integer> tempList, int[] nums, int start) {
result.add(new ArrayList<>(tempList));
for (int i = start; i < nums.length; i++) {
tempList.add(nums[i]);
backtrackSubsets(result, tempList, nums, i + 1);
tempList.remove(tempList.size() - 1);
}
}
}

跳台阶 (Climbing Stairs)

这是经典的动态规划问题,与斐波那契数列类似。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class MathProblems {
public static int climbStairs(int n) {
if (n <= 2) {
return n;
}
int[] dp = new int[n + 1];
dp[1] = 1;
dp[2] = 2;
for (int i = 3; i <= n; i++) {
dp[i] = dp[i - 1] + dp[i - 2];
}
return dp[n];
}
}

实现一个简单的 Trie (前缀树)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
import java.util.HashMap;
import java.util.Map;

class TrieNode {
Map<Character, TrieNode> children;
boolean isEndOfWord;

public TrieNode() {
children = new HashMap<>();
isEndOfWord = false;
}
}

class Trie {
private TrieNode root;

public Trie() {
root = new TrieNode();
}

public void insert(String word) {
TrieNode current = root;
for (char c : word.toCharArray()) {
current = current.children.computeIfAbsent(c, k -> new TrieNode());
}
current.isEndOfWord = true;
}

public boolean search(String word) {
TrieNode current = root;
for (char c : word.toCharArray()) {
if (!current.children.containsKey(c)) {
return false;
}
current = current.children.get(c);
}
return current.isEndOfWord;
}

public boolean startsWith(String prefix) {
TrieNode current = root;
for (char c : prefix.toCharArray()) {
if (!current.children.containsKey(c)) {
return false;
}
current = current.children.get(c);
}
return true;
}
}

反转链表 II (Reverse Linked List II)

反转链表从位置 mn 的部分。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
public class LinkedListProblems {
public static ListNode reverseBetween(ListNode head, int m, int n) {
ListNode dummy = new ListNode(0);
dummy.next = head;
ListNode pre = dummy;

for (int i = 0; i < m - 1; i++) {
pre = pre.next; // 移动到反转部分的前一个节点
}

ListNode start = pre.next; // 反转部分的起始节点
ListNode then = start.next; // 待反转的第二个节点

for (int i = 0; i < n - m; i++) {
start.next = then.next;
then.next = pre.next;
pre.next = then;
then = start.next;
}

return dummy.next;
}
}

LRU 缓存 (LRU Cache)

使用 LinkedHashMap 可以非常方便地实现 LRU 缓存,因为它本身就维护了插入顺序或访问顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import java.util.LinkedHashMap;
import java.util.Map;

class LRUCache<K, V> extends LinkedHashMap<K, V> {
private final int capacity;

public LRUCache(int capacity) {
super(capacity, 0.75f, true); // true 表示按访问顺序排序
this.capacity = capacity;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
// 当容量超过限制时,自动移除最老的(最久未访问的)条目
return size() > capacity;
}

public V get(K key) {
return super.getOrDefault(key, null);
}

public void put(K key, V value) {
super.put(key, value);
}
}

判断回文链表 (Palindrome Linked List)

可以使用快慢指针找到中点,然后反转后半部分,最后比较两部分是否相等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class LinkedListProblems {
public static boolean isPalindrome(ListNode head) {
if (head == null || head.next == null) {
return true;
}

// 1. 快慢指针找中点
ListNode slow = head;
ListNode fast = head;
while (fast != null && fast.next != null) {
slow = slow.next;
fast = fast.next.next;
}

// 2. 反转后半部分
ListNode secondHalf = reverseList(slow);

// 3. 比较前半部分和反转后的后半部分
ListNode firstHalf = head;
while (secondHalf != null) {
if (firstHalf.val != secondHalf.val) {
return false;
}
firstHalf = firstHalf.next;
secondHalf = secondHalf.next;
}
return true;
}

private static ListNode reverseList(ListNode head) {
ListNode prev = null;
ListNode current = head;
while (current != null) {
ListNode nextTemp = current.next;
current.next = prev;
prev = current;
current = nextTemp;
}
return prev;
}
}

ReadWriteLock

ReadWriteLock 允许多个线程同时进行读操作,但只允许一个线程进行写操作,从而提高读多写少的场景下的并发性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

class SharedResource {
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
private String data = "初始数据";

// 读操作:允许多个线程同时访问
public String readData() {
rwLock.readLock().lock(); // 获取读锁
try {
System.out.println(Thread.currentThread().getName() + " 正在读取数据...");
Thread.sleep(100); // 模拟耗时操作
return data;
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return null;
} finally {
rwLock.readLock().unlock(); // 释放读锁
}
}

// 写操作:同一时刻只能有一个线程访问
public void writeData(String newData) {
rwLock.writeLock().lock(); // 获取写锁
try {
System.out.println(Thread.currentThread().getName() + " 正在写入数据...");
Thread.sleep(1000); // 模拟耗时操作
this.data = newData;
System.out.println(Thread.currentThread().getName() + " 写入完成。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
rwLock.writeLock().unlock(); // 释放写锁
}
}
}

CountDownLatch

CountDownLatch 允许一个线程等待,直到其他线程都完成了某项工作。它就像一个倒计时器,一旦计数为零,等待的线程就会被唤醒。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
final int workerCount = 5;
// 创建一个计数器,初始值为 5
CountDownLatch latch = new CountDownLatch(workerCount);
ExecutorService executor = Executors.newFixedThreadPool(workerCount);

System.out.println("主线程:启动 " + workerCount + " 个工作线程...");
for (int i = 0; i < workerCount; i++) {
final int workerId = i;
executor.execute(() -> {
System.out.println("工作线程 " + workerId + " 开始执行任务...");
try {
Thread.sleep((long) (Math.random() * 2000)); // 模拟任务执行
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
System.out.println("工作线程 " + workerId + " 任务完成。");
latch.countDown(); // 任务完成后,计数器减 1
}
});
}

System.out.println("主线程:等待所有工作线程完成...");
latch.await(); // 阻塞主线程,直到计数器为 0
System.out.println("主线程:所有工作线程已完成,继续执行下一步。");

executor.shutdown();
}
}

CyclicBarrier

CyclicBarrier 允许一组线程在到达一个共同的屏障点后,再一起继续执行。它就像赛跑的起跑线,所有选手都准备好后,发令枪才响。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class CyclicBarrierDemo {
public static void main(String[] args) {
final int runnerCount = 3;
// 创建一个屏障,当 3 个线程都到达时,执行一个屏障动作
CyclicBarrier barrier = new CyclicBarrier(runnerCount, () -> {
System.out.println("\n所有赛跑者都已就位,发令枪响!\n");
});
ExecutorService executor = Executors.newFixedThreadPool(runnerCount);

for (int i = 0; i < runnerCount; i++) {
final int runnerId = i;
executor.execute(() -> {
try {
System.out.println("赛跑者 " + runnerId + " 正在走向起跑线...");
Thread.sleep((long) (Math.random() * 3000)); // 模拟准备时间
System.out.println("赛跑者 " + runnerId + " 到达起跑线,准备就绪。");
barrier.await(); // 线程在此处等待,直到所有线程都到达

System.out.println("赛跑者 " + runnerId + " 开始跑步!");
} catch (InterruptedException | BrokenBarrierException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}

Semaphore

Semaphore(信号量)用来控制对某个资源的并发访问数量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import java.util.concurrent.Semaphore;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

class SemaphoreDemo {
// 停车场最多允许 3 辆车停放
private final Semaphore semaphore = new Semaphore(3);

public void park(int carId) {
try {
System.out.println("汽车 " + carId + " 正在寻找车位...");
semaphore.acquire(); // 获取一个许可
System.out.println("汽车 " + carId + " 成功进入停车场。");
Thread.sleep((long) (Math.random() * 3000)); // 模拟停车时间
System.out.println("汽车 " + carId + " 离开停车场。");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release(); // 释放许可
}
}

public static void main(String[] args) {
SemaphoreDemo demo = new SemaphoreDemo();
ExecutorService executor = Executors.newFixedThreadPool(10); // 10 辆车

for (int i = 0; i < 10; i++) {
final int carId = i;
executor.execute(() -> demo.park(carId));
}

executor.shutdown();
}
}

死锁预防 (按锁顺序)

通过资源有序分配来打破死锁的循环等待条件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
class DeadlockPrevention {
private static final Object lockA = new Object();
private static final Object lockB = new Object();

public void threadOne() {
synchronized (lockA) {
System.out.println("线程 1: 已获得 lockA,尝试获取 lockB...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lockB) {
System.out.println("线程 1: 已获得 lockB。");
}
}
}

public void threadTwo() {
// 两个线程都按相同的顺序(先 A 后 B)获取锁
synchronized (lockA) {
System.out.println("线程 2: 已获得 lockA,尝试获取 lockB...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
synchronized (lockB) {
System.out.println("线程 2: 已获得 lockB。");
}
}
}

public static void main(String[] args) {
DeadlockPrevention demo = new DeadlockPrevention();
new Thread(demo::threadOne, "Thread-1").start();
new Thread(demo::threadTwo, "Thread-2").start();
}
}

线程池异常

使用 Future.get() 或在任务中捕获异常来处理线程池中任务抛出的异常。execute() 方法无法直接捕获异常。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

class ThreadPoolExceptionDemo {
public static void main(String[] args) throws InterruptedException {
ExecutorService executor = Executors.newFixedThreadPool(2);

// 提交一个会抛出运行时异常的任务
Future<?> future = executor.submit(() -> {
System.out.println("任务开始执行...");
throw new RuntimeException("这是一个模拟的任务执行异常。");
});

// 尝试获取任务结果来捕获异常
try {
future.get();
} catch (ExecutionException e) {
System.err.println("捕获到任务执行异常:" + e.getCause().getMessage());
}

// 使用 execute() 提交任务,异常会被吞掉,除非自定义 UncaughtExceptionHandler
executor.execute(() -> {
System.out.println("另一个任务开始执行...");
throw new IllegalArgumentException("这个异常会被吞掉。");
});

executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
}

ThreadLocal

ThreadLocal 为每个线程提供了独立的变量副本,实现了数据隔离。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class ThreadLocalDemo {
private static final ThreadLocal<String> threadLocal = new ThreadLocal<>();

public void setAndPrint(String value) {
threadLocal.set(value); // 设置当前线程的变量副本
try {
Thread.sleep(1000); // 模拟任务
System.out.println(Thread.currentThread().getName() + " 的变量值: " + threadLocal.get());
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
threadLocal.remove(); // 最佳实践:使用完后移除,防止内存泄漏
}
}

public static void main(String[] args) {
ThreadLocalDemo demo = new ThreadLocalDemo();
new Thread(() -> demo.setAndPrint("线程 A 的数据"), "Thread-A").start();
new Thread(() -> demo.setAndPrint("线程 B 的数据"), "Thread-B").start();
}
}

LockSupport

LockSupport.park()LockSupport.unpark() 提供了更灵活的线程阻塞和唤醒机制,类似于 wait()notify(),但不需要依赖锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.locks.LockSupport;

class LockSupportDemo {
public static void main(String[] args) throws InterruptedException {
Thread workerThread = new Thread(() -> {
System.out.println("工作线程: 任务准备就绪,即将阻塞...");
LockSupport.park(); // 阻塞当前线程
System.out.println("工作线程: 被唤醒,继续执行。");
});

workerThread.start();
Thread.sleep(2000); // 确保工作线程已执行 park()

System.out.println("主线程: 唤醒工作线程。");
LockSupport.unpark(workerThread); // 唤醒指定线程
}
}

线程通信 (按顺序打印)

使用 wait()notifyAll() 实现三个线程按顺序打印。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
class SequentialPrinter {
private int state = 0; // 0: A, 1: B, 2: C
private final Object lock = new Object();

public void printA() {
printLetter("A", 0);
}

public void printB() {
printLetter("B", 1);
}

public void printC() {
printLetter("C", 2);
}

private void printLetter(String letter, int expectedState) {
for (int i = 0; i < 10; i++) {
synchronized (lock) {
while (state != expectedState) {
try {
lock.wait();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
return;
}
}
System.out.print(letter);
state = (state + 1) % 3; // 切换到下一个状态
lock.notifyAll();
}
}
}

public static void main(String[] args) throws InterruptedException {
SequentialPrinter printer = new SequentialPrinter();
Thread threadA = new Thread(printer::printA);
Thread threadB = new Thread(printer::printB);
Thread threadC = new Thread(printer::printC);

threadA.start();
threadB.start();
threadC.start();

threadA.join();
threadB.join();
threadC.join();
System.out.println("\n打印完成。");
}
}

线程池

ThreadPoolExecutor 的七个参数

  1. corePoolSize: 核心线程数。线程池中常驻的线程数量,即使空闲也不会被销毁。
  2. maximumPoolSize: 最大线程数。当工作队列已满,且任务量继续增加时,线程池可以创建的最大线程数。
  3. keepAliveTime: 空闲线程存活时间。当线程数大于 corePoolSize 时,非核心线程的空闲存活时间。
  4. unit: keepAliveTime 的时间单位。
  5. workQueue: 工作队列。用于存放等待执行的任务,常用的有 ArrayBlockingQueueLinkedBlockingQueue 等。
  6. threadFactory: 线程工厂。用于创建新线程,可以自定义线程的名称、优先级等。
  7. handler: 拒绝策略。当线程池和工作队列都已满时,用于处理新来的任务,例如抛出异常、由调用者执行等。

手写一个自定义线程池

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

public class CustomThreadPool {
public static void main(String[] args) {
// 自定义线程池
ExecutorService executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60L, // 空闲线程存活时间
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(10), // 工作队列容量为 10
Executors.defaultThreadFactory(), // 默认线程工厂
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:由调用者线程执行
);

for (int i = 0; i < 20; i++) {
final int taskId = i;
executor.execute(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行。");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
}
executor.shutdown();
}
}

原子类

使用 AtomicBooleanAtomicReference 解决并发问题。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import java.util.concurrent.atomic.AtomicBoolean;

class AtomicBooleanDemo {
// 确保只有一个线程执行初始化操作
private static final AtomicBoolean initialized = new AtomicBoolean(false);

public static void initialize() {
// 只有当 initialized 为 false 时,才将其设置为 true 并执行初始化
if (initialized.compareAndSet(false, true)) {
System.out.println(Thread.currentThread().getName() + ": 开始执行初始化操作...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println(Thread.currentThread().getName() + ": 初始化完成。");
} else {
System.out.println(Thread.currentThread().getName() + ": 初始化已被其他线程执行,跳过。");
}
}

public static void main(String[] args) {
for (int i = 0; i < 5; i++) {
new Thread(AtomicBooleanDemo::initialize, "Thread-" + i).start();
}
}
}

volatile 内存语义

volatile 确保了可见性有序性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class VolatileMemorySemantics {
// 1. 可见性:当一个线程修改了 ready 的值,其他线程能立即看到最新值
private static volatile boolean ready = false;
private static int number = 0;

public static class WriterThread extends Thread {
@Override
public void run() {
number = 42; // 修改 number
ready = true; // 修改 ready
}
}

public static class ReaderThread extends Thread {
@Override
public void run() {
while (!ready) {
// 等待 ready 变为 true
// 如果没有 volatile,这里可能会陷入死循环
}
// 2. 有序性:写 volatile 变量(ready = true)之前的操作(number = 42)
// 对其他线程都是可见的。保证了 number 的值是 42。
System.out.println("读取到的 number 值: " + number);
}
}

public static void main(String[] args) throws InterruptedException {
new WriterThread().start();
new ReaderThread().start();
}
}

中断机制

一个线程通过响应 interrupt() 调用来正确停止自身。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class InterruptibleThreadDemo {
public static void main(String[] args) {
Thread worker = new Thread(() -> {
while (!Thread.currentThread().isInterrupted()) { // 检查中断标志
try {
System.out.println("线程正在执行...");
// sleep()、wait() 等方法会抛出 InterruptedException 并清除中断标志
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("线程被中断,即将退出...");
// 重新设置中断标志,以便外层循环能正确退出
Thread.currentThread().interrupt();
// 或者直接 break 或 return 退出
break;
}
}
System.out.println("线程已优雅地退出。");
});

worker.start();
try {
Thread.sleep(3500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
System.out.println("主线程: 发送中断信号。");
worker.interrupt();
}
}

ConcurrentHashMap

ConcurrentHashMap 的并发原理是**分段锁( 7)**或 CAS + Synchronized( 8),只对操作的桶进行锁定,大大提高了并发性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class ConcurrentHashMapDemo {
public static void main(String[] args) throws InterruptedException {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
ExecutorService executor = Executors.newFixedThreadPool(10);

// 启动 10 个线程,每个线程向 map 中添加 1000 个键值对
for (int i = 0; i < 10; i++) {
final int threadId = i;
executor.execute(() -> {
for (int j = 0; j < 1000; j++) {
String key = "key-" + (threadId * 1000 + j);
map.put(key, j);
}
});
}
executor.shutdown();
executor.awaitTermination(1, TimeUnit.MINUTES);

System.out.println("最终 map 的大小: " + map.size()); // 期望值为 10000
}
}

ForkJoinPool

ForkJoinPool 是一个用于分治任务的线程池,RecursiveTask 是可返回结果的分治任务。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
import java.util.concurrent.ForkJoinPool;
import java.util.concurrent.RecursiveTask;

class SumTask extends RecursiveTask<Long> {
private final long[] array;
private final int start;
private final int end;
private static final int THRESHOLD = 10000; // 任务分解的阈值

public SumTask(long[] array, int start, int end) {
this.array = array;
this.start = start;
this.end = end;
}

@Override
protected Long compute() {
// 如果任务规模小于等于阈值,则直接计算
if (end - start <= THRESHOLD) {
long sum = 0;
for (int i = start; i < end; i++) {
sum += array[i];
}
return sum;
} else {
// 否则,将任务分解成两个子任务
int mid = start + (end - start) / 2;
SumTask leftTask = new SumTask(array, start, mid);
SumTask rightTask = new SumTask(array, mid, end);

leftTask.fork(); // 提交左子任务
long rightResult = rightTask.compute(); // 同步计算右子任务
long leftResult = leftTask.join(); // 等待左子任务结果

return leftResult + rightResult;
}
}

public static void main(String[] args) {
long[] array = new long[1000000];
for (int i = 0; i < array.length; i++) {
array[i] = i;
}

ForkJoinPool pool = new ForkJoinPool();
SumTask task = new SumTask(array, 0, array.length);
long result = pool.invoke(task);
System.out.println("大数组的和为: " + result);

pool.shutdown();
}
}

1. 语言基础

变量与数据类型

的基本数据类型决定了变量可以存储的数据范围。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
public class DataTypesDetailed {
public static void main(String[] args) {
// 整数类型:默认为 int
byte b = 100; // 占用 1 字节,-128 到 127
short s = 10000; // 占用 2 字节
int i = 100000; // 占用 4 字节
long l = 10000000000L; // 占用 8 字节,需要 L 或 l 后缀

// 浮点类型:默认为 double
float f = 3.14f; // 占用 4 字节,需要 f 或 F 后缀
double d = 3.1415926535; // 占用 8 字节

// 字符类型
char c1 = 'A'; // 单个字符,占用 2 字节
char c2 = 65; // 也可以使用 ASCII 码或 Unicode
System.out.println("c1 和 c2 是否相等? " + (c1 == c2));

// 布尔类型
boolean isFun = true;
System.out.println("学Java有趣吗?" + isFun);
}
}

类型转换

中,从小范围类型向大范围类型转换是自动的(隐式转换);从大范围向小范围转换需要强制转换(显式转换),可能造成数据丢失。

1
2
3
4
5
6
7
8
9
10
11
12
13
public class TypeCasting {
public static void main(String[] args) {
// 隐式转换:int -> long
int myInt = 100;
long myLong = myInt;
System.out.println("隐式转换后的 long 类型: " + myLong);

// 显式转换:double -> int
double myDouble = 9.99;
int myInteger = (int) myDouble; // 强制转换,小数部分被丢弃
System.out.println("显式转换后的 int 类型: " + myInteger); // 输出 9
}
}

数组

数组是存储固定大小同类型元素的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ArrayExample {
public static void main(String[] args) {
// 声明并初始化一个整数数组
int[] numbers = new int[5]; // 创建一个长度为 5 的数组
numbers[0] = 10;
numbers[1] = 20;

// 声明、初始化并赋值
String[] fruits = {"Apple", "Banana", "Cherry"};

// 遍历数组
System.out.println("所有水果:");
for (String fruit : fruits) { // 增强 for 循环
System.out.println(fruit);
}
}
}

2. 面向对象编程 (OOP)

构造方法与方法重载

构造方法是一种特殊方法,用于创建对象时初始化。方法重载是指在同一个类中,方法名相同但参数列表不同(参数类型、数量或顺序)的方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class Student {
String name;
int age;
String id;

// 无参构造方法
public Student() {
this("未知", 0, "000"); // 调用本类的三参构造方法
}

// 重载构造方法
public Student(String name, int age) {
this.name = name;
this.age = age;
this.id = "000";
}

// 重载构造方法
public Student(String name, int age, String id) {
this.name = name;
this.age = age;
this.id = id;
}

// 方法重载: 计算两个数的和
public int add(int a, int b) {
return a + b;
}

public double add(double a, double b) {
return a + b;
}
}

继承、多态与抽象

继承实现代码复用,多态实现行为多样化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
// 父类:抽象类
abstract class Vehicle {
protected String brand; // 子类可以访问

public Vehicle(String brand) {
this.brand = brand;
}

public abstract void run(); // 抽象方法,子类必须实现

public void displayBrand() {
System.out.println("品牌是: " + brand);
}
}

// 子类:继承 Vehicle
class Car extends Vehicle {
public Car(String brand) {
super(brand); // 调用父类的构造方法
}

@Override
public void run() {
System.out.println("汽车正在路上行驶...");
}
}

// 子类:继承 Vehicle
class Bicycle extends Vehicle {
public Bicycle(String brand) {
super(brand);
}

@Override
public void run() {
System.out.println("自行车正在骑行...");
}
}

public class PolymorphismDemo {
public static void main(String[] args) {
Vehicle myCar = new Car("BMW"); // 多态引用
Vehicle myBicycle = new Bicycle("Giant");

// 同一个方法调用,不同行为
myCar.run(); // 输出 "汽车正在路上行驶..."
myBicycle.run(); // 输出 "自行车正在骑行..."
}
}

3. 核心类库

好的,这次我们将把各种方法的使用代码直接嵌入到每个知识点的解释中,让您在学习概念的同时,就能看到具体的代码实现和效果。我们将专注于数组、字符串和集合这三大核心部分,把它们的创建、遍历、和各种常用方法的代码示例都清晰地展示出来。


1. 数组(Array)

数组是一种固定大小的、用于存储同类型元素的容器。

创建和遍历

这里展示两种最常见的创建数组的方式,并使用两种循环进行遍历。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;

public class ArrayExample {
public static void main(String[] args) {
// 方式一:声明并分配空间,系统赋默认值
int[] intArray = new int[3]; // 创建一个包含3个整数的数组,默认值都是0
intArray[0] = 10;
intArray[1] = 20;
intArray[2] = 30;

System.out.println("数组 intArray 的第一个元素是: " + intArray[0]); // 输出: 10

// 方式二:声明并直接初始化
String[] stringArray = {"Hello", "World", "Java"};

// 遍历方式一:使用 for 循环
System.out.println("\n--- 使用 for 循环遍历 ---");
for (int i = 0; i < stringArray.length; i++) {
System.out.println("stringArray[" + i + "] = " + stringArray[i]);
}

// 遍历方式二:使用增强 for 循环(更简洁)
System.out.println("\n--- 使用增强 for 循环遍历 ---");
for (String element : stringArray) {
System.out.println("元素: " + element);
}
}
}

Arrays 类的常用方法

java.util.Arrays 类提供了很多静态方法,方便我们对数组进行操作,比如排序、查找、复制等。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.Arrays;

public class ArraysMethodExample {
public static void main(String[] args) {
int[] numbers = {4, 2, 8, 1, 6};

// 1. 排序:Arrays.sort()
Arrays.sort(numbers);
System.out.println("排序后: " + Arrays.toString(numbers)); // 输出: [1, 2, 4, 6, 8]

// 2. 查找:Arrays.binarySearch() (必须先排序)
int index = Arrays.binarySearch(numbers, 6);
System.out.println("元素 6 的索引是: " + index); // 输出: 3

// 3. 填充:Arrays.fill()
int[] newArray = new int[5];
Arrays.fill(newArray, 99);
System.out.println("填充后: " + Arrays.toString(newArray)); // 输出: [99, 99, 99, 99, 99]

// 4. 复制:Arrays.copyOf()
int[] copiedArray = Arrays.copyOf(numbers, 3); // 复制前3个元素
System.out.println("复制前3个元素: " + Arrays.toString(copiedArray)); // 输出: [1, 2, 4]

// 5. 比较:Arrays.equals()
int[] anotherArray = {1, 2, 4, 6, 8};
System.out.println("两个数组是否相等: " + Arrays.equals(numbers, anotherArray)); // 输出: true
}
}

2. 字符串(String)

String 是一个不可变的字符序列,这意味着一旦创建,它的内容就不能被修改。所有修改操作都会返回一个新的 String 对象。

创建和常用方法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class StringExample {
public static void main(String[] args) {
// 创建方式
String str1 = " Java is a great language. "; // 字面量
String str2 = new String("Java is a great language."); // 使用 new 关键字

// 常用方法
// 1. 获取长度
int length = str1.length();
System.out.println("字符串长度: " + length); // 输出: 29

// 2. 访问字符
char firstChar = str1.charAt(2);
System.out.println("第3个字符是: " + firstChar); // 输出: J

// 3. 截取子串
String sub = str1.substring(5, 7);
System.out.println("截取子串: " + sub); // 输出: is

// 4. 查找位置
int index = str1.indexOf("great");
System.out.println("'great' 的索引: " + index); // 输出: 11

// 5. 判断
boolean contains = str1.contains("language");
System.out.println("是否包含 'language': " + contains); // 输出: true
boolean startsWith = str1.startsWith(" Java");
System.out.println("是否以 ' Java' 开头: " + startsWith); // 输出: true

// 6. 替换
String replacedStr = str1.replace("great", "wonderful");
System.out.println("替换后: " + replacedStr); // 输出: Java is a wonderful language.

// 7. 大小写转换和去空格
String trimmedStr = str1.trim();
System.out.println("去除首尾空格: '" + trimmedStr + "'"); // 输出: 'Java is a great language.'
System.out.println("转为大写: " + trimmedStr.toUpperCase());

// 8. 分割和连接
String data = "apple,banana,orange";
String[] fruits = data.split(",");
System.out.println("分割后: " + Arrays.toString(fruits)); // 输出: [apple, banana, orange]

String joinedString = String.join(" - ", fruits);
System.out.println("连接后: " + joinedString); // 输出: apple - banana - orange
}
}

StringBuilderStringBuffer

对于需要频繁修改字符串的场景,应使用 StringBuilderStringBuffer 以提高性能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class StringBuilderExample {
public static void main(String[] args) {
StringBuilder sb = new StringBuilder("Hello");

// 1. 追加内容
sb.append(" World");
System.out.println("追加后: " + sb); // 输出: Hello World

// 2. 插入内容
sb.insert(6, "Beautiful ");
System.out.println("插入后: " + sb); // 输出: Hello Beautiful World

// 3. 删除内容
sb.delete(6, 15);
System.out.println("删除后: " + sb); // 输出: Hello World
}
}

3. 集合(Collections)

Java 集合框架提供了强大的数据结构来存储和操作对象。

List(列表)

List 是一种有序、可重复的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import java.util.ArrayList;
import java.util.LinkedList;
import java.util.List;

public class ListExample {
public static void main(String[] args) {
// 创建 ArrayList(查询快)
List<String> fruits = new ArrayList<>();
fruits.add("Apple"); // 添加元素
fruits.add("Banana");
fruits.add("Apple"); // 允许重复

System.out.println("列表元素: " + fruits); // 输出: [Apple, Banana, Apple]
System.out.println("第一个元素: " + fruits.get(0)); // 获取元素
System.out.println("列表大小: " + fruits.size());

fruits.set(1, "Grape"); // 替换第二个元素
System.out.println("替换后: " + fruits); // 输出: [Apple, Grape, Apple]

fruits.remove(1); // 删除第二个元素
System.out.println("删除后: " + fruits); // 输出: [Apple, Apple]
}
}

Set(集)

Set 是一种无序、不可重复的集合。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import java.util.HashSet;
import java.util.Set;

public class SetExample {
public static void main(String[] args) {
// 创建 HashSet
Set<String> uniqueFruits = new HashSet<>();
uniqueFruits.add("Apple");
uniqueFruits.add("Banana");
uniqueFruits.add("Apple"); // 添加重复元素,会失败

System.out.println("集合元素: " + uniqueFruits); // 输出: [Apple, Banana] (顺序不定)

boolean containsBanana = uniqueFruits.contains("Banana");
System.out.println("是否包含 'Banana': " + containsBanana); // 输出: true

uniqueFruits.remove("Banana");
System.out.println("删除后: " + uniqueFruits); // 输出: [Apple]
}
}

Map(映射)

Map 存储键值对,键是唯一的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.util.HashMap;
import java.util.Map;

public class MapExample {
public static void main(String[] args) {
// 创建 HashMap
Map<String, Integer> studentScores = new HashMap<>();
studentScores.put("Alice", 95); // 添加键值对
studentScores.put("Bob", 88);
studentScores.put("Alice", 100); // 键已存在,会覆盖旧值

System.out.println("Alice 的分数: " + studentScores.get("Alice")); // 输出: 100

// 遍历 Map 的三种方式
// 方式一:遍历键集
System.out.println("\n--- 遍历键集 ---");
for (String name : studentScores.keySet()) {
System.out.println("姓名: " + name + ", 分数: " + studentScores.get(name));
}

// 方式二:遍历值集
System.out.println("\n--- 遍历值集 ---");
for (Integer score : studentScores.values()) {
System.out.println("分数: " + score);
}

// 方式三:遍历键值对集 (Entry Set),最常用且高效
System.out.println("\n--- 遍历键值对集 ---");
for (Map.Entry<String, Integer> entry : studentScores.entrySet()) {
System.out.println("姓名: " + entry.getKey() + ", 分数: " + entry.getValue());
}
}
}

基础背诵

网络基础

OSI七层模型

OSI (Open Systems Interconnection) 参考模型是一个抽象的、概念性的框架,旨在定义网络通信的功能分层,促进不同系统之间的互操作性。它将网络通信过程分为七个独立的层次,每层负责特定的功能。数据在发送端从上层向下层传输,每层添加自己的控制信息(封装),在接收端则从下层向上层传输,每层剥离对应的控制信息(解封装)。

  1. 物理层 (Physical Layer)

  • 功能:负责传输比特流,即原始的二进制数据。定义了物理媒介的电气特性、机械特性、功能特性和过程特性。
  • 工作方式:不关心比特的含义,只负责将其从一个节点传输到另一个节点。例如,规定网络的接口形状、电压、传输速率等。
  • 协议/设备:网线 (Ethernet Cable)、光纤 (Fiber Optic)、集线器 (Hub)、中继器 (Repeater)、网卡 (NIC) 的物理部分。
  • 数据单位:比特 (Bit)。
  • 功能:在物理层提供的不可靠比特流服务的基础上,将比特组合成帧,提供节点到节点之间的数据传输。负责差错控制(检测和纠正帧传输错误)、流量控制(防止发送方传输速度过快)和物理寻址 (MAC 地址)。
  • 子层:通常分为 LLC (Logical Link Control) 子层(负责逻辑链路控制,向上层提供服务接口)和 MAC (Media Access Control) 子层(负责媒体访问控制,处理共享介质的访问,例如 CSMA/CD 协议)。
  • 工作方式:在局域网内,通过 MAC 地址识别不同的设备,确保数据能够正确地在相邻节点之间传输。
  • 协议/设备:Ethernet、PPP (Point-to-Point Protocol)、交换机 (Switch)、网桥 (Bridge)。
  • 数据单位:帧 (Frame)。
  1. 网络层 (Network Layer)

  • 功能:负责将数据包从源主机传输到目标主机,可能需要跨越多个网络(即路由)。主要功能包括逻辑寻址 (IP 地址)、路由选择(确定数据包传输路径)和拥塞控制。
  • 工作方式:根据数据包中的 IP 地址,通过路由表决定数据包的转发路径,使其能够跨越不同的网络到达最终目的地。
  • 协议/设备:IP (Internet Protocol)、ICMP (Internet Control Message Protocol,用于错误报告和查询)、IGMP (Internet Group Management Protocol,用于多播组管理)、路由器 (Router)。
  • 数据单位:数据包 (Packet) 或 数据报 (Datagram)。
  1. 传输层 (Transport Layer)

  • 功能:负责端到端 (End-to-End) 的数据传输,即从源主机上的某个进程到目标主机上的某个进程的通信。提供分段与重组、错误检测、流量控制和拥塞控制。
  • 工作方式:通过端口号来标识应用进程,使得数据能够正确地交付给目标主机上的特定应用程序。
  • 协议/设备:
    • TCP (Transmission Control Protocol):面向连接、可靠的、基于字节流的服务,提供顺序、无损的数据传输。
    • UDP (User Datagram Protocol):无连接、不可靠的数据报服务,速度快,开销小,适用于对实时性要求高但允许少量丢包的应用(如音视频传输)。
  • 数据单位:报文段 (Segment) (TCP) 或 用户数据报 (Datagram) (UDP)。
  1. 会话层 (Session Layer)

  • 功能:负责管理和协调应用程序之间的会话(对话)。包括建立、管理和终止会话,以及数据同步(如设置同步点,在网络故障时从上一个同步点恢复)。
  • 工作方式:提供了一种机制,使得应用程序可以有条不紊地进行通信,例如,在一次文件传输中,可以在传输中断后从断点续传。
  • 协议/设备:NetBIOS, RPC (Remote Procedure Call), SQL, NFS。
  • 数据单位:会话数据 (Session Data)。
  1. 表示层 (Presentation Layer)

  • 功能:负责处理两个系统之间的数据表示,确保应用程序层接收到的信息是可用的。包括数据格式化、数据加密/解密、数据压缩/解压缩。
  • 工作方式:将应用层的数据转换为网络标准格式(如 ASCII 转 EBCDIC),或将网络格式的数据转换为应用层可用的格式。例如,JPEG, MPEG 等图像视频格式的处理,以及 SSL/TLS 加密解密。
  • 协议/设备:JPEG, MPEG, ASCII, Unicode, TLS/SSL (通常认为 TLS/SSL 位于会话层与表示层之间,或独立于此)。
  • 数据单位:表示数据 (Presentation Data)。
  1. 应用层 (Application Layer)

  • 功能:最靠近用户的一层,为用户提供网络服务。直接与用户应用程序进行交互,负责处理特定的应用程序细节。
  • 工作方式:定义了应用程序如何访问网络服务,以及不同应用程序之间如何交换数据。
  • 协议/设备:HTTP (Web 浏览)、FTP (文件传输)、SMTP (电子邮件发送)、POP3/IMAP (电子邮件接收)、DNS (域名解析)、Telnet, SNMP (简单网络管理协议)等。
  • 数据单位:应用数据 (Application Data)。

TCP/IP 协议族

TCP/IP 模型是互联网的基础,它简化了 OSI 模型,通常分为四层:应用层、传输层、网络层、网络接口层(数据链路层和物理层合并)。我重点阐述其中的核心协议:

  • TCP(传输控制协议):我理解 TCP 是一种可靠的、面向连接的、基于字节流的传输协议。其可靠性通过以下机制实现:
    • 三次握手:这是 TCP 建立连接的过程,旨在确保通信双方都能正常收发数据,并协商初始序列号。这个过程就像打电话:

      1. 第一次握手 (SYN):
      • 发起方:客户端。
      • 报文类型:SYN 报文段 (SYN_FLAG=1)。SYN 标志位表示请求建立连接。
      • 序列号 (Sequence Number):客户端随机生成一个初始序列号 client_ISN。这个序列号代表客户端接下来发送数据的第一个字节的序号。
      • 确认号 (Acknowledgment Number):0(或随机值,但在此次请求中无实际意义,因为是首次请求,尚未收到对方的数据)。
      • 窗口大小 (Window Size):客户端告知服务器自己的接收窗口大小 W1,用于流量控制。
      • 客户端状态变化:客户端从 CLOSED 状态转换到 SYN_SENT 状态。
      • 作用:客户端向服务器发送连接请求,并告知服务器自己发送数据时将使用的起始序列号 client_ISN。此时客户端进入同步发送状态,等待服务器确认。
      1. 第二次握手 (SYN-ACK):
      • 发起方:服务器。
      • 报文类型:SYN-ACK 报文段 (SYN_FLAG=1, ACK_FLAG=1)。SYN 标志位表示同意建立连接并发送自己的初始序列号,ACK 标志位表示确认收到客户端的请求。
      • 序列号 (Sequence Number):服务器随机生成一个初始序列号 server_ISN。这个序列号代表服务器接下来发送数据的第一个字节的序号。
      • 确认号 (Acknowledgment Number):服务器发送 ack = client_ISN + 1。这表示服务器已成功接收到客户端的 SYN 报文(其序列号为 client_ISN),并期望接收客户端的下一个字节的序列号是 client_ISN + 1。
      • 窗口大小 (Window Size):服务器告知客户端自己的接收窗口大小 W2,用于流量控制。
      • 服务器状态变化:服务器收到客户端的 SYN 后,从 LISTEN 状态转换到 SYN_RCVD 状态。发送 SYN-ACK 后,服务器仍然处于 SYN_RCVD 状态。
      • 作用:服务器确认已收到客户端端的 SYN 请求,并同意建立连接,同时告知客户端自己发送数据时将使用的起始序列号 server_ISN,并确认收到了客户端的 SYN 报文。
      1. 第三次握手(ACK):
      • 发起方:客户端。
      • 报文类型:ACK 报文段(ACK_FLAG=1)。ACK 标志位表示确认收到服务器的响应。
      • 序列号(Sequence Number):客户端发送 seq = client_ISN + 1。这表示客户端的 SYN 报文已经消耗了一个序列号,现在数据将从 client_ISN + 1 开始发送。
      • 确认号(Acknowledgment Number):客户端发送 ack = server_ISN + 1。这表示客户端已成功接收到服务器的 SYN-ACK 报文(其序列号为 server_ISN),并期望接收服务器的下一个字节的序列号是 server_ISN + 1。
      • 客户端状态变化:客户端收到服务器的 SYN-ACK 后,从 SYN_SENT 状态转换到 ESTABLISHED 状态。
      • 服务器状态变化:服务器收到客户端的 ACK 后,从 SYN_RCVD 状态转换到 ESTABLISHED 状态。
      • 作用:客户端确认已收到服务器的 SYN-ACK,并告知服务器已经准备好发送和接收数据。至此,客户端和服务器都确认了对方的收发能力,以及彼此的初始序列号。连接正式建立,可以开始数据传输。
    • 为什么需要三次握手?

      • 防止已失效的连接请求报文段突然又传到服务器,导致错误建立连接。如果只有两次握手,客户端发送的第一个连接请求(SYN报文)因网络延迟迟迟未到服务器,客户端随时重传并成功建立了连接并传输了数据。旧的SYN报文在某个时刻到达了服务器,服务器收到后会以为是新的连接请求,发送SYN-ACK给客户端。如果只有两次握手,服务器会认为连接建立成功并进入ESTABLISHED状态,但客户端已经处理完之前的连接并关闭,不会理会服务器。这会导致服务器一直等待客户端发送数据,浪费资源。三次握手确保客户端能识别出这是旧的SYN,从而不发送ACK,服务器也就不会进入ESTABLISHED。
      • 确保双方都具备发送和接收能力。
        • 第一次握手:客户端发送 SYN,服务器收到。服务器能确认客户端能发。
        • 第二次握手:服务器发送 SYN-ACK,客户端收到。客户端能确认服务器能收也能发。
        • 第三次握手:客户端发送 ACK,服务器收到。服务器能确认客户端能收。
        • 至此,双方都确认了对方的“发”和“收”能力,实现了双向的同步确认。
    • 四次握手:这是 TCP 终止连接的过程,目的是确保全双工连接的两个方向都能独立、优雅地关闭,避免数据丢失。由于 TCP 是全双工的,每个方向的数据传输都需要独立关闭。

      • 全双工特性:TCP 连接是全双工的,这意味着数据可以在两个方向上独立传输。当一方完成数据发送时,另一方可能仍然有数据需要发送。

      • 假设客户端主动关闭连接:

        1. 第一次挥手(FIN):
        • 发起方:客户端(数据发送完毕,想关闭发送方向)。
        • 报文类型:FIN 报文段(FIN_FLAG=1, ACK_FLAG=1)。FIN 标志位表示发送方没有数据要发送了。
        • 序列号(Sequence Number):seq = u(其中 u 是客户端上次发送的最后一个字节的序列号加 1)。
        • 确认号(Acknowledgment Number):ack = v(如果之前收到了服务器的数据,这是对服务器最近一次发送数据包的确认号)。
        • 客户端状态变化:客户端从 ESTABLISHED 状态转换到 FIN_WAIT_1 状态。
        • 作用:客户端告知服务器,它已经没有数据要发送了,它单方面地关闭了发送方向的连接。但是,客户端仍然可以接收服务器发送的数据。
        1. 第二次挥手(ACK):
        • 发起方:服务器。
        • 报文类型:ACK 报文段(ACK_FLAG=1)。
        • 序列号(Sequence Number):seq = v(服务器上次发送的最后一个字节的序列号加 1)。
        • 确认号(Acknowledgment Number):ack = u + 1。这表示服务器已成功接收到客户端的 FIN 报文段,并期望接收客户端的下一个字节的序列号是 u + 1(尽管客户端已经表示不再发送数据,这只是一个确认)。
        • 服务器状态变化:服务器收到客户端的 FIN 后,从 ESTABLISHED 状态转换到 CLOSE_WAIT 状态。
        • 客户端状态变化:客户端收到服务器的 ACK 后,从 FIN_WAIT_1 状态转换到 FIN_WAIT_2 状态。
        • 作用:服务器确认已收到客户端的关闭请求。此时,客户端到服务器方向的连接已半关闭。服务器处于 CLOSE_WAIT 状态,这意味着服务器应用层还在处理数据或者还有数据需要发送给客户端。这个状态会持续到服务器端也准备好关闭连接。
        1. 第三次挥手(FIN):
        • 发起方:服务器(当服务器也发送完所有数据,并且准备关闭自己的发送方向时)。
        • 报文类型:FIN 报文段 (FIN_FLAG=1, ACK_FLAG=1)。
        • 序列号 (Sequence Number):seq = w (服务器上次发送的最后一个字节的序列号加 1, w 可能等于 v, 如果服务器在发送 ACK 后没有再发送数据)。
        • 确认号 (Acknowledgment Number):ack = u + 1 (再次确认客户端 FIN, 表示服务器对客户端发送的数据已经全部接收并确认)。
        • 服务器状态变化:服务器发送 FIN 后,从 CLOSE_WAIT 状态转换到 LAST_ACK 状态。
        • 作用:服务器告知客户端,它也没有数据要发送了,并请求关闭服务器到客户端方向的连接。
        1. 第四次挥手 (ACK):
        • 发起方:客户端。
        • 报文类型:ACK 报文段 (ACK_FLAG=1)。
        • 序列号 (Sequence Number):seq = u + 1。
        • 确认号 (Acknowledgment Number):ack = w + 1。这表示客户端已成功接收到服务器的 FIN 报文段,并期望接收服务器的下一个字节的序列号是 w + 1 (尽管服务器也表示不再发送数据)。
        • 客户端状态变化:客户端收到服务器的 FIN 后,从 FIN_WAIT_2 状态转换到 TIME_WAIT 状态。
        • 服务器状态变化:服务器收到客户端的 ACK 后,从 LAST_ACK 状态转换到 CLOSED 状态。
        • 作用:客户端确认已收到服务器的 FIN。
    • TIME_WAIT 状态的必要性:客户端在发送最后一个 ACK 后会进入 TIME_WAIT 状态,并持续等待 2MSL (Maximum Segment Lifetime, 最长报文段寿命) 的时间才进入 CLOSED 状态。MSL 是一个报文在网络中能够存活的最长时间。通常设置为 30 秒或 1 分钟。

      • 确保可靠关闭:为了确保最后一个 ACK 报文段能够到达服务器。如果这个 ACK 在网络中丢失了,服务器将因为没有收到确认而超时并重传第三次挥手的 FIN 报文。客户端处于 TIME_WAIT 状态时,可以接收到这个重传的 FIN,并再次发送 ACK,从而保证服务器能够可靠地关闭连接。如果客户端立即进入 CLOSED 状态,它就无法响应服务器重传的 FIN,服务器将永远停留在 LAST_ACK 状态。
      • 避免旧连接的报文干扰新连接:在 2MSL 时间内,确保本连接中所有可能在网络中滞留的报文(如迟到的数据包或重传的 FIN)都已从网络中消失。这可以防止它们在连接关闭后被一个新的、使用相同源端口和目标端口的连接接收到,从而避免数据混乱。

MySQL 数据库

我对 MySQL 数据库有丰富的操作经验和深入的理解,包括 SQL 操作、事务特性、索引原理以及存储引擎。

SQL 操作与优化

SQL (Structured Query Language) 是用于管理关系型数据库的标准语言。我熟练掌握各种 SQL 操作,并能进行性能优化。

  • **DML (Data Manipulation Language):**用于操作数据库中的数据。

    • SELECT:查询数据。
      • SELECT column1, column2 FROM table_name WHERE condition ORDER BY column DESC/ASC LIMIT offset, count;
        
        1
        2
        3
        4
        5
        6
        7
        8
        9
        10
        11
        12
        13
        14

        - 聚合函数:COUNT(), SUM(), AVG(), MAX(), MIN()。

        - 分组查询:GROUP BY, HAVING (用于过滤分组)。

        - 联接查询 **(JOIN)**:
        - INNER JOIN:只返回两个表中都匹配的行。
        - LEFT JOIN / LEFT OUTER JOIN:返回左表所有行,以及右表中匹配的行。如果右表没有匹配,则右表列为 NULL。
        - RIGHT JOIN / RIGHT OUTER JOIN:返回右表所有行,以及左表中匹配的行。如果左表没有匹配,则左表列为 NULL。
        - FULL JOIN / FULL OUTER JOIN (MySQL 不直接支持,通常通过 LEFT JOIN UNION ALL RIGHT JOIN 模拟):返回两个表中的所有行,不匹配的行用 NULL 填充。
        - INSERT:插入数据。
        - ```sql
        - INSERT INTO table_name (column1, column2) VALUES (value1, value2);
        - INSERT INTO table_name VALUES (value1, value2, ...); (所有列)
    • UPDATE: 更新数据。
      • UPDATE table_name SET column1 = value1, column2 = value2 WHERE condition;
        
        1
        2
        3
        4

        - DELETE: 删除数据。
        - ```sql
        DELETE FROM table_name WHERE condition; (不带 WHERE 子句将删除所有行,但不会释放空间,比 TRUNCATE TABLE 慢)
  • DDL (Data Definition Language): 用于定义数据库对象,如表、索引、视图等。

    • - CREATE DATABASE database_name;
      - CREATE TABLE table_name (column1 datatype constraints, column2 datatype constraints, PRIMARY KEY (column_name));
      - ALTER TABLE table_name ADD column_name datatype;
      - ALTER TABLE table_name DROP COLUMN column_name;
      - ALTER TABLE table_name MODIFY COLUMN column_name datatype;
      - DROP TABLE table_name;
      - CREATE INDEX index_name ON table_name (column_name);
      - DROP INDEX index_name ON table_name;
      
      1
      2
      3
      4
      5

      - **DCL (Data Control Language):** 用于控制数据库的访问权限。
      - ```sql
      - GRANT privileges ON database.table TO 'user'@'host';
      - REVOKE privileges ON database.table FROM 'user'@'host';
  • TCL (Transaction Control Language): 用于管理事务。

    • - START TRANSACTION; (或 BEGIN;)
      - COMMIT;
      - ROLLBACK;
      - SAVEPOINT savepoint_name;
      
      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      63
      64
      65
      66
      67
      68
      69
      70
      71
      72
      73
      74
      75
      76
      77
      78
      79
      80
      81
      82
      83
      84
      85
      86
      87
      88
      89
      90
      91
      92
      93
      94
      95
      96
      97
      98
      99
      100
      101
      102
      103
      104
      105
      106
      107
      108
      109
      110
      111
      112
      113
      114
      115
      116
      117
      118
      119
      120
      121
      122
      123
      124
      125
      126
      127
      128
      129
      130
      131
      132
      133
      134
      135
      136
      137
      138
      139
      140
      141
      142
      143
      144
      145
      146
      147
      148
      149
      150
      151
      152
      153
      154
      155
      156
      157
      158
      159
      160
      161
      162
      163
      164
      165
      166
      167
      168
      169
      170
      171
      172
      173
      174
      175
      176
      177
      178
      179
      180
      181
      182
      183
      184
      185
      186
      187
      188
      189
      190
      191
      192
      193
      194
      195
      196
      197
      198
      199
      200
      201
      202
      203
      204
      205
      206
      207
      208
      209
      210
      211
      212
      213
      214
      215
      216
      217
      218
      219
      220
      221
      222
      223
      224
      225
      226
      227
      228
      229
      230
      231
      232
      233
      234
      235
      236
      237
      238
      239
      240
      241
      242
      243
      244
      245
      246
      247
      248
      249
      250
      251
      252
      253
      254
      255
      256
      257
      258
      259
      260
      261
      262
      263
      264
      265
      266
      267
      268
      269
      270
      271
      272
      273
      274
      275
      276
      277
      278
      279
      280
      281
      282
      283
      284
      285
      286
      287
      288
      289
      290
      291
      292
      293
      294
      295
      296
      297
      298
      299
      300
      301
      302
      303
      304
      305
      306
      307
      308
      309
      310
      311
      312
      313
      314
      315
      316
      317
      318
      319
      320
      321
      322
      323
      324
      325
      326
      327
      328
      329
      330
      331
      332
      333
      334
      335
      336
      337
      338
      339
      340
      341
      342
      343
      344
      345
      346
      347
      348
      349
      350
      351
      352
      353
      354
      355
      356
      357
      358
      359
      360
      361
      362
      363
      364
      365
      366
      367
      368
      369
      370
      371
      372
      373
      374
      375
      376
      377
      378
      379
      380
      381
      382
      383
      384

      - **EXPLAIN 命令深入分析:**
      - EXPLAIN 是 MySQL SQL 优化的核心工具,它显示了 MySQL 如何执行 SELECT 查询(以及 INSERT, UPDATE, DELETE 语句的执行计划)。通过分析 EXPLAIN 的输出,可以判断查询是否使用了索引,以及索引使用的情况,从而进行性能优化。
      - id: SELECT 查询的序列号。一个大的 SELECT 查询可以被分解为多个子查询,每个子查询都有一个唯一的 id。id 越大,执行优先级越高(优先级高的先执行);如果 id 相同,则从上到下顺序执行。
      - select_type: 查询的类型。
      - SIMPLE: 简单的 SELECT 查询,不包含子查询或 UNION。
      - PRIMARY: 最外层的 SELECT 查询(对于包含子查询或 UNION 的查询)。
      - SUBQUERY: 子查询中的第一个 SELECT。
      - DEPENDENT SUBQUERY: 依赖外部查询结果的子查询。
      - UNION: UNION 中的第二个或后面的 SELECT 语句。
      - DEPENDENT UNION: UNION 中的第二个或后面的 SELECT 语句,且依赖外部查询。
      - UNION RESULT: UNION 的结果。
      - DERIVED: 派生表(FROM 子句中的子查询)。table: 正在访问的表名。
      - partitions: 匹配到的分区信息(如果表有分区)。
      - type: 最重要的指标,表示连接类型,即 MySQL 查找数据的方式。这是评估查询性能的关键,从好到差依次是:
      - system: 表中只有一行记录(系统表)。const 的特例,性能极高。
      - const: 通过主键或唯一索引查找,最多匹配一行。非常快。例如 SELECT * FROM users WHERE id = 1;
      - eq_ref: 对于前一个表的每个行组合,从表中读取一行。通常用于连接操作,主键或唯一索引的所有列都被用于连接条件。例如 SELECT * FROM orders o JOIN users u ON o.user_id = u.id; (假设 u.id 是主键或唯一索引)。
      - ref: 非唯一索引的等值查找。可能找到多行。例如 SELECT * FROM orders WHERE user_id = 100; (假设 user_id 是普通索引)。
      - range: 范围查找,使用索引来检索给定范围的行。例如 WHERE id > 100 AND id < 200; 或 BETWEEN, IN。
      - index: 全索引扫描。遍历整个索引树,比 ALL 好,因为索引通常比数据文件小,且索引是按顺序存储的,可以避免随机 I/O。如果使用了覆盖索引,效率会更高。
      - ALL: 全索引扫描。扫描整个表来查找匹配的行。性能最差,数据量大时应极力避免。
      - possible_keys: MySQL 认为可能用于查询的索引。
      - key: MySQL 实际决定使用的索引。如果为 NULL,表示没有使用索引。
      - key_len: 实际使用的索引的长度(字节数)。这个值越小越好,表示使用的索引越短。对于复合索引,可以判断索引哪些列被用到了。
      - ref: 表示使用哪个列或常量来和 key 所指的索引列做比较。
      - rows: MySQL 估计要扫描的行数。这是最重要的指标之一,越少越好。
      - filtered: 表示查询条件过滤掉的行数的百分比。越高越好,表示过滤效率高。
      - Extra: 额外信息,对查询的解释提供了额外细节,非常重要。
      - Using filesort: 表示 MySQL 需要对结果进行外部排序(即不在索引中排序)。这通常发生在 ORDER BY 或 GROUP BY 的列没有索引或索引无法被有效利用时。会增加 CPU 开销和 I/O,应尽量通过创建合适的索引或调整查询来避免。
      - Using temporary: 表示 MySQL 需要创建临时表来处理查询。通常发生在 GROUP BY 或 ORDER BY 中包含不相关列,或者 DISTINCT 和 ORDER BY 混用时。临时表可能在内存中,也可能在磁盘上,都会增加性能开销,应尽量避免。
      - Using index: 表示使用了覆盖索引 (Covering Index)。查询所需的所有列都可以在索引中找到,无需回表访问数据行。这是最有效的索引使用方式。
      - Using where: 表示使用了 WHERE 子句进行过滤。这是正常的,但如果 type 是 ALL,说明 WHERE 过滤是基于全表扫描的,效率不高。
      - Using index condition: MySQL 5.6+ 引入的索引条件下推 (Index Condition Pushdown, ICP) 优化。它将 WHERE 子句中的部分条件下推到存储引擎层,在索引扫描过程中就进行过滤,减少了回表次数和从存储引擎层返回给服务层的数据量。
      - Using join buffer (Block Nested Loop): 当关联查询无法使用索引时,MySQL 会使用连接缓存来优化。

      ## 事务 (Transaction)

      事务是一组操作的集合,这些操作要么全部成功,要么全部失败。它具有四大特性 (ACID)。

      - **ACID 特性:**
      1. **原子性 (Atomicity)**: 事务是最小的执行单位,不可再分割。事务中的所有操作要么全部成功提交,要么全部失败回滚到事务开始前的状态。
      2. **一致性 (Consistency)**: 事务执行前后,数据库的完整性约束 (如主键唯一性、外键约束、检查约束、自定义业务规则 ) 没有被破坏,数据从一个一致性状态转换到另一个一致性状态。
      3. **隔离性 (Isolation)**: 并发执行的事务之间是隔离的,一个事务的执行不应影响其他事务的执行,反之亦然。事务的中间状态对其他事务是不可见的。
      4. **持久性 (Durability)**: 一旦事务提交,它对数据库中数据的改变就是永久性的,即使系统崩溃也不会丢失。

      - **并发事务带来的问题:**
      1. **脏读 (Dirty Read)**: 一个事务读取了另一个未提交事务的数据。如果这个未提交事务最终回滚,那么之前读取的数据就是“脏数据”。
      2. **不可重复读 (Non-Repeatable Read)**:一个事务在两次相同的查询中,读取到了不同的数据。通常是因为另一个已提交事务对这些数据进行了 UPDATE 或 DELETE 操作。
      3. **幻读 (Phantom Read)**:一个事务在两次相同的查询中,读取到了不同数量的行。通常是因为另一个已提交事务对数据进行了 INSERT 操作,导致第二次查询出现了之前没有的行。
      4. **丢失更新 (Lost Update)**:当两个事务都读取同一数据并进行修改时,其中一个事务的修改覆盖了另一个事务的修改,导致数据丢失。

      - **事务隔离级别 (从低到高):**
      1. **READ UNCOMMITTED (读未提交):**
      - 最低的隔离级别。
      - 允许脏读、不可重复读和幻读。
      - 性能最高,但数据一致性最差。
      2. **READ COMMITTED (读已提交):**
      - 解决了脏读问题。一个事务只能看到其他事务已经提交的数据。
      - 仍然存在不可重复读和幻读问题。
      - 多数数据库 (如 Oracle, SQL Server) 的默认隔离级别。
      3. **REPEATABLE READ (可重复读):**
      - MySQL (InnoDB 存储引擎) 的默认隔离级别。
      - 解决了脏读和不可重复读问题。在同一个事务中,多次读取同一数据会得到相同的结果 (通过 MVCC - 多版本并发控制)。
      - 仍然可能存在幻读问题 (但在 InnoDB 默认隔离级别下,通过间隙锁解决了大部分幻读问题)。
      4. **SERIALIZABLE (串行化):**
      - 最高的隔离级别。
      - 完全解决了脏读、不可重复读和幻读问题。
      - 强制事务串行执行,避免所有并发问题。
      - 性能最低,因为它牺牲了并发性。

      ## 索引 (Index)

      索引是帮助 MySQL 高效获取数据的数据结构。它类似于书籍的目录,可以快速定位到所需的数据,而无需扫描整个表。

      - **索引的优点:**
      - 大大加快数据检索速度。
      - 减少服务器的 I/O 次数。
      - 在分组 (GROUP BY) 和排序 (ORDER BY) 操作中减少 CPU 消耗。

      - **索引的缺点:**
      - 创建和维护索引需要时间成本,数据增加时需要更新索引。
      - 索引需要占用磁盘空间。
      - 虽然查询性能提升,但对写入操作有一定性能损耗。

      - **索引的底层实现: B+ 树**
      - B+ 树 (B+Tree):
      - 所有数据都存储在叶子节点上,非叶子节点只存储键值 (索引) 用于导航。
      - 叶子节点之间通过链表连接,方便范围查询和遍历。
      - 树的层高更低:相比 B 树,B+ 树的非叶子节点不存储数据,因此一个节点可以存储更多的索引键,使得树的高度更低,从而减少磁盘 I/O 次数。
      - 适用于磁盘存储:磁盘 I/O 是数据库操作的主要瓶颈,B+ 树的设计(节点大小与磁盘块大小匹配)能够最大程度地减少磁盘寻道次数。

      - **索引的分类:**
      1. 主键索引 (Primary Key Index):
      - 一种特殊的唯一索引,一个表只能有一个主键。
      - 不允许有空值 (NULL)。
      - 通常是聚集索引 (InnoDB)。
      2. 唯一索引 (Unique Index):
      - 索引列的值必须唯一,但允许有空值 (NULL),且可以有多个 NULL 值。
      - CREATE UNIQUE INDEX index_name ON table_name (column_name):
      3. 普通索引 (Normal Index / Non-Unique Index):
      - 最基本的索引,没有任何限制。
      - CREATE INDEX index_name ON table_name (column_name):
      4. 全文索引 (Fulltext Index):
      - 用于文本内容的模糊查询,在大文本字段上进行高效搜索。
      - 只支持 MyISAM 和 InnoDB 存储引擎。
      - CREATE FULLTEXT INDEX index_name ON table_name (column_name):
      5. 复合索引 (Composite Index / Multi-Column Index):
      - 在多个列上创建的索引。
      - 遵循“最左前缀原则”:如果查询条件使用了复合索引的第一个列,则整个索引会被利用;如果只使用了非第一个列,则索引可能无法完全利用。
      - CREATE INDEX index_name ON table_name (column1, column2, column3);

      - **聚集索引(Clustered Index)与 非聚集索引(Non-Clustered Index):**
      - 聚集索引:
      - 索引的叶子节点存储的就是完整的数据行。
      - 一个表只能有一个聚集索引(通常是主键)。
      - 数据物理存储的顺序与索引的逻辑顺序一致。
      - 查询效率高,因为找到索引叶子节点就找到了数据。
      - 对数据插入、更新和删除有一定影响,因为需要维护数据的物理顺序。
      - 在 InnoDB 存储引擎中,主键索引就是聚集索引。如果我没有显式定义主键,InnoDB 会选择一个唯一非空索引作为聚集索引;如果没有,则会自动创建一个隐藏的 6 字节的行 ID 作为聚集索引。
      - 非聚集索引:
      - 索引的叶子节点存储的是主键值或指向数据行的指针。
      - 一个表可以有多个非聚集索引。
      - 数据物理存储顺序与非聚集索引的逻辑顺序无关。
      - 查询需要回表:如果查询的列不在非聚集索引中,需要通过索引找到主键值,再通过主键索引(聚集索引)找到完整的数据行。
      - 在 InnoDB 中,所有非主键索引都是非聚集索引。

      - **索引优化策略:**
      - 选择合适的列创建索引:
      - 在 WHERE 子句、JOIN 子句、ORDER BY 子句中经常出现的列。
      - 选择区分度高(唯一值多)的列。
      - 不为小型表、频繁更新的表、重复值多的列创建索引。
      - 遵循最左前缀原则:对于复合索引 (a, b, c),查询条件 WHERE a = 1,WHERE a = 1 AND b = 2 会使用索引,但 WHERE b = 2 或 WHERE c = 3 不会完全使用索引。
      - 避免索引失效:
      - 在索引列上进行函数运算或表达式运算(如 WHERE YEAR(date_column) = 2023)。
      - 对索引列进行隐式类型转换。
      - 使用 OR 连接条件(除非 OR 的两边都有索引,并且优化器认为合并索引更高效)。
      - 使用 LIKE '%keyword'(左模糊匹配)会导致全索引扫描或全表扫描。
      - 使用 NOT IN 或 != 通常无法使用索引。
      - 覆盖索引:查询只从索引中获取数据,无需回表。
      - SELECT column_in_index FROM table_name WHERE indexed_column = value;
      - 防止回表:尽可能让 SELECT 语句中的列都被索引覆盖,或者只查询主键。
      - 定期维护索引:重建或优化碎片化的索引。

      - #### SQL优化

      ![图片](https://i-blog.csdnimg.cn/blog_migrate/bfe55c106fb1ceb9316542c424cecf9f.png)

      ## 存储引擎

      MySQL 是一个关系型数据库管理系统,它的独特之处在于支持多种可插拔的存储引擎。存储引擎负责 MySQL 中数据的存储和提取。

      - **InnoDB (默认):**
      - 特性:
      - 支持事务(ACID 特性)。
      - 支持行级锁定:在并发写入时,只锁定需要修改的行,而不是整个表,大大提高了并发性能。
      - 支持外键约束:维护数据完整性。
      - 支持崩溃恢复:通过事务日志(redo log 和 undo log)保证数据持久性。
      - 默认使用聚集索引:数据存储在 B+ 树的叶子节点中,数据行和主键一起存储,查询效率高。
      - MVCC(多版本并发控制):通过快照读取实现非阻塞的读操作,提高并发性能。
      - 适用场景:事务性应用、高并发读写、对数据完整性和一致性要求要求高的场景(如电商、金融系统)。

      - **MyISAM(非事务性):**
      - 特性:
      - 不支持事务。
      - 支持表级锁定:并发写入时,需要锁定整个表,并发性能差。
      - 不支持外键约束。
      - 不支持崩溃恢复:可能导致数据丢失。
      - 非聚集索引:数据和索引是分离的,索引叶子节点存储的是数据行的地址。
      - SELECT COUNT(*) 效率高,因为表的总行数存储在内存中。
      - 适用场景:只读或读多写少,对事务性要求不高,需要频繁执行 COUNT(*) 的应用(如一些日志记录、数据仓库)。在 MySQL 5.5 后,InnoDB 逐渐替代 MyISAM 成为默认和首选。

      - **其他存储引擎(了解):**
      - Memory (HEAP):数据存储在内存中,速度极快,但重启服务器数据会丢失。适用于临时表或缓存。
      - Archive:用于存储大量不常访问的历史数据,支持高速插入和查询(不支持更新和删除)。数据会被高度压缩。
      - CSV:以 CSV 文件格式存储数据,便于与其他应用程序进行数据交换。

      ## MVCC(多版本并发控制):

      好的,我们来深入探讨一下 **MVCC (Multi-Version Concurrency Control)**,并分析一些常见的面试题。

      ------

      ### **什么是 MVCC?**

      **MVCC**,即**多版本并发控制**,是一种在数据库中用于解决并发访问问题的方法。它不是通过加锁的方式来控制并发,而是通过**为每个事务生成一个数据快照**,让读操作在快照上进行。

      你可以把它想象成一个“时光机”。当一个事务开始时,数据库会为它“拍一张照片”,也就是生成一个数据快照。这个事务的所有读操作都只会看到这个快照里的数据,而不会受到其他并发事务修改的影响。这样,读操作就不需要等待写锁释放,从而实现了**读写分离**,大大提高了并发性能。

      简而言之,MVCC 的核心思想是:

      - **读不加锁**:读取数据时,直接从数据的历史版本中读取,不需要等待其他事务的写锁。
      - **写不阻塞读**:写操作修改数据时,会创建一个新的版本,而旧版本依然保留,供其他读事务使用。

      ------

      ### **MVCC 的实现原理**

      MVCC 的实现通常依赖于以下几个核心要素:

      1. 隐藏列(Hidden Columns):

      每个表都会有几个隐藏的列,用于记录版本信息:

      - **DB_TRX_ID**:事务 ID,记录最近一次修改数据的事务 ID。
      - **DB_ROLL_PTR**:回滚指针,指向这条记录的上一个版本。
      - **DB_ROW_ID**:行 ID,是插入新行时分配的隐藏 ID,当主键是字符串时,可能用于辅助索引。

      2. Undo Log (回滚日志):

      Undo Log 记录了数据在被修改之前的值。每次修改数据时,都会将修改前的数据版本记录在 Undo Log 中,并通过回滚指针 DB_ROLL_PTR 将新版本与旧版本连接起来,形成一个版本链。这样,通过版本链,我们就可以追溯到这条数据的历史版本。

      3. Read View (读视图):

      Read View 是 MVCC 的核心,它是一个在事务启动时生成的、用来判断某个数据版本对当前事务是否可见的数据快照。它主要包含以下几个关键信息:

      - `m_ids`:在生成 `Read View` 时,当前系统中**所有活跃事务**的 ID 列表。
      - `min_trx_id`:在生成 `Read View` 时,`m_ids` 中最小的事务 ID。
      - `max_trx_id`:在生成 `Read View` 时,系统将要分配给下一个事务的 ID。
      - `creator_trx_id`:创建 `Read View` 的事务 ID。

      当一个事务想要读取一条数据时,会根据 `Read View` 的规则来判断这条数据的**`DB_TRX_ID`**是否可见。

      - 如果 `DB_TRX_ID` 小于 `min_trx_id`,说明这个修改操作在当前事务启动前就已经提交了,数据**可见**。
      - 如果 `DB_TRX_ID` 大于等于 `max_trx_id`,说明这个修改操作是在当前事务启动后才发生的,数据**不可见**。
      - 如果 `DB_TRX_ID` 在 `min_trx_id` 和 `max_trx_id` 之间,那么需要判断 `DB_TRX_ID` 是否在 `m_ids` 列表中。如果在,说明这个修改操作是和当前事务同时启动的,数据**不可见**;如果不在,说明这个修改操作在当前事务启动前就已经提交了,数据**可见**。

      如果当前版本不可见,事务就会通过回滚指针 `DB_ROLL_PTR` 沿着版本链找到上一版本,直到找到一个**可见**的版本。

      ------

      ### **面试题分析**

      #### **1. 什么是 MVCC?它解决了什么问题?**

      **回答要点:**

      - **概念**:多版本并发控制,通过维护数据历史版本实现并发。
      - **解决问题**:在数据库隔离级别为**读已提交(RC)**和**可重复读(RR)**时,实现了读写不冲突。它解决了 **读写锁冲突** 和 **脏读** 问题,但无法完全解决幻读。
      - **核心思想**:读操作读取数据快照,写操作创建新版本。

      #### **2. MVCC 是如何实现可重复读(Repeatable Read)的?**

      **回答要点:**

      - **核心**:`Read View` 的创建时机。
      - **可重复读**:事务在第一次读操作时创建 `Read View`,并且在**整个事务的生命周期内都使用这个 Read View**。这意味着无论事务中执行多少次读,看到的都是同一个数据快照,所以能保证多次读取结果一致。
      - **读已提交**:相比之下,读已提交的隔离级别是**每次执行读操作时都重新生成一个 Read View**。因此,如果其他事务在两次读操作之间提交了修改,第二次读就能看到新数据,导致不可重复读。

      #### **3. MVCC 能解决幻读吗?**

      **回答要点:**

      - **部分解决,但不能完全解决。**
      - **幻读(Phantom Read)**:当一个事务在两次查询之间,另一个事务插入了新的数据,导致第一次查询不存在的数据,第二次查询却出现了。
      - **MVCC 的作用**:MVCC 可以防止**更新幻读**(即一个事务在两次查询之间,另一个事务更新了数据),因为它总是读取事务启动时的快照。
      - **无法解决**:MVCC 无法完全解决**插入幻读**。例如,事务 A 两次查询 `WHERE id > 10`,但在两次查询之间,事务 B 插入了一条 `id=11` 的记录并提交。虽然事务 A 的 `Read View` 看不到这条新记录,但如果事务 A 执行 `UPDATE ... WHERE id > 10` 时,它会发现这条新记录并对其加锁,从而更新成功。这会打破可重复读的承诺。
      - **InnoDB 的解决方案**:InnoDB 数据库在 `可重复读` 隔离级别下,除了 MVCC,还会结合**间隙锁(Gap Lock)**来彻底解决幻读问题。

      #### **4. Undo Log 和 Redo Log 有什么区别?**

      **回答要点:**

      - **Undo Log (回滚日志)**:
      - **作用**:用于**回滚事务**和实现 **MVCC**。
      - **记录内容**:记录的是**数据修改前**的版本。
      - **生命周期**:在事务提交后,如果数据有其他事务在使用(用于 MVCC),`Undo Log` 依然保留;如果没有,`Undo Log` 会被清除。
      - **Redo Log (重做日志)**:
      - **作用**:用于保证事务的**持久性**。
      - **记录内容**:记录的是**数据修改后**的日志,比如“某某页的某某偏移量改成了某某值”。
      - **生命周期**:在数据同步到磁盘后,`Redo Log` 就会被清除。
      - **作用点**:`Redo Log` 作用于**崩溃恢复**。当数据库发生宕机时,可以根据 `Redo Log` 将已提交但尚未写入磁盘的数据重新写入,以保证数据不丢失。

      ## MySQL日志

      MySQL的日志系统是其数据库管理系统(DBMS)中至关重要的组成部分,扮演着监控、审计、故障恢复和数据复制等多种关键角色。用户提到的错误日志、查询日志、慢查询日志、事务日志和二进制日志构成了MySQL日志体系的核心。下面将对这些主要日志进行详细的梳理和解析。

      ## 1. 错误日志(Error Log)

      错误日志是MySQL中最基础的日志之一,它记录了mysqld服务器启动、运行和关闭过程中遇到的所有严重错误和警告。

      - 主要内容:
      - 服务器启动和关闭的详细信息。
      - 运行过程中发生的错误,例如表损坏、无法访问特定文件等。
      - 事件调度器运行出错时的信息。
      - 在主从复制架构中,从服务器上启动和关闭复制线程,连接主服务器时发生的错误等。
      - 作用:错误日志是诊断和解决MySQL服务器问题的首要工具。当数据库无法启动或运行异常时,应首先检查此日志。
      - 配置:默认情况下,错误日志是开启的。其文件名通常为<hostname>.err,位于数据目录(datadir)下。可以通过在my.cnf或my.ini配置文件中设置log_error变量来指定其路径。

      ## 2. 查询日志(Query Log)/通用查询日志(General Query Log)

      通用查询日志记录了MySQL服务器接收到的每一个客户端连接和执行的每一条SQL语句。

      - 主要内容:
      - 客户端的连接信息,包括连接时间、用户名和主机。
      - 客户端发送给服务器的所有SQL语句,无论其是否正确执行。
      - 作用:该日志对于数据库的审计和问题排查非常有用,可以精确复现用户的操作序列。然而,由于它会记录所有操作,对系统性能会产生显著影响,并会迅速占用大量磁盘空间。因此,不建议在生产环境中长期开启。
      - 配置:默认关闭。可以通过设置general_log为ON来启用,并使用general_log_file指定日志文件路径。

      ## 3. 慢查询日志(Slow Query Log)

      慢查询日志用于记录执行时间超过指定阈值的SQL查询语句,是数据库性能优化的关键工具。

      - 主要内容:
      - 执行时间超过long_query_time阈值的SQL语句。
      - 查询执行时的相关信息,如执行时间、锁定时间、扫描的行数、返回的行数以及执行该查询的用户和主机。
      - 作用:通过分析慢查询日志,开发者和数据库管理员(DBA)可以定位到效率低下的SQL语句,并针对性地进行优化,例如添加索引、改写查询等。
      - 配置:默认关闭。需在配置文件中设置slow_query_log为ON开启。long_query_time参数用于设定慢查询的时间阈值(单位:秒),slow_query_log_file用于指定日志文件位置。log_queries_not_using_indexes参数还可以记录未使用索引的查询。

      ## 4. 事务日志(Transaction Log)

      用户提到的"事务日志"在InnoDB存储引擎中,主要由两种日志构成:重做日志(Redo Log)和回滚日志(Undo Log)。它们共同保证了事务的ACID特性(原子性、一致性、隔离性、持久性)。

      - 重做日志(Redo Log):
      - 作用:保证事务的持久性。它记录了数据被修改后的物理变化。当事务提交后,即使数据尚未完全写入数据文件,只要Redo Log已经持久化,在数据库发生崩溃时,也可以通过重放Redo Log来恢复已提交的事务,确保数据不丢失。这种技术被称为预写日志(Write-Ahead Logging, WAL)。
      - 特点:Redo Log是以循环写的方式记录在连续的物理文件中,大小固定。
      - 回滚日志(Undo Log):
      - 作用:保证事务的原子性和实现多版本并发控制(MVCC)。Undo Log记录的是数据被修改前的状态。当事务需要回滚时,可以通过Undo Log将数据恢复到修改之前的版本。同时,在读已提交(Read Committed)和可重复读(Repeatable Read)隔离级别下,当一个事务需要读取被另一个未提交事务修改的行时,会通过Undo Log读取该行之前的版本,从而实现非锁定读。
      - 特点:Undo Log逻辑上记录了每个修改操作的逆操作。

      ## 5. 二进制日志(Binary Log/Binlog)

      二进制日志是MySQL中功能最强大、用途最广泛的日志之一。它以二进制格式记录了所有修改数据库数据的操作(DML)以及数据定义语言(DDL)的操作,但不包括SELECT和SHOW等不修改数据的查询。

      - 主要内容:记录了导致数据发生更改的所有事件。根据格式不同,可以记录为SQL语句(STATEMENT格式)、行的变更(ROW格式)或两者的混合(MIXED格式)。
      - 主要作用:
      - 数据恢复(Point-in-Time Recovery):通过备份的数据文件和之后的二进制日志,可以将数据库恢复到过去的任意一个时间点。
      - 主从复制(Replication):在主从架构中,主服务器将二进制日志传送给从服务器,从服务器重放这些日志中的事件,从而实现与主服务器的数据同步。
      - 配置:默认情况下可能关闭,需要通过配置文件中的log_bin选项来启用。启用后,会生成一个索引文件(默认为<hostname>-bin.index)和一系列的二进制日志文件。

      ## 扩展:中继日志(Relay Log)

      在主从复制环境中,还有一个重要的日志类型——中继日志。

      - 作用:从服务器的I/O线程从主服务器获取二进制日志,并将其写入本地的中继日志中。然后,从服务器的SQL线程读取中继日志中的事件,并在从服务器上执行,以实现数据同步。
      - 特点:中继日志的格式与二进制日志完全相同。它的存在使得从服务器的I/O和SQL执行可以解耦,即使在网络不稳定的情况下,只要I/O线程将日志拉到本地,SQL线程就可以持续执行。

      ## 总结

      | 日志类型 | 主要作用 | 生产环境建议 |
      | ---------- | ---------------------------------------- | -------------------------------------------- |
      | 错误日志 | 记录服务器启停和运行错误 | 始终开启 |
      | 查询日志 | 记录所有连接和SQL语句,用于审计 | 默认关闭,仅在调试时短期开启 |
      | 慢查询日志 | 记录执行缓慢的SQL,用于性能优化 | 建议开启 |
      | 事务日志 | | |
      | - Redo Log | 保证事务持久性,用于崩溃恢复 | InnoDB引擎核心组件,始终开启 |
      | - Undo Log | 保证事务原子性,支持MVCC | InnoDB引擎核心组件,始终开启 |
      | 二进制日志 | 数据恢复、主从复制 | 强烈建议开启,尤其是需要数据恢复和复制的场景 |
      | 中继日志 | 主从复制中,从库用于暂存主库的二进制日志 | 在从服务器上自动创建和管理 |

      ## MySQL中的锁

      ## 第一部分:数据库锁系统

      ### 1. 锁的分类体系

      #### 1.1 按锁粒度的层次分类

      **表级锁(Table-Level Lock)** 表级锁是最粗粒度的锁机制,一次锁定整个表的所有数据。MyISAM存储引擎主要使用表级锁,其内部维护一个全局的表锁列表。当线程需要访问表时,首先检查表锁状态,如果表已被其他线程以不兼容模式锁定,则当前线程进入等待队列。表级锁的优势在于锁管理开销极小,只需要维护少量的锁对象;缺点是并发度极低,即使访问不同行的操作也会相互阻塞。

      **页级锁(Page-Level Lock)** 页级锁锁定数据页,是表级锁和行级锁的折中方案。BDB存储引擎使用页级锁,每个数据页通常包含多条记录。页级锁的实现需要在页头维护锁信息,包括锁模式、持有者信息等。这种锁粒度在空间局部性较好的应用中表现优秀,因为相关的数据通常存储在相邻的页面中。

      **行级锁(Row-Level Lock)** 行级锁是最细粒度的锁机制,InnoDB存储引擎的核心特性。行锁的实现依赖于索引结构,实际上锁定的是索引记录而不是数据行本身。当查询没有使用索引时,InnoDB会扫描整个表并对所有记录加锁,退化为类似表锁的行为。行级锁提供最高的并发度,但也带来最大的管理开销。

      #### 1.2 按锁模式的功能分类

      **共享锁(Shared Lock, S锁)** 共享锁允许多个事务同时读取同一资源,但阻止任何事务修改该资源。在InnoDB中,共享锁通过在锁对象的type_mode字段中设置LOCK_S标志位来标识。多个共享锁可以并存,这是通过锁兼容性矩阵来判断的。共享锁的获取相对简单,只需要检查是否存在冲突的排他锁。

      **排他锁(Exclusive Lock, X锁)** 排他锁提供独占访问,同一时间只能有一个事务持有资源的排他锁。排他锁与任何其他锁都不兼容,包括共享锁和其他排他锁。在InnoDB实现中,排他锁的获取需要等待所有现有的锁释放,这通过等待队列机制来实现。

      **意向锁(Intention Lock)** 意向锁是一种表级锁,用于表明事务在表的某些行上持有或即将请求某种类型的锁。意向共享锁(IS)表示事务意图在某些行上获取共享锁,意向排他锁(IX)表示事务意图在某些行上获取排他锁。意向锁的引入大大简化了表级操作的锁冲突检测,避免了遍历所有行锁的开销。

      #### 1.3 按锁算法的实现分类

      **记录锁(Record Lock)** 记录锁锁定索引中的一条具体记录,是最基本的行级锁形式。在InnoDB的实现中,记录锁通过在B+树的叶子节点记录上设置锁标记来实现。锁对象中的heap_no字段精确标识被锁定的记录在页面中的位置。记录锁只能防止其他事务修改或删除该记录,但不能防止在该记录前后插入新记录。

      **间隙锁(Gap Lock)** 间隙锁锁定索引记录之间的间隙,防止其他事务在该间隙中插入新记录。间隙锁的范围是开区间,不包含边界记录本身。InnoDB通过比较索引键值来确定间隙的边界,对于复合索引,间隙的比较需要考虑所有键值列的组合。间隙锁之间不冲突,多个事务可以同时持有相同间隙的间隙锁。

      **临键锁(Next-Key Lock)** 临键锁是记录锁和间隙锁的组合,锁定一个记录以及该记录前面的间隙。这是InnoDB在可重复读隔离级别下的默认锁算法。临键锁有效解决了幻读问题,因为它不仅锁定已存在的记录,还锁定了可能插入新记录的位置。临键锁的范围是左开右闭区间。

      ### 2. InnoDB锁系统的深层实现

      #### 2.1 锁对象的数据结构设计

      struct lock_t { trx_t* trx; // 拥有该锁的事务 UT_LIST_NODE_T(lock_t) trx_locks; // 事务锁链表节点 dict_table_t* tab_lock; // 表锁信息 dict_index_t* index; // 索引信息 hash_node_t hash; // 哈希表节点 ulint type_mode; // 锁类型和模式 ulint n_bits; // 位图大小 ulint n_granted_locks; // 已授予的锁数量 ulint n_waiting_locks; // 等待的锁数量 };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

**锁哈希表的设计:** InnoDB使用一个全局的锁哈希表来管理所有的锁对象,哈希函数基于space_id和page_no计算。每个哈希桶包含一个锁对象链表,相同页面的所有锁对象都链接在同一个桶中。这种设计支持快速的锁查找和冲突检测,时间复杂度接近O(1)。

**锁位图的精巧设计:** 对于页面级别的锁管理,InnoDB使用位图来标识页面中哪些记录被锁定。每个记录对应位图中的一位,设置为1表示该记录被锁定。这种压缩表示法大大节省了内存空间,特别是在锁定大量记录时。位图操作使用高效的位运算指令,支持快速的锁状态查询和更新。

#### 2.2 死锁检测的高级算法

**增量式死锁检测:** InnoDB不是每次都重新构建整个等待图,而是采用增量式检测。当新的等待关系建立时,算法只检查从新等待者开始的路径是否形成环。这种优化大大减少了检测的计算开销,特别是在高并发场景下效果显著。

**死锁检测的优先级调度:** 死锁检测器运行在独立的后台线程中,具有较高的调度优先级。检测频率根据系统负载动态调整:在高并发时增加检测频率,在低负载时降低频率以节省CPU资源。检测器还会维护统计信息,包括死锁发生频率、平均解决时间等,用于系统调优。

**复杂死锁场景的处理:** 实际生产环境中可能出现涉及多个资源类型的复杂死锁,比如同时涉及行锁、表锁、和外键约束锁的死锁。InnoDB的死锁检测器需要构建一个多层次的等待图,包含不同类型的资源节点和等待边。解决这类死锁时,需要考虑不同锁类型的回滚代价。

#### 2.3 MVCC与锁的深度集成

**ReadView的内部结构:**

struct ReadView {
trx_id_t low_limit_id; // 生成ReadView时的下一个事务ID
trx_id_t up_limit_id; // 生成ReadView时最小的活跃事务ID
trx_id_t creator_trx_id; // 创建ReadView的事务ID
trx_ids_t m_ids; // 生成ReadView时的活跃事务ID列表
m_low_limit_no; // 最大的事务编号
};

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220

**版本可见性的判断算法:** 当事务读取记录时,需要判断记录的某个版本是否对当前事务可见。判断逻辑基于记录的DB_TRX_ID字段和ReadView的信息:如果DB_TRX_ID小于up_limit_id,则该版本对所有事务可见;如果DB_TRX_ID大于等于low_limit_id,则该版本对当前事务不可见;如果DB_TRX_ID在两者之间,则需要检查是否在活跃事务列表中。

**undo log的链式结构:** 每个事务的undo log形成一个链式结构,记录了该事务的所有修改操作。undo log不仅用于事务回滚,还用于MVCC的版本构建。当需要构建某个历史版本时,InnoDB会从当前版本开始,沿着undo log链向前回溯,逐步应用逆向操作直到达到目标版本。

### 3. 数据库锁的性能优化策略

#### 3.1 锁等待的自适应策略

**等待时间的动态调整:** InnoDB实现了自适应的锁等待策略,根据系统负载和锁竞争情况动态调整等待超时时间。在低负载时延长等待时间以减少不必要的回滚,在高负载时缩短等待时间以快速释放资源。这种策略通过维护系统级的统计信息来实现,包括平均锁持有时间、锁竞争频率等。

**等待队列的优先级管理:** 对于同一资源的多个等待者,InnoDB可以根据事务的重要性、等待时间、资源消耗等因素进行优先级排序。高优先级的事务可以优先获得锁,这对于关键业务操作特别重要。优先级的计算考虑多个维度,包括事务的年龄、修改的数据量、是否为只读事务等。

#### 3.2 索引设计对锁性能的影响

**覆盖索引与锁范围优化:** 当查询能够通过覆盖索引获取所有需要的数据时,InnoDB只需要在二级索引上加锁,而不需要回表访问聚簇索引。这大大减少了锁的数量和范围,提高了并发性能。覆盖索引的设计需要仔细分析查询模式,将经常一起访问的列组合成复合索引。

**索引前缀长度对锁粒度的影响:** 对于字符串类型的索引,前缀长度的选择直接影响锁的粒度。较短的前缀可能导致多个不同的值映射到相同的索引前缀,从而扩大锁的范围;较长的前缀提供更精确的锁定,但增加了索引的存储开销。最优前缀长度需要在锁粒度和存储效率之间平衡。

#### 3.3 事务设计对锁性能的优化

**事务边界的精细控制:** 事务边界的设计直接影响锁的持有时间。长事务会长时间持有锁,阻塞其他事务的执行。优化策略包括:将大事务拆分为多个小事务,减少每个事务的锁持有时间;将只读操作和写操作分离,只读操作使用快照读避免加锁;在事务中优先执行可能失败的操作,减少无效锁持有。

**批量操作的锁优化:** 对于批量插入、更新、删除操作,可以采用特殊的锁策略。比如批量插入时可以使用INSERT ... ON DUPLICATE KEY UPDATE语法,减少锁冲突;批量更新时可以按照索引顺序进行,避免死锁;批量删除时可以分批进行,避免长时间持有大量锁。

## 行锁(Row Lock)

## InnoDB的行锁实现

InnoDB是MySQL中支持行锁的主要存储引擎:
实现原理:

1. 当事务需要锁定某行时,InnoDB会在该行对应的索引记录上加锁
2. 如果没有索引,会锁定整个表(实际是锁定所有行)
3. 锁信息存储在内存中的哈希表结构中

## 表锁(Table Lock)

## MyISAM的表锁实现

MyISAM只支持表级锁

1. 维护一个全局的表锁队列
2. 写锁具有更高优先级
3. 锁信息存储在服务器层面

# Java 核心技术

## 数据类型

------

### **八个基本数据类型(Primitive Data Types)**



#### **1. 整型**

整型用于存储不带小数的整数。Java 提供了四种整型,它们的区别在于所占内存空间的大小,从而影响可存储的数值范围。

- **byte**
- 大小:1 字节(8 位)
- 范围:−128 到 127
- 用途:主要用于节省内存,尤其是在处理大量数据时,例如在文件或网络传输中。
- **short**
- 大小:2 字节(16 位)
- 范围:−32768 到 32767
- 用途:同样用于节省内存,比 `byte` 的范围更大。
- **int**
- 大小:4 字节(32 位)
- 范围:约 −2.1×109 到 2.1×109
- 用途:这是**最常用**的整型,通常用来表示普通的整数。
- **long**
- 大小:8 字节(64 位)
- 范围:约 −9.2×1018 到 9.2×1018
- 用途:当 `int` 类型的范围不足以存储一个大数时使用,例如处理时间戳、文件大小等。在数值后面需要加上 `L` 或 `l` 来表示,例如 `long myLong = 100L;`。

#### **2. 浮点型**

浮点型用于存储带有小数点的数值。

- **float**
- 大小:4 字节(32 位)
- 用途:单精度浮点数,有效位数为 6-7 位。在数值后面需要加上 `F` 或 `f` 来表示,例如 `float myFloat = 3.14f;`。
- **double**
- 大小:8 字节(64 位)
- 用途:双精度浮点数,有效位数为 15-16 位。这是**最常用**的浮点型,Java 默认的浮点数类型就是 `double`。

> **注意**:浮点数存在精度问题,不适合用于精确的金融计算。如果需要精确计算,应使用 `java.math.BigDecimal` 类。

#### **3. 字符型**

- **char**
- 大小:2 字节(16 位)
- 用途:存储单个字符。在 Java 中,`char` 采用 Unicode 编码,可以表示包括中文在内的所有字符。它可以用单引号 `'` 括起来,例如 `char myChar = 'A';`。

#### **4. 布尔型**

- **boolean**
- 大小:在内存中通常被视为 1 位,但在数组中会占用 1 字节。
- 用途:用于存储 `true` 或 `false`,表示逻辑值。

------

### **String 类型**

`String` 是一个非常特殊的类型,它**不是基本数据类型**,而是一个**引用数据类型(Reference Data Type)**。

#### **String 的核心特性**

1. **不可变性(Immutability)**
- `String` 对象一旦被创建,它的值就不能被改变。
- 当你对 `String` 进行修改操作时,比如拼接字符串,实际上是**创建了一个新的 String 对象**,而原始对象没有改变。
- 这种不可变性使得 `String` 对象是线程安全的,可以在多线程环境中被安全地共享。
2. **String 的创建**
- **字面量方式**:`String s = "hello";`
- 这种方式创建的字符串会被存储在**字符串常量池(String Pool)**中。
- 当创建第二个值为 `"hello"` 的字符串时,JVM 会直接从常量池中返回已存在的对象引用,而不会创建新的对象。这是一种重要的内存优化。
- **new 关键字方式**:`String s = new String("hello");`
- 这种方式会创建一个新的 `String` 对象在堆(Heap)中。即使常量池中已经存在 `"hello"`,也会在堆中再创建一个新的对象。

#### **String 的常用方法**

- **拼接**:
- 使用 `+` 运算符:`String s3 = s1 + s2;`
- 使用 `concat()` 方法:`s1.concat(s2);`
- 推荐使用 `StringBuilder` 或 `StringBuffer` 进行大量字符串拼接,因为它们是可变的,效率更高。
- **比较**:
- `equals()`:比较字符串的内容,返回 `boolean` 类型。
- `equalsIgnoreCase()`:忽略大小写比较字符串内容。
- `==`:比较的是**对象的引用地址**,只有当两个引用指向同一个对象时才为 `true`。
- **查找**:
- `indexOf(char)`:查找字符第一次出现的索引。
- `contains(String)`:判断是否包含某个子字符串。
- **修改**:
- `replace(oldChar, newChar)`:替换字符串中的字符。
- `substring(beginIndex, endIndex)`:截取子字符串。

#### **为什么不用 char 数组代替 String?**

虽然 `String` 内部也是用 `char` 数组实现的,但 `String` 的不可变性提供了很多优势:

- **安全性**:作为方法参数时,可以防止方法内部修改原始字符串。
- **线程安全**:可以在多线程环境下共享。
- **哈希码缓存**:`String` 的哈希码会被缓存,在作为 `HashMap` 的键时,性能更好。

------

### **基本类型与 String 的转换**

- **基本类型转 String**:
- `String.valueOf(int i)`
- `Integer.toString(int i)`
- `"" + i`
- **String 转基本类型**:
- `Integer.parseInt("123")`
- `Double.parseDouble("3.14")`
- `Float.parseFloat("1.23")`
- 等等。

> **注意**:`String` 转基本类型时,如果字符串格式不正确,会抛出 `NumberFormatException`。

## 锁

### 1. 内置锁:`synchronized` 关键字

`synchronized`是Java语言层面的同步机制,使用起来相对简单。它可以修饰方法或代码块,由JVM负责加锁和释放锁,开发者不需要手动管理。

- **同步方法:** 当你用`synchronized`修饰一个非静态方法时,锁对象是这个方法的实例对象(`this`)。当一个线程进入这个同步方法时,它会获得该实例的锁,其他线程就无法进入这个实例的任何同步方法,直到它释放锁。
- **同步静态方法:** 当`synchronized`修饰一个静态方法时,锁对象是该类的`Class`对象。这意味着所有线程在访问这个类的任何同步静态方法时都会争抢同一把锁,无论有多少个实例。
- **同步代码块:** `synchronized (object)`。这是最灵活的使用方式,你可以指定任何对象作为锁。通过缩小同步范围,可以减少锁的持有时间,从而提高程序的并发性能。

**synchronized的特点:**

- **可重入性(Reentrant):** 一个线程可以重复获取它已经持有的锁,避免死锁。
- **非公平性(Unfair):** 默认情况下,线程获取锁的顺序是不确定的,先来的不一定先得到。
- **锁的升级:** 为了优化性能,`synchronized`锁的实现经历了从**偏向锁**、**轻量级锁**到**重量级锁**的升级过程。这使得在竞争不激烈的情况下,加锁的开销非常小。
- **内存可见性(Visibility):** `synchronized`可以保证当一个线程释放锁时,它对共享变量所做的修改会立即刷新到主内存,而当另一个线程获取锁时,它会从主内存中读取最新的变量值。这是由Java内存模型(JMM)的`happens-before`规则保证的。

### 底层实现机制

synchronized本质上是基于JVM的**内置锁(Intrinsic Lock)**实现的。每个Java对象都天生具备一个监视器锁(Monitor),这是JVM级别的原生支持。

**监视器的工作原理:**

- 当线程进入synchronized代码块时,会执行monitorenter指令获取对象的监视器
- 当线程退出时,执行monitorexit指令释放监视器
- 如果获取失败,线程会被阻塞在操作系统级别

### 锁升级的演进过程

synchronized的性能优化经历了一个复杂的演进过程,从最初的重量级锁逐步优化:

**偏向锁阶段:**
当只有一个线程反复进入同步块时,JVM会将锁偏向于这个线程。此时的开销极低,几乎等同于无锁操作。对象头中会记录偏向的线程ID,后续该线程进入时直接通过简单的比较就能获取锁。

**轻量级锁阶段:**
当有其他线程开始竞争时,偏向锁会升级为轻量级锁。JVM在当前线程的栈帧中创建锁记录(Lock Record),通过CAS操作尝试将对象头的Mark Word替换为指向锁记录的指针。这个过程避免了操作系统层面的阻塞。

**重量级锁阶段:**
当CAS操作失败次数过多或者有多个线程同时竞争时,锁会膨胀为重量级锁。此时会创建Monitor对象,利用操作系统的互斥量(Mutex)来实现同步,涉及用户态和内核态的切换。

### 自动化的特点

synchronized的最大特点是**完全自动化**。程序员无需关心锁的获取和释放时机,JVM会自动处理所有细节。即使在异常情况下,JVM也能保证锁的正确释放,这大大降低了死锁和资源泄露的风险。

### 2. 显式锁:`java.util.concurrent.locks.Lock`

从JDK 1.5开始,`java.util.concurrent.locks`包提供了更灵活、更强大的锁机制,其核心是`Lock`接口。最常用的实现类是**`ReentrantLock`**。

使用`Lock`需要手动进行加锁和解锁操作,通常是在`try...finally`块中进行,以确保锁在任何情况下都能被释放,防止死锁。

```java
Lock lock = new ReentrantLock();
lock.lock();
try {
// 访问共享资源的代码
} finally {
lock.unlock();
}

Lock相比synchronized的优势:

  • 中断锁(Interruptibly): Lock提供了lockInterruptibly()方法,允许在等待锁的过程中响应中断。而synchronized的线程如果陷入等待锁的状态,是无法被中断的。
  • 尝试获取锁(tryLock): Lock提供了tryLock()tryLock(long timeout, TimeUnit unit)方法,可以尝试获取锁,如果失败则立即返回或在指定时间内放弃,避免无限等待。
  • 公平锁与非公平锁: ReentrantLock可以创建公平锁(Fair Lock)。公平锁会按照线程请求锁的顺序来分配锁,虽然这可能会带来一些性能开销。而synchronized只能是非公平锁。
  • 绑定多个条件(Condition): Lock配合Condition接口,可以实现更灵活的线程等待和唤醒机制,类似Objectwait()notify(),但功能更强大,一个锁可以有多个等待队列。
  • 读写锁(ReadWriteLock): ReadWriteLockLock的另一个重要实现,它维护了一对锁:一个用于读操作,一个用于写操作。在读多写少的场景下,多个线程可以同时获取读锁,大大提高了并发性能,只有写操作才需要获取独占的写锁。ReentrantReadWriteLock是其具体实现。

Lock接口的设计理念

显式锁机制

Lock接口代表了Java并发包中显式锁的设计思想。与synchronized的隐式锁不同,Lock要求程序员明确控制锁的生命周期,这带来了更大的灵活性,同时也增加了使用的复杂性。

AQS框架的核心思想

Lock接口的实现基于AbstractQueuedSynchronizer(AQS)框架,这是Doug Lea设计的一个并发框架的杰作。

同步状态的抽象:
AQS使用一个int值来表示同步状态,不同的锁实现可以赋予这个状态不同的含义。比如ReentrantLock用它表示重入次数,Semaphore用它表示许可证数量。

队列化的等待机制:
当线程无法获取锁时,AQS会将其包装成节点加入到一个FIFO队列中。这个队列使用双向链表实现,每个节点都包含了线程引用和等待状态信息。

自旋与阻塞的平衡:
AQS巧妙地结合了自旋和阻塞两种等待策略。线程在入队后会先进行有限次数的自旋尝试,只有在确定无法获取锁时才会调用LockSupport.park()进入阻塞状态。

3. volatile 关键字

volatile关键字并不是一个锁,它是一种轻量级的同步机制,主要用于保证共享变量的可见性有序性

  • 可见性(Visibility): 当一个变量被volatile修饰后,一个线程对它的修改会立即被其他线程可见。这是通过在写操作后添加内存屏障,强制将修改后的值刷新到主内存,并在读操作前添加内存屏障,强制从主内存中读取最新值来实现的。
  • 有序性(Ordering): volatile可以禁止指令重排序,确保代码的执行顺序不会被打乱。
  • 无法保证原子性(Atomicity): volatile无法保证复合操作(如i++)的原子性,因为i++实际上是读、加、写三个操作的组合,这三个操作并非一次完成。如果要保证原子性,需要使用synchronizedLockjava.util.concurrent.atomic包下的原子类。

总结一下:

特性 synchronized Lock (如ReentrantLock) volatile
功能 独占锁,保证原子性、可见性、有序性 独占锁,功能更强大,保证原子性、可见性、有序性 轻量级同步,只保证可见性和有序性
使用方式 关键字,自动加锁和解锁 接口,需要手动加锁和解锁,必须在finally块中释放 关键字,修饰变量
灵活性 较差,功能固定 强,提供了更多高级功能,如可中断、超时、公平锁等 较差,只针对变量
性能 JVM优化后性能较高,开销相对较小 高性能,在竞争激烈时通常优于synchronized 非常高,几乎没有开销
适用场景 简单的同步需求,大部分情况都适用 高级同步需求,需要灵活控制锁的获取和释放 变量的写操作不依赖于当前值,需要保证变量的可见性。

4. 乐观锁 (Optimistic Locking)

与之前讨论的悲观锁synchronizedLock)不同,乐观锁并非一个具体的Java关键字或类,而是一种并发控制的思想和策略。悲观锁认为“总会有其他线程来修改数据”,所以在访问共享资源前,先对资源加锁,确保独占访问。而乐观锁则认为“数据冲突发生的概率很小”,所以它不加锁,而是假设所有线程都能正常执行,只有在数据更新提交时,才去检查在此期间数据是否被其他线程修改过。

如果检查到数据没有被修改,则更新成功。如果发现数据已经被修改,则更新失败。处理失败的方式通常有两种:

  1. 重试: 循环尝试,直到更新成功为止。
  2. 放弃: 抛出异常或直接返回失败,由调用方处理。

乐观锁的实现方式:

乐观锁的核心在于如何“检查数据是否被修改”。在Java中,常见的实现方式有两种:

  • 版本号(Version Number):

    在数据表中增加一个version字段。每次读取数据时,也把version字段读出来。当要进行数据更新时,带上之前读取的version值,在更新语句中加入WHERE version = <当前版本号>的条件。如果更新成功,同时把version值加1。

    • SQL示例:

      1
      2
      UPDATE products SET stock = 100, version = version + 1
      WHERE id = 123 AND version = <之前读取的版本号>;
    • 原理: 如果在更新时,其他线程已经修改了这条数据,那么version值已经改变,上述UPDATE语句的WHERE条件将不成立,导致更新失败,影响行数为0。此时,你可以选择重试或放弃。

  • CAS (Compare-And-Swap) 算法:

    这是乐观锁在硬件层面的支持,也是Java中实现乐观锁的核心机制。CAS是一种原子操作,它包含三个操作数:

    • V (Value): 内存地址中存放的旧值。
    • A (Expected): 预期的旧值。
    • B (New): 想要写入的新值。

    CAS的操作逻辑是:

    如果内存地址V中的值等于预期值A,那么就将V的值更新为新值B。否则,什么都不做。整个操作是原子性的,由CPU指令直接完成。

    Java中java.util.concurrent.atomic包下的所有原子类,如AtomicIntegerAtomicLong等,都是基于CAS实现的。

    AtomicInteger示例:

    1
    2
    3
    AtomicInteger count = new AtomicInteger();
    // 假设多个线程同时执行以下操作
    count.incrementAndGet(); // 内部就是CAS操作

    incrementAndGet()方法的内部实现类似于一个自旋重试的循环:

    1. 获取当前值current
    2. 计算新值next = current + 1
    3. 使用CAS尝试将current更新为next
    4. 如果更新失败(说明current已经被其他线程修改),则重新回到第一步,再次获取最新值并尝试更新。

悲观锁与乐观锁的比较:

特性 悲观锁 (synchronized, Lock) 乐观锁 (CAS, 版本号)
加锁方式 独占资源时先加锁,阻止其他线程访问 不加锁,只在提交时进行冲突检测
冲突处理 线程排队等待锁,串行执行 线程失败后重试或放弃,并发执行
适用场景 写操作多、竞争激烈的场景。数据冲突概率高。 读操作多、写操作少的场景。数据冲突概率低。
性能 在高竞争环境下,线程切换和上下文开销大,性能下降。 在低竞争环境下,无锁开销,性能极高。在高竞争环境下,大量重试可能导致CPU开销增加。

总结:

在Java中,悲观锁乐观锁是两种截然不同的并发控制策略。悲观锁(synchronized, Lock)适合写多读少的场景,能够保证数据的一致性,但会牺牲一定的性能。而乐观锁(CAS版本号)则适合读多写少的场景,通过无锁的并发操作提高了性能,但在高竞争下可能导致频繁重试,反而降低效率。理解这两种锁的思想,可以帮助你根据具体的业务场景选择最合适的并发控制方案。

好的,我们来继续完善Java中关于锁的介绍,增加死锁及其解决方案的内容。


5. 死锁 (Deadlock)

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力作用,它们都将无法继续执行。

死锁的产生是一个非常经典的多线程问题,通常发生在线程需要同时持有多个锁的场景中。一个简单的死锁场景是:线程A持有锁1,想获取锁2;而线程B持有锁2,想获取锁1。此时两个线程都无法继续执行,从而进入死锁状态。

Java

1
2
3
4
5
6
7
8
9
10
11
12
13
// 线程A
synchronized (lock1) {
synchronized (lock2) {
// ...
}
}

// 线程B
synchronized (lock2) {
synchronized (lock1) {
// ...
}
}

死锁的四个必要条件

死锁的发生需要同时满足以下四个条件,缺一不可:

  1. 互斥条件(Mutual Exclusion): 至少有一个资源是独占的,即一次只能被一个线程使用。这是锁本身的基本特性。
  2. 请求与保持条件(Hold and Wait): 一个线程因请求资源而阻塞时,它对自己已获得的资源保持不放。
  3. 不剥夺条件(No Preemption): 线程已获得的资源在未使用完之前,不能被强行剥夺,只能由该线程自己释放。
  4. 循环等待条件(Circular Wait): 存在一个线程等待链,其中每个线程都持有下一个线程所需的资源。例如:线程A等待线程B,线程B等待线程C,线程C又等待线程A。

死锁的解决方案

解决死锁的根本思想是破坏上述四个必要条件之一或多个。通常,我们无法破坏互斥条件(因为资源就是独占的),因此主要从其他三个条件入手。

  1. 破坏“请求与保持”条件:
    • 一次性获取所有锁: 线程在开始执行时,就一次性获取所有需要的锁。如果获取不成功,则释放所有已获得的锁,然后等待一段时间后再次尝试。
    • 优点: 简单有效。
    • 缺点: 可能会降低并发性,因为线程在很早就持有了锁,即使这些锁在后面才被使用。
  2. 破坏“不剥夺”条件:
    • 使用可中断的锁: 使用Lock接口提供的tryLock()方法。当一个线程尝试获取锁失败时,它可以选择放弃并释放已持有的锁,而不是一直等待。Lock.tryLock(long time, TimeUnit unit)方法可以在指定时间内尝试获取锁,超时后会放弃。
    • 优点: 提高了灵活性,线程可以响应中断或超时,避免无限等待。
    • 缺点: 实现起来相对复杂,需要开发者手动处理获取锁失败的情况。
  3. 破坏“循环等待”条件:
    • 按顺序获取锁: 对所有的锁进行排序,并强制所有线程都按照相同的顺序获取锁。
    • 示例: 如果线程A和线程B都需要lock1lock2,那么它们都必须先获取lock1,再获取lock2。这样就杜绝了线程A持有lock1等待lock2,同时线程B持有lock2等待lock1的循环。
    • 优点: 这是最常用、最有效的死锁解决方案,实现起来也相对简单。
    • 缺点: 有时很难对所有锁进行全局排序,特别是在代码模块化程度较高、依赖关系复杂的情况下。

总结:

在实际开发中,预防死锁的最佳实践通常是破坏循环等待条件,即统一锁的获取顺序。这是最简单且最有效的方案。如果业务场景需要更灵活的控制,可以考虑使用Lock接口,利用tryLock()方法来破坏“不剥夺”条件,实现更复杂的死锁处理逻辑。

好的,我们来详细介绍银行家算法 (Banker’s Algorithm)


银行家算法概述

银行家算法是一种著名的死锁避免算法,由荷兰计算机科学家Dijkstra在1965年提出。它的核心思想是:在每次分配资源之前,先进行一次安全性检查。如果分配后系统仍然处于安全状态,则分配资源;否则,不予分配,线程需要等待。

  • 优点:比死锁预防更灵活,能提高资源利用率。
  • 缺点:算法复杂,需要预知进程的最大资源需求,并且系统开销大。

之所以叫“银行家算法”,是因为它的工作原理类似于银行管理贷款。银行家在发放贷款时,会先确保这笔贷款发放后,自己还有足够的资金来满足所有客户可能提出的最大取款需求,从而避免因无法支付而破产的风险。

简单来说,银行家算法通过以下两个步骤来避免死锁:

  1. 安全状态的定义: 系统能够找到一个安全序列,使得所有线程都能按照这个序列执行完毕。
  2. 资源分配策略: 当一个线程请求资源时,算法会先假设分配成功,然后检查系统是否仍处于安全状态。如果安全,就真的分配;如果不安全,就拒绝分配。

银行家算法中的几个重要数据结构

为了实现算法,需要维护以下几个关键数据结构,假设系统中有n个线程和m种资源:

  • Available (可用资源矩阵): 一个长度为m的向量。Available[j]表示第j种资源目前可用的数量。
  • Max (最大需求矩阵): 一个n * m的矩阵。Max[i, j]表示线程i最多需要第j种资源多少个。
  • Allocation (已分配资源矩阵): 一个n * m的矩阵。Allocation[i, j]表示线程i目前已拥有第j种资源多少个。
  • Need (需求矩阵): 一个n * m的矩阵。Need[i, j]表示线程i还需要第j种资源多少个才能完成任务。
    • Need[i, j] = Max[i, j] - Allocation[i, j]

银行家算法的核心:安全状态的判断

判断系统是否处于安全状态是银行家算法的核心。一个系统处于安全状态,当且仅当存在一个安全序列<P1, P2, ..., Pn>。这个序列满足:对于序列中每一个线程Pi,它所需要的资源都能由系统中当前可用的资源,以及前面所有已完成的线程释放的资源来满足。

安全性检查算法的步骤:

  1. 初始化:
    • 创建一个Work向量,初始化为Available(即当前可用资源)。
    • 创建一个Finish向量,初始化为false,表示所有线程都未完成。
  2. 寻找安全线程:
    • 从所有线程中找到一个线程i,满足以下两个条件:
      • Finish[i]false
      • Need[i]向量中的每一个值都小于或等于Work向量中对应的值。
      • 换句话说,线程i所需要的资源小于或等于当前可用的资源。
  3. 释放资源:
    • 如果找到了这样的线程i,则认为它可以顺利执行完毕。
    • 模拟该线程执行完毕并释放资源,更新Work向量:Work = Work + Allocation[i]
    • Finish[i]设置为true
  4. 循环检查:
    • 重复步骤2和步骤3,直到找不到满足条件的线程。
  5. 判断结果:
    • 如果最终所有线程的Finish都为true,则说明找到了一个安全序列,系统处于安全状态
    • 如果还有线程的Finishfalse,则说明系统处于不安全状态,可能存在死锁。

银行家算法的流程:资源分配

当一个线程P请求资源时,银行家算法会执行以下步骤:

  1. 请求检查: 检查线程P请求的资源数量是否小于或等于其Need向量中的需求量。如果不是,说明线程P的请求不合理,拒绝分配。
  2. 可用性检查: 检查线程P请求的资源数量是否小于或等于当前Available中的资源数量。如果不是,说明资源不足,线程P需要等待。
  3. 预分配并进行安全性检查:
    • 假设资源可以分配,临时进行以下操作:
      • Available = Available - Request
      • Allocation[P] = Allocation[P] + Request
      • Need[P] = Need[P] - Request
    • 调用上面的安全性检查算法,判断系统是否处于安全状态。
  4. 正式分配或拒绝:
    • 如果安全性检查的结果是安全,则正式分配资源,并保留步骤3中的修改。
    • 如果安全性检查的结果是不安全,则回滚步骤3中的所有临时修改,拒绝分配资源,线程P需要等待。

银行家算法的优缺点

  • 优点:
    • 可以有效地避免死锁的发生,保证系统的安全性。
    • 通过提前检查,可以最大化地利用资源,提高系统的并发性。
  • 缺点:
    • 计算开销大: 每次分配资源都需要运行安全性检查算法,增加了系统的开销。
    • 过于保守: 安全状态不等于无死锁,不安全状态也不等于一定发生死锁。算法为了确保安全,可能会拒绝一些本可以成功分配的请求,从而降低了系统的吞吐量。
    • 条件苛刻: 算法要求线程在开始前就声明其所需的最大资源量,这在实际应用中很难做到。
    • 资源数量固定: 算法假设系统中资源的数量是固定的,不能动态增减。

因此,银行家算法虽然在理论上非常完美,但在实际操作系统中很少被直接完整地实现。然而,它的核心思想——通过安全性检查来避免死锁——仍然是许多并发控制策略的重要理论基础。

实际应用中的死锁处理

在实际的并发编程和数据库系统中,最常见的死锁处理方式是:

  • 死锁预防(通过编程规范):

    • 加锁顺序一致:规定所有线程在获取多个锁时,必须按照相同的顺序。这是最有效的预防死锁的编程实践。
    • 使用超时锁:尝试获取锁时设置一个超时时间,如果超时未获取到锁,则放弃本次操作并释放已持有的锁,然后重试。这破坏了”请求与保持”条件。例如Java的ReentrantLocktryLock(timeout)。
    • 避免嵌套锁:尽量减少持有多个锁的情况。
  • 死锁检测与恢复(数据库系统):

    • 大多数关系型数据库(如MySQL InnoDB)都实现了死锁检测机制。当检测到死锁时,数据库会自动选择一个成本较低的事务(“牺牲品”)进行回滚,从而解除死锁。客户端应用会收到相应的错误码(例如MySQL中的Deadlock found when trying to get lock; try restarting transaction)。应用程序通常需要捕获这个错误并重试事务。理解死锁的四个必要条件是关键,因为解决死锁的根本方法就是破坏其中一个或多个条件。

多线程与线程池

在 Java 中,多线程是实现并发编程的关键技术,它允许程序同时执行多个任务。线程池则是管理和复用线程的重要机制,能有效提升系统性能和资源利用率。

线程(Thread)的概念与生命周期

线程是操作系统调度的最小单位,是进程中的一个执行路径。一个进程可以包含多个线程,这些线程共享进程的内存空间。

线程的生命周期通常包含以下六种状态(定义在 java.lang.Thread.State 枚举中):

  • NEW(新建):线程被创建但尚未启动。当使用 new Thread() 创建一个线程实例后,它就处于此状态。

    1
    2
    Thread myThread = new Thread(() -> System.out.println("Hello from a new thread!"));
    // 此时 myThread 处于 NEW 状态,尚未执行 start()
  • RUNNABLE(可运行):线程已调用 start() 方法,正在 JVM 中运行(可能正在执行,也可能在等待 CPU 调度)。一个 RUNNABLE 状态的线程可能正在运行,也可能并没有运行,它仅仅是具备了运行的资格。

    1
    2
    myThread.start();
    // 此时 myThread 进入 RUNNABLE 状态,等待 CPU 调度执行 run() 方法
  • BLOCKED (阻塞):线程正在等待获取一个监视器锁(例如,进入 synchronized 块或方法)。当一个线程试图访问被其他线程锁定的资源时,它会进入此状态。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Object lock = new Object();
    // 线程已经持有 lock 对象的锁
    synchronized (lock) {
    // ... 线程 A 正在执行
    }
    // 线程 B 尝试获取 lock 对象的锁,但被线程 A 占用,会进入 BLOCKED 状态
    synchronized (lock) {
    // ...
    }
  • WAITING (等待):线程无限制地等待另一个线程执行特定操作。例如,调用 Object.wait(), Thread.join() (无参数) 或 LockSupport.park().这些方法会使线程放弃 CPU 使用权,并进入无限制等待,直到被其他线程 notify(), notifyAll() 或 unpark() 唤醒。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    Object sharedObject = new Object();
    // 线程 A:
    synchronized (sharedObject) {
    sharedObject.wait(); // 线程 A 进入 WAITING 状态,并释放 sharedObject 的锁
    }
    // 线程 B:
    Thread threadA = new Thread(() -> { /* ... */ });
    threadA.start();
    threadA.join(); // 线程 B 等待 threadA 执行完毕,进入 WAITING 状态
  • TIMED_WAITING (有时限等待):线程在指定的时间内等待另一个线程执行特定操作,或者休眠。例如,调用 Thread.sleep(long millis)、Object.wait(long timeout)、Thread.join(long millis)、LockSupport.parkNanos() 或 LockSupport.parkUntil()。一旦超时间到达,线程会自动从等待状态唤醒,并尝试重新进入 RUNNABLE 状态。

    1
    2
    Thread.sleep(1000);
    // 线程进入 TIMED_WAITING 状态 1 秒
  • TERMINATED (终止): 线程已执行完毕其 run() 方法, 或者因未捕获的异常退出。线程一旦进入此状态, 就不能再被重新启动。

    1
    2
    // 线程的 run() 方法执行完毕
    // 线程在执行过程中抛出未捕获的异常

创建线程的方式

继承 Thread 类:

  • 通过创建 Thread 类的子类, 并重写其 run() 方法, 在该方法中定义线程执行的任务。
  • 创建 Thread 子类的实例, 并调用其 start() 方法来启动线程。调用 start() 方法会使线程进入 RUNNABLE 状态, 并由 JVM 调度执行 run() 方法; 直接调用 run() 方法则只是在当前线程中执行普通方法, 不会启动新线程。
  • 优点: 实现简单直观, 代码结构清晰。
  • 缺点: Java 是单继承的, 如果你的类已经继承了其他类, 就不能再继承 Thread 类。这限制了类的灵活性。此外, 任务 (run() 方法中的逻辑 ) 与线程本身 (Thread 对象) 紧密耦合, 不利于任务的复用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
// MyThread.java
class MyThread extends Thread {
private String threadName;

public MyThread(String name) {
this.threadName = name;
System.out.println("Creating " + threadName );
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running via extending Thread.");
try {
for(int i = 4; i > 0; i--) {
System.out.println("Thread: " + threadName + ", " + i);
// 模拟耗时操作
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Thread " + threadName + " interrupted.");
Thread.currentThread().interrupt(); // 重新设置中断标志
}
System.out.println("Thread " + threadName + " exiting.");
}
}

// ThreadCreationDemo.java
public class ThreadCreationDemo {
public static void main(String[] args) {
System.out.println("Main thread started.");
MyThread thread1 = new MyThread("MyThread-1");
thread1.start(); // 启动新线程
MyThread thread2 = new MyThread("MyThread-2");
thread2.start(); // 启动另一个新线程
System.out.println("Main thread finished starting other threads.");
}
}

实现 Runnable 接口:

  • 定义一个类实现 Runnable 接口,并实现其抽象方法 public void run()。run() 方法中包含线程执行的具体任务。
  • 创建 Runnable 实现类的实例,然后将其作为参数传入 Thread 类的构造器 (new Thread(Runnable target)),再调用 Thread 实例的 start() 方法。
  • 优点:
    • 推荐方式:避免了 Java 单继承的限制,你的类可以同时继承其他类来实现 Runnable 接口。
    • 任务与线程解耦:Runnable 对象只负责定义任务,而 Thread 对象负责执行任务。这意味着同一个 Runnable 对象可以被多个 Thread 实例共享执行,从而更好地实现资源的共享和任务的复用。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
// MyRunnable.java
class MyRunnable implements Runnable {
private String taskName;

public MyRunnable(String name) {
this.taskName = name;
System.out.println("Creating " + taskName );
}

@Override
public void run() {
System.out.println(Thread.currentThread().getName() + " is running via implementing Runnable.");
try {
for(int i = 4; i > 0; i--) {
System.out.println("Task: " + taskName + ", " + i);
Thread.sleep(50);
}
} catch (InterruptedException e) {
System.out.println("Task " + taskName + " interrupted.");
Thread.currentThread().interrupt();
}
System.out.println("Task " + taskName + " exiting.");
}
}

// ThreadCreationDemo.java
public class ThreadCreationDemo {
public static void main(String[] args) {
System.out.println("Main thread started.");
Runnable runnable1 = new MyRunnable("Runnable-Task-1");
Thread thread3 = new Thread(runnable1);
thread3.start();

Runnable runnable2 = new MyRunnable("Runnable-Task-2");
Thread thread4 = new Thread(runnable2, "Custom-Thread-Name"); // 可以给线程命名
thread4.start();
System.out.println("Main thread finished starting runnable threads.");
}
}

实现Callable接口

Callable 是 Java 并发编程中一个非常重要的接口,它与 Runnable 类似,都用于定义一个可在线程中执行的任务。但 Callable 提供了更强大的功能,主要体现在两个方面:

  1. 可以返回结果: Callablecall() 方法可以返回一个泛型类型的结果。
  2. 可以抛出异常: Callablecall() 方法可以声明抛出任何 Exception

这与 Runnable 接口形成了鲜明对比,Runnablerun() 方法没有返回值,也不能抛出受检异常(checked exception)。

  • Callable 的基本结构

Callable 接口是一个泛型接口,定义如下:

Java

1
2
3
4
5
6
7
8
9
@FunctionalInterface
public interface Callable<V> {
/**
* 计算并返回结果,或在无法做到时抛出异常。
* @return 计算结果
* @throws Exception 如果无法计算结果
*/
V call() throws Exception;
}
  • <V>:泛型参数,代表 call() 方法将要返回的结果类型。

  • V call()call() 方法的签名,它返回一个 V 类型的结果,并且可以抛出异常。

  • 如何使用 Callable

Callable 接口本身并不能直接作为 Thread 的构造参数。它需要配合 ExecutorService 线程池和 Future 接口一起使用。

典型使用流程:

  1. 创建 Callable 任务: 实现 Callable 接口,并在 call() 方法中编写具体的业务逻辑,返回一个结果。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    import java.util.concurrent.Callable;

    public class MyCallableTask implements Callable<String> {
    @Override
    public String call() throws Exception {
    Thread.sleep(2000); // 模拟耗时操作
    return "任务执行完毕,返回结果";
    }
    }
  2. 创建 ExecutorService 线程池: 使用 Executors 工厂类创建线程池。

    1
    2
    3
    4
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;

    ExecutorService executor = Executors.newFixedThreadPool(2);
  3. 提交 Callable 任务: 使用 ExecutorServicesubmit() 方法提交任务。submit() 方法会返回一个 Future 对象。

    1
    2
    3
    4
    import java.util.concurrent.Future;

    Callable<String> task = new MyCallableTask();
    Future<String> future = executor.submit(task);
  4. 获取任务结果: 通过 Future 对象的 get() 方法来获取 Callable 任务的执行结果。

    • future.get() 是一个阻塞方法,它会一直等待,直到任务执行完毕并返回结果。
    • 如果任务执行过程中抛出了异常,get() 方法也会将这个异常包装在 ExecutionException 中重新抛出。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    import java.util.concurrent.ExecutionException;

    try {
    String result = future.get(); // 阻塞等待结果
    System.out.println(result);
    } catch (InterruptedException e) {
    // 线程被中断
    Thread.currentThread().interrupt();
    } catch (ExecutionException e) {
    // 任务执行过程中抛出的异常
    e.printStackTrace();
    } finally {
    executor.shutdown();
    }
  • CallableRunnable 的区别

特性 Callable Runnable
返回值 call() 方法有返回值(泛型 V run() 方法没有返回值(void
异常处理 call() 方法可以抛出受检异常 run() 方法不能直接抛出受检异常
执行方式 必须配合 ExecutorService.submit() 执行 可以直接作为 Thread 构造函数的参数,也可以通过 ExecutorService.execute()submit() 执行
功能 适用于需要返回计算结果或可能抛出异常的异步任务 适用于简单的异步任务,不需要返回结果

  • Future 接口

Future 接口是 Callable 的重要伴侣,它代表一个异步计算的结果。它提供了检查任务是否完成、等待任务完成以及获取任务结果的方法。

Future 接口的主要方法:

  • V get(): 阻塞式地等待任务完成,并返回结果。

  • V get(long timeout, TimeUnit unit): 在指定时间内等待任务完成,超时则抛出 TimeoutException

  • boolean isDone(): 检查任务是否已经完成。

  • boolean cancel(boolean mayInterruptIfRunning): 尝试取消任务。

  • 总结

Callable 是 Java 并发编程中一个更高级的任务抽象,它解决了 Runnable 接口无法返回结果和处理受检异常的痛点。通过与 ExecutorServiceFuture 接口的组合使用,Callable 使得异步编程变得更加简单和灵活,非常适合那些需要耗时计算并返回结果的场景,比如网络请求、数据处理等。

线程池 (Thread Pool)

线程池是一种基于池化思想的线程管理机制,用于管理和复用线程,而不是在每次需要执行任务时都创建新线程。

  • 为什么使用线程池?

    • 降低资源消耗:通过重复利用已创建的线程,降低线程创建和销毁的开销。
    • 提高响应速度:当任务到达时,任务可以直接执行,无需等待线程创建。
    • 提高线程的可管理性:线程是稀缺资源,如果无限制地创建,不仅会消耗系统资源,还会降低系统的稳定性,使用线程池可以统一分配、调优和监控。
    • 提供更多功能:如定时执行、周期执行、单线程化等。
  • 线程池的核心参数 (ThreadPoolExecutor 构造方法)

    1
    2
    3
    4
    5
    6
    7
    public ThreadPoolExecutor(int corePoolSize,
    int maximumPoolSize,
    long keepAliveTime,
    TimeUnit unit,
    BlockingQueue<Runnable> workQueue,
    ThreadFactory threadFactory,
    RejectedExecutionHandler handler)
    • corePoolSize: 核心线程数。线程池中始终保持的线程数量,即使它们处于空闲状态,除非设置了 allowCoreThreadTimeOut(true)。
    • maximumPoolSize: 最大线程数。线程池中允许存在的最大线程数量。当工作队列已满且核心线程都在忙碌时,线程池会创建新的非核心线程,直到达到这个数量。
    • keepAliveTime: 当线程池中的线程数量超过 corePoolSize 时,这些空闲的非核心线程在终止之前等待新任务的最长时间。
    • unit: keepAliveTime 参数的时间单位。
    • workQueue: 任务队列 (阻塞队列)。用于存放等待执行的任务。
      • ArrayBlockingQueue: 基于数组的有界阻塞队列,需要指定容量。
      • LinkedBlockingQueue: 基于链表的阻塞队列,容量可以指定,也可以是无界的(默认)。如果使用无界队列,maximumPoolSize 参数将失效。
      • SynchronousQueue: 一个不存储元素的阻塞队列。每个插入操作必须等待一个对应的移除操作,反之亦然。
      • PriorityBlockingQueue: 支持优先级的无界阻塞队列,按照自然顺序或自定义比较器排序。
    • threadFactory: 线程工厂。用于创建新线程,可以自定义线程的命名、优先级等。
    • handler: 拒绝策略 (当线程池和工作队列都已满时,新的任务到来时的处理方式)。
      • ThreadPoolExecutor.AbortPolicy (默认): 直接抛出 RejectedExecutionException 异常。
      • ThreadPoolExecutor.CallerRunsPolicy: 由调用线程 (提交任务的线程) 执行任务。
      • ThreadPoolExecutor.DiscardOldestPolicy: 丢弃队列中最老的任务,然后尝试提交当前任务。
      • ThreadPoolExecutor.DiscardPolicy: 直接丢弃当前新提交的任务。
  • 线程池的执行流程

    1. 当一个任务提交到线程池时,如果当前运行的线程数小于 corePoolSize,即使有空闲线程,也会创建并启动一个新线程来执行任务。
    2. 如果当前运行的线程数大于或等于 corePoolSize,但任务队列 workQueue 未满,任务会被添加到 workQueue 中等待执行。
    3. 如果 workQueue 已满,但当前运行的线程数小于 maximumPoolSize,线程池会创建新的非核心线程来执行任务。
    4. 如果当前运行的线程数等于 maximumPoolSize 且 workQueue 已满,线程池会根据拒绝策略来处理新提交的任务。
  • Java 内置的四种常用线程池 (通过 Executors 工厂类创建)

    • FixedThreadPool (固定大小线程池)
      • Executors.newFixedThreadPool(int nThreads)
      • corePoolSize = maximumPoolSize = nThreads
      • 使用无界 LinkedBlockingQueue。
      • 特点:可控制并发的线程数,超出的任务会在队列中等待。
      • 问题:当任务提交速度远大于处理速度时,队列会不断增长,可能导致 OOM。
    • SingleThreadExecutor (单线程线程池)
      • Executors.newSingleThreadExecutor()
      • corePoolSize = maximumPoolSize = 1
      • 使用无界 LinkedBlockingQueue。
      • 特点:保证所有任务都在一个线程中按顺序执行。
      • 问题:同 FixedThreadPool,队列无限增长可能导致 OOM。
    • CachedThreadPool(可缓存线程池)
      • Executors.newCachedThreadPool()
      • corePoolSize = 0, maximumPoolSize = Integer.MAX_VALUE
      • 使用 SynchronousQueue。
      • keepAliveTime = 60s
      • 特点:当任务到来时,有空闲线程则复用,无空闲线程则创建新线程。适用于大量短时任务。
      • 问题:maximumPoolSize 过大,当任务并发量极高时,可能创建大量线程,导致系统资源耗尽 (OOM)。
    • ScheduledThreadPool(定时任务线程池)
      • Executors.newScheduledThreadPool(int corePoolSize)
      • 特点:支持定时及周期性任务执行。
      • 内部使用 DelayedWorkQueue,一个无界队列,可以按时间进行排序。
  • 阿里巴巴开发手册建议:不推荐使用 Executors 创建线程池,而是手动通过 ThreadPoolExecutor 的构造方法创建,以明确线程池的运行规则,避免资源耗尽的风险。

ThreadLocal:

好的,我们来详细聊聊 ThreadLocal

ThreadLocal 是什么?

ThreadLocal(线程本地变量)并不是用来解决线程间共享数据问题的,它的核心作用是为每个使用该变量的线程都提供一个独立的、隔离的副本

你可以把 ThreadLocal 想象成一个“线程专属的储物柜”。每个线程都可以往这个储物柜里存东西(通过 set() 方法),取东西(通过 get() 方法),但它只能看到自己储物柜里的东西,无法访问其他线程的。

ThreadLocal 内部其实是通过一个 ThreadLocalMap 来实现的。这个 Map 的键是 ThreadLocal 对象本身,值就是你存入的那个变量。每个线程都有一个属于自己的 ThreadLocalMap


为什么需要 ThreadLocal

我们通常在开发中会遇到两种数据共享问题:

  1. 多个线程共享一个变量:这种情况下,需要通过 synchronizedvolatileLock 来保证线程安全。
  2. 每个线程需要一个独立的变量:这是 ThreadLocal 的主要应用场景。

如果不用 ThreadLocal,我们可能需要自己手动维护一个 Map<Thread, Object>,每次存取数据时都以当前线程作为键。这样不仅麻烦,还容易出错。ThreadLocal 帮我们封装了这些细节,让使用变得非常简单。


ThreadLocal 的常见应用场景

ThreadLocal 最常见的应用场景是在 Web 开发中,用于存储与当前请求相关的上下文信息

例如,一个 HTTP 请求从进入服务器到返回响应,可能由多个方法或组件来处理,但它们都属于同一个线程。如果需要传递一些请求相关的状态(比如用户身份、事务 ID、数据库连接),我们有很多种做法:

  • 参数传递:将这些信息作为参数层层传递。这会导致方法签名变得臃肿,并且增加了代码的耦合性。
  • 静态变量:如果用静态变量,多个请求同时到达时会互相覆盖,导致线程不安全。
  • ThreadLocal:这是最优雅的解决方案。你可以把这些信息存入 ThreadLocal,然后在任何需要的地方直接通过 get() 方法获取,无需在方法间显式传递。

典型的例子:

  • Spring 的事务管理:Spring 框架在处理事务时,会使用 ThreadLocal 来保存每个线程的数据库连接,确保在同一个事务中的所有操作都使用同一个连接。
  • 上下文信息:例如,在请求处理链中,将用户登录信息、语言偏好等数据存入 ThreadLocal,下游的业务逻辑可以随时获取。

ThreadLocal 可能带来的问题

虽然 ThreadLocal 很好用,但如果使用不当,也可能导致一些问题。

内存泄漏

ThreadLocal 可能会导致内存泄漏。这是一个非常重要的问题。

ThreadLocalMap 使用的是弱引用(Weak Reference) 作为键。这意味着,当 ThreadLocal 对象没有其他强引用时,即使它还在 ThreadLocalMap 中,垃圾回收器也会回收它。

但是,ThreadLocalMap 的值(也就是你存入的对象)是强引用。如果线程一直存活,但你不再使用 ThreadLocal 对象,ThreadLocalMap 中的键就会变成 null,但值还在。这样,值对象就无法被回收,导致内存泄漏。

如何避免?

解决这个问题的关键在于:在 ThreadLocal 使用完毕后,务必调用 remove() 方法。

在 Web 应用中,请求处理结束后,线程会被放回线程池。如果 ThreadLocal 没有被清除,那么下一次其他请求再拿到这个线程时,它会读取到上一个请求残留的数据,导致业务逻辑出错。因此,正确使用模式通常是:

Java

1
2
3
4
5
6
7
8
9
ThreadLocal<String> threadLocal = new ThreadLocal<>();

try {
threadLocal.set("My Data");
// do some work
} finally {
// 确保在任何情况下都执行清理操作
threadLocal.remove();
}

继承问题

ThreadLocal 的值不会自动传递给子线程。如果你需要父线程创建子线程时,让子线程也能访问父线程的 ThreadLocal 值,你需要使用 InheritableThreadLocal。不过,InheritableThreadLocal 同样需要注意内存泄漏问题,并且在线程池环境下使用时可能会有意外行为,需要格外小心。

Collection (集合框架):

  • 核心接口: Collection (父接口), List, Set, Map。
  • Iterable 接口: Collection 接口继承了 Iterable 接口,使得所有集合都可以通过增强for 循环(foreach)进行遍历。

A. List 接口及其实现类

List 是一种有序集合,可以包含重复元素。

1. ArrayList
  • 底层实现:基于动态数组(Object[] elementData)实现。

  • 特点:

    • 有序:元素有插入顺序,可以通过索引访问(get(index))。
    • 可重复:允许存储重复元素。
    • 随机访问效率高:通过索引访问元素(get(index))速度非常快,时间复杂度为O(1)。这是因为数组在内存中是连续存储的,可以通过基地址和偏移量直接计算出元素的内存地址。
    • 插入和删除效率低:
      • 在数组末尾添加或删除元素效率较高(平均O(1))。
      • 在数组中间插入或删除元素时,需要使用System.arraycopy()移动被影响位置之后的所有元素,时间复杂度为 O(n)。
    • 线程不安全:在多线程环境下,如果一个线程正在修改 ArrayList,而另一个线程正在读取或修改它,可能会导致数据不一致或 ConcurrentModificationException(在使用迭代器时)。
  • 扩容机制:

    • 初始容量:默认情况下,当你创建一个无参的ArrayList时,它的底层数组是空的(DEFAULTCAPACITY_EMPTY_ELEMENTDATA, 即 new Object[0])。首次添加元素时,内部数组会被初始化为默认容量 DEFAULT_CAPACITY (JDK8为10)。如果你在创建时指定了容量(new ArrayList<>(capacity)),则初始容量就是你指定的。
    • 扩容时机:当ArrayList 的当前元素个数(size)等于底层数组的容量(elementData.length)时,就会触发扩容。
    • 扩容方式:扩容逻辑位于grow()方法中。
      • 计算新的容量:newCapacity = oldCapacity + (oldCapacity >> 1),即新容量是旧容量的1.5倍。
      • 如果计算出的新容量仍然小于需要的最小容量(minCapacity,即当前元素个数 size + 1),则直接将 minCapacity 作为新容量。
      • 如果新容量超出了MAX_ARRAY_SIZE(通常是 Integer.MAX_VALUE-8),则会尝试使用 Integer.MAX_VALUE,如果仍不足则抛出 OutOfMemoryError。
      • 创建一个新数组,并将旧数组中的元素复制到新数组中(Arrays.copyOf()内部调用 System.arraycopy())。
  • 为什么这么扩容(1.5倍):

    • 平衡空间与时间:
      • 相比于每次只增加1个元素,1.5倍的扩容策略减少了扩容的次数,从而减少了频繁进行数组复制带来的性能开销(数组复制是O(n)操作)。
      • 相比于2倍扩容,1.5倍的策略在空间利用率上更优,避免了过度分配和浪费过多内存。
    • 这是一个在时间和空间之间权衡的选择,旨在提供一个相对高效且内存友好的动态数组实现。
  • 可能出现的问题:

    • ConcurrentModificationException: 在多线程环境中,如果一个线程正在遍历 ArrayList(通过迭代器或增强 for循环),而另一个线程同时对其进行结构性修改(添加、删除元素等),就会抛出此异常。这是因为ArrayList 的迭代器是快速失败(fail-fast)的,它会检查 modCount(修改次数)是否与迭代器创建时一致。不一致则抛出异常。
    • 内存开销:如果预估容量不准确,频繁扩容会导致多次数组复制,增加 CPU和内存开销。
    • 内存浪费:如果初始容量设置过大,而实际使用的元素很少,会导致内存浪费。
  • 常用方法:

    • add(E e): 在列表末尾添加元素。
    • add(int index, E e): 在指定位置插入元素。
    • remove(int index) / remove(Object o): 删除指定位置或指定元素的第一个匹配项。
    • get(int index):获取指定位置的元素。
    • set(int index, E e): 替换指定位置的元素。
    • size(): 返回列表中元素的个数。
    • indexOf(Object o) / lastIndexOf(Object o):返回元素第一次/最后一次出现的索引。
    • contains(Object o): 判断是否包含某个元素。
    • clear(): 清空列表。
  • 遍历方式:

    1. 传统 for 循环:

      Java

      1
      2
      3
      for (int i = 0; i < list.size(); i++) {
      System.out.println(list.get(i));
      }
    2. 增强 for 循环(foreach):

      Java

      1
      2
      3
      for (E element: list) { //内部使用迭代器
      System.out.println(element);
      }
    3. 迭代器(Iterator):

      Java

      1
      2
      3
      4
      5
      6
      7
      Iterator<E> it = list.iterator();
      while (it.hasNext()) {
      System.out.println(it.next());
      // 如果需要删除元素,必须使用 it.remove(),否则会抛出
      // ConcurrentModificationException
      // it.remove();
      }
    4. Java 8 Stream API:

      Java

      1
      2
      3
      list.forEach(System.out::println);
      // 或
      list.stream().forEach(System.out::println);
2. LinkedList
  • 底层实现:基于双向链表(Doubly Linked List)实现。每个节点都包含数据,以及指向前一个节点和后一个节点的引用。

  • 特点:

    • 有序:元素有插入顺序。
    • 可重复:允许存储重复元素。
    • 插入和删除效率高:在链表的任何位置插入或删除元素,只需修改前后节点的引用,时间复杂度为O(1)。
    • 随机访问效率低:get(index)操作需要从头节点或尾节点开始遍历链表直到目标索引,时间复杂度为O(n)。
    • 内存开销大:每个节点除了存储数据本身,还需要额外的内存空间存储两个指针(prev 和 next),因此相比 ArrayList,在存储相同数量元素时, LinkedList 占用更多内存。
    • 线程不安全:与ArrayList 类似,在多线程环境下不安全,可能抛出 ConcurrentModificationException。
  • 扩容机制:

    • LinkedList 基于链表实现,没有固定容量的概念,也无需进行扩容。每次添加元素就是创建一个新节点并连接到链表中。因此不存在 ArrayList 那样的数组复制开销。
  • 可能出现的问题:

    • ConcurrentModification Exception:同样在多线程环境下使用迭代器进行修改时可能发生。
    • 内存碎片/开销:频繁的节点创建和销毁,以及每个节点额外的指针开销,可能导致一定的内存碎片和更高的内存占用。
  • 常用方法:

    • add(E e) / addFirst(E e) / addLast(E e): 添加元素。
    • remove() / removeFirst() / removeLast():删除元素。
    • get(int index) / getFirst() / getLast(): 获取元素(get(int index)效率低)。
    • peek() / peekFirst() / peekLast(): 获取但不移除头部/尾部元素。
    • offer(E e) / offerFirst(E e) / offerLast (E e): 添加元素到队列/双端队列(通常不抛异常)。
    • poll() / pollFirst() / pollLast(): 获取并移除头部/尾部元素(为空返回null)。
    • push(E e) / pop():实现栈的入栈和出栈操作。
    • size(), isEmpty(), contains(Object o), clear().
  • 遍历方式:

    1. 传统 for 循环: for (int i = 0; i < list.size(); i++) { System.out.println(list.get(i)); }(不推荐,效率低)

    2. 增强 for 循环(foreach): for (E element: list) { System.out.println(element); }(推荐)

    3. 迭代器(Iterator):

      Java

      1
      2
      3
      4
      Iterator<E> it = list.iterator();
      while (it.hasNext()) {
      System.out.println(it.next());
      }
    4. Java 8 Stream API: list.forEach(System.out::println);

3. Vector
  • 底层实现:基于动态数组实现,与ArrayList 类似。
  • 特点:
    • 线程安全:所有公共方法都使用了synchronized 关键字进行同步,因此是线程安全的。
    • 效率低:因为所有操作都被同步,在单线程或并发读多写少的场景下,性能比 ArrayList 差。
    • 扩容机制: Vector 的扩容策略与 ArrayList 类似,但默认是翻倍扩容(即新容量是旧容量的2倍)。可以通过构造函数指定扩容增量。
  • 可能出现的问题:
    • 性能瓶颈:全局锁导致并发性能差。
  • 使用场景:已经被 java.util.concurrent 包中的并发集合(如 CopyOnWriteArrayList)取代,基本不再推荐使用。
4. Stack
  • 底层实现:继承自Vector,因此也是基于数组实现,并具有Vector 的线程安全性。
  • 特点:实现了后进先出(LIFO)的栈结构。
  • 常用方法:
    • push(E item):元素入栈。
    • pop():元素出栈。
    • peek(): 查看栈顶元素但不移除。
    • empty(): 判断栈是否为空。
    • search(Object o): 查找元素并返回离栈顶的距离。
  • 使用场景:不推荐使用,因为Stack 继承了Vector,而 Vector 本身有很多不适合栈操作的方法。通常使用 Deque 接口的实现类(如ArrayDeque 或 LinkedList)来代替栈,它们更灵活高效。

小结 List:

  • ArrayList: 随机访问多,插入删除少(尤其末尾操作)的场景。
  • LinkedList: 插入删除多,随机访问少的场景;或需要作为队列/栈使用的场景。
  • Vector / Stack:不推荐在现代Java开发中使用,除非有特殊历史兼容需求。

B. Set 接口及其实现类

Set 是一种无序集合,不允许重复元素。

1. HashSet
  • 底层实现:基于HashMap 实现。HashSet 内部使用一个 HashMap 实例来存储元素,HashSet 的元素作为HashMap的键(Key),而HashMap 的值(Value)则是一个固定的、无关紧要的 PRESENT 静态 Object 对象。

  • 特点:

    • 无序:不保证元素的存储顺序和迭代顺序。
    • 不可重复:元素唯一。通过元素的hashCode() 和 equals() 方法来判断元素的唯一性。当添加元素时,首先计算元素的hashCode(),然后根据哈希值找到对应的“桶”,再在该桶中遍历,如果存在 equals()为true 的元素,则不添加。
    • 允许 null元素:允许且只能存储一个 null 元素。
    • 查询、添加、删除的平均时间复杂度为(1) (在不发生哈希冲突或冲突较少的情况下)。最坏情况下(所有元素哈希冲突到同一个桶),会退化为 O(n)。
    • 线程不安全:与HashMap 类似,非同步。
  • 扩容机制:

    • 由于底层是HashMap,其扩容机制与 HashMap 完全相同。
    • 初始容量:默认初始容量为16。
    • 负载因子:默认负载因子为0.75。
    • 扩容时机:当HashSet 中存储的元素数量达到容量*负载因子时,就会进行扩容,新容量是旧容量的2倍。
    • 扩容过程:创建一个新的两倍大小的底层数组,然后遍历旧数组中的所有元素,重新计算它们的哈希值,并将它们放入新数组的正确位置。
  • 可能出现的问题:

    • 性能下降: 如果自定义类作为元素存储在 HashSet 中,但没有正确重写 hashCode() 和 equals() 方法,可能会导致元素重复,或者哈希冲突严重,从而导致性能急剧下降。
    • ConcurrentModicationException: 同步性问题,在多线程环境下使用迭代器修改集合时会抛出。
  • 常用方法:

    • add(E e): 添加元素。
    • remove(Object o): 删除元素。
    • contains(Object o): 判断是否包含元素。
    • size(): 返回集合中元素的个数。
    • isEmpty(), clear().
  • 遍历方式:

    1. 增强 for 循环 (foreach): for (E element : set) { System.out.println(element); }

    2. 迭代器 (Iterator):

      Java

      1
      2
      3
      4
      Iterator<E> it = set.iterator();
      while (it.hasNext()) {
      System.out.println(it.next());
      }
    3. Java 8 Stream API: set.forEach(System.out::println);

2. LinkedHashSet
  • 底层实现: 继承自 HashSet,内部使用 LinkedHashMap 实现。
  • 特点:
    • 除了具备 HashSet 的所有特性外,最大的特点是保持元素的插入顺序(或者访问顺序,如果配置为 LRU 缓存)。这意味着遍历 LinkedHashSet时,元素的顺序与它们被添加到集合中的顺序一致。
    • 维护了一个双向链表,用于维护元素的插入顺序。
  • 扩容机制: 与 HashSet 和 HashMap 相同。
  • 使用场景: 需要去重,同时又需要保持元素插入顺序的场景。
3. TreeSet
  • 底层实现: 基于 TreeMap 实现。TreeSet 内部使用一个 TreeMap 实例来存储元素,TreeSet 的元素作为 TreeMap 的键,而值则是一个固定的 Object。
  • 特点:
    • 有序: 元素会根据其自然排序(元素必须实现 Comparable 接口)或者在创建 TreeSet 时提供的 Comparator 进行排序。
    • 不可重复: 元素唯一,唯一性通过比较结果判断(compareTo() 或 compare() 方法返回 0)。
    • 不允许 null 元素: 不允许存储 null 元素(因为 null 无法进行比较)。
    • 查询、添加、删除的时间复杂度为 O(log n),因为底层是红黑树。
    • 线程不安全: 非同步。
  • 扩容机制:
    • 由于底层是红黑树,没有传统意义上的扩容机制。每次添加元素就是增加一个节点,并根据红黑树的平衡规则进行调整(旋转和变色)来保持树的平衡。
  • 可能出现的问题:
    • 性能: 相比 HashSet,性能略低,因为涉及比较和树的平衡操作。
    • 元素必须可比较: 如果存储的元素没有实现 Comparable 接口,或者创建 TreeSet 时没有提供 Comparator,则会抛出 ClassCastException。
    • ConcurrentModicationException: 同步性问题。
  • 常用方法:
    • 与 HashSet 类似,但额外提供了与排序相关的方法,如 rst(), last(), headSet(), tailSet(), subSet() 等。

小结 Set:

  • HashSet: 最常用,需要快速查找、去重,不关心元素顺序的场景。
  • LinkedHashSet: 需要去重,同时又需要保持元素插入顺序的场景。
  • TreeSet: 需要去重,并且希望元素自动按照自然顺序或自定义顺序排序的场景。

C. Map 接口及其实现类

Map 存储键值对,键是唯一的,值可以重复。

1. HashMap
  • 底层实现: 基于哈希表实现,JDK 8 及以后是数组 + 链表 + 红黑树。

    • 数组: Node
    • 链表: 用于解决哈希冲突,将哈希到同一个索引位置的键值对以链表形式连接。
    • 红黑树: 当链表长度达到一定阈值(JDK 8 默认为 8)时,为了提高查找效率,该链表会转换为红黑树。当红黑树节点数少于一定阈值(JDK 8 默认为 6)时,会退化为链表。
  • 特点:

    • 无序: 不保证键值对的存储和迭代顺序。
    • 键唯一,值可重复: 键通过 hashCode() 和 equals() 方法确定唯一性。
    • 允许 null 键和 null 值: 只能有一个 null 键(存储在索引 0 的位置),可以有多个 null 值。
    • 查询、添加、删除的平均时间复杂度为 O(1),最坏情况下为 O(n)(链表)或 O(logn)(红黑树,JDK 8 及以后)。
  • 扩容机制:

    • 初始容量 (initialCapacity): 默认值为 16。最好在创建 HashMap 时预估并指定一个合适的初始容量,以减少扩容次数。

    • 负载因子 (loadFactor): 默认值为 0.75。表示哈希表在进行扩容前的填充比例。

    • 扩容时机: 当 HashMap 中存储的元素数量 (size) 达到 容量 * 负载因子 (即 threshold) 时,就会触发扩容。

    • 扩容方式: resize() 方法。

      1. 创建一个新的两倍大小的底层数组。
      2. 遍历旧数组中的所有键值对。
      3. 重新计算每个键的哈希值,并根据新的容量大小,将其放入新数组的正确位置。这个过程被称为再哈希 (rehash)。
      • JDK 8 优化: 在链表转换时,避免了每个节点单独重新计算哈希值,而是根据原索引和新容量的关系,直接判断节点在新数组中的位置,提高了效率。
  • 为什么这么扩容 (2 倍):

    • 位运算优化: 容量始终保持 2 的幂次方,可以利用位运算 (h & (length - 1)) 来替代取模运算 h % length,提高哈希值到索引的映射效率。
    • 减少哈希冲突: 扩容为 2 倍可以有效分散哈希冲突,使得更多的键能够映射到不同的桶,从而降低链表/红黑树的长度,保持 O(1) 的平均性能。
  • 为什么负载因子是 0.75:

    • 这是一个在“空间利用率”和“查询效率”之间的权衡。
    • 如果负载因子过小,会频繁扩容,浪费空间。
    • 如果负载因子过大,哈希冲突会增加,链表/红黑树变长,导致查询效率下降。
    • 0.75 这个值是经过实践验证,在大多数情况下能够提供较好性能的平衡点。
  • 可能出现的问题:

    • 性能下降: 如果自定义类作为键存储在 HashMap 中,但没有正确重写 hashCode() 和 equals() 方法,会导致元素重复,或者哈希冲突严重,从而导致性能急剧下降。
    • 多线程问题: 在多线程环境下,对 HashMap 进行修改操作可能导致数据丢失、死循环(JDK 7 及以前),或 ConcurrentModicationException。这是其最大的问题。
    • 内存开销: 频繁扩容会带来数组复制的开销。
  • 常用方法:

    • put(K key, V value): 关联键值对。
    • get(Object key):
  • 遍历方式:

    1. 遍历键集 (keySet()):

      Java

      1
      2
      3
      4
      for (K key : map.keySet()) {
      V value = map.get(key);
      System.out.println("Key: " + key + ", Value: " + value);
      }
    2. 遍历键值对集 (entrySet()) - 推荐,效率最高:

      Java

      1
      2
      3
      for (Map.Entry<K, V> entry : map.entrySet()) {
      System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
      }
    3. 遍历值集 (values()):

      Java

      1
      2
      3
      for (V value : map.values()) {
      System.out.println("Value: " + value);
      }
    4. 迭代器 (Iterator):

      Java

      1
      2
      3
      4
      5
      Iterator<Map.Entry<K, V>> it = map.entrySet().iterator();
      while (it.hasNext()) {
      Map.Entry<K, V> entry = it.next();
      System.out.println("Key: " + entry.getKey() + ", Value: " + entry.getValue());
      }
    5. Java 8 Stream API:

      1
      map.forEach((key, value) -> System.out.println("Key: " + key + ", Value: " + value));
2. LinkedHashMap
  • 底层实现: 继承自 HashMap,额外维护了一个双向链表,用于维护插入顺序或访问顺序。
  • 特点:
    • 除了具备 HashMap 的所有特性外,最大的特点是保持键值对的插入顺序(或访问顺序)。这意味着遍历 LinkedHashMap 时,元素的顺序与它们被添加到 Map 中的顺序一致。
    • 可以用于实现 LRU (Least Recently Used) 缓存策略,通过覆盖 removeEldestEntry 方法并设置访问顺序为 true。
  • 扩容机制: 与 HashMap 相同。
  • 使用场景: 需要快速查找,同时又需要保持插入顺序的场景。
3. TreeMap
  • 底层实现: 基于红黑树(Red-Black Tree)实现。
  • 特点:
    • 有序: 键会根据其自然排序(键的类型必须实现 Comparable 接口)或者在创建 TreeMap 时提供的 Comparator 进行排序。
    • 键唯一,值可重复: 唯一性判断依赖于键的比较结果(compareTo() 或 compare() 方法返回 0)。
    • 不允许 null 键: 不允许存储 null 键(因为 null 无法进行比较),但允许 null 值。
    • 查询、添加、删除的时间复杂度为 O(log n),因为底层是红黑树。
    • 线程不安全: 非同步。
  • 扩容机制:
    • 由于底层是红黑树,没有传统意义上的扩容机制。每次添加键值对就是增加一个节点,并根据红黑树的平衡规则进行调整(旋转和变色)来保持树的平衡。
  • 可能出现的问题:
    • 性能: 相比 HashMap,性能略低,因为涉及比较和树的平衡操作。
    • 键必须可比较: 如果键没有实现 Comparable 接口,或者创建 TreeMap 时没有提供 Comparator,则会抛出 ClassCastException。
    • ConcurrentModicationException: 同步性问题。
  • 常用方法:
    • 与 HashMap 类似,但额外提供了与排序相关的方法,如 rstKey(), lastKey(), ceilingEntry(), oorEntry() 等。
4. Hashtable
  • 底层实现: 基于哈希表实现,与 HashMap 类似,但所有方法都使用了 synchronized 关键字。
  • 特点:
    • 线程安全: 所有公共方法都进行了同步处理。
    • 效率低: 全局锁导致并发性能差。
    • 不允许 null 键和 null 值。
    • 初始容量和扩容机制: 默认初始容量 11,负载因子 0.75。扩容时新容量是旧容量的 2 倍 + 1。
  • 使用场景: 已被 ConcurrentHashMap 取代,基本不再推荐使用。
5. ConcurrentHashMap (JUC 包中的并发集合)
  • 底层实现:

    • JDK 7 及以前: 采用分段锁 (Segment) 的方式,将 HashMap 内部数据分成多个段(Segment),每个段是一个独立的 ReentrantLock。锁住某个段时,不影响其他段的操作。
    • JDK 8 及以后: 放弃了分段锁,改为使用 CAS (Compare-And-Swap) 操作和 synchronized 关键字(只在链表/红黑树头节点发生竞争时才使用,锁住的范围更小)来保证线程安全。
  • 特点:

    • 线程安全: 高并发环境下性能优异。
    • 不允许 null 键和 null 值。
    • 读操作基本无锁。
  • 扩容机制: 与 HashMap 类似,JDK 8 中,每个 Node 数组的扩容是独立的,通过 transfer 方法实现。

  • 使用场景: 高并发场景下替代 HashMap 和 Hashtable 的首选。

  • java.util.concurrent (JUC) 包是 Java 并发编程的高级工具包,它提供了比传统 synchronized 关键字和 wait/notify 机制更强大、更灵活的并发控制手段。

    并发工具类

    这些工具类都是为了解决特定场景下的线程协作问题而设计的,它们是 JUC 包的精华所在。

    • CountDownLatch (倒计时器):就像一个比赛的倒计时牌。主线程在等待,而其他多个子线程在执行自己的任务。每完成一个任务,倒计时就减一。当倒计时减到零时,主线程才能继续执行。这非常适合“一等多”的场景,比如主线程需要等待所有子线程的数据加载完成后再开始处理。
    • CyclicBarrier (循环屏障):可以看作是一个“集合点”。多个线程都跑到这里,互相等待。当所有线程都到达集合点后,它们才会一起继续前进。这个过程可以重复使用(“循环”),适合需要多个线程分阶段同步执行的场景。
    • Semaphore (信号量):用来控制同时访问某个资源的线程数量。你可以把它想象成一个拥有固定数量许可证的停车场。当一个线程需要访问资源时,它就获取一个许可证。当许可证用完时,其他线程就必须等待。线程用完资源后归还许可证,下一个线程才能进入。这可以用来限制并发连接数,比如数据库连接池。
    • Exchanger (交换器):提供一个两个线程间的数据交换点。当两个线程都运行到这个同步点时,它们会互相交换数据。这通常用于两个线程互相传递数据的场景,比如生产者和消费者线程之间的协作。

    原子操作类

    • AtomicInteger, AtomicLong 等原子类:这些类提供了一些“原子性”的操作,意思是这些操作是不可分割的。例如,i++ 在多线程环境下不是一个原子操作,可能导致数据不一致。而 AtomicIntegerincrementAndGet() 方法就能保证自增操作是线程安全的,并且性能比使用 synchronized 关键字要高。它们通常用于需要对单个变量进行安全、高效地更新的场景。

小结 Map:

  • HashMap: 最常用,需要快速查找,不关心键值对顺序,且在单线程或由外部同步机制保证线程安全的场景。
  • LinkedHashMap: 需要快速查找,同时需要保持插入顺序或访问顺序的场景(如实现 LRU 缓存)。
  • TreeMap: 需要根据键的自然顺序或自定义顺序排序的场景。
  • ConcurrentHashMap: 高并发场景下对 Map 进行读写操作的首选。
  • Hashtable: 已被淘汰,不推荐使用。

Java IO 流

Java IO (Input/Output) 流是用于处理计算机与外部设备之间数据传输的抽象概念。它将数据抽象为流 (Stream),通过流可以实现数据的输入和输出。

IO 流的分类

Java IO 流根据不同的标准有多种分类方式:

  • 按数据类型分:
    • 字节流:处理字节数据,所有文件类型(文本、图片、音视频等)都可以用字节流处理。
      • 抽象基类:InputStream(输入流)、OutputStream(输出流)。
      • 常用实现:FileInputStream/FileOutputStream(文件操作),BufferedInputStream/BufferedOutputStream(带缓冲),ObjectInputStream/ObjectOutputStream(对象序列化),ByteArrayInputStream/ByteArrayOutputStream(内存操作),DataInputStream/DataOutputStream(基本数据类型操作)。
    • 字符流:处理字符数据,专门用于处理文本文件。
      • 抽象基类:Reader(输入流)、Writer(输出流)。
      • 常用实现:FileReader/FileWriter(文件操作),BufferedReader/BufferedWriter(带缓冲),InputStreamReader/OutputStreamWriter(字节流与字符流的转换)。
  • 按流向分:
    • 输入流:从数据源读取数据到程序中。
    • 输出流:从程序中写入数据到目的地。
  • 按功能分:
    • 节点流(或源头流):直接与数据源(如文件、内存、网络连接)连接的流。例如FileInputStream、FileReader。
    • 处理流(或包装流):对已存在的节点流进行包装,增加新的功能或提升性能。例如BufferedInputStream、BufferedReader。

常用 IO 操作

  • 文件读写(字节流)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 写入文件
    try (FileOutputStream fos = new FileOutputStream("output.txt")) {
    fos.write("Hello, World!".getBytes());
    } catch (IOException e) {
    e.printStackTrace();
    }

    // 读取文件
    try (FileInputStream fis = new FileInputStream("output.txt")) {
    int data;
    while ((data = fis.read()) != -1) {
    System.out.print((char) data);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
  • 文件读写(字符流)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    // 写入文件
    try (FileWriter fw = new FileWriter("output_char.txt")) {
    fw.write("你好,世界!");
    } catch (IOException e) {
    e.printStackTrace();
    }

    // 读取文件
    try (FileReader fr = new FileReader("output_char.txt")) {
    int data;
    while ((data = fr.read()) != -1) {
    System.out.print((char) data);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
  • 缓冲流

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    // 使用缓冲字节流复制文件
    try (BufferedInputStream bis = new BufferedInputStream(new FileInputStream("source.txt"));
    BufferedOutputStream bos = new BufferedOutputStream(new FileOutputStream("destination.txt"))) {
    byte[] buffer = new byte[1024];
    int bytesRead;
    while ((bytesRead = bis.read(buffer)) != -1) {
    bos.write(buffer, 0, bytesRead);
    }
    } catch (IOException e) {
    e.printStackTrace();
    }
  • 对象序列化

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    // 定义一个可序列化的类
    class User implements Serializable {
    private static final long serialVersionUID = 1L; // 序列化版本UID
    String name;
    int age;
    transient String password; // transient 关键字修饰的字段不参与序列化

    public User(String name, int age, String password) {
    this.name = name;
    this.age = age;
    this.password = password;
    }

    @Override
    public String toString() {
    return "User(name=" + name + ", age=" + age + ", password=" + password + ")";
    }
    }

    // 序列化
    try (ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("user.ser"))) {
    User user = new User("Alice", 30, "123456");
    oos.writeObject(user);
    } catch (IOException e) {
    e.printStackTrace();
    }

    // 反序列化
    try (ObjectInputStream ois = new ObjectInputStream(new FileInputStream("user.ser"))) {
    User deserializedUser = (User) ois.readObject();
    System.out.println(deserializedUser); // password 字段将为 null
    } catch (IOException | ClassNotFoundException e) {
    e.printStackTrace();
    }

NIO (New Input/Output)

Java NIO (New I/O) 是在 JDK 1.4 中引入的一套新的 I/O API,它提供了非阻塞 I/O 的能力,并引入了“通道 (Channel)”和“缓冲区 (Buffer)”的概念,与传统的基于流 (Stream) 的 I/O 相比,NIO 更加高效。

核心组件:

  • Channel (通道): 类似于传统 IO 中的流,但可以双向读写。数据总是通过通道读入缓冲区或从缓冲区写入通道。
    • 常用实现: FileChannel (文件), SocketChannel (TCP 客户端), ServerSocketChannel (TCP 服务器), DatagramChannel (UDP)。
  • Buffer (缓冲区): 用于存储数据 (字节数组),与通道进行交互。所有数据读写都是通过缓冲区完成的。缓冲区有多种类型,如 ByteBuffer、CharBuffer、IntBuffer 等。
    • 核心属性:
      • capacity: 缓冲区可容纳的最大数据量。一旦创建,容量不可变。
      • limit: 缓冲区中可读或可写的上限。
      • position: 下一个读或写的位置。
      • mark: 标记当前 position,可以通过 reset() 恢复到 mark 的位置。
    • 主要方法:
      • put(): 向缓冲区写入数据。
      • get(): 从缓冲区读取数据。
      • flip(): 将缓冲区从写模式切换到读模式。limit 会设置为当前的 position, position 会重置为 0。
      • clear(): 清空缓冲区,为新的写入做准备。position 设为 0, limit 设为 capacity。
      • compact(): 压缩缓冲区,将未读的数据移到缓冲区开头,position 设为未读数据数量,limit 设为 capacity。
      • rewind(): 将 position 设为 0,可以重复读取缓冲区中的数据。
  • Selector (选择器): 用于监听多个通道上的事件 (如连接就绪、读就绪、写就绪等)。一个单线程可以管理多个通道,从而实现非阻塞 I/O。

NIO 与传统 IO 的区别:

  • I/O 模式:传统 IO 是阻塞式 I/O, NIO 是非阻塞式 I/O。
  • 流与缓冲区:传统 IO 基于流(单向), NIO 基于通道和缓冲区(双向)。
  • 同步与异步:传统 IO 是同步阻塞的, NIO 是同步非阻塞的(在多路复用模型下)。

NIO 文件复制示例

1
2
3
4
5
6
7
8
9
10
11
12
13
try (FileInputStream fis = new FileInputStream("source.txt");
FileChannel inChannel = fis.getChannel();
FileOutputStream fos = new FileOutputStream("destination_nio.txt");
FileChannel outChannel = fos.getChannel()) {
ByteBuffer buffer = ByteBuffer.allocate(1024);
while (inChannel.read(buffer) != -1) {
buffer.flip(); // 切换到读模式
outChannel.write(buffer); // 从缓冲区写入通道
buffer.clear(); // 清空缓冲区, 为下次写入做准备
}
} catch (IOException e) {
e.printStackTrace();
}

AIO (Asynchronous Input/Output)

Java AIO (Asynchronous I/O) 是在 JDK 7 中引入的,也称为 NIO 2.0。它提供了真正的异步非阻塞 I/O,通过回调机制来处理 I/O 操作的结果。与 NIO 的同步非阻塞不同,AIO 在 I/O 操作完成后会主动通知应用程序。

  • 核心概念:

    • AsynchronousFileChannel:异步文件通道。
    • AsynchronousSocketChannel:异步 Socket 通道。
    • AsynchronousServerSocketChannel:异步 Server Socket 通道。
    • CompletionHandler:回调处理器接口,定义了 completed() (操作成功) 和 failed() (操作失败) 方法。
    • Future:也可以通过返回 Future 对象来获取异步操作的结果。
  • **工作原理:**当发起一个 I/O 操作时,不再需要等待操作完成,而是立即返回。I/O 操作由操作系统在后台完成,完成后通过回调函数通知应用程序。

  • AIO 优势:

    • 真正的异步非阻塞:应用程序不需要等待 I/O 操作,可以将 CPU 资源用于其他任务。
    • 提高并发性:特别适合高并发、长连接的网络应用。
  • AIO 劣势:

    • 复杂性:编程模型相对于 NIO 更复杂,需要处理回调逻辑。
    • 适用场景:对于连接数较多且连接时间长的应用,如聊天服务器,AIO 表现优异。对于短连接、高并发的场景,NIO(基于 Selector)可能表现更好。
  • AIO 读文件示例

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    Path file = Paths.get("async_read.txt");
    ByteBuffer buffer = ByteBuffer.allocate(1024);

    try (AsynchronousFileChannel fileChannel = AsynchronousFileChannel.open(file, StandardOpenOption.READ)) {
    fileChannel.read(buffer, 0, buffer, new CompletionHandler<Integer, ByteBuffer>() {
    @Override
    public void completed(Integer result, ByteBuffer attachment) {
    System.out.println("Read " + result + " bytes.");
    attachment.flip();
    byte[] data = new byte[attachment.remaining()];
    attachment.get(data);
    System.out.println("Content: " + new String(data));
    }

    @Override
    public void failed(Throwable exc, ByteBuffer attachment) {
    System.err.println("Read failed: " + exc.getMessage());
    }
    });
    // 为了让主线程不立即退出,等待异步操作完成
    Thread.sleep(1000);
    } catch (IOException | InterruptedException e) {
    e.printStackTrace();
    }

Java 反射 (Reflection)

Java 反射机制是指在程序运行时,能够动态地获取类的信息(包括类的属性、方法、构造器等),并能够动态地操作类或对象(如创建对象、调用方法、修改属性)。

  • 核心类与接口:

    • Class 类:代表类的字节码文件,是反射的入口。
    • Constructor 类:代表类的构造器。
    • Method 类:代表类的方法。
    • Field 类:代表类的成员变量(属性)。
    • AccessibleObject:Field, Method, Constructor 的共同父类,提供了 setAccessible(true) 方法,用于抑制 Java 语言访问检查,从而访问私有成员。
  • 获取 Class 对象的三种方式:

    1. Class.forName(“全限定类名”):最常用,动态加载类。
    1
    Class<?> clazz = Class.forName("java.lang.String");
    1. 类名.class:已知具体类名时使用,编译时加载。
    1
    Class<?> clazz = String.class;
    1. 对象.getClass():通过对象实例获取,运行时获取。
    1
    2
    String s = "hello";
    Class<?> clazz = s.getClass();
  • 反射的应用:

    • 动态创建对象:
      1
      2
      3
      4
      5
      Class<?> personClass = Class.forName("com.example.Person");
      Object person = personClass.newInstance(); // 调用无参构造器
      // 或者调用指定构造器
      Constructor<?> constructor = personClass.getConstructor(String.class, int.class);
      Object person2 = constructor.newInstance("Alice", 25);
    • 动态调用方法:
      1
      2
      3
      4
      5
      6
      Class<?> personClass = Class.forName("com.example.Person");
      Object person = personClass.newInstance();
      Method setNameMethod = personClass.getMethod("setName", String.class);
      setNameMethod.invoke(person, "Bob"); // 调用 setName 方法
      Method getNameMethod = personClass.getMethod("getName");
      String name = (String) getNameMethod.invoke(person); // 调用 getName 方法
    • 动态操作属性:
      1
      2
      3
      4
      5
      6
      Class<?> personClass = Class.forName("com.example.Person");
      Object person = personClass.newInstance();
      Field nameField = personClass.getDeclaredField("name"); // 获取私有属性
      nameField.setAccessible(true); // 允许访问私有属性
      nameField.set(person, "Charlie"); // 设置属性值
      String name = (String) nameField.get(person); // 获取属性值
  • 反射的优缺点:

    • 优点:
      • 灵活性和动态性:在运行时动态获取类信息和操作对象,大大增强了程序的灵活性,是许多框架(如Spring、ORM框架)和工具(如JSON解析库)的基础。
      • 解耦:允许代码在编译时不知道具体的类,只在运行时加载和使用,实现高度解耦。
    • 缺点:
      • 性能开销:反射操作比直接调用有更高的性能开销,因为涉及到动态解析和查找。
      • 安全性问题:setAccessible(true)可以绕过Java的访问控制,可能破坏封装性。
      • 可维护性差:反射代码通常比直接调用更复杂,更难调试和维护。
      • 编译时检查缺失:反射操作在编译时无法检查类型错误,只能在运行时发现。

JVM内存结构与垃圾收集器

JVM内存结构

JVM(Java Virtual Machine)在执行Java程序时,会将内存划分为几个不同的区域,这些区域有各自的用途和生命周期。

  • 程序计数器 (Program Counter Register)

    • 功能:一块较小的内存空间,用于存储当前线程所执行的字节码的行号指示器。
    • 特点:
      • 每个线程私有,生命周期与线程一致。
      • JVM规范中唯一没有规定任何OutOfMemoryError情况的区域。
      • 在多线程切换时,程序计数器记录了当前线程的执行位置,使得线程切换回来后能够知道从哪里继续执行。
  • Java虚拟机栈 (Java Virtual Machine Stacks)

    • 功能:每个线程私有的内存区域,用于存储栈帧(Stack Frame)。每个方法被执行时都会创建一个栈帧,用于存储局部变量表、操作数栈、动态链接、方法出口等信息。
    • 特点:
      • 生命周期与线程一致。
      • 局部变量表:存储方法参数和方法内部定义的局部变量。
      • 操作数栈:用于存放方法执行时的操作数和中间结果。
      • 动态链接:指向运行时常量池中该栈帧所属方法的引用。
      • 方法出口:记录方法执行完后回到哪里。
      • 可能抛出StackOverflowError(栈深度超过虚拟机允许的深度)或OutOfMemoryError(栈扩展时无法申请到足够的内存)。
  • 本地方法栈 (Native Method Stacks)

    • 功能:与虚拟机栈类似,但是为JVM执行Native方法(即用C/C++等语言实现的方法)服务。
    • 特点:
      • 每个线程私有。
      • 可能抛出 StackOverflowError 或 OutOfMemoryError。
  • Java 堆 (Java Heap)

    • 功能:JVM 管理的最大一块内存区域,被所有线程共享,用于存放对象实例和数组。
    • 特点:
      • 是垃圾收集器管理的主要区域 (GC 堆)。

      • 是 Java 应用程序对象存放的“老家”。

      • 根据垃圾回收的特性,可以分为新生代 (Young Generation) 和老年代 (Old Generation)。

      • 新生代:通常分为 Eden 空间和两个 Survivor 空间 (From 和 To)。新创建的对象优先在 Eden 区分配,经过 Minor GC 后存活的对象进入 Survivor 区,多次 GC 后仍存活的对象进入老年代。

      • 老年代:存放生命周期较长的对象。

      • 在 JVM 内存模型中,新生代被划分为三个区域,默认的比例通常是 8:1:1

        • Eden 区:占比 80%。这是新创建对象的主要分配区域。
        • Survivor S0 区:占比 10%
        • Survivor S1 区:占比 10%

        为什么要这样划分?

        这种划分是为了配合 Minor GC 的垃圾回收流程,从而提高垃圾回收的效率。

        新生代的垃圾回收流程

        1. 对象创建:新创建的对象首先在 Eden 区 分配。
        2. Minor GC:当 Eden 区 满了之后,会触发一次 Minor GC(也叫 Young GC)。
        3. 存活对象转移
          • 在 Eden 区和其中一个 Survivor 区(比如 S0)中,所有存活的对象会被复制到另一个空的 Survivor 区(比如 S1)。
          • 同时,对象的年龄(age)会加一。
        4. 清空 Eden 和 S0:垃圾回收后,Eden 区和 S0 区都会被清空。
        5. 角色互换:下一次 Minor GC 时,Eden 区和 S1 区中存活的对象会被复制到 S0 区。S0 和 S1 两个 Survivor 区会不断地进行角色互换。
        6. 晋升老年代
          • 当对象的年龄达到一个设定的阈值(默认为 15),或者 Survivor 区中同一年龄段的对象大小超过了一定比例,这些对象就会被移动到老年代
          • 这种设计也被称为复制算法(Copying Algorithm),它在新生代存活对象较少的情况下,效率非常高。

        为什么是 8:1:1?

        这个比例是一个经验值,基于大多数 Java 应用的特点:

        • 大多数对象都是朝生夕灭的。因此,将 Eden 区设置得更大,可以容纳更多的新对象,减少 Minor GC 的频率。
        • 两个 Survivor 区只需要用来暂存存活的对象,因此不需要太大。10% 的空间通常足以容纳一次 Minor GC 后存活的对象。

        当然,这个比例不是固定的。在某些特殊情况下,如果新生代存活对象较多,导致 Survivor 区无法容纳所有存活对象,JVM 会发生空间分配担保,将这些对象直接晋升到老年代。

        你可以通过 JVM 参数来调整这个比例,例如: java -Xmn100m -XX:SurvivorRatio=8

        这个命令设置新生代总大小为 100MB,Eden 区和 Survivor 区的比例为 8:1:1。

      • 可能抛出 OutOfMemoryError。

  • 方法区 (Method Area)

    • 功能:被所有线程共享的内存区域,用于存储已被虚拟机加载的类信息、常量、静态变量、即时编译器编译后的代码等数据。
    • 特点:
      • 在 JDK 1.7 及之前,方法区被称为“永久代 (Permanent Generation)”,它属于堆内存的一部分。
      • 在 JDK 1.8 及之后,永久代被移除,方法区的实现改为“元空间 (Metaspace)”,并且元空间不使用 JVM 内存,而是直接使用本地内存 (Native Memory)。
      • 可能抛出 OutOfMemoryError。
  • 运行时常量池 (Runtime Constant Pool)

    • 功能:方法区的一部分,用于存放编译期生成的各种字面量和符号引用。
    • 特点:
      • 动态性:Java 语言并不要求常量池在编译期就全部确定,运行时也可以将新的常量放入池中 (如 String.intern())。
      • 可能抛出 OutOfMemoryError。

JVM 调优参数

  • 堆内存大小设置:

    • -Xms:设置 JVM 堆的初始内存大小。
    • -Xmx:设置 JVM 堆的最大内存大小。
    • 最佳实践:通常建议 -Xms 和 -Xmx 设置为相同值,以避免 JVM 在运行时动态调整堆大小带来的额外开销和 GC 停顿。例如 -Xms4g -Xmx4g。
  • 新生代大小设置:

    • -Xmn:设置新生代内存大小。
    • -XX:NewRatio=:设置老年代与新生代的比例,例如 -XX:NewRatio=2 表示老年代:新生代 = 2:1。
    • 考量:
      • 新生代过小:频繁 Minor GC, 导致对象过早进入老年代。
      • 新生代过大:Minor GC 间隔长, 但每次 GC 耗时可能长。
  • 元空间大小设置 (JDK 1.8+):

    • -XX:MetaspaceSize=:设置元空间的初始大小。
    • -XX:MaxMetaspaceSize=:设置元空间的最大大小。
    • 考量:如果应用加载大量类或使用动态代码生成, 可能需要调大。
  • 选择垃圾收集器:

    • -XX:+UseG1GC:启用 G1 垃圾收集器。这是 JDK 9+ 的默认收集器。
    • -XX:MaxGCPauseMillis=:设置 G1 收集器可接受的最大停顿时间(G1 会尽量接近这个目标, 但不保证完全达到)。例如 -XX:MaxGCPauseMillis=200。

垃圾收集器 (Garbage Collector)

垃圾收集器是 JVM 的一个重要组成部分, 负责自动管理 Java 堆内存中的对象的生命周期, 回收不再使用的对象所占用的内存。

垃圾判断算法:

  • 引用计数算法:当一个对象被引用一次, 计数器加1;引用失效, 计数器减1。当计数器为0时, 对象被判定为可回收。
    • 缺点:难以解决对象之间的循环引用问题。Java 虚拟机不采用此算法。
  • 可达性分析算法 (Root Tracing):通过一系列称为 “GC Roots” 的对象作为起始点, 从这些节点向下搜索, 搜索所走过的路径称为引用链 (Reference Chain)。当一个对象到 GC Roots 没有任何引用链相连时, 则证明此对象是不可用的。
    • 可作为 GC Roots 的对象:
      • 虚拟机栈 (栈帧中的局部变量表) 中引用的对象。
      • 本地方法栈 (Native 方法) 中引用的对象。
      • 方法区中类静态属性引用的对象。
      • 方法区中常量引用的对象。
      • 被同步锁持有的对象。
      • JVM 内部的引用 (如基本数据类型对应的 Class 对象)。

常见垃圾收集器:

  • Serial 收集器:
    • 特点:单线程, 工作时需要停止所有用户线程 (“Stop The World”, STW)。简单高效, 适用于单核 CPU 或内存较小的客户端应用。
    • 新生代使用:复制算法。
    • 老年代使用:标记-整理算法。
  • ParNew 收集器:
    • 特点:Serial 收集器的多线程版本, 用于新生代。并行收集时也需要 STW。
    • 新生代使用:复制算法。
    • 常与 CMS 收集器配合使用。
  • Parallel Scavenge 收集器:
    • 特点:关注吞吐量(Throughput = 用户代码执行时间 / (用户代码执行时间 + GC 时间)),可以设置最大吞吐量或最大 GC 停顿时间。
    • 新生代使用:复制算法。
    • 老年代使用:与 Parallel Old 配合使用,使用标记-整理算法。
  • CMS (Concurrent Mark Sweep) 收集器:
    • 特点:以获取最短回收停顿时间为目标,并发收集(与用户线程一起执行)。适用于对响应时间要求高的应用(如 Web 服务器)。
    • 工作步骤:
      1. 初始标记 (Initial Mark):STW,标记 GC Roots 能直接关联到的对象,速度快。
      2. 并发标记 (Concurrent Mark):与用户线程并发执行,进行 GC Roots Tracing 过程,耗时最长。
      3. 重新标记 (Remark):STW,修正并发标记期间因用户程序继续运行而导致标记产生变动的对象,比初始标记耗时长,但远比并发标记短。
      4. 并发清除 (Concurrent Sweep):与用户线程并发执行,清除已标记为垃圾的对象。
    • 缺点:
      • 对 CPU 资源敏感:并发阶段会占用一部分 CPU。
      • 无法处理浮动垃圾:并发清除阶段产生的垃圾(新生成的对象)只能下次 GC 再处理。
      • 可能产生大量空间碎片:采用“标记-清除”算法,不进行整理,可能导致大对象无法分配空间而提前触发 Full GC。
  • G1 (Garbage-First) 收集器:
    • 特点:JDK 9+ 的默认垃圾收集器。面向服务端应用,分区(将 Java 堆划分为多个独立区域 Region),可预测的停顿时间模型。
    • 工作原理:
      • 将堆内存划分为多个大小相等的 Region。
      • G1 跟踪每个 Region 的垃圾回收价值 (Garbage-First),优先回收垃圾最多的 Region。
      • 年轻化和老年代不再是物理隔离,而是逻辑上的概念,Region 可以动态地成为 Eden、Survivor 或 Old 区域。
      • 并发与并行兼容:并发标记,但回收阶段并行。
      • 基本无碎片:采用复制和标记-整理算法结合。
    • 工作步骤:
      1. 初始标记 (Initial Mark):STW,标记 GC Roots 能直接关联的对象。
      2. 并发标记 (Concurrent Mark):与用户线程并发,遍历对象图。
      3. 最终标记 (Final Mark):STW,处理并发标记阶段结束后仍然存活的对象。
      4. 筛选回收 (Evacuation):STW,对各个 Region 的回收价值进行排序,根据预期停顿时间来回收 Region,采用复制算法将存活对象复制到新的 Region。
    • 优势:在保持高吞吐量的同时,降低了 GC 停顿时间,适合大内存、多核处理器场景。
  • zGC (Z Garbage Collector) 和 Shenandoah 收集器:
    • 特点:低延迟、并发 GC 收集器,旨在实现毫秒级的 GC 停顿。
    • ZGC: JDK 11 引入,支持 TB 级别的堆内存,停顿时间与堆大小无关。
    • Shenandoah: JDK 12 引入,与 G1 类似,但能进一步降低停顿时间。
    • 应用场景:对延迟要求极高的应用

类加载机制 (Class Loading Mechanism)

  • 作用: 将 .class 文件中的字节码加载到 JVM 内存中,并转换为运行时数据结构。

  • 生命周期: 加载 -> 验证 -> 准备 -> 解析 -> 初始化 -> 使用 -> 卸载。

  • 主要阶段:

    • 加载 (Loading):
      • 通过类的全限定名获取该类的二进制字节流。
      • 将这个字节流所代表的静态存储结构转化为方法区的运行时数据结构。
      • 在内存中生成一个代表该类的 java.lang.Class 对象。
    • 验证 (Verification): 确保 Class 文件的字节流符合 JVM 规范,没有安全问题。
    • 准备 (Preparation): 为类的静态变量(static fields)分配内存并初始化为默认值(如 int 变量为 0,引用类型为 null)。
    • 解析 (Resolution): 将常量池中的符号引用替换为直接引用。
    • 初始化 (Initialization): 执行类的构造器 <clinit>() 方法,真正开始执行类中定义的 Java 程序代码(为静态变量赋予初始值,执行静态代码块)。
  • 类加载器 (Class Loaders):

    • 类加载器的分类:

      (1)Bootstrap class loader (使用C++编写的)

      ​ 简称:启动类加载器

      加载路径:JAVA_HOME/jre/lib

      ​ 显示形式:null

      (2)Platform class loader(由Java编写的)

      ​ 简称:扩展类加载器

      加载路径:JAVA_HOME/jre/lib/ext

      ​ 显示形式:ExtClassLoader

      (3)System class loader(由Java编写的)

      ​ 简称:应用程序类加载器

      加载路径:类路径(src目录)

      ​ 显示形式:AppClassLoader //sun.misc.Launcher$AppClassLoader@18b4aac2

      (4)自定义类加载器(由Java编写的)

      ​ 简称:自定义类加载器

      ​ 加载路径:自定义

      1. 启动类加载器 (Bootstrap ClassLoader)

      • 作用:它负责加载 Java 核心库,比如 rt.jar(包含 java.lang.*, java.util.* 等核心类)。
      • 实现:它不是用 Java 写的,而是由 C++ 实现的,是 JVM 自身的一部分。因此,你无法在 Java 代码中直接获取到它的对象,调用 getClassLoader() 得到的会是 null
      • 父加载器:它没有父加载器。它是类加载器层次结构的顶端。

      2. 扩展类加载器 (Extension ClassLoader)

      • 作用:它负责加载 JVM 扩展目录中的所有 jar 包,通常是 JRE/lib/ext 目录下的库。
      • 实现:它是由 Java 语言实现的。
      • 父加载器:它的父加载器是启动类加载器

      3. 应用程序类加载器 (Application ClassLoader)

      • 作用:它负责加载我们自己编写的 Java 程序中的类,也就是你项目中 classpath 路径下的所有类。
      • 实现:它也是由 Java 语言实现的。
      • 父加载器:它的父加载器是扩展类加载器

      自定义类加载器

      除了上面三个,你也可以根据自己的需求创建自定义类加载器

      • 作用:当你需要加载一些特定来源的类时(比如从网络下载的类、对字节码进行加密或解密),自定义类加载器就很有用。例如,Web 服务器(如 Tomcat)就是通过自定义类加载器来隔离不同 Web 应用的类。
      • 如何实现:通常,你需要继承 java.lang.ClassLoader 类,并重写 findClass() 方法。在 findClass() 方法中,你需要自己定义如何获取类的字节码(比如从文件系统、网络或数据库),然后调用 defineClass() 方法将字节码转换为 Class 对象。
  • 双亲委派模型 (Parents Delegation Model):

    • 原理: 如果一个类加载器收到了类加载的请求,它首先不会自己去尝试加载这个类,而是把这个请求委派给父类加载器去完成。只有当父加载器反馈自己无法完成这个加载请求时,子加载器才会尝试自己去加载。
    • 优点: 避免类的重复加载,保证 Java 核心库的类型安全。

面试题:

为什么需要四个类加载器,而不是一个?

这是一个很好的问题。为什么 Java 要设计多个类加载器,而不是用一个大一统的加载器呢?这背后主要有三个核心原因:隔离性安全性可扩展性


1. 隔离性(Isolation)

多类加载器的最大优势就是实现了类隔离

想象一下,你有一个 Web 服务器,上面部署了两个不同的 Web 应用(比如一个博客系统和一个论坛)。这两个应用可能依赖同一个第三方库的不同版本(例如,博客用的是 Spring 框架的 5.x 版本,而论坛用的是 4.x 版本)。

如果只有一个类加载器,它会把所有 classpath 上的类都加载到同一个内存空间。这样一来,两个不同版本的 Spring 框架就会产生冲突,JVM 根本无法区分它们,程序就会报错。

而有了自定义类加载器,服务器就可以为每个 Web 应用创建一个独立的类加载器。每个加载器负责加载自己应用目录下的类,这样两个应用即使使用了同一个库的不同版本,也能在内存中和谐共存,互不干扰。


2. 安全性(Security)

Java 的核心库(如 java.lang, java.util 等)对 JVM 的稳定和安全至关重要。

双亲委派模型正是为了保护这些核心库不被恶意代码或普通用户代码所篡改。

例如,如果你尝试自己写一个 java.lang.String 类并把它放在应用的 classpath 下,双亲委派机制会确保这个类不会被加载。因为当加载请求到达应用程序类加载器时,它会先委派给它的父加载器——扩展类加载器,然后再委派给最顶层的启动类加载器。启动类加载器会优先加载 JDK 自带的 java.lang.String,从而你的恶意代码就永远没有机会被执行。


3. 可扩展性(Extensibility)

Java 是一种非常灵活的语言,需要适应各种复杂的运行时环境。

如果只有一个类加载器,所有的类都必须来自文件系统。但在现实世界中,类可能来自各种地方:

  • 从网络上下载的字节码。
  • 通过数据库存储和加载。
  • 对字节码进行加密和解密。

自定义类加载器让 Java 能够动态地加载处理非标准来源的类。例如,热部署技术(在不重启应用的情况下更新代码)就是通过自定义类加载器实现的。它会创建一个新的加载器来加载新版本的类,而旧的加载器和它的类则可以被垃圾回收。


总结

所以,一个大一统的类加载器虽然简单,但它无法解决复杂的类版本冲突核心库安全动态加载等问题。通过分层设计和双亲委派模型,Java 的类加载器体系变得既安全又灵活,能够满足从简单的桌面应用到复杂的企业级服务器等各种场景的需求。

常见的 OOM (OutOfMemoryError) 和 StackOverflowError

  • Java heap space: 堆内存不足,最常见。通常通过调整 JVM 启动参数 -Xms (初始堆大小) 和 -Xmx (最大堆大小) 来解决。
  • StackOverflowError: 虚拟机栈溢出。通常是递归调用过深导致栈帧不断入栈。
  • PermGen space / Metaspace: 方法区溢出。通常是加载的类过多,或者存在大量字符串常量。通过 -XX:MaxPermSize (JDK 1.7) 或 -XX:MaxMetaspaceSize (JDK 1.8) 调整。
  • Direct buffer memory: 直接内存溢出。通常是使用了 NIO 或 Netty 等直接操作堆外内存的库。
  • unable to create new native thread: 无法创建新线程。通常是系统线程数达到上限或内存不足以分配新线程的栈空间。

OOM常见场景:


1. java.lang.OutOfMemoryError: Java heap space

这是最常见、也最广为人知的内存溢出错误。它表示 Java 堆(Heap) 中没有足够的空间来分配新的对象。

常见场景:

  • 内存泄漏(Memory Leak):这是最主要的原因。你的程序中创建了对象,但本应被垃圾回收器(GC)回收的对象却因为某些原因(比如被一个长生命周期的对象引用着)而无法被回收。例如:
    • 一个静态的 MapList 集合,不断地往里面添加对象,但从不删除。
    • 监听器或回调函数没有正确移除,导致被监听的对象无法被回收。
    • 数据库连接或文件流没有正确关闭,长时间占用资源。
  • 内存使用不当:一次性加载大量数据到内存中。例如,从数据库查询数百万条记录,并把它们全部加载到一个 List 中;或者处理一个超大的图片或文件,导致瞬间占用大量内存。
  • 配置问题:JVM 的堆内存设置得太小,无法满足程序的正常运行需求。这在部署应用时很常见,可以通过调整 -Xmx 参数来解决。

2. java.lang.StackOverflowError

这个错误表示 虚拟机栈(Stack) 溢出。每个线程都有一个独立的栈,用于存储方法调用的栈帧。当栈的深度超过了 JVM 允许的最大深度时,就会抛出此错误。

常见场景:

  • 无限递归(Infinite Recursion):这是最典型的场景。一个方法不断地调用自身,且没有正确的退出条件。例如:

    Java

    1
    2
    3
    4
    public void recursiveMethod() {
    // 没有退出条件
    recursiveMethod();
    }

    另一个例子是两个方法互相调用,形成循环:A 调用 B,B 又调用 A。

  • 递归调用层级过深:即使递归有正确的退出条件,如果数据量过大,导致递归调用层级非常深,也可能导致栈溢出。例如,处理一个深度非常大的树形结构。


3. java.lang.OutOfMemoryError: PermGen space 或 Metaspace

这个错误发生在 方法区 溢出。方法区用于存储类的元数据信息,如类的结构、字段、方法、常量池等。

  • JDK 1.7 及之前:方法区在堆中,被称为永久代(PermGen)。溢出错误为 PermGen space
  • JDK 1.8 及之后:永久代被移除,方法区改为使用元空间(Metaspace),并且默认使用本地内存。溢出错误为 Metaspace

常见场景:

  • 动态生成大量类:在运行时生成大量新的类。这在一些使用字节码增强技术的框架(如 CGLib)或动态代理的场景中很常见。
  • 热部署:在像 Tomcat 这样的 Web 服务器中进行频繁的热部署操作,如果没有正确清理旧的类加载器,会导致旧的类元数据无法被回收,从而逐渐耗尽方法区内存。
  • 常量池溢出:在 JDK 1.7 之前,字符串常量池也在永久代中。如果程序创建了大量不同的字符串(例如在循环中不断生成新的字符串),也可能导致永久代溢出。

4. java.lang.OutOfMemoryError: Direct buffer memory

这个错误与 直接内存(Direct Memory) 相关,它不是 Java 堆的一部分,而是通过 ByteBuffer.allocateDirect() 在堆外分配的内存。

常见场景:

  • NIO 和网络编程:在使用 Java NIO、Netty、或者其他依赖堆外内存的库时,如果频繁地分配直接内存但没有及时释放,就可能导致此错误。
  • 内存泄漏:直接内存的回收不像堆内存那样由 GC 自动管理。如果程序中没有调用 ByteBuffercleaner() 方法,或者在没有关闭资源的情况下直接内存泄漏,就会耗尽系统的直接内存。

5. java.lang.OutOfMemoryError: unable to create new native thread

这个错误通常不是因为 Java 堆内存不足,而是因为系统资源耗尽

常见场景:

  • 线程创建过多:程序中创建了大量的线程,导致系统无法为新的线程分配内存空间。每个线程除了 Java 堆中的栈空间外,还需要分配一些本地内存。
  • 系统限制:操作系统对单个进程创建的线程数有限制。如果达到了这个上限,JVM 就会抛出此错误。在 Linux 系统中,你可以通过 ulimit -u 命令查看这个限制。
  • 内存不足:系统内存(包括堆外内存)已经所剩无几,JVM 无法为新的线程栈分配足够的内存 r。