第一阶段:Java核心基础巩固

- **知识点**:
- 堆、栈、方法区的区别。

在 Java 中,堆、栈和方法区是内存中的重要区域,它们在存储内容、生命周期、访问速度等方面存在明显区别,以下为你详细介绍:

### 1. 存储内容
- **堆(Heap)**:用于存储对象实例和数组。Java 中使用 `new` 关键字创建的对象都会被分配到堆内存中。
- **栈(Stack)**:主要存储局部变量、方法调用信息和操作数栈等。每个线程都有自己独立的栈,栈中的数据是线程私有的。
- **方法区(Method Area)**:用于存储类的元数据信息(如类的结构、字段、方法、构造函数等)、静态变量、常量池等。

### 2. 生命周期
- **堆**:堆内存的生命周期与整个应用程序的生命周期相同,在应用程序启动时创建,关闭时销毁。
- **栈**:栈内存的生命周期与线程相关,线程创建时分配栈内存,线程结束时栈内存被回收。
- **方法区**:方法区的生命周期与整个应用程序的生命周期相同,在应用程序启动时创建,关闭时销毁。

### 3. 访问速度
- **堆**:堆内存的访问速度相对较慢,因为对象存储在堆中,需要通过引用进行访问。
- **栈**:栈内存的访问速度较快,因为栈中的数据是按照后进先出(LIFO)的顺序存储的,访问时直接通过栈指针进行操作。
- **方法区**:方法区的访问速度介于堆和栈之间,因为类的元数据信息和常量池等需要进行一定的查找和解析操作。

### 4. 举例说明

public class MemoryExample {
// 静态变量,存储在方法区
public static final int CONSTANT = 10;
// 实例变量,对象实例存储在堆中
private int instanceVariable;

public MemoryExample(int value) {
this.instanceVariable = value;
}

public void printValue() {
// 局部变量,存储在栈中
int localVar = 20;
System.out.println("Instance variable: " + instanceVariable + ", Local variable: " + localVar);
}

public static void main(String[] args) {
// 创建对象,对象存储在堆中,引用存储在栈中
MemoryExample example = new MemoryExample(30);
// 调用方法,方法调用信息存储在栈中
example.printValue();
}
}

```

```

### 代码解释
- **堆**:在 `main` 方法中,使用 `new MemoryExample(30)` 创建了一个 `MemoryExample` 对象,该对象存储在堆中。`example` 是一个引用变量,存储在栈中,指向堆中的对象。
- **栈**:`main` 方法中的 `example` 引用变量和 `printValue` 方法中的 `localVar` 局部变量都存储在栈中。当 `main` 方法调用 `printValue` 方法时,会在栈中创建一个新的栈帧,用于存储该方法的局部变量和方法调用信息。
- **方法区**:`CONSTANT` 是一个静态常量,存储在方法区中。`MemoryExample` 类的元数据信息(如类的结构、方法、构造函数等)也存储在方法区中。

通过这个例子,可以清楚地看到 Java 堆、栈和方法区在存储内容和使用方式上的区别。

- 对象创建过程(类加载、内存分配、初始化)。

在 Java 中,对象的创建是一个复杂且涉及多个步骤的过程,主要包括类加载、内存分配和初始化三个主要阶段。下面将详细解释每个阶段,并结合示例代码进行说明。

### 1. 类加载(Class Loading)
类加载是对象创建的第一步,当 Java 程序试图使用某个类时,Java 虚拟机(JVM)会检查该类是否已经被加载到内存中。如果尚未加载,JVM 会通过类加载器将该类的字节码文件(.class 文件)加载到内存中,并创建一个 `java.lang.Class` 对象来表示这个类。类加载过程又可以细分为以下几个子步骤:
- **加载(Loading)**:通过类的全限定名查找并读取对应的字节码文件,将其加载到内存中。
- **验证(Verification)**:确保加载的字节码文件符合 JVM 的规范,不会对 JVM 造成安全威胁。
- **准备(Preparation)**:为类的静态变量分配内存,并设置默认初始值,如 `int` 类型为 0,`boolean` 类型为 `false` 等。
- **解析(Resolution)**:将常量池中的符号引用转换为直接引用,也就是将类、方法、字段等的符号引用替换为它们在内存中的实际地址。
- **初始化(Initialization)**:执行类的静态代码块和静态变量的赋值操作。

#### 示例代码
```java
class MyClass {
static {
System.out.println("静态代码块执行,类开始初始化");
}
public static int staticVariable = 10;
}
```

#### 解释
当程序第一次使用 `MyClass` 时,JVM 会进行类加载。在准备阶段,`staticVariable` 会被分配内存并初始化为 0;在初始化阶段,静态代码块会执行,输出相应信息,然后 `staticVariable` 被赋值为 10。

### 2. 内存分配(Memory Allocation)
一旦类被成功加载,就可以开始创建对象了。在创建对象时,JVM 需要为对象分配内存空间。通常,对象的内存分配发生在堆(Heap)上。JVM 会根据对象的类型和大小,在堆中找到一块足够的连续内存空间来存储对象的实例变量。

#### 示例代码
```java
MyClass obj = new MyClass();
```

#### 解释
当执行 `new MyClass()` 时,JVM 会在堆上为 `MyClass` 对象分配内存空间,用于存储对象的实例变量。同时,在栈(Stack)上会创建一个引用变量 `obj`,该引用变量指向堆上刚分配的对象。

### 3. 初始化(Initialization)
内存分配完成后,需要对对象进行初始化操作,主要包括以下两个方面:
- **实例变量初始化**:为对象的实例变量赋予初始值。这些初始值可以是显式赋值(如在类中定义实例变量时直接赋值),也可以是默认值(如果没有显式赋值)。
- **构造函数调用**:执行对象的构造函数,完成对象的初始化逻辑。构造函数可以用于对对象的实例变量进行更复杂的初始化操作。

#### 示例代码
```java
class MyClass {
int instanceVariable = 20; // 实例变量显式赋值

public MyClass() {
System.out.println("构造函数执行,对象开始初始化");
}
}

public class ObjectCreationExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
}
}
```

#### 解释
在创建 `MyClass` 对象时,首先会为 `instanceVariable` 分配内存并初始化为 20(显式赋值)。然后,JVM 会调用 `MyClass` 的构造函数,执行构造函数中的代码,输出相应信息,完成对象的初始化。

### 完整对象创建过程总结
以下是一个完整的 Java 对象创建过程示例:
```java
class MyClass {
static {
System.out.println("静态代码块执行,类开始初始化");
}
public static int staticVariable = 10;

int instanceVariable = 20;

public MyClass() {
System.out.println("构造函数执行,对象开始初始化");
}
}

public class ObjectCreationExample {
public static void main(String[] args) {
MyClass obj = new MyClass();
}
}
```

#### 执行结果及过程解释
运行上述代码,输出结果如下:
```
静态代码块执行,类开始初始化
构造函数执行,对象开始初始化
```
执行过程如下:
1. **类加载**:当 `main` 方法中第一次使用 `MyClass` 时,JVM 进行类加载。首先加载字节码文件,经过验证、准备(为 `staticVariable` 分配内存并初始化为 0)、解析等步骤,最后执行静态代码块,输出“静态代码块执行,类开始初始化”,并将 `staticVariable` 赋值为 10。
2. **内存分配**:执行 `new MyClass()` 时,JVM 在堆上为 `MyClass` 对象分配内存空间,用于存储 `instanceVariable` 等实例变量。同时,在栈上创建引用变量 `obj` 指向该对象。
3. **初始化**:为 `instanceVariable` 赋值为 20(显式赋值),然后调用 `MyClass` 的构造函数,输出“构造函数执行,对象开始初始化”,完成对象的初始化。

 - 内存可见性、指令重排序、`volatile`关键字。

