贝博恩创新科技网

Java并发编程如何高效入门?

Java 并发编程教程:从入门到精通

目录

  1. 为什么需要并发编程?
  2. 核心基础概念
    • 1 线程 vs 进程
    • 2 并发 vs 并行
    • 3 多线程的优势与风险
  3. Java 并发基础:ThreadRunnable
    • 1 继承 Thread
    • 2 实现 Runnable 接口 (推荐)
    • 3 CallableFuture (获取线程返回值)
  4. 核心工具:synchronized 关键字与锁
    • 1 synchronized 的原理:内置锁/监视器锁
    • 2 synchronized 的三种用法
    • 3 Lock 接口与 ReentrantLock (更灵活的锁)
  5. 线程间通信:wait(), notify(), notifyAll()
  6. 高级并发工具:java.util.concurrent
    • 1 原子类 (java.util.concurrent.atomic)
    • 2 并发集合 (java.util.concurrent)
    • 3 线程池 (java.util.concurrent.Executors)
  7. 强大的 java.util.concurrent 工具类
    • 1 CountDownLatch (倒计时门闩)
    • 2 CyclicBarrier (循环栅栏)
    • 3 Semaphore (信号量)
    • 4 Exchanger (交换器)
  8. Java 内存模型
    • 1 什么是 JMM?
    • 2 可见性、原子性、有序性
    • 3 volatile 关键字
    • 4 happens-before 原则
  9. 最佳实践与常见陷阱
    • 1 避免锁竞争
    • 2 优先使用并发工具而非 synchronized
    • 3 避免死锁
    • 4 理解线程池的参数
  10. 总结与学习路径

为什么需要并发编程?

  • 提高 CPU 利用率:现代计算机都是多核的,单线程程序无法充分利用多核 CPU 的计算能力,并发可以将计算任务分配到多个核心上同时执行。
  • 提升程序响应速度:对于 I/O 密集型任务(如网络请求、文件读写),线程可以在等待 I/O 时让出 CPU,让其他线程继续执行,避免整个程序被阻塞。
  • 简化编程模型:某些问题(如服务器处理多个客户端请求)天然地适合用多线程来建模,使代码逻辑更清晰。

核心基础概念

1 线程 vs 进程

  • 进程:是操作系统进行资源分配和调度的基本单位,它拥有独立的内存空间和系统资源,一个进程可以包含一个或多个线程。
  • 线程:是 CPU 调度和分派的基本单位,也被称为轻量级进程,同一进程中的多个线程共享该进程的内存空间和资源,但每个线程有自己独立的程序计数器、栈和局部变量。

简单比喻:一个进程就像一家公司,拥有自己的办公大楼(内存空间),线程就像公司里的员工,他们共享办公大楼和设备(共享资源),但每个员工都有自己的办公桌(栈空间)和要做的工作(程序计数器)。

Java并发编程如何高效入门?-图1
(图片来源网络,侵删)

2 并发 vs 并行

  • 并发:逻辑上同时处理多个任务,在单核 CPU 上,通过快速切换线程,让多个任务“看起来像”在同时运行,宏观上是同时,微观上是交替。
  • 并行:物理上同时处理多个任务,在多核 CPU 上,真正地将不同的任务分配给不同的核心同时执行。

3 多线程的优势与风险

  • 优势:如上所述,提高资源利用率和响应速度。
  • 风险
    • 线程安全问题:当多个线程同时读写同一个共享资源时,可能会导致数据不一致或损坏。
    • 死锁:多个线程因互相等待对方持有的锁而无限期地阻塞。
    • 上下文切换开销:CPU 在线程间切换需要保存和恢复线程状态,会消耗一定的资源。
    • 增加复杂性:并发程序比单线程程序更难设计、调试和维护。

Java 并发基础:ThreadRunnable

创建线程主要有三种方式。

1 继承 Thread

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
    }
}
public class Main {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        t1.start(); // 启动线程,会调用 run() 方法
    }
}

缺点:Java 不支持多重继承,继承 Thread 类后无法再继承其他类。

