13、Java 多线程与并发编程详解

【投稿赢 iPhone 17】「我的第一个开源项目」故事征集:用代码换C位出道! 10w+人浏览 1.6k人参与

开篇概览:并发编程的核心价值

并发(Concurrency) 是指多个任务在同一时间段内交替执行,而并行(Parallelism) 是指多个任务在同一时刻同时执行。Java 通过多线程(Multithreading) 实现并发,使程序能够:

  • 提升 CPU 利用率(尤其在多核系统);
  • 改善响应性(如 GUI 应用不卡顿);
  • 提高吞吐量(如 Web 服务器处理多请求)。

本章将系统讲解 Java 并发编程的核心知识体系:

  1. 线程与进程基础概念
  2. 创建线程的两种方式
  3. 线程生命周期与状态
  4. 线程同步与锁机制synchronizedLock);
  5. 线程间通信wait()/notify());
  6. 线程池ExecutorService);
  7. 高级并发工具简介java.util.concurrent 包)。

⚠️ 重要前提

  • 线程是轻量级进程,共享进程内存;
  • 多线程带来性能提升的同时,也引入线程安全问题(如竞态条件、死锁);
  • 并发 ≠ 并行:单核 CPU 也能并发(时间片轮转),多核才能并行。

一、线程与进程的概念

1.1 进程(Process)

  • 操作系统分配资源的基本单位;
  • 拥有独立的内存空间(堆、栈、代码段等);
  • 进程间通信(IPC)成本高(如管道、消息队列)。

1.2 线程(Thread)

  • CPU 调度的基本单位;
  • 同一进程内的线程共享堆内存,但栈独立
  • 线程间通信成本低(直接读写共享变量);
  • 创建/切换开销远小于进程。

1.3 Java 线程模型

  • Java 线程是1:1 模型(每个 Java 线程对应一个 OS 线程);
  • 由 JVM 和操作系统共同管理;
  • 主线程(main 方法所在线程)启动后,可创建子线程。

二、创建线程的两种方式

2.1 方式一:继承 Thread

步骤:
  1. 自定义类继承 Thread
  2. 重写 run() 方法(线程执行体);
  3. 创建对象并调用 start() 启动线程。
示例:继承 Thread
// 中文注释:定义一个下载任务线程类
class DownloadThread extends Thread {
    private String fileName;

    public DownloadThread(String fileName) {
        this.fileName = fileName;
    }

    // 重写 run() 方法:线程启动后执行的逻辑
    @Override
    public void run() {
        System.out.println("开始下载文件: " + fileName);
        try {
            // 模拟下载耗时操作(休眠2秒)
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            System.err.println("下载被中断: " + e.getMessage());
        }
        System.out.println("文件下载完成: " + fileName);
    }
}

public class ThreadInheritanceDemo {
    public static void main(String[] args) {
        // 创建两个下载线程
        DownloadThread thread1 = new DownloadThread("report.pdf");
        DownloadThread thread2 = new DownloadThread("image.jpg");

        // 启动线程(调用 start(),而非 run()!)
        thread1.start();
        thread2.start();

        // 主线程继续执行
        System.out.println("主线程继续执行其他任务...");
    }
}

关键点

  • 必须调用 start(),直接调用 run() 是同步执行;
  • Java 单继承限制:若类已继承其他类,则无法再继承 Thread

2.2 方式二:实现 Runnable 接口(推荐)

步骤:
  1. 自定义类实现 Runnable 接口;
  2. 实现 run() 方法;
  3. 创建 Thread 对象,传入 Runnable 实例;
  4. 调用 start() 启动线程。
示例:实现 Runnable
// 中文注释:定义一个任务类,实现 Runnable 接口
class PrintTask implements Runnable {
    private String taskName;

    public PrintTask(String taskName) {
        this.taskName = taskName;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 3; i++) {
            System.out.println(taskName + " 执行第 " + i + " 次");
            try {
                Thread.sleep(500); // 模拟任务耗时
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt(); // 恢复中断状态
                System.err.println(taskName + " 被中断");
            }
        }
    }
}

public class RunnableDemo {
    public static void main(String[] args) {
        // 创建 Runnable 任务
        PrintTask task1 = new PrintTask("任务A");
        PrintTask task2 = new PrintTask("任务B");

        // 将任务提交给线程执行
        Thread thread1 = new Thread(task1);
        Thread thread2 = new Thread(task2);

        thread1.start();
        thread2.start();

        System.out.println("主线程结束");
    }
}

优势

  • 避免单继承限制
  • 任务与线程解耦,更符合面向对象设计;
  • 可被线程池复用(见后文)。

三、线程的生命周期与状态

Java 线程有 6 种状态java.lang.Thread.State):

状态说明触发条件
NEW新建new Thread() 后,未调用 start()
RUNNABLE可运行调用 start() 后,等待 CPU 调度(含运行中)
BLOCKED阻塞等待获取 synchronized
WAITING等待调用 wait(), join(), LockSupport.park()
TIMED_WAITING计时等待调用 sleep(), wait(timeout), join(timeout)
TERMINATED终止run() 执行完毕或异常退出

状态转换图