### 1. 内存可见性
#### 详细说明
在 Java 多线程编程中,每个线程都有自己的工作内存,它是主内存的一个副本。当线程对变量进行读写操作时,首先会将变量从主内存复制到自己的工作内存中,然后在工作内存中进行操作,最后再将结果写回主内存。这就可能导致一个线程对变量的修改在其他线程中不可见,即内存可见性问题。

#### 案例说明
```java
public class VisibilityExample {
private static boolean flag = false;

public static void main(String[] args) {
// 线程 1 修改 flag 的值
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("线程 1 已将 flag 设置为 true");
}).start();

// 线程 2 不断检查 flag 的值
new Thread(() -> {
while (!flag) {
// 空循环
}
System.out.println("线程 2 检测到 flag 变为 true");
}).start();
}
}
```
在这个例子中,线程 1 修改了 `flag` 的值,但线程 2 可能一直看不到这个修改,因为线程 2 可能一直使用自己工作内存中的 `flag` 副本,导致陷入死循环。

### 2. 指令重排序
#### 详细说明
指令重排序是指编译器或处理器为了提高程序的性能,在不改变程序语义的前提下,对指令的执行顺序进行重新排列。指令重排序分为编译器重排序、指令级并行重排序和内存系统重排序。虽然指令重排序在单线程环境下不会影响程序的正确性,但在多线程环境下可能会导致程序出现错误。

#### 案例说明
```java
public class ReorderingExample {
private static int x = 0, y = 0;
private static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
Thread one = new Thread(() -> {
a = 1;
x = b;
});

Thread two = new Thread(() -> {
b = 1;
y = a;
});

one.start();
two.start();
one.join();
two.join();

System.out.println("(x,y) = (" + x + "," + y + ")");
}
}
```
在这个例子中,由于指令重排序的存在,可能会出现 `x = 0` 且 `y = 0` 的情况。比如线程 1 中 `x = b` 先于 `a = 1` 执行,线程 2 中 `y = a` 先于 `b = 1` 执行。

### 3. `volatile` 关键字
#### 详细说明
`volatile` 是 Java 中的一个关键字,用于保证变量的内存可见性和禁止指令重排序。当一个变量被声明为 `volatile` 时,对该变量的写操作会立即刷新到主内存中,而读操作会从主内存中读取最新的值,从而保证了不同线程之间对该变量操作的可见性。同时,`volatile` 关键字会禁止编译器和处理器对该变量相关的指令进行重排序。

#### 案例说明
##### 解决内存可见性问题
```java
public class VolatileVisibilityExample {
private static volatile boolean flag = false;

public static void main(String[] args) {
// 线程 1 修改 flag 的值
new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("线程 1 已将 flag 设置为 true");
}).start();

// 线程 2 不断检查 flag 的值
new Thread(() -> {
while (!flag) {
// 空循环
}
System.out.println("线程 2 检测到 flag 变为 true");
}).start();
}
}
```
在这个例子中,将 `flag` 声明为 `volatile` 后,线程 1 对 `flag` 的修改会立即刷新到主内存中,线程 2 会从主内存中读取最新的 `flag` 值,从而避免了死循环。

##### 禁止指令重排序
```java
public class VolatileReorderingExample {
private static volatile int a = 0;
private static int b = 0;

public static void main(String[] args) {
Thread one = new Thread(() -> {
a = 1;
b = 1;
});

Thread two = new Thread(() -> {
while (b == 0) {
// 空循环
}
System.out.println("a 的值为: " + a);
});

one.start();
two.start();
}
}
```
在这个例子中,由于 `a` 被声明为 `volatile`,`a = 1` 不会被重排序到 `b = 1` 之后,保证了线程 2 在看到 `b = 1` 时,`a` 一定已经被赋值为 1。

#### **2. JVM垃圾回收机制(GC)**
- **知识点**:
- 垃圾回收算法(标记-清除、标记-整理、复制算法)。

在 Java 中,垃圾回收(Garbage Collection,GC)是自动内存管理的核心机制,用于回收不再使用的对象所占用的内存空间。以下详细介绍三种常见的垃圾回收算法:标记 - 清除、标记 - 整理和复制算法,并结合示例说明。

1. 标记 - 清除算法(Mark - Sweep)

算法原理

标记 - 清除算法分为两个阶段:标记阶段和清除阶段。

 

  • 标记阶段:从根对象(如栈中的引用、静态变量等)开始遍历所有可达对象,并标记这些对象。
  • 清除阶段:遍历整个堆内存,将未被标记的对象(即不可达对象)所占用的内存空间释放。

优缺点

 

  • 优点:实现简单,不需要额外的内存空间。
  • 缺点:会产生大量的内存碎片,导致后续分配大对象时可能因找不到连续的内存空间而触发更多的垃圾回收。

示例说明

假设堆内存初始状态如下,有 5 个对象 A、B、C、D、E,其中 A 被栈中的引用指向,C 被 A 引用,D 被静态变量引用:
初始状态: | A | B | C | D | E |
  • 标记阶段:从根对象开始标记,栈中的引用指向 A,A 引用 C,静态变量引用 D,所以 A、C、D 被标记:
  • 标记后: | A(标记) | B | C(标记) | D(标记) | E |

 

  • 清除阶段:清除未被标记的对象 B 和 E:
  • 清除后: | A | | C | D | |
  • 此时,内存中出现了两个空闲的碎片空间。 

2. 标记 - 整理算法(Mark - Compact)

算法原理

 

标记 - 整理算法同样分为标记阶段和整理阶段:

 

  • 标记阶段:与标记 - 清除算法的标记阶段相同,从根对象开始遍历所有可达对象,并标记这些对象。
  • 整理阶段:将所有存活的对象(即被标记的对象)向内存的一端移动,然后清理掉边界以外的内存空间。

优缺点

 

  • 优点:不会产生内存碎片,保证了内存的连续性,有利于大对象的分配。
  • 缺点:需要移动对象,会增加系统开销。

示例说明

 

还是以上面的堆内存为例:
初始状态: | A | B | C | D | E |
  • 标记阶段:标记可达对象 A、C、D:

标记后: | A(标记) | B | C(标记) | D(标记) | E |

  • 整理阶段:将存活的对象 A、C、D 向内存的一端移动:

整理后: | A | C | D | | |

此时,内存中没有碎片,空闲空间是连续的。

3. 复制算法(Copying)

算法原理

 

复制算法将堆内存划分为两个大小相等的区域: Eden 区和 Survivor 区(通常 Survivor 区又分为 From 区和 To 区)。在进行垃圾回收时,将 Eden 区和 From 区中存活的对象复制到 To 区,然后清空 Eden 区和 From 区。最后,交换 From 区和 To 区的角色。

优缺点

 

  • 优点:实现简单,效率高,不会产生内存碎片。
  • 缺点:需要额外的内存空间,因为总有一半的内存空间处于闲置状态。

示例说明

 

假设堆内存被划分为 Eden 区、From 区和 To 区,初始状态如下:
Eden 区:| A | B | C | D | From 区:| E | F | G | To 区: | | | |
  • 标记阶段:标记存活的对象 A、C、E、G:

Eden 区:| A(标记) | B | C(标记) | D | From 区:| E(标记) | F | G(标记) | To 区: | | | |

  • 复制阶段:将存活的对象 A、C、E、G 复制到 To 区:

Eden 区:| A(标记) | B | C(标记) | D | From 区:| E(标记) | F | G(标记) | To 区: | A | C | E | G |

  • 清除阶段:清空 Eden 区和 From 区,交换 From 区和 To 区的角色:

