Java并发编程从入门到进阶 多场景实战

51 阅读7分钟

t0165545c108e11a468.jpg

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;
    }
}

教育要点

  1. 尝试去掉 synchronized:你会看到输出中出现重复的票号,甚至负数票,这就是典型的线程安全问题。
  2. 理解 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,运行这些代码,修改它们,观察结果,并思考背后的原理。当你能自如地指挥多线程“团队”协同作战时,你就真正掌握了这门强大的艺术。