NEW 
  ↓ start()
RUNNABLE ←→ BLOCKED (等待锁)
  ↓ 
WAITING / TIMED_WAITING (主动等待)
  ↓ notify()/超时/中断
RUNNABLE
  ↓ run()结束
TERMINATED
示例:观察线程状态
public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread thread = new Thread(() -> {
            try {
                Thread.sleep(2000); // TIMED_WAITING
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        });

        System.out.println("1. 初始状态: " + thread.getState()); // NEW

        thread.start();
        System.out.println("2. 启动后状态: " + thread.getState()); // RUNNABLE 或 TIMED_WAITING

        Thread.sleep(100); // 确保线程进入 sleep
        System.out.println("3. sleep 中状态: " + thread.getState()); // TIMED_WAITING

        thread.join(); // 等待线程结束
        System.out.println("4. 结束后状态: " + thread.getState()); // TERMINATED
    }
}

四、线程同步与锁机制

4.1 问题:竞态条件(Race Condition)

当多个线程并发修改共享数据时,结果依赖于线程执行顺序,导致不可预测的错误

示例:未同步的计数器
class Counter {
    private int count = 0;

    public void increment() {
        count++; // 非原子操作:读取 → 修改 → 写入
    }

    public int getCount() {
        return count;
    }
}

public class RaceConditionDemo {
    public static void main(String[] args) throws InterruptedException {
        Counter counter = new Counter();
        Thread[] threads = new Thread[10];

        // 创建10个线程,每个线程递增1000次
        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        // 等待所有线程结束
        for (Thread t : threads) {
            t.join();
        }

        System.out.println("期望值: 10000, 实际值: " + counter.getCount()); 
        // 实际值 < 10000(因竞态条件)
    }
}

4.2 解决方案一:synchronized 关键字

原理:
  • 基于 JVM 内置锁(Monitor Lock)
  • 同一时刻只有一个线程能进入同步代码块/方法。
三种用法:
  1. 同步实例方法:锁当前对象(this);
  2. 同步静态方法:锁类对象(Class);
  3. 同步代码块:锁指定对象。
示例:使用 synchronized 修复计数器
class SafeCounter {
    private int count = 0;

    // 方式1:同步实例方法(锁 this)
    public synchronized void increment() {
        count++;
    }

    // 方式2:同步代码块(更灵活)
    public void decrement() {
        synchronized (this) {
            count--;
        }
    }

    public int getCount() {
        return count;
    }
}

public class SynchronizedDemo {
    public static void main(String[] args) throws InterruptedException {
        SafeCounter counter = new SafeCounter();
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println("期望值: 10000, 实际值: " + counter.getCount()); // 10000
    }
}

关键点

  • synchronized可重入锁(同一线程可多次获取);
  • 锁对象必须一致(如都用 this)。

4.3 解决方案二:Lock 接口(java.util.concurrent.locks

优势 vs synchronized
  • 显式加锁/解锁,更灵活;
  • 支持尝试加锁tryLock());
  • 支持可中断锁lockInterruptibly());
  • 支持公平锁(按请求顺序获取)。
核心接口:Lock
方法说明
lock()获取锁(阻塞)
unlock()释放锁(必须在 finally 中调用)
tryLock()尝试获取锁(立即返回)
lockInterruptibly()可中断地获取锁
示例:使用 ReentrantLock
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class LockCounter {
    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;
    }
}

public class LockDemo {
    public static void main(String[] args) throws InterruptedException {
        LockCounter counter = new LockCounter();
        Thread[] threads = new Thread[10];

        for (int i = 0; i < 10; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 1000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println("期望值: 10000, 实际值: " + counter.getCount()); // 10000
    }
}

最佳实践

  • 永远在 finally 块中释放锁
  • 优先使用 synchronized(简单场景),复杂场景用 Lock

五、线程间通信:wait() / notify() / notifyAll()

5.1 为什么需要线程通信?

  • 线程间需要协调工作(如生产者-消费者);
  • synchronized 仅保证互斥,不提供等待/唤醒机制。

5.2 核心方法(必须在 synchronized 块中调用)

方法说明
wait()释放锁,进入等待队列
notify()唤醒一个等待线程
notifyAll()唤醒所有等待线程

⚠️ 重要规则

  • 必须在同步代码块中调用;
  • 调用对象必须是锁对象
  • wait()释放锁notify() 不会。

5.3 示例:生产者-消费者模型

import java.util.LinkedList;
import java.util.Queue;

// 中文注释:共享缓冲区(线程安全)
class Buffer {
    private final Queue<Integer> queue = new LinkedList<>();
    private final int capacity = 5;

    // 生产者放入数据
    public synchronized void produce(int item) throws InterruptedException {
        // 缓冲区满时等待
        while (queue.size() == capacity) {
            System.out.println("缓冲区已满,生产者等待...");
            wait(); // 释放锁,进入等待队列
        }

        queue.offer(item);
        System.out.println("生产: " + item + ", 缓冲区大小: " + queue.size());

        notifyAll(); // 唤醒所有消费者
    }