Eden 区:| | | | | From 区:| A | C | E | G | To 区: | | | | |

 

Java 代码示例

 

以下是一个简单的 Java 代码示例,演示了对象的创建和垃圾回收的触发:

public class GarbageCollectionExample {
public static void main(String[] args) {
// 创建大量对象,触发垃圾回收
for (int i = 0; i < 100000; i++) {
new Object();
}
// 手动触发垃圾回收
System.gc();
}
}

 

在这个示例中,创建了大量的 Object 对象,可能会触发垃圾回收机制。不同的垃圾回收器会根据具体情况选择合适的垃圾回收算法来回收这些不再使用的对象。

----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 - 垃圾回收器(Serial、Parallel、CMS、G1、ZGC)。

在 Java 中,垃圾回收器负责自动回收不再使用的对象所占用的内存,不同的垃圾回收器有不同的特点和适用场景。以下是对几种常见垃圾回收器(Serial、Parallel、CMS、G1、ZGC)的详细介绍及示例说明。

### 1. Serial 垃圾回收器
#### 特点
- 单线程的垃圾回收器,在进行垃圾回收时,会暂停所有应用线程(Stop The World,简称 STW)。
- 简单高效,适用于单 CPU 环境下的小型应用。

#### 示例说明
以下是一个简单的 Java 代码示例,使用 `-XX:+UseSerialGC` 参数指定使用 Serial 垃圾回收器:
```java
public class SerialGCExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
System.gc();
}
}
```
若要使用 Serial 垃圾回收器运行该程序,可以在命令行中使用以下命令:
```sh
java -XX:+UseSerialGC SerialGCExample
```
在垃圾回收时,Serial 回收器会暂停所有应用线程,单线程完成标记和清除操作。对于小型应用,由于没有线程切换的开销,它能高效地完成垃圾回收任务。

### 2. Parallel 垃圾回收器
#### 特点
- 多线程的垃圾回收器,同样会产生 STW 事件。
- 以并行的方式进行垃圾回收,提高了垃圾回收的效率,适用于多核 CPU 环境下对吞吐量要求较高的应用。

#### 示例说明
使用 `-XX:+UseParallelGC` 参数指定使用 Parallel 垃圾回收器:
```java
public class ParallelGCExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
System.gc();
}
}
```
在命令行中使用以下命令运行程序:
```sh
java -XX:+UseParallelGC ParallelGCExample
```
Parallel 回收器会利用多个线程并行地进行垃圾回收,能在多核 CPU 上充分发挥性能优势,减少垃圾回收的时间,提高应用的吞吐量。

### 3. CMS(Concurrent Mark Sweep)垃圾回收器
#### 特点
- 以获取最短回收停顿时间为目标,在大部分时间里可以与应用线程并发执行,减少 STW 的时间。
- 采用标记 - 清除算法,可能会产生内存碎片。

#### 示例说明
使用 `-XX:+UseConcMarkSweepGC` 参数指定使用 CMS 垃圾回收器:
```java
public class CMSGCExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
System.gc();
}
}
```
在命令行中使用以下命令运行程序:
```sh
java -XX:+UseConcMarkSweepGC CMSGCExample
```
CMS 回收器在垃圾回收过程中,会尽量与应用线程并发执行,减少对应用性能的影响。例如,在标记阶段,它会与应用线程并发地标记可达对象,只有在初始标记和重新标记阶段会产生短暂的 STW。

### 4. G1(Garbage - First)垃圾回收器
#### 特点
- 面向服务器端应用的垃圾回收器,将堆内存划分为多个大小相等的区域(Region)。
- 可以预测垃圾回收的停顿时间,能在有限的时间内获得尽可能高的垃圾回收效率。
- 整体上采用标记 - 整理算法,局部采用复制算法,不会产生内存碎片。

#### 示例说明
使用 `-XX:+UseG1GC` 参数指定使用 G1 垃圾回收器:
```java
public class G1GCExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
System.gc();
}
}
```
在命令行中使用以下命令运行程序:
```sh
java -XX:+UseG1GC G1GCExample
```
G1 回收器会根据每个 Region 中垃圾的数量和回收所需的时间,优先回收垃圾最多的 Region,从而提高垃圾回收的效率。同时,它能在回收过程中避免产生内存碎片,保证内存的连续性。

### 5. ZGC(Z Garbage Collector)
#### 特点
- 可扩展的低延迟垃圾回收器,停顿时间几乎可以忽略不计,最大停顿时间不超过 10 毫秒。
- 支持处理大内存堆,能在 TB 级别的堆内存上高效运行。
- 采用染色指针和读屏障技术,实现了并发的标记、转移和重定位操作。

#### 示例说明
使用 `-XX:+UseZGC` 参数指定使用 ZGC 垃圾回收器:
```java
public class ZGCExample {
public static void main(String[] args) {
for (int i = 0; i < 100000; i++) {
new Object();
}
System.gc();
}
}
```
在命令行中使用以下命令运行程序:
```sh
java -XX:+UseZGC ZGCExample
```
ZGC 回收器在垃圾回收过程中,大部分操作都是与应用线程并发执行的,几乎不会产生明显的 STW 事件,非常适合对响应时间要求极高的应用,如实时交易系统等。

   - GC日志分析。

 

在 Java 中,GC(Garbage Collection)日志能帮助开发者了解垃圾回收器的工作情况,排查性能问题。以下详细介绍如何开启 GC 日志记录,并结合示例对不同垃圾回收器产生的 GC 日志进行分析。

### 开启 GC 日志记录
要开启 GC 日志记录,需要在启动 Java 程序时添加特定的 JVM 参数。以下是常用的参数:
```plaintext
-XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:/path/to/gc.log
```
- `-XX:+PrintGCDetails`:打印详细的 GC 信息。
- `-XX:+PrintGCDateStamps`:打印 GC 发生的日期和时间。
- `-Xloggc:/path/to/gc.log`:将 GC 日志输出到指定的文件中。

### 示例代码
```java
import java.util.ArrayList;
import java.util.List;

public class GCTest {
public static void main(String[] args) {
List<byte[]> list = new ArrayList<>();
for (int i = 0; i < 10000; i++) {
list.add(new byte[1024 * 1024]);
}
}
}
```
这个程序会不断创建大对象,从而触发垃圾回收。

### 不同垃圾回收器的 GC 日志分析

#### 1. Parallel 垃圾回收器
使用以下命令启动程序并使用 Parallel 垃圾回收器:
```sh
java -XX:+UseParallelGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:parallel_gc.log GCTest
```
以下是一段典型的 Parallel GC 日志示例:
```plaintext
2024-11-20T10:30:00.123+0800: 0.234: [GC (Allocation Failure) [PSYoungGen: 33280K->512K(38400K)] 33280K->10240K(125952K), 0.0103456 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
```
- **日期和时间**:`2024-11-20T10:30:00.123+0800` 表示 GC 发生的具体时间。
- **GC 触发原因**:`Allocation Failure` 表示由于对象分配失败触发了 GC。
- **新生代信息**:`[PSYoungGen: 33280K->512K(38400K)]` 表示新生代在 GC 前使用了 33280KB 内存,GC 后剩余 512KB 内存,新生代总容量为 38400KB。
- **堆内存信息**:`33280K->10240K(125952K)` 表示整个堆内存在 GC 前使用了 33280KB 内存,GC 后剩余 10240KB 内存,堆总容量为 125952KB。
- **GC 耗时**:`0.0103456 secs` 表示本次 GC 操作花费的时间。
- **CPU 时间**:`user=0.03 sys=0.00, real=0.01 secs` 分别表示用户态 CPU 时间、内核态 CPU 时间和实际耗时。

