Java 多线程终极教程
目录
-
(图片来源网络,侵删)- 1 什么是线程?
- 2 多线程的优势
- 3 多线程的挑战
-
- 1
Thread类 - 2
Runnable接口 - 3
Callable与Future(有返回值的线程) - 4 继承
Threadvs 实现Runnable
- 1
-
- 1 六种线程状态
- 2 状态转换图
-
- 1 为什么需要同步?——
synchronized的原理 - 2
synchronized关键字的三种用法 - 3
Lock接口与ReentrantLock - 4
synchronizedvsLock
- 1 为什么需要同步?——
-
(图片来源网络,侵删)- 1
wait(),notify(),notifyAll() - 2
join()方法 - 3
volatile关键字
- 1
-
- 1 线程池 (
Executor框架) - 2
CountDownLatch(倒计时门闩) - 3
CyclicBarrier(循环栅栏) - 4
Semaphore(信号量) - 5
BlockingQueue(阻塞队列)
- 1 线程池 (
-
- 1
java.util.concurrent.atomic包 - 2 CAS (Compare-And-Swap) 原理
- 1
-
- 1 JMM 是什么?
- 2 可见性、有序性与原子性
- 3
happens-before原则
-
- 1 最佳实践
- 2 常见陷阱 (死锁、活锁、饥饿)
第一章:为什么需要多线程?
1 什么是线程?
进程 是操作系统进行资源分配和调度的基本单位,它拥有独立的内存空间和系统资源,一个应用程序可以启动多个进程。
线程 是进程中的一个执行单元,是 CPU 调度的基本单位,一个进程可以包含多个线程,它们共享该进程的内存空间和系统资源。
比喻:
- 进程 就像一个工厂。
- 线程 就像工厂里的工人,工人(线程)共享工厂的资源(内存),但每个工人可以同时独立地完成自己的任务。
2 多线程的优势
- 提高 CPU 利用率:当一个线程因为 I/O 操作(如读写文件、网络请求)而阻塞时,CPU 可以切换到其他就绪的线程去执行,而不是空闲等待。
- 提高程序响应速度:对于有图形界面的应用,可以将耗时操作放在后台线程执行,避免界面卡顿。
- 简化程序模型:对于某些复杂任务,可以将其分解为多个独立的子任务,每个子任务由一个线程处理,使程序结构更清晰。
3 多线程的挑战
多线程在带来便利的同时,也引入了复杂的问题:
- 线程安全问题:当多个线程同时读写共享数据时,可能会导致数据不一致或损坏,这是最核心、最常见的问题。
- 死锁:两个或多个线程互相等待对方释放资源,导致所有线程都无法继续执行。
- 上下文切换开销:CPU 在不同线程之间切换需要保存和恢复线程的上下文,这会带来一定的性能开销。
- 复杂性增加:多线程程序比单线程程序更难设计、调试和维护。
第二章:Java 线程基础
创建线程在 Java 中主要有三种方式。
1 Thread 类
Thread 类是 Java 中对线程的抽象,我们可以通过继承 Thread 类并重写其 run() 方法来创建一个线程。
示例代码:
// 1. 继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println("Thread " + Thread.currentThread().getName() + " is running, i = " + i);
}
}
}
public class ThreadDemo {
public static void main(String[] args) {
// 2. 创建线程对象
MyThread thread1 = new MyThread();
MyThread thread2 = new MyThread();
// 3. 启动线程
thread1.start(); // 注意:是调用 start() 方法,而不是 run()
thread2.start();
}
}
重要提示:调用 thread.start() 会告诉 JVM 创建一个新的线程,并让这个新线程去执行 run() 方法,如果直接调用 thread.run(),则只是在当前线程中执行了 run() 方法,并没有创建新线程。
2 Runnable 接口
实现 Runnable 接口是更推荐的方式,因为它避免了 Java 单继承的限制。
示例代码:
// 1. 实现 Runnable 接口
class MyRunnable implements Runnable {
@Override
public void run() {
// 线程要执行的任务
for (int i = 0; i < 5; i++) {
System.out.println("Runnable " + Thread.currentThread().getName() + " is running, i = " + i);
}
}
}
public class RunnableDemo {
public static void main(String[] args) {
// 2. 创建 Runnable 实现类对象
MyRunnable myRunnable = new MyRunnable();
// 3. 创建 Thread 对象,并将 Runnable 对象作为构造参数传入
Thread thread1 = new Thread(myRunnable, "Thread-A");
Thread thread2 = new Thread(myRunnable, "Thread-B");
// 4. 启动线程
thread1.start();
thread2.start();
}
}
3 Callable 与 Future (有返回值的线程)
Runnable 的 run() 方法不能返回结果,也不能抛出受检异常,如果需要线程返回结果,可以使用 Callable 接口。
Callable 是一个泛型接口,其 call() 方法有返回值。
Future 接口代表一个异步计算的未来结果,可以用来检查计算是否完成、获取计算结果或取消计算。
示例代码:
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 0; i <= 100; i++) {
sum += i;
}
// 模拟耗时操作
Thread.sleep(2000);
return sum;
}
}
public class CallableDemo {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 1. 创建线程池
ExecutorService executor = Executors.newSingleThreadExecutor();
// 2. 提交 Callable 任务,并获取 Future 对象
Future<Integer> future = executor.submit(new MyCallable());
// 3. 可以做其他事情...
// 4. 获取结果(此方法会阻塞,直到计算完成)
Integer result = future.get();
System.out.println("计算结果是: " + result);
// 5. 关闭线程池
executor.shutdown();
}
}
4 继承 Thread vs 实现 Runnable
| 特性 | 继承 Thread |
实现 Runnable / Callable |
|---|---|---|
| 继承 | 单继承,无法再继承其他类 | 可以继承其他类,实现多个接口 |
| 共享资源 | 不易于共享(除非静态变量) | 非常方便,只需将同一个 Runnable 实例传给多个 Thread |
| 面向接口 | 面向对象,设计上不够灵活 | 符合“面向接口编程”的设计思想,更灵活 |
| 推荐度 | 不推荐 | 强烈推荐 |
第三章:线程的生命周期与状态
Java 线程在其生命周期中会经历多种状态,在 Thread 类中定义了六种状态:
- NEW (新建):线程被创建,但尚未调用
start()方法。 - RUNNABLE (可运行):线程调用了
start()方法,正在等待 CPU 时间片,或者正在执行,在操作系统中,它可能处于“就绪”或“运行”状态,但在 Java API 层面,它们被统一称为RUNNABLE。 - BLOCKED (阻塞):线程因为等待监视器锁(即
synchronized锁)而进入阻塞状态。 - WAITING (等待):线程因为调用了
wait(),join()或LockSupport.park()方法而无限期等待,直到其他线程唤醒它。 - TIMED_WAITING (超时等待):与
WAITING类似,但它可以在指定时间后自动唤醒,例如调用了sleep(long),wait(long),join(long)等。 - TERMINATED (终止):线程已经执行完毕或因异常退出。
1 状态转换图
+----------------+ +------------------+
| NEW | --> | RUNNABLE |
+----------------+ +------------------+
| |
| start() | 获取CPU时间片
v v
+----------------+ +------------------+
| TERMINATED | <-- | RUNNING |
+----------------+ +------------------+
^ |
| 执行完毕/异常 | I/O阻塞 / 等待锁
| v
| +------------------+
+-----------> | BLOCKED |
+------------------+
^ |
| 被唤醒 / 锁释放 | 超时 / 被唤醒
| v
+----------------+ +------------------+
| TIMED_WAITING | <-- | WAITING |
+----------------+ +------------------+
第四章:线程的核心:synchronized 与锁
1 为什么需要同步?—— synchronized 的原理
当多个线程同时访问共享资源(如一个变量)时,可能会发生竞态条件,导致数据不一致。
示例: 一个简单的计数器
class Counter {
private int count = 0;
public void increment() {
count++; // 这不是原子操作!
}
public int getCount() {
return count;
}
}
count++ 实际上包含三个步骤:
- 读取
count的值。 - 增加 1。
- 写入 新值回
count。
如果两个线程 A 和 B 同时执行 increment(),它们可能在读取到同一个旧值(如 0)后,各自增加 1,然后都写回 1,导致结果为 1 而不是预期的 2。
synchronized 关键字可以确保代码块或方法在同一时间只有一个线程可以进入,从而保证了原子性。
2 synchronized 关键字的三种用法
-
修饰实例方法:锁是当前对象实例(
this)。public synchronized void increment() { count++; } -
修饰静态方法:锁是当前类的
Class对象。public static synchronized void doSomething() { // ... } -
修饰代码块:可以指定锁对象,更灵活。
public void increment() { synchronized (this) { // 锁是当前对象实例 count++; } } public void anotherMethod() { synchronized (Counter.class) { // 锁是当前类的 Class 对象 // ... } }
3 Lock 接口与 ReentrantLock
java.util.concurrent.locks.Lock 接口提供了比 synchronized 更广泛的锁定操作。ReentrantLock 是 Lock 接口最常用的实现。
synchronized vs ReentrantLock
| 特性 | synchronized |
ReentrantLock |
|---|---|---|
| 锁的获取 | JVM 自动管理 | 手动 lock() 和 unlock() |
| 可中断性 | 不可中断 | 可以 lockInterruptibly(),可以被中断 |
| 公平性 | 非公平锁 | 可以指定是否为公平锁 |
| 条件变量 | 一个(wait/notify) |
多个 Condition 对象 |
| 灵活性 | 较低 | 高,可以实现更复杂的锁逻辑 |
ReentrantLock 使用示例:
import java.util.concurrent.locks.ReentrantLock;
class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 加锁
try {
count++;
} finally {
lock.unlock(); // 确保锁被释放
}
}
// 使用 tryLock 避免阻塞
public boolean tryIncrement() {
if (lock.tryLock()) {
try {
count++;
return true;
} finally {
lock.unlock();
}
}
return false;
}
}
最佳实践:总是在 finally 块中释放锁,以确保锁一定会被释放,避免死锁。
第五章:线程间的通信与协作
1 wait(), notify(), notifyAll()
这三个方法用于在 synchronized 代码块或方法中实现线程间的等待和通知机制。
void wait():导致当前线程等待,直到其他线程调用此对象的notify()或notifyAll()方法,释放当前持有的锁。void notify():唤醒在此对象监视器上等待的单个线程,选择哪个线程是任意的。void notifyAll():唤醒在此对象监视器上等待的所有线程。
经典生产者-消费者模型示例:
class Buffer {
private int content;
private boolean isEmpty = true;
public synchronized void put(int value) throws InterruptedException {
while (!isEmpty) { // 使用 while 而不是 if,防止虚假唤醒
wait(); // 如果缓冲区不为空,生产者等待
}
this.content = value;
isEmpty = false;
System.out.println("生产者生产: " + value);
notifyAll(); // 唤醒消费者
}
public synchronized int get() throws InterruptedException {
while (isEmpty) { // 使用 while 而不是 if
wait(); // 如果缓冲区为空,消费者等待
}
isEmpty = true;
System.out.println("消费者消费: " + content);
notifyAll(); // 唤醒生产者
return content;
}
}
注意:
- 必须在
synchronized上下文中使用这三个方法。 - 建议使用
while循环来检查条件,而不是if,以应对“虚假唤醒”(Spurious Wakeup)的情况。
2 join() 方法
join() 方法的作用是等待调用该方法的线程执行完毕。
public class JoinDemo {
public static void main(String[] args) throws InterruptedException {
Thread threadB = new Thread(() -> {
try {
Thread.sleep(1000);
System.out.println("线程B执行完毕");
} catch (InterruptedException e) {
e.printStackTrace();
}
});
threadB.start();
System.out.println("主线程等待线程B...");
threadB.join(); // 主线程会在这里阻塞,直到 threadB 执行完毕
System.out.println("主线程继续执行");
}
}
3 volatile 关键字
volatile 关键字可以保证可见性和有序性,但不能保证原子性。
- 可见性:当一个线程修改了
volatile变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。 - 禁止指令重排:
volatile关键字会插入内存屏障,禁止 JVM 和 CPU 对其前后的指令进行重排序优化。
适用场景:一个线程写,多个线程读的简单状态标志。
class Worker implements Runnable {
// 使用 volatile 保证 flag 的可见性
private volatile boolean flag = true;
public void stop() {
this.flag = false;
}
@Override
public void run() {
while (flag) {
// do something
System.out.println("Worker is running...");
}
System.out.println("Worker stopped.");
}
}
第六章:高级并发工具
JUC (java.util.concurrent) 包是 Java 并发包的精髓,提供了大量强大的并发工具。
1 线程池 (Executor 框架)
频繁地创建和销毁线程非常消耗资源,线程池可以复用已创建的线程,提高性能。
核心接口和类:
Executor:顶层接口,只定义了execute(Runnable)方法。ExecutorService:扩展了Executor,添加了生命周期管理(shutdown(),submit()等)。ThreadPoolExecutor:最核心的线程池实现类。Executors:一个工具类,提供了创建预定义配置线程池的静态工厂方法。
常用线程池:
FixedThreadPool(固定大小线程池)ExecutorService executor = Executors.newFixedThreadPool(10);
CachedThreadPool(缓存线程池)ExecutorService executor = Executors.newCachedThreadPool();
SingleThreadExecutor(单线程执行器)ExecutorService executor = Executors.newSingleThreadExecutor();
最佳实践:避免使用 Executors 创建线程池,因为它创建的线程池队列(如 LinkedBlockingQueue)是无界的,可能导致内存溢出,推荐直接使用 ThreadPoolExecutor 自定义参数。
// 推荐的自定义线程池创建方式
ThreadPoolExecutor executor = new ThreadPoolExecutor(
corePoolSize, // 核心线程数
maximumPoolSize, // 最大线程数
keepAliveTime, // 空闲线程存活时间
TimeUnit.SECONDS, // 时间单位
new ArrayBlockingQueue<>(100), // 有界任务队列
new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);
2 CountDownLatch (倒计时门闩)
CountDownLatch 允许一个或多个线程等待其他一组线程完成操作。
场景:主线程等待所有子任务线程执行完毕后再继续。
import java.util.concurrent.CountDownLatch;
public class CountDownLatchDemo {
public static void main(String[] args) throws InterruptedException {
int workerCount = 3;
CountDownLatch latch = new CountDownLatch(workerCount);
for (int i = 0; i < workerCount; i++) {
new Thread(() -> {
System.out.println("Worker " + Thread.currentThread().getName() + " is working...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Worker " + Thread.currentThread().getName() + " finished.");
latch.countDown(); // 完成任务,计数器减一
}).start();
}
System.out.println("Main thread is waiting for workers to finish...");
latch.await(); // 阻塞,直到计数器为 0
System.out.println("All workers finished. Main thread continues.");
}
}
3 CyclicBarrier (循环栅栏)
CyclicBarrier 让一组线程到达一个屏障(也可以叫同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行。CyclicBarrier 可以被循环使用。
场景:多个线程分阶段处理任务,每个阶段都需要所有线程都准备好后才能进入下一阶段。
import java.util.concurrent.BrokenBarrierException;
import java.util.concurrent.CyclicBarrier;
public class CyclicBarrierDemo {
public static void main(String[] args) {
int parties = 3;
CyclicBarrier barrier = new CyclicBarrier(parties, () -> {
System.out.println("所有线程都已到达屏障,开始下一阶段!");
});
for (int i = 0; i < parties; i++) {
new Thread(() -> {
System.out.println(Thread.currentThread().getName() + " 到达屏障 A");
try {
barrier.await(); // 等待其他线程
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 到达屏障 B");
try {
barrier.await(); // 再次等待
} catch (InterruptedException | BrokenBarrierException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 所有任务完成");
}).start();
}
}
}
4 Semaphore (信号量)
Semaphore 用于控制同时访问特定资源的线程数量。
场景:停车场有 5 个车位,最多只能停 5 辆车。
import java.util.concurrent.Semaphore;
public class SemaphoreDemo {
public static void main(String[] args) {
int permits = 3; // 模拟 3 个资源
Semaphore semaphore = new Semaphore(permits);
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
System.out.println(Thread.currentThread().getName() + " 尝试获取资源...");
semaphore.acquire(); // 获取一个许可,如果已满则阻塞
System.out.println(Thread.currentThread().getName() + " 成功获取资源,剩余: " + semaphore.availablePermits());
Thread.sleep(2000); // 模拟使用资源
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
semaphore.release(); // 释放一个许可
System.out.println(Thread.currentThread().getName() + " 释放资源,剩余: " + semaphore.availablePermits());
}
}).start();
}
}
}
5 BlockingQueue (阻塞队列)
BlockingQueue 是一个在队列基础上又支持了两个附加操作的队列:
- 支持阻塞的插入方法:当队列满时,队列会阻塞插入元素的线程,直到队列不满。
- 支持阻塞的移除方法:当队列空时,获取元素的线程会等待队列变为非空。
实现类:ArrayBlockingQueue, LinkedBlockingQueue, SynchronousQueue 等。
场景:生产者-消费者模型的最完美实现。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
class Producer implements Runnable {
private final BlockingQueue<Integer> queue;
public Producer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
for (int i = 0; i < 10; i++) {
System.out.println("生产者生产: " + i);
queue.put(i); // 如果队列满,这里会阻塞
Thread.sleep(500);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
class Consumer implements Runnable {
private final BlockingQueue<Integer> queue;
public Consumer(BlockingQueue<Integer> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
Integer item = queue.take(); // 如果队列空,这里会阻塞
System.out.println("消费者消费: " + item);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
public class BlockingQueueDemo {
public static void main(String[] args) {
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(5);
new Thread(new Producer(queue)).start();
new Thread(new Consumer(queue)).start();
}
}
第七章:原子类与无锁编程
1 java.util.concurrent.atomic 包
Atomic 包下的类都利用了 CAS (Compare-And-Swap) 机制来实现原子操作,它们通常比 synchronized 性能更高,因为它们不涉及线程阻塞和上下文切换。
常用类:
AtomicInteger: 原子整数AtomicLong: 原子长整型AtomicBoolean: 原子布尔值AtomicReference: 原子引用
示例: 使用 AtomicInteger 实现线程安全的计数器
import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
private final AtomicInteger count = new AtomicInteger(0);
public void increment() {
// 内部使用 CAS 实现,是原子操作
count.incrementAndGet();
}
public int getCount() {
return count.get();
}
}
2 CAS (Compare-And-Swap) 原理
CAS 是一条 CPU 并发原语,其思想是“我认为这个值应该是 V,如果是的话,就更新为新值 N,否则就不更新,并告诉我现在的值是多少”。
CAS 包含三个操作数:
- V:要更新的变量
- A:预期值
- B:新值
当且仅当 V 的值等于 A 时,CAS 会通过原子方式将 V 的值更新为 B;否则,不会执行任何操作。
CAS 的优点是无锁,避免了线程阻塞,在高并发下性能更好,但它的缺点是:
- ABA 问题:如果一个值原来是 A,变成了 B,又变回了 A,CAS 操作会认为它没有变化,可以通过在变量上附加版本号(如
AtomicStampedReference)来解决。 - 自旋开销:CAS 失败,会不断重试,在竞争激烈时,重试开销可能很大。
第八章:Java 内存模型
1 JMM 是什么?
Java 内存模型 是一个抽象的概念,它定义了一套规则,用于规范在多线程环境下,哪些内存操作是可见的,以及如何进行指令重排序,它的目标是解决在多线程下,由于处理器缓存、指令重排序等导致的内存可见性问题。
2 可见性、有序性与原子性
- 原子性:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。
synchronized和Lock可以保证原子性。 - 可见性:当一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。
volatile、synchronized、final可以保证可见性。 - 有序性:即程序执行的顺序按照代码的先后顺序执行。
volatile和synchronized可以保证有序性。
3 happens-before 原则
happens-before 原则是判断内存可见性的重要依据,如果两个操作之间存在 happens-before 关系,那么前一个操作的结果对后一个操作就是可见的。
- 程序次序规则:在一个线程内,书写在前面的代码
happens-before书写在后面的代码。 - 管程锁定规则:一个 unlock 操作
happens-before后面对同一个锁的 lock 操作。 - volatile 变量规则:对一个 volatile 变量的写操作
happens-before后面对这个变量的读操作。 - 线程启动规则:线程的
start()方法happens-before于此线程的每一个动作。 - 线程终止规则:线程中的所有操作都
happens-before对此线程的终止检测。 - 传递性:A
happens-beforeB,且 Bhappens-beforeC,Ahappens-beforeC。
第九章:最佳实践与常见陷阱
1 最佳实践
- 优先使用并发工具类:尽量使用 JUC 包中的高级工具,而不是自己用
synchronized从零开始实现。 - 避免过度同步:只在必要时进行同步,否则会降低并发性能。
- 优先使用
volatile和Atomic类:对于简单的状态标志或计数器,它们比锁更高效。 - 谨慎使用
synchronized:尽量使用synchronized代码块而不是同步整个方法,并指定精确的锁对象。 - 使用线程池:不要手动创建线程,使用
ThreadPoolExecutor。 - 持有锁的时间尽可能短:在
synchronized块内不要调用耗时操作(如 I/O)。 - 优先使用
BlockingQueue实现生产者-消费者:它简洁、高效且不易出错。 - 文档化线程安全策略:明确说明类或方法的线程安全级别。
2 常见陷阱
-
死锁
- 原因:两个或多个线程互相等待对方持有的锁,导致谁也无法继续执行。
- 四个必要条件:互斥、占有且等待、不可剥夺、循环等待。
- 预防:破坏循环等待条件(按固定顺序获取锁)、使用超时锁、避免嵌套锁。
-
活锁
- 现象:线程虽然没有阻塞,但彼此之间互相谦让,导致谁也无法继续执行。
- 示例:两个人在过道相遇,都往左边让,结果又堵住了;然后又都往右边让,还是堵住。
- 解决:引入随机性,或者设置一个“不谦让”的规则。
-
饥饿
- 现象:某个线程因为无法获取到所需的资源,导致一直无法执行。
- 原因:通常是因为锁的实现是“不公平”的,某些线程可能一直获取不到锁。
- 解决:使用公平锁(但会降低吞吐量)。
总结与学习路径
Java 多线程是一个庞大而复杂的领域,但也是成为一名优秀 Java 工程师的必备技能。
学习路径建议:
- 入门阶段:掌握
Thread,Runnable,synchronized的基本用法,理解线程状态和生命周期,能够写出简单的多线程程序。 - 进阶阶段:深入学习
Lock,volatile,wait/notify,理解线程间的通信和协作,开始使用Executor框架和 JUC 包中的工具类,如CountDownLatch,BlockingQueue等。 - 精通阶段:深入理解 Java 内存模型,掌握
happens-before原则,学习CAS原理和Atomic类,理解无锁编程,能够分析并解决死锁、活锁等复杂问题,并能根据业务场景设计出高效、健壮的并发方案。 - 实践阶段:在实际项目中大量运用并发知识,阅读优秀开源框架的源码(如 Netty, Disruptor),学习大师们的并发设计思想。
希望这份教程能为你开启 Java 多世界的大门,理论结合实践是掌握任何技术的最佳途径,祝你学习顺利!
