Eden 区:| A(标记) | B | C(标记) | D | From 区:| E(标记) | F | G(标记) | To 区: | A | C | E | G |
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` 方法。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 分享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知识梳理