#### 2. CMS 垃圾回收器
使用以下命令启动程序并使用 CMS 垃圾回收器:
```sh
java -XX:+UseConcMarkSweepGC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:cms_gc.log GCTest
```
以下是一段典型的 CMS GC 日志示例:
```plaintext
2024-11-20T10:35:00.456+0800: 0.345: [GC (Allocation Failure) [ParNew: 33280K->512K(38400K), 0.0098765 secs] 33280K->10240K(125952K), 0.0099876 secs] [Times: user=0.03 sys=0.00, real=0.01 secs]
2024-11-20T10:35:01.567+0800: 1.456: [GC (CMS Initial Mark) [1 CMS-initial-mark: 9728K(87552K)] 10240K(125952K), 0.0001234 secs] [Times: user=0.00 sys=0.00, real=0.00 secs]
```
- **新生代 GC**:与 Parallel 垃圾回收器的新生代 GC 日志类似,`ParNew` 是 CMS 回收器的新生代回收器。
- **CMS 初始标记**:`[CMS Initial Mark]` 表示进入 CMS 回收过程的初始标记阶段,该阶段会产生短暂的 STW(Stop The World),用于标记根对象直接引用的对象。

#### 3. G1 垃圾回收器
使用以下命令启动程序并使用 G1 垃圾回收器:
```sh
java -XX:+UseG1GC -XX:+PrintGCDetails -XX:+PrintGCDateStamps -Xloggc:g1_gc.log GCTest
```
以下是一段典型的 G1 GC 日志示例:
```plaintext
2024-11-20T10:40:00.789+0800: 0.456: [GC pause (G1 Evacuation Pause) (young), 0.0123456 secs]
[Parallel Time: 10.0 ms, GC Workers: 4]
[GC Worker Start (ms): Min: 456.0, Avg: 456.1, Max: 456.2, Diff: 0.2]
[Ext Root Scanning (ms): Min: 0.1, Avg: 0.2, Max: 0.3, Diff: 0.2]
...
[Code Root Fixup: 0.0 ms]
[Code Root Purge: 0.0 ms]
[Clear CT: 0.1 ms]
[Other: 2.2 ms]
[Choose CSet: 0.0 ms]
[Ref Proc: 1.0 ms]
[Ref Enq: 0.1 ms]
...
[Eden: 12.0M(12.0M)->0.0B(10.0M) Survivors: 2048.0K->4096.0K Heap: 20.0M(256.0M)->10.0M(256.0M)]
[Times: user=0.04 sys=0.00, real=0.01 secs]
```
- **GC 触发原因**:`GC pause (G1 Evacuation Pause) (young)` 表示由于 G1 进行新生代对象转移触发了 GC。
- **并行时间和工作线程**:`Parallel Time: 10.0 ms, GC Workers: 4` 表示并行阶段耗时 10 毫秒,使用了 4 个 GC 工作线程。
- **各阶段耗时**:日志中详细记录了根扫描、引用处理等各个阶段的耗时情况。
- **内存变化**:`Eden: 12.0M(12.0M)->0.0B(10.0M) Survivors: 2048.0K->4096.0K Heap: 20.0M(256.0M)->10.0M(256.0M)` 表示 Eden 区、Survivor 区和整个堆内存的使用情况在 GC 前后的变化。

 

#### **3. 多线程与并发编程**

- **知识点**:
- 线程创建方式(继承`Thread`、实现`Runnable`、`Callable`)。

在 Java 中,有多种方式可以创建线程,常见的有继承 `Thread` 类、实现 `Runnable` 接口和使用 `Callable` 接口。下面将分别介绍这三种方式,并给出具体的示例代码。

### 1. 继承 `Thread` 类
#### 实现步骤
- 创建一个类,继承自 `Thread` 类。
- 重写 `Thread` 类的 `run()` 方法,在该方法中定义线程要执行的任务。
- 创建该类的实例,并调用 `start()` 方法启动线程。