2 实现 Runnable 接口 (推荐)

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("Thread " + Thread.currentThread().getName() + " is running.");
    }
}
public class Main {
    public static void main(String[] args) {
        MyRunnable myRunnable = new MyRunnable();
        Thread t1 = new Thread(myRunnable);
        t1.start();
    }
}

优点:更灵活,避免了单继承的限制,将任务(Runnable)和执行线程(Thread)解耦。

3 CallableFuture (获取线程返回值)

Runnablerun() 方法没有返回值,如果需要从线程中获取结果,可以使用 Callable

Java并发编程如何高效入门?-图2
(图片来源网络,侵删)
import java.util.concurrent.*;
class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        System.out.println("Callable is running.");
        return 123; // 可以有返回值
    }
}
public class Main {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService executor = Executors.newSingleThreadExecutor();
        Future<Integer> future = executor.submit(new MyCallable());
        // 主线程可以做其他事情...
        // 获取线程执行结果,如果线程未完成,get() 会阻塞
        Integer result = future.get();
        System.out.println("Result from Callable: " + result);
        executor.shutdown();
    }
}

Future 代表一个异步计算的结果,你可以通过它来检查计算是否完成,以及获取最终的计算结果。

核心工具:synchronized 关键字与锁

为了解决线程安全问题,我们需要使用同步机制来保证共享资源在同一时间只被一个线程访问。

1 synchronized 的原理:内置锁/监视器锁

synchronized 是 Java 内置的锁机制,也称为监视器锁,它的工作原理是:

  1. 当一个线程访问一个对象的 synchronized 代码块或方法时,它会尝试获取该对象的
  2. 如果获取成功,它就可以执行代码块内的代码。
  3. 在执行期间,其他任何试图访问该对象 synchronized 代码块或方法的线程都会被阻塞,直到持有锁的线程执行完毕并释放锁

2 synchronized 的三种用法

  1. 实例方法:锁是当前对象实例 (this)。
    public synchronized void instanceMethod() {
        // 代码
    }
  2. 静态方法:锁是当前类的 Class 对象。
    public static synchronized void staticMethod() {
        // 代码
    }
  3. 代码块:锁可以是任意对象,更灵活。
    public void someMethod() {
        synchronized (this) { // 锁是当前对象实例
            // 代码
        }
        synchronized (MyClass.class) { // 锁是类的Class对象
            // 代码
        }
        Object lock = new Object();
        synchronized (lock) { // 锁是任意对象
            // 代码
        }
    }

3 Lock 接口与 ReentrantLock (更灵活的锁)

java.util.concurrent.locks.Lock 接口提供了比 synchronized 更强大的功能,例如尝试获取锁可中断地获取锁超时获取锁

Java并发编程如何高效入门?-图3
(图片来源网络,侵删)
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class Counter {
    private int count = 0;
    private final Lock lock = new ReentrantLock();
    public void increment() {
        lock.lock(); // 加锁
        try {
            count++;
        } finally {
            lock.unlock(); // 在 finally 块中解锁,确保锁一定会被释放
        }
    }
    public int getCount() {
        return count;
    }
}

ReentrantLock vs synchronized:

  • ReentrantLock 提供了更灵活的锁定机制。
  • synchronized 是 JVM 内置的,代码更简洁,在发生异常时 JVM 会自动释放锁。
  • 在 Java 5 中,ReentrantLock 的性能通常优于 synchronized,但在 Java 6 之后,JVM 对 synchronized 做了大量优化,两者性能差距已不大,通常优先选择 synchronized,除非需要 Lock 提供的特殊功能。

线程间通信:wait(), notify(), notifyAll()

这些方法在 Object 类中定义,用于线程间的协作。

  • void wait(): 让当前线程等待,直到其他线程调用此对象的 notify()notifyAll() 方法。必须在 synchronized 代码块或方法中调用
  • void notify(): 唤醒在此对象监视器上等待的单个线程。
  • void notifyAll(): 唤醒在此对象监视器上等待的所有线程。

经典生产者-消费者模型示例

