Java并发编程从入门到进阶 多场景实战---youkeit.xyz/6121/
在单核 CPU 时代,程序如同一人独行,按部就班。而在多核 CPU 普及的今天,程序如同一个团队,若能让成员们协同工作,便能爆发出数倍的效能。Java 并发编程,正是教会我们如何指挥这支“团队”的艺术。然而,这门艺术也以其复杂性和易错性闻名。本教程旨在为学习者铺设一条清晰、平缓的道路,通过从核心概念到多场景实战的递进式学习,让你真正掌握 Java 并发的精髓。
一、并发第一课:从“单线程”到“多线程”的思维转变
一切并发问题,都始于一个核心概念:线程(Thread) 。如果说一个进程是一个独立的“应用程序”,那么线程就是这个程序内部的“执行流”。传统的单线程程序只有一条执行流,而多线程程序则拥有多条,它们可以“同时”运行。
核心知识点:
- 创建线程:Java 中有三种创建线程的方式,但推荐使用实现
Runnable接口的方式,因为它更符合面向对象的设计原则(避免单继承的局限)。 - 启动线程:调用
thread.start()而非thread.run()。start()会向 JVM 申请一个新的线程来执行run()方法,而run()只是在当前线程中普通调用。
场景一:模拟多任务处理
想象一个场景,你需要同时下载一个文件和播放音乐。在单线程世界里,你必须等下载完才能听音乐。但在多线程世界里,两者可以并行。
// 1. 定义任务
class DownloadTask implements Runnable {
@Override
public void run() {
System.out.println("开始下载文件...");
try {
// 模拟下载耗时
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("文件下载完成!");
}
}
class MusicTask implements Runnable {
@Override
public void run() {
System.out.println("开始播放音乐...");
for (int i = 1; i <= 5; i++) {
System.out.println("播放第 " + i + " 首歌曲");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("音乐播放完毕。");
}
}
// 2. 创建并启动线程
public class BasicConcurrencyDemo {
public static void main(String[] args) {
System.out.println("主线程开始执行任务调度。");
Thread downloadThread = new Thread(new DownloadTask());
Thread musicThread = new Thread(new MusicTask());
downloadThread.start();
musicThread.start();
System.out.println("主线程已将任务派发,继续执行其他工作。");
}
}
教育要点:运行此代码,观察输出顺序。你会发现“主线程已将任务派发…”会很快打印出来,而下载和播放任务在“后台”交错进行。这就是并发最直观的体现。
二、进阶挑战:当线程需要“沟通”与“协作”
多线程带来了效率,也带来了新的问题:资源竞争和数据不一致。如果多个线程同时修改同一个数据,结果就可能错乱。这时,我们就需要引入“锁”的机制。
核心知识点:
- 临界区:访问共享资源的代码块。
- 互斥锁(
synchronized) :Java 提供的关键字,用于保护临界区,确保同一时间只有一个线程能执行它。
场景二:模拟多人同时售票
一个经典的并发问题:多个售票窗口(线程)同时售卖同一列车的车票(共享资源)。如果没有锁,可能会出现同一张票被卖给多个人,或者卖出负数票的荒谬情况。
class TicketSeller {
private int tickets = 100; // 共享资源:100张票
// 使用 synchronized 关键字保护售票方法
public synchronized void sell() {
if (tickets > 0) {
try {
// 模拟出票耗时
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " 卖出第 " + (tickets--) + " 张票。");
} else {
System.out.println(Thread.currentThread().getName() + " 发现票已售罄。");
}
}
}
public class TicketSellingDemo {
public static void main(String[] args) {
TicketSeller seller = new TicketSeller();
// 创建4个售票窗口(4个线程)
for (int i = 1; i <= 4; i++) {
new Thread(() -> {
while (true) {
seller.sell();
// 简单判断,当票数为0时退出循环
if (seller.getTicketCount() <= 0) {
break;
}
}
}, "窗口-" + i).start();
}
}
// 为了让外部能查询票数,可以加一个方法
public int getTicketCount() {
return tickets;
}
}
教育要点:
- 尝试去掉
synchronized:你会看到输出中出现重复的票号,甚至负数票,这就是典型的线程安全问题。 - 理解
synchronized:它像一把锁,当一个线程进入sell()方法时,就拿到了锁,其他线程只能在门外等待,直到该线程执行完方法并释放锁。这保证了tickets--操作的原子性。
三、高级实战:构建高效的“生产者-消费者”模型
当线程间的协作变得更复杂时,简单的 synchronized 就不够用了。我们不仅需要锁,还需要线程间的“通信机制”,即让一个线程在特定条件下等待,并在条件满足时被其他线程唤醒。
核心知识点:
wait():让当前线程等待,并释放锁。notify()/notifyAll():唤醒一个或所有正在等待的线程。java.util.concurrent包:JDK 提供的强大工具包,极大地简化了并发编程。BlockingQueue(阻塞队列)就是实现生产者-消费者模式的利器。
场景三:异步任务处理中心
想象一个网站的后台,用户请求(生产者)不断产生,而服务器的工作线程(消费者)负责处理这些请求。我们希望请求能被有序处理,当没有请求时,工作线程能休息,而不是空转。
方法一:使用 wait() 和 notify()(传统方式)
import java.util.LinkedList;
import java.util.Queue;
class TaskQueue {
private final Queue<String> queue = new LinkedList<>();
private final int capacity;
public TaskQueue(int capacity) {
this.capacity = capacity;
}
// 生产者方法:添加任务
public synchronized void addTask(String task) throws InterruptedException {
while (queue.size() >= capacity) {
System.out.println("队列已满,生产者等待...");
wait();
}
queue.add(task);
System.out.println("生产者添加任务: " + task);
notifyAll(); // 唤醒可能正在等待的消费者
}
// 消费者方法:处理任务
public synchronized String consumeTask() throws InterruptedException {
while (queue.isEmpty()) {
System.out.println("队列为空,消费者等待...");
wait();
}
String task = queue.poll();
System.out.println("消费者处理任务: " + task);
notifyAll(); // 唤醒可能正在等待的生产者
return task;
}
}
方法二:使用 BlockingQueue(现代推荐方式)
BlockingQueue 内部已经实现了所有等待和通知的逻辑,使用起来非常简单和安全。
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
// 生产者线程
class Producer implements Runnable {
private final BlockingQueue<String> queue;
private final int taskCount;
public Producer(BlockingQueue<String> queue, int taskCount) {
this.queue = queue;
this.taskCount = taskCount;
}
@Override
public void run() {
try {
for (int i = 1; i <= taskCount; i++) {
String task = "Task-" + i;
queue.put(task); // 如果队列满,put()方法会自动阻塞
System.out.println("Produced: " + task);
Thread.sleep(50); // 模拟生产耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
// 消费者线程
class Consumer implements Runnable {
private final BlockingQueue<String> queue;
public Consumer(BlockingQueue<String> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
while (true) {
String task = queue.take(); // 如果队列空,take()方法会自动阻塞
System.out.println("Consumed: " + task);
Thread.sleep(200); // 模拟处理耗时
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}
public class ProducerConsumerDemo {
public static void main(String[] args) {
int capacity = 5;
BlockingQueue<String> taskQueue = new ArrayBlockingQueue<>(capacity);
// 启动一个生产者和两个消费者
new Thread(new Producer(taskQueue, 10), "Producer-1").start();
new Thread(new Consumer(taskQueue), "Consumer-1").start();
new Thread(new Consumer(taskQueue), "Consumer-2").start();
}
}
教育要点:对比两种方法。BlockingQueue 将复杂的同步逻辑封装起来,让开发者可以更专注于业务逻辑本身。这正是 java.util.concurrent 包设计的初衷:提供更高级、更安全的并发工具。
四、未来之路:拥抱现代并发工具
Java 并发编程的世界博大精深。掌握了以上基础后,你可以继续探索:
- 线程池(
ExecutorService) :复用线程,降低创建和销毁线程的开销,是现代服务端应用的标准配置。 - 原子类(
AtomicInteger等) :在简单场景下,提供比synchronized更轻量级的、无锁的线程安全操作。 CompletableFuture:优雅地处理异步回调,构建复杂的异步编程流程。- 锁(
ReentrantLock) :比synchronized更灵活的锁,支持公平锁、可中断锁等高级特性。
结语
Java 并发编程的学习曲线或许陡峭,但每一步的攀登都会让你对程序的运行有更深刻的理解。从创建第一个线程,到解决线程安全问题,再到构建高效的协作模型,你不仅是在学习技术,更是在锻炼一种“并发思维”。记住,最好的学习方式永远是动手实践。打开你的 IDE,运行这些代码,修改它们,观察结果,并思考背后的原理。当你能自如地指挥多线程“团队”协同作战时,你就真正掌握了这门强大的艺术。