#### 示例代码
```java
// 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,计数: " + i);
try {
// 线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class ThreadInheritanceExample {
public static void main(String[] args) {
// 创建线程实例
MyThread thread = new MyThread();
// 启动线程
thread.start();

for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,计数: " + i);
try {
// 主线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
#### 代码解释
- `MyThread` 类继承自 `Thread` 类,并重写了 `run()` 方法,在该方法中定义了线程要执行的任务。
- 在 `main` 方法中,创建了 `MyThread` 类的实例,并调用 `start()` 方法启动线程。同时,主线程也在执行自己的任务。

### 2. 实现 `Runnable` 接口
#### 实现步骤
- 创建一个类,实现 `Runnable` 接口。
- 实现 `Runnable` 接口的 `run()` 方法,在该方法中定义线程要执行的任务。
- 创建该类的实例,并将其作为参数传递给 `Thread` 类的构造函数,创建 `Thread` 类的实例。
- 调用 `Thread` 类实例的 `start()` 方法启动线程。

#### 示例代码
```java
// 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,计数: " + i);
try {
// 线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}

public class RunnableExample {
public static void main(String[] args) {
// 创建实现 Runnable 接口的类的实例
MyRunnable myRunnable = new MyRunnable();
// 创建 Thread 类的实例,并将 MyRunnable 实例作为参数传递
Thread thread = new Thread(myRunnable);
// 启动线程
thread.start();

for (int i = 0; i < 5; i++) {
System.out.println(Thread.currentThread().getName() + " 正在执行,计数: " + i);
try {
// 主线程休眠 1 秒
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
```
#### 代码解释
- `MyRunnable` 类实现了 `Runnable` 接口,并重写了 `run()` 方法,在该方法中定义了线程要执行的任务。
- 在 `main` 方法中,创建了 `MyRunnable` 类的实例,并将其作为参数传递给 `Thread` 类的构造函数,创建 `Thread` 类的实例,最后调用 `start()` 方法启动线程。

### 3. 使用 `Callable` 接口
#### 实现步骤
- 创建一个类,实现 `Callable` 接口,需要指定返回值的类型。
- 实现 `Callable` 接口的 `call()` 方法,在该方法中定义线程要执行的任务,并返回一个结果。
- 创建该类的实例,并将其作为参数传递给 `FutureTask` 类的构造函数,创建 `FutureTask` 类的实例。
- 将 `FutureTask` 类的实例作为参数传递给 `Thread` 类的构造函数,创建 `Thread` 类的实例。
- 调用 `Thread` 类实例的 `start()` 方法启动线程。
- 可以通过 `FutureTask` 类的 `get()` 方法获取线程执行的结果。

#### 示例代码
```java
import java.util.concurrent.*;

// 实现 Callable 接口,指定返回值类型为 Integer
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 10; i++) {
sum += i;
}
return sum;
}
}

public class CallableExample {
public static void main(String[] args) {
// 创建实现 Callable 接口的类的实例
MyCallable myCallable = new MyCallable();
// 创建 FutureTask 类的实例,并将 MyCallable 实例作为参数传递
FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
// 创建 Thread 类的实例,并将 FutureTask 实例作为参数传递
Thread thread = new Thread(futureTask);
// 启动线程
thread.start();

try {
// 获取线程执行的结果
Integer result = futureTask.get();
System.out.println("线程执行结果: " + result);
} catch (InterruptedException | ExecutionException e) {
e.printStackTrace();
}
}
}
```
#### 代码解释
- `MyCallable` 类实现了 `Callable` 接口,并重写了 `call()` 方法,在该方法中计算 1 到 10 的整数和,并返回结果。
- 在 `main` 方法中,创建了 `MyCallable` 类的实例,并将其作为参数传递给 `FutureTask` 类的构造函数,创建 `FutureTask` 类的实例。然后将 `FutureTask` 类的实例作为参数传递给 `Thread` 类的构造函数,创建 `Thread` 类的实例,最后调用 `start()` 方法启动线程。
- 通过 `FutureTask` 类的 `get()` 方法获取线程执行的结果。如果线程还未执行完毕,`get()` 方法会阻塞当前线程,直到线程执行完毕并返回结果。

 

-线程池(`ThreadPoolExecutor`、`Executors`工具类)。

在 Java 中,线程池是一种重要的多线程处理机制,它可以有效地管理和复用线程,减少线程创建和销毁的开销,提高系统的性能和稳定性。下面分别介绍 `ThreadPoolExecutor` 和 `Executors` 工具类,并给出相应的示例。

### 1. `ThreadPoolExecutor`
`ThreadPoolExecutor` 是 Java 中线程池的核心实现类,通过它可以自定义线程池的各种参数,如核心线程数、最大线程数、线程空闲时间等。

#### 构造函数参数说明
```java
public ThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue,
ThreadFactory threadFactory,
RejectedExecutionHandler handler)
```
- `corePoolSize`:核心线程数,线程池长期保持的线程数量。
- `maximumPoolSize`:最大线程数,线程池允许创建的最大线程数量。
- `keepAliveTime`:线程空闲时间,当线程空闲时间超过该值时,非核心线程会被销毁。
- `unit`:线程空闲时间的单位。
- `workQueue`:任务队列,用于存储等待执行的任务。
- `threadFactory`:线程工厂,用于创建线程。
- `handler`:拒绝策略,当任务队列已满且线程池已达到最大线程数时,如何处理新提交的任务。

#### 示例代码
```java
import java.util.concurrent.*;

public class ThreadPoolExecutorExample {
public static void main(String[] args) {
// 创建一个 ThreadPoolExecutor 实例
ThreadPoolExecutor executor = new ThreadPoolExecutor(
2, // 核心线程数
5, // 最大线程数
60, // 线程空闲时间
TimeUnit.SECONDS, // 时间单位
new LinkedBlockingQueue<>(10), // 任务队列
Executors.defaultThreadFactory(), // 线程工厂
new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);

// 提交任务到线程池
for (int i = 0; i < 15; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行");
try {
// 模拟任务执行时间
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 关闭线程池
executor.shutdown();
}
}
```
#### 代码解释
- 创建了一个 `ThreadPoolExecutor` 实例,设置核心线程数为 2,最大线程数为 5,线程空闲时间为 60 秒,任务队列使用 `LinkedBlockingQueue`,容量为 10,拒绝策略使用 `AbortPolicy`(直接抛出异常)。
- 提交 15 个任务到线程池,每个任务会打印当前执行的任务 ID 和线程名称,并模拟 2 秒的执行时间。
- 最后调用 `shutdown()` 方法关闭线程池。

### 2. `Executors` 工具类
`Executors` 是 Java 提供的一个工具类,它提供了一些静态方法,用于创建不同类型的线程池。

#### 常见的线程池创建方法
- `Executors.newFixedThreadPool(int nThreads)`:创建一个固定大小的线程池,核心线程数和最大线程数相等。
- `Executors.newCachedThreadPool()`:创建一个可缓存的线程池,线程数会根据任务数量自动调整。
- `Executors.newSingleThreadExecutor()`:创建一个单线程的线程池,所有任务按顺序执行。
- `Executors.newScheduledThreadPool(int corePoolSize)`:创建一个定时任务线程池,可执行定时任务和周期性任务。

#### 示例代码
```java
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

// 提交任务到线程池
for (int i = 0; i < 10; i++) {
final int taskId = i;
executor.submit(() -> {
System.out.println("任务 " + taskId + " 正在由线程 " + Thread.currentThread().getName() + " 执行");
try {
// 模拟任务执行时间
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
});
}

// 关闭线程池
executor.shutdown();
}
}
```
#### 代码解释
- 使用 `Executors.newFixedThreadPool(3)` 创建一个固定大小为 3 的线程池。
- 提交 10 个任务到线程池,每个任务会打印当前执行的任务 ID 和线程名称,并模拟 1 秒的执行时间。
- 最后调用 `shutdown()` 方法关闭线程池。

### 注意事项
- `Executors` 工具类创建的线程池可能存在一些潜在的风险,如 `newFixedThreadPool` 和 `newSingleThreadExecutor` 使用的是无界队列,可能会导致内存溢出;`newCachedThreadPool` 允许创建的线程数量为 `Integer.MAX_VALUE`,可能会导致创建过多的线程,耗尽系统资源。因此,在实际开发中,建议使用 `ThreadPoolExecutor` 自定义线程池。

- 锁机制(`synchronized`、`ReentrantLock`)。

在 Java 里,锁机制用于保障多线程环境下数据的一致性与线程安全,`synchronized` 和 `ReentrantLock` 是两种常用的锁实现方式。下面会详细介绍它们,并给出相应的示例。

### 1. `synchronized` 关键字
`synchronized` 是 Java 内置的同步机制,可修饰方法或代码块,确保同一时刻只有一个线程能执行被修饰的代码。

#### 修饰实例方法
当 `synchronized` 修饰实例方法时,它会锁定当前对象实例,同一时刻只有一个线程可以调用该实例的此方法。

```java
class Counter {
private int count = 0;

// 同步实例方法
public synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " 执行后 count 的值为: " + count);
}
}

public class SynchronizedInstanceMethodExample {
public static void main(String[] args) {
Counter counter = new Counter();

// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

thread1.start();
thread2.start();
}
}
```

**解释**:
- `Counter` 类中的 `increment` 方法被 `synchronized` 修饰,意味着同一时刻只有一个线程能够调用该方法。
- 在 `main` 方法里创建了两个线程,它们都会调用 `counter` 对象的 `increment` 方法,由于方法被同步,所以不会出现数据竞争问题。

#### 修饰静态方法
当 `synchronized` 修饰静态方法时,它会锁定当前类的 `Class` 对象,同一时刻只有一个线程可以调用该类的此静态方法。

```java
class StaticCounter {
private static int count = 0;

// 同步静态方法
public static synchronized void increment() {
count++;
System.out.println(Thread.currentThread().getName() + " 执行后 count 的值为: " + count);
}
}

public class SynchronizedStaticMethodExample {
public static void main(String[] args) {
// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
StaticCounter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
StaticCounter.increment();
}
});

thread1.start();
thread2.start();
}
}
```

**解释**:
- `StaticCounter` 类中的 `increment` 方法是静态且被 `synchronized` 修饰,这意味着同一时刻只有一个线程可以调用该静态方法。
- 在 `main` 方法中创建两个线程来调用该静态方法,保证了对静态变量 `count` 的操作是线程安全的。

#### 修饰代码块
`synchronized` 也可以用于修饰代码块,这样可以更细粒度地控制同步范围。

```java
class BlockCounter {
private int count = 0;

public void increment() {
// 同步代码块
synchronized (this) {
count++;
System.out.println(Thread.currentThread().getName() + " 执行后 count 的值为: " + count);
}
}
}

public class SynchronizedBlockExample {
public static void main(String[] args) {
BlockCounter counter = new BlockCounter();

// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

thread1.start();
thread2.start();
}
}
```

**解释**:
- `BlockCounter` 类的 `increment` 方法中有一个 `synchronized` 代码块,它锁定了当前对象 `this`。
- 只有获得该对象锁的线程才能执行代码块中的内容,确保了对 `count` 变量操作的线程安全性。

### 2. `ReentrantLock`
`ReentrantLock` 是 `java.util.concurrent.locks` 包下的一个类,它提供了比 `synchronized` 更灵活的锁机制。

```java
import java.util.concurrent.locks.ReentrantLock;

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