class SharedResource {
    private int item = 0;
    private boolean isProduced = false;
    public synchronized void produce() throws InterruptedException {
        while (isProduced) { // 使用 while 而不是 if,防止虚假唤醒
            wait();
        }
        item++;
        System.out.println("Produced: " + item);
        isProduced = true;
        notify(); // 唤醒消费者
    }
    public synchronized void consume() throws InterruptedException {
        while (!isProduced) {
            wait();
        }
        System.out.println("Consumed: " + item);
        isProduced = false;
        notify(); // 唤醒生产者
    }
}

高级并发工具:java.util.concurrent

JUC 包是 Java 并发包的精髓,提供了大量高性能、高可靠性的并发工具。

1 原子类 (java.util.concurrent.atomic)

AtomicInteger, AtomicLong 等,它们使用 CAS (Compare-And-Swap) 操作,是一种无锁算法,在保证原子性的同时,避免了线程阻塞和上下文切换,性能非常高。

import java.util.concurrent.atomic.AtomicInteger;
class AtomicCounter {
    private AtomicInteger count = new AtomicInteger(0);
    public void increment() {
        count.incrementAndGet(); // 原子操作
    }
    public int getCount() {
        return count.get();
    }
}

2 并发集合 (java.util.concurrent)

ConcurrentHashMap: 线程安全的 HashMap,采用分段锁或 CAS 优化,并发性能远高于 Collections.synchronizedMap(new HashMap<>())CopyOnWriteArrayList: 写时复制列表,适合读多写少的场景,每次修改操作都会复制底层数组,保证了读操作的线程安全。 BlockingQueue: 阻塞队列,是生产者-消费者模式的绝佳实现,当队列为空时,消费者线程会阻塞;当队列满时,生产者线程会阻塞。

3 线程池 (java.util.concurrent.Executors)

频繁创建和销毁线程是昂贵的,线程池可以复用已创建的线程,提高性能。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class Main {
    public static void main(String[] args) {
        // 创建一个固定大小为 5 的线程池
        ExecutorService executor = Executors.newFixedThreadPool(5);
        for (int i = 0; i < 10; i++) {
            Runnable worker = new WorkerThread("Task " + i);
            executor.execute(worker); // 提交任务到线程池
        }
        executor.shutdown(); // 关闭线程池,不再接受新任务
        while (!executor.isTerminated()) {
            // 等待所有任务完成
        }
        System.out.println("All threads finished.");
    }
}

更推荐使用 ThreadPoolExecutor 直接创建,因为它可以更精细地控制线程池参数(核心线程数、最大线程数、队列大小、拒绝策略等)。

强大的 java.util.concurrent 工具类

这些类用于控制线程的执行流程。

1 CountDownLatch (倒计时门闩)

让一个或多个线程等待其他一组线程完成操作后再执行。

CountDownLatch latch = new CountDownLatch(3); // 初始计数为 3
// 三个工作线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // do something
        latch.countDown(); // 计数减一
    }).start();
}
// 主线程等待
latch.await(); // 阻塞,直到计数变为 0
System.out.println("All workers have finished.");

2 CyclicBarrier (循环栅栏)

让一组线程到达一个屏障(同步点)时被阻塞,直到最后一个线程到达屏障时,所有线程才会继续执行,屏障可以重复使用。

CyclicBarrier barrier = new CyclicBarrier(3);
for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        try {
            // do something
            System.out.println(Thread.currentThread().getName() + " is waiting at the barrier.");
            barrier.await(); // 等待其他线程
            System.out.println(Thread.currentThread().getName() + " has crossed the barrier.");
        } catch (Exception e) {}
    }).start();
}

3 Semaphore (信号量)

用于控制同时访问某个特定资源的线程数量,类似于“许可证”。

Semaphore semaphore = new Semaphore(3); // 只允许 3 个线程同时访问
for (int i = 0; i < 10; i++) {
    new Thread(() -> {
        try {
            semaphore.acquire(); // 获取许可证,如果满了则阻塞
            System.out.println(Thread.currentThread().getName() + " acquired a permit.");
            // do something...
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            semaphore.release(); // 释放许可证
            System.out.println(Thread.currentThread().getName() + " released a permit.");
        }
    }).start();
}