    // 消费者取出数据
    public synchronized int consume() throws InterruptedException {
        // 缓冲区空时等待
        while (queue.isEmpty()) {
            System.out.println("缓冲区为空,消费者等待...");
            wait(); // 释放锁,进入等待队列
        }

        int item = queue.poll();
        System.out.println("消费: " + item + ", 缓冲区大小: " + queue.size());

        notifyAll(); // 唤醒所有生产者
        return item;
    }
}

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();

        // 生产者线程
        Thread producer = new Thread(() -> {
            for (int i = 1; i <= 10; i++) {
                try {
                    buffer.produce(i);
                    Thread.sleep(100); // 模拟生产间隔
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        // 消费者线程
        Thread consumer = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                try {
                    buffer.consume();
                    Thread.sleep(150); // 模拟消费间隔
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        });

        producer.start();
        consumer.start();
    }
}

关键点

  • 使用 while 而非 if 检查条件(避免虚假唤醒);
  • 优先使用 notifyAll()(除非确定只需唤醒一个)。

六、线程池(ExecutorService)

6.1 为什么需要线程池?

  • 避免频繁创建/销毁线程(开销大);
  • 控制并发数量(防止资源耗尽);
  • 统一管理线程生命周期

6.2 核心接口:ExecutorService

方法说明
submit(Runnable/Callable)提交任务,返回 Future
execute(Runnable)提交任务(无返回值)
shutdown()平滑关闭(等待任务完成)
shutdownNow()立即关闭(尝试中断任务)

6.3 创建线程池:Executors 工厂类

方法说明
newFixedThreadPool(n)固定大小线程池
newCachedThreadPool()缓存线程池(空闲60秒回收)
newSingleThreadExecutor()单线程池(保证顺序执行)
newScheduledThreadPool(n)支持定时/周期任务

⚠️ 生产环境建议
不要使用 Executors!应使用 ThreadPoolExecutor 手动创建,避免 FixedThreadPoolLinkedBlockingQueue 无界队列导致 OOM。


6.4 示例:使用线程池处理任务

import java.util.concurrent.*;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        // 创建固定大小为3的线程池
        ExecutorService executor = Executors.newFixedThreadPool(3);

        // 提交10个任务
        for (int i = 1; i <= 10; i++) {
            final int taskId = i;
            executor.execute(() -> {
                System.out.println("任务 " + taskId + " 由线程 " + 
                    Thread.currentThread().getName() + " 执行");
                try {
                    Thread.sleep(1000); // 模拟任务耗时
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
                System.out.println("任务 " + taskId + " 完成");
            });
        }

        // 关闭线程池(不再接受新任务)
        executor.shutdown();
        try {
            // 等待所有任务完成(最多60秒)
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow(); // 强制关闭
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }

        System.out.println("所有任务已完成");
    }
}

输出特点

  • 同一时刻最多3个任务并发执行;
  • 线程被复用(如 pool-1-thread-1 执行多个任务)。

七、高级并发工具简介(java.util.concurrent)

7.1 CallableFuture

  • Callable:类似 Runnable,但有返回值可抛出异常
  • Future:获取异步计算结果。
示例:
ExecutorService executor = Executors.newSingleThreadExecutor();
Callable<Integer> task = () -> {
    Thread.sleep(1000);
    return 42;
};

Future<Integer> future = executor.submit(task);
System.out.println("结果: " + future.get()); // 阻塞直到完成

7.2 CountDownLatch

  • 允许一个或多个线程等待其他线程完成操作。
示例:等待所有服务启动
CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 模拟服务启动
        System.out.println("服务启动中...");
        latch.countDown(); // 计数减1
    }).start();
}

latch.await(); // 等待计数归零
System.out.println("所有服务已启动");

7.3 CyclicBarrier

  • 让一组线程互相等待,直到所有线程都到达屏障点。
示例:多线程计算后汇总
CyclicBarrier barrier = new CyclicBarrier(3, () -> {
    System.out.println("所有线程计算完成,开始汇总结果");
});

for (int i = 0; i < 3; i++) {
    new Thread(() -> {
        // 模拟计算
        System.out.println("线程 " + Thread.currentThread().getName() + " 计算中");
        try {
            barrier.await(); // 等待其他线程
        } catch (Exception e) {
            e.printStackTrace();
        }
    }).start();
}

7.4 并发集合

  • ConcurrentHashMap:线程安全的 HashMap;
  • CopyOnWriteArrayList:写时复制的 List(读多写少场景);
  • BlockingQueue:阻塞队列(如 ArrayBlockingQueue, LinkedBlockingQueue)。

八、总结:并发编程核心原则

问题解决方案
线程创建优先 Runnable + 线程池
线程安全synchronized / Lock
线程通信wait()/notify()Condition
任务管理ExecutorService
高级协调CountDownLatch, CyclicBarrier, Semaphore
安全集合ConcurrentHashMap, BlockingQueue

📌 核心思想
“并发编程的目标不是让程序跑得更快,而是让程序在多任务环境下正确、高效地运行。”
掌握 Java 并发,是构建高性能、高可靠系统的必备能力。务必牢记:简单场景用 synchronized,复杂场景用 java.util.concurrent

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙茶清欢

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值