public void increment() {
// 获取锁
lock.lock();
try {
count++;
System.out.println(Thread.currentThread().getName() + " 执行后 count 的值为: " + count);
} finally {
// 释放锁
lock.unlock();
}
}
}

public class ReentrantLockExample {
public static void main(String[] args) {
LockCounter counter = new LockCounter();

// 创建两个线程并启动
Thread thread1 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

Thread thread2 = new Thread(() -> {
for (int i = 0; i < 5; i++) {
counter.increment();
}
});

thread1.start();
thread2.start();
}
}
```

**解释**:
- `LockCounter` 类中使用 `ReentrantLock` 来保护对 `count` 变量的操作。
- 在 `increment` 方法中,通过 `lock.lock()` 获取锁,执行完操作后在 `finally` 块中使用 `lock.unlock()` 释放锁,确保无论是否发生异常,锁最终都会被释放。

### `synchronized` 和 `ReentrantLock` 的对比
- **语法**:`synchronized` 是 Java 内置的关键字,使用起来更简洁;`ReentrantLock` 是一个类,需要手动调用 `lock()` 和 `unlock()` 方法。
- **灵活性**:`ReentrantLock` 提供了更多的高级特性,如可中断锁、公平锁等,比 `synchronized` 更加灵活。
- **异常处理**:`synchronized` 不需要手动释放锁,发生异常时会自动释放;`ReentrantLock` 需要在 `finally` 块中手动释放锁,以避免死锁。

 

- 并发工具类(`CountDownLatch`、`CyclicBarrier`、`Semaphore`) 

在 Java 并发编程中,`CountDownLatch`、`CyclicBarrier` 和 `Semaphore` 是非常实用的并发工具类,它们各自有不同的用途和特点。下面将详细介绍这三个工具类,并给出相应的示例代码。

### 1. `CountDownLatch`
`CountDownLatch` 允许一个或多个线程等待其他线程完成操作。它使用一个计数器来实现,计数器的初始值是线程的数量。每当一个线程完成自己的任务后,计数器的值就会减 1。当计数器的值为 0 时,等待的线程就会被唤醒,继续执行后续的操作。

#### 示例代码
```java
import java.util.concurrent.CountDownLatch;

public class CountDownLatchExample {
public static void main(String[] args) throws InterruptedException {
// 创建一个 CountDownLatch 实例,计数器初始值为 3
CountDownLatch latch = new CountDownLatch(3);

// 创建并启动 3 个工作线程
for (int i = 0; i < 3; i++) {
final int workerId = i;
new Thread(() -> {
try {
System.out.println("工作线程 " + workerId + " 开始工作");
// 模拟工作耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println("工作线程 " + workerId + " 完成工作");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 工作完成,计数器减 1
latch.countDown();
}
}).start();
}

// 主线程等待所有工作线程完成
System.out.println("主线程等待工作线程完成...");
latch.await();
System.out.println("所有工作线程已完成,主线程继续执行");
}
}
```
#### 代码解释
- 创建 `CountDownLatch` 实例,初始计数器值为 3,表示有 3 个工作线程需要完成任务。
- 启动 3 个工作线程,每个线程模拟一段时间的工作,完成后调用 `latch.countDown()` 方法将计数器减 1。
- 主线程调用 `latch.await()` 方法进入等待状态,直到计数器的值为 0 时才会继续执行。

### 2. `CyclicBarrier`
`CyclicBarrier` 用于让一组线程在到达某个屏障(同步点)时进行等待,直到所有线程都到达该屏障后,所有线程才会继续执行后续的操作。与 `CountDownLatch` 不同的是,`CyclicBarrier` 可以重复使用。

#### 示例代码
```java
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;

public class CyclicBarrierExample {
public static void main(String[] args) {
// 创建一个 CyclicBarrier 实例,线程数量为 3,并指定所有线程到达屏障后执行的任务
CyclicBarrier barrier = new CyclicBarrier(3, () -> System.out.println("所有线程都已到达屏障,继续执行"));

// 创建并启动 3 个线程
for (int i = 0; i < 3; i++) {
final int threadId = i;
new Thread(() -> {
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) {
e.printStackTrace();
}
}).start();
}
}
}
```
#### 代码解释
- 创建 `CyclicBarrier` 实例,指定线程数量为 3,并传入一个 `Runnable` 任务,当所有线程都到达屏障时会执行该任务。
- 启动 3 个线程,每个线程模拟一段时间的工作,完成后调用 `barrier.await()` 方法到达屏障并等待其他线程。
- 当所有线程都到达屏障后,会执行传入的 `Runnable` 任务,然后所有线程继续执行后续的操作。

### 3. `Semaphore`
`Semaphore` 用于控制同时访问某个资源的线程数量,它使用一个许可证(permit)的概念。线程在访问资源之前需要先获取许可证,如果许可证数量不足,线程会被阻塞;线程访问完资源后需要释放许可证,以便其他线程可以获取。

#### 示例代码
```java
import java.util.concurrent.Semaphore;

public class SemaphoreExample {
public static void main(String[] args) {
// 创建一个 Semaphore 实例,初始许可证数量为 2
Semaphore semaphore = new Semaphore(2);

// 创建并启动 5 个线程
for (int i = 0; i < 5; i++) {
final int threadId = i;
new Thread(() -> {
try {
// 获取许可证
semaphore.acquire();
System.out.println("线程 " + threadId + " 获得许可证,开始访问资源");
// 模拟访问资源耗时
Thread.sleep((long) (Math.random() * 1000));
System.out.println("线程 " + threadId + " 访问资源结束,释放许可证");
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
// 释放许可证
semaphore.release();
}
}).start();
}
}
}
```
#### 代码解释
- 创建 `Semaphore` 实例,初始许可证数量为 2,表示最多允许 2 个线程同时访问资源。
- 启动 5 个线程,每个线程在访问资源之前调用 `semaphore.acquire()` 方法获取许可证,如果许可证数量不足,线程会被阻塞。
- 线程访问完资源后调用 `semaphore.release()` 方法释放许可证,以便其他线程可以获取。

通过上述示例可以看出,`CountDownLatch`、`CyclicBarrier` 和 `Semaphore` 在不同的并发场景中发挥着重要的作用,可以帮助我们更方便地实现线程间的同步和协作。

 

#### **4. 集合框架**
- **知识点**:
- `ArrayList`、`LinkedList`、`HashMap`、`ConcurrentHashMap`源码分析。
- 集合的线程安全问题。
- **详细讲解**:
- `HashMap`的底层是数组+链表/红黑树,`ConcurrentHashMap`通过分段锁实现线程安全。

下面分别对 `ArrayList`、`LinkedList`、`HashMap`、`ConcurrentHashMap` 进行源码分析,并结合示例说明。

### 1. `ArrayList`

#### 源码分析
`ArrayList` 是基于数组实现的动态数组,它允许存储任意数量的元素,并且可以根据需要动态扩容。其核心属性和方法如下:
- **核心属性**:
- `elementData`:用于存储元素的数组。
- `size`:当前列表中元素的数量。
- **扩容机制**:当添加元素时,如果数组容量不足,会调用 `grow` 方法进行扩容,新容量一般为旧容量的 1.5 倍。

#### 示例代码
```java
import java.util.ArrayList;