Java 内存模型

JMM 是一个抽象的概念,它定义了一套规则,用来规范在多线程环境下,哪些操作是可见的,以及如何避免指令重排序。

1 什么是 JMM?

JMM 定义了线程和主内存之间的抽象关系:每个线程都有自己的工作内存,线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,不能直接读写主内存中的变量,不同线程之间无法直接访问对方的工作内存,变量值的传递均需要通过主内存来完成。

2 可见性、原子性、有序性

  • 原子性:一个或多个操作,要么全部执行且执行的过程不会被任何因素打断,要么就都不执行。synchronizedLock 可以保证原子性。
  • 可见性:当一个线程修改了一个共享变量的值,其他线程能够立即得知这个修改。volatilesynchronized 可以保证可见性。
  • 有序性:即程序执行的顺序按照代码的先后顺序执行,JMM 允许编译器和处理器进行指令重排序优化。volatilesynchronized 可以保证有序性。

3 volatile 关键字

volatile 是一个轻量级的同步机制,它有两个主要作用:

  1. 保证可见性:当一个线程修改了 volatile 变量,新值会立刻同步到主内存,并且其他线程读取时会从主内存读取,保证了线程间的可见性。
  2. 禁止指令重排序:通过插入内存屏障,防止 volatile 变量与它的前后的指令进行重排序优化,保证了程序的执行顺序。

注意volatile 不保证原子性。volatile int count = 0; count++; 不是原子操作。

4 happens-before 原则

它是判断数据是否存在竞争、线程是否安全的重要依据,核心思想是:如果两个操作之间存在 happens-before 关系,那么前一个操作的结果对后一个操作就是可见的,主要规则包括:

  • 程序次序规则:在一个线程内,书写在前面的代码 happens-before 书写在后面的代码。
  • 管程锁定规则:一个 unlock 操作 happens-before 后面对同一个锁的 lock 操作。
  • volatile 变量规则:对一个 volatile 变量的写操作 happens-before 后面对这个变量的读操作。
  • 线程启动规则:线程的 start() 方法 happens-before 于此线程的每一个动作。
  • 线程终止规则:线程中的所有操作都 happens-before 对此线程的终止检测。
  • 传递性:A happens-before B,且 B happens-before C,A happens-before C。

最佳实践与常见陷阱

  • 1 避免锁竞争:尽量缩小同步代码块的范围,只保护必要的共享资源,考虑使用 ConcurrentHashMap 等并发集合。
  • 2 优先使用并发工具而非 synchronized:优先使用 java.util.concurrent 包中的高级工具(如 CountDownLatch, Semaphore, ConcurrentHashMap),它们经过了精心设计和优化,通常比手动使用 synchronized 更安全、更高效。
  • 3 避免死锁:死锁的四个必要条件(互斥、请求与保持、不可剥夺、循环等待),破坏其中一个即可避免,最实用的方法是按顺序获取锁
  • 4 理解线程池的参数:创建线程池时,合理设置核心线程数、最大线程数和队列大小,不要使用 Executors.newCachedThreadPool()Executors.newSingleThreadExecutor(),它们可能因任务队列无界而导致 OutOfMemoryError,推荐使用 ThreadPoolExecutor 直接构造。

总结与学习路径

学习路径建议

  1. 打好基础:深刻理解线程、进程、并发、并行的概念。
  2. 掌握基本同步:熟练使用 synchronizedvolatile 解决简单的线程安全问题。
  3. 深入 JUC 包:这是 Java 并发的核心,重点学习线程池、ConcurrentHashMap、原子类和几个常用的同步工具类(CountDownLatch, CyclicBarrier)。
  4. 理解 JMM:学习 happens-before 原则,理解可见性、原子性、有序性在底层是如何实现的。
  5. 实践与调优:通过实际项目练习,学习如何分析和解决并发问题,以及如何进行性能调优。

Java 并发编程是一门博大精深的学问,需要大量的阅读和编码实践才能真正掌握,希望这份教程能为你提供一个清晰的路线图,祝你学习顺利!

分享:
扫描分享到社交APP
上一篇
下一篇