public class ArrayListExample {
public static void main(String[] args) {
ArrayList<String> list = new ArrayList<>();
list.add("apple");
list.add("banana");
System.out.println(list.get(0));
}
}
```
#### 源码关联解释
- `list.add("apple")`:调用 `add(E e)` 方法,首先会检查数组容量是否足够,如果不足则调用 `grow` 方法扩容,然后将元素添加到数组的末尾。
- `list.get(0)`:调用 `get(int index)` 方法,直接通过数组下标访问元素,时间复杂度为 O(1)。

### 2. `LinkedList`

#### 源码分析
`LinkedList` 是基于双向链表实现的列表,它允许存储任意数量的元素,并且可以高效地进行插入和删除操作。其核心属性和方法如下:
- **核心属性**:
- `first`:指向链表的头节点。
- `last`:指向链表的尾节点。
- `size`:当前列表中元素的数量。
- **插入和删除操作**:插入和删除操作只需要修改节点的指针,时间复杂度为 O(1)。

#### 示例代码
```java
import java.util.LinkedList;

public class LinkedListExample {
public static void main(String[] args) {
LinkedList<String> list = new LinkedList<>();
list.add("apple");
list.add("banana");
list.removeFirst();
System.out.println(list.getFirst());
}
}
```
#### 源码关联解释
- `list.add("apple")`:调用 `add(E e)` 方法,将元素添加到链表的末尾,通过修改 `last` 节点的指针实现。
- `list.removeFirst()`:调用 `removeFirst()` 方法,删除链表的头节点,通过修改 `first` 节点的指针实现。
- `list.getFirst()`:调用 `getFirst()` 方法,直接返回 `first` 节点的元素,时间复杂度为 O(1)。

### 3. `HashMap`

#### 源码分析
`HashMap` 是基于哈希表实现的键值对存储结构,它允许使用 `null` 作为键和值。其核心属性和方法如下:
- **核心属性**:
- `table`:存储键值对的数组,每个元素是一个链表或红黑树的头节点。
- `size`:当前哈希表中键值对的数量。
- `threshold`:扩容阈值,当键值对数量达到该值时,会进行扩容。
- **哈希冲突处理**:使用链地址法处理哈希冲突,当链表长度达到 8 且数组长度达到 64 时,链表会转换为红黑树。

#### 示例代码
```java
import java.util.HashMap;

public class HashMapExample {
public static void main(String[] args) {
HashMap<String, Integer> map = new HashMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map.get("apple"));
}
}
```
#### 源码关联解释
- `map.put("apple", 1)`:调用 `put(K key, V value)` 方法,首先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置为空,则直接插入新节点;如果不为空,则遍历链表或红黑树,查找是否存在相同的键,如果存在则更新值,否则插入新节点。
- `map.get("apple")`:调用 `get(Object key)` 方法,首先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置不为空,则遍历链表或红黑树,查找是否存在相同的键,如果存在则返回对应的值,否则返回 `null`。

### 4. `ConcurrentHashMap`

#### 源码分析
`ConcurrentHashMap` 是线程安全的哈希表,它在多线程环境下可以高效地进行并发操作。其核心属性和方法如下:
- **核心属性**:
- `table`:存储键值对的数组,每个元素是一个链表或红黑树的头节点。
- `sizeCtl`:控制表初始化和扩容的参数。
- **并发控制**:使用 CAS(Compare-And-Swap)和 `synchronized` 关键字实现并发控制,不同的操作采用不同的锁粒度,提高并发性能。

#### 示例代码
```java
import java.util.concurrent.ConcurrentHashMap;

public class ConcurrentHashMapExample {
public static void main(String[] args) {
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
map.put("apple", 1);
map.put("banana", 2);
System.out.println(map.get("apple"));
}
}
```
#### 源码关联解释
- `map.put("apple", 1)`:调用 `put(K key, V value)` 方法,首先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置为空,则使用 CAS 操作插入新节点;如果不为空,则使用 `synchronized` 关键字对该位置的节点加锁,然后进行插入或更新操作。
- `map.get("apple")`:调用 `get(Object key)` 方法,首先计算键的哈希值,然后根据哈希值找到对应的数组位置。如果该位置不为空,则直接访问链表或红黑树,不需要加锁,因为读取操作是无锁的。

通过以上源码分析和示例代码,可以更好地理解 `ArrayList`、`LinkedList`、`HashMap` 和 `ConcurrentHashMap` 的工作原理和使用场景。

#### **5. IO与NIO**
- **知识点**:
- 传统IO(`InputStream`、`OutputStream`)。
- NIO(`Channel`、`Buffer`、`Selector`)。

 

### 传统 IO(`InputStream`、`OutputStream`)

#### 详细解释
传统的 Java IO(输入/输出)是基于流(Stream)的方式进行操作的。流是一种顺序的、单向的数据传输通道,分为输入流(`InputStream`)和输出流(`OutputStream`)。

- **`InputStream`**:是所有字节输入流的抽象基类,用于从数据源(如文件、网络连接等)读取字节数据。常见的子类有 `FileInputStream`、`BufferedInputStream` 等。
- **`OutputStream`**:是所有字节输出流的抽象基类,用于将字节数据写入到目标(如文件、网络连接等)。常见的子类有 `FileOutputStream`、`BufferedOutputStream` 等。

#### 示例代码
以下是一个使用传统 IO 进行文件复制的示例:
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;

public class TraditionalIOExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("destination.txt")) {

int byteRead;
// 从输入流读取一个字节,直到文件末尾返回 -1
while ((byteRead = fis.read()) != -1) {
// 将读取的字节写入输出流
fos.write(byteRead);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
#### 代码解释
- 创建 `FileInputStream` 对象用于读取 `source.txt` 文件的数据。
- 创建 `FileOutputStream` 对象用于将数据写入 `destination.txt` 文件。
- 使用 `while` 循环不断从输入流读取字节,直到文件末尾(返回 -1),并将读取的字节写入输出流。

### NIO(`Channel`、`Buffer`、`Selector`)

#### 详细解释
Java NIO(New IO)是 Java 1.4 引入的新的 IO 模型,它基于通道(`Channel`)和缓冲区(`Buffer`)进行操作,并且支持非阻塞 IO 和选择器(`Selector`)。

- **`Channel`**:通道是对传统 IO 中流的模拟,它可以进行双向的数据传输,类似于铁路轨道,数据可以在通道中流动。常见的通道实现有 `FileChannel`、`SocketChannel` 等。
- **`Buffer`**:缓冲区是一个用于存储数据的容器,本质上是一个数组。数据总是先从通道读取到缓冲区,或者从缓冲区写入到通道。常见的缓冲区实现有 `ByteBuffer`、`CharBuffer` 等。
- **`Selector`**:选择器用于实现非阻塞 IO,它可以监听多个通道的事件(如连接就绪、读就绪、写就绪等),从而实现单线程处理多个通道的功能。

#### 示例代码
以下是一个使用 NIO 进行文件复制的示例:
```java
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;

public class NIOExample {
public static void main(String[] args) {
try (FileInputStream fis = new FileInputStream("source.txt");
FileOutputStream fos = new FileOutputStream("destination.txt");
// 获取输入文件的通道
FileChannel inChannel = fis.getChannel();
// 获取输出文件的通道
FileChannel outChannel = fos.getChannel()) {

// 创建一个容量为 1024 字节的字节缓冲区
ByteBuffer buffer = ByteBuffer.allocate(1024);

// 从输入通道读取数据到缓冲区
while (inChannel.read(buffer) != -1) {
// 切换缓冲区为读模式
buffer.flip();
// 将缓冲区的数据写入输出通道
outChannel.write(buffer);
// 清空缓冲区,为下一次读取做准备
buffer.clear();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
```
#### 代码解释
- 创建 `FileInputStream` 和 `FileOutputStream` 对象,并分别获取它们的通道 `inChannel` 和 `outChannel`。
- 创建一个容量为 1024 字节的 `ByteBuffer` 缓冲区。
- 使用 `while` 循环从输入通道读取数据到缓冲区,每次读取后将缓冲区切换为读模式(`flip` 方法),然后将缓冲区的数据写入输出通道,最后清空缓冲区(`clear` 方法)。

### 传统 IO 与 NIO 的对比

#### 1. 阻塞与非阻塞
- **传统 IO**:是阻塞式的,当进行读写操作时,线程会一直阻塞直到操作完成。例如,在读取文件时,如果文件数据没有准备好,线程会一直等待。
- **NIO**:支持非阻塞 IO,通过选择器可以监听多个通道的事件,线程可以在等待过程中处理其他任务,提高了系统的并发性能。

#### 2. 数据处理方式
- **传统 IO**:基于流的方式,数据是顺序的、单向的,每次只能处理一个字节或一个字符。
- **NIO**:基于缓冲区和通道的方式,数据可以批量处理,提高了数据传输的效率。

#### 3. 适用场景
- **传统 IO**:适用于对数据处理简单、并发量不高的场景,如简单的文件读写操作。
- **NIO**:适用于高并发、大数据量的场景,如网络编程、服务器开发等。

#### **6. 反射与动态代理**
- **知识点**:
- 反射机制(`Class`、`Method`、`Field`)。
- 动态代理(JDK动态代理、CGLIB)。

### 反射机制(`Class`、`Method`、`Field`)

#### 详细解释

##### `Class` 类
在 Java 中,`Class` 类是反射机制的核心,每个类都有一个对应的 `Class` 对象,它包含了该类的所有信息,如类名、方法、字段等。可以通过以下几种方式获取 `Class` 对象:
- **类名.class**:适用于在编译时就知道类名的情况。
- **对象.getClass()**:通过对象实例获取其对应的 `Class` 对象。
- **Class.forName("全限定类名")**:在运行时根据类的全限定名获取 `Class` 对象。

##### `Method` 类
`Method` 类用于表示类中的方法。通过 `Class` 对象可以获取该类的所有方法,也可以根据方法名和参数类型获取指定的方法。获取到 `Method` 对象后,可以使用 `invoke` 方法调用该方法。

##### `Field` 类
`Field` 类用于表示类中的字段。通过 `Class` 对象可以获取该类的所有字段,也可以根据字段名获取指定的字段。获取到 `Field` 对象后,可以使用 `get` 和 `set` 方法获取和设置字段的值。

#### 示例代码
```java
import java.lang.reflect.Field;
import java.lang.reflect.Method;

// 定义一个示例类
class Person {
private String name;
public int age;

public Person(String name, int age) {
this.name = name;
this.age = age;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}
}

public class ReflectionExample {
public static void main(String[] args) throws Exception {
// 获取 Person 类的 Class 对象
Class<?> personClass = Person.class;

// 创建 Person 类的实例
Person person = new Person("Alice", 20);

// 使用反射调用方法
Method getNameMethod = personClass.getMethod("getName");
String name = (String) getNameMethod.invoke(person);
System.out.println("Name: " + name);

Method setNameMethod = personClass.getMethod("setName", String.class);
setNameMethod.invoke(person, "Bob");
name = (String) getNameMethod.invoke(person);
System.out.println("New Name: " + name);

// 使用反射访问字段
Field ageField = personClass.getField("age");
int age = ageField.getInt(person);
System.out.println("Age: " + age);

Field nameField = personClass.getDeclaredField("name");
// 设置可访问私有字段
nameField.setAccessible(true);
String privateName = (String) nameField.get(person);
System.out.println("Private Name: " + privateName);
}
}
```
#### 代码解释
- 通过 `Person.class` 获取 `Person` 类的 `Class` 对象。
- 使用 `getMethod` 方法获取 `getName` 和 `setName` 方法,并使用 `invoke` 方法调用这些方法。
- 使用 `getField` 方法获取 `age` 字段,并使用 `getInt` 方法获取其值。
- 使用 `getDeclaredField` 方法获取私有字段 `name`,并通过 `setAccessible(true)` 打破封装,使用 `get` 方法获取其值。

### 动态代理(JDK 动态代理、CGLIB)

#### 详细解释

##### JDK 动态代理
JDK 动态代理是 Java 原生提供的一种动态代理机制,它基于接口实现。要使用 JDK 动态代理,被代理的类必须实现至少一个接口。JDK 动态代理通过 `java.lang.reflect.Proxy` 类和 `java.lang.reflect.InvocationHandler` 接口来实现。

##### CGLIB 动态代理
CGLIB(Code Generation Library)是一个强大的、高性能的代码生成库,它可以在运行时扩展 Java 类与实现 Java 接口。CGLIB 动态代理基于继承机制,通过生成被代理类的子类来实现代理。

#### 示例代码

##### JDK 动态代理示例
```java
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

// 定义一个接口
interface Subject {
void request();
}

// 实现接口的具体类
class RealSubject implements Subject {
@Override
public void request() {
System.out.println("RealSubject: Handling request.");
}
}

// 实现 InvocationHandler 接口
class ProxyHandler implements InvocationHandler {
private final Object target;

public ProxyHandler(Object target) {
this.target = target;
}

@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("Before method call.");
Object result = method.invoke(target, args);
System.out.println("After method call.");
return result;
}
}

public class JDKDynamicProxyExample {
public static void main(String[] args) {
RealSubject realSubject = new RealSubject();
ProxyHandler proxyHandler = new ProxyHandler(realSubject);

// 创建代理对象
Subject proxySubject = (Subject) Proxy.newProxyInstance(
Subject.class.getClassLoader(),
new Class<?>[]{Subject.class},
proxyHandler
);

proxySubject.request();
}
}
```
#### 代码解释
- 定义 `Subject` 接口和实现该接口的 `RealSubject` 类。
- 实现 `InvocationHandler` 接口的 `ProxyHandler` 类,在 `invoke` 方法中可以添加额外的逻辑。
- 使用 `Proxy.newProxyInstance` 方法创建代理对象,该方法接受三个参数:类加载器、接口数组和 `InvocationHandler` 对象。

##### CGLIB 动态代理示例
```java
import net.sf.cglib.proxy.Enhancer;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;

import java.lang.reflect.Method;

// 定义一个类
class RealClass {
public void doSomething() {
System.out.println("RealClass: Doing something.");
}
}

// 实现 MethodInterceptor 接口
class CglibProxy implements MethodInterceptor {
@Override
public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable {
System.out.println("Before method call.");
Object result = proxy.invokeSuper(obj, args);
System.out.println("After method call.");
return result;
}
}

public class CglibDynamicProxyExample {
public static void main(String[] args) {
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(RealClass.class);
enhancer.setCallback(new CglibProxy());

// 创建代理对象
RealClass proxy = (RealClass) enhancer.create();
proxy.doSomething();
}
}
```
#### 代码解释
- 定义 `RealClass` 类。
- 实现 `MethodInterceptor` 接口的 `CglibProxy` 类,在 `intercept` 方法中可以添加额外的逻辑。
- 使用 `Enhancer` 类创建代理对象,通过 `setSuperclass` 方法指定被代理的类,通过 `setCallback` 方法指定回调对象。

### 对比总结
- **JDK 动态代理**:基于接口,要求被代理的类必须实现接口,代理对象和被代理对象实现相同的接口。
- **CGLIB 动态代理**:基于继承,通过生成被代理类的子类来实现代理,不要求被代理的类实现接口,但不能代理 `final` 类和 `final` 方法。

 

 

posted @   皇问天  阅读(2)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 上周热点回顾(2.24-3.2)
历史上的今天:
2019-02-20 浏览器进程/线程模型及JS运行机制
2019-02-20 linux环境下安装nginx步骤
2017-02-20 hibernate 框架的简单使用
2017-02-20 Hibernate知识梳理
点击右上角即可分享
微信分享提示