本文涵盖 Java 多线程 28 个核心知识点,从基础概念、线程创建、线程控制,到同步机制、锁机制、线程池、线程安全问题,全体系覆盖。


多线程 01:概述

1.1 什么是多线程

多线程是指 在一个程序中同时运行多个线程,每个线程执行不同的任务。线程是进程内的执行单元,一个进程可以包含多个线程,共享进程的内存资源(如堆内存、文件句柄等)。

1.2 多线程的核心价值

  • 提高程序执行效率:充分利用 CPU 多核资源,避免单线程阻塞导致程序闲置。
  • 提升用户体验:例如 GUI 程序中,后台线程执行耗时任务,主线程保持响应。
  • 支持并发场景:如电商秒杀、聊天系统、日志处理等高并发场景。

1.3 进程与线程的区别

维度 进程 线程
资源分配 操作系统分配独立内存空间 共享所属进程的内存,仅拥有独立栈、程序计数器
切换开销 进程切换开销大(需切换内存、CPU 等资源) 线程切换开销小(仅切换栈、程序计数器)
依赖关系 进程是资源分配的基本单位 线程是 CPU 调度的基本单位,依赖进程存在

多线程 02:线程、进程、多线程

2.1 核心概念拆解

  1. 进程:操作系统进行资源分配和调度的独立单位,一个应用程序对应一个进程(如 QQ 进程、浏览器进程)。
  2. 线程:进程内的执行路径,是 CPU 调度的最小单位,一个进程可包含多个线程。
  3. 多线程:同一进程中创建多个线程,并发执行不同任务,实现 “同时处理多件事”。

2.2 多线程的执行原理

CPU 通过 时间片轮转 调度线程:给每个线程分配短暂的 CPU 时间片,线程在时间片内执行任务,时间片耗尽后切换至下一个线程,宏观上呈现 “多线程同时执行” 的效果。

2.3 多线程的使用场景

  • 后台任务:如日志写入、文件下载、数据解析。
  • 前端交互:如 GUI 程序的按钮点击、页面刷新。
  • 高并发业务:如电商订单处理、秒杀系统、消息推送。

多线程 03:继承 Thread 类

3.1 核心原理

Thread 类是 Java 中表示线程的核心类,继承 Thread 类并重写 run() 方法,即可创建自定义线程。

3.2 实现步骤

  1. 定义类继承 Thread 类。
  2. 重写 run() 方法,在方法中编写线程执行的任务逻辑。
  3. 创建线程对象,调用 start() 方法启动线程(不能直接调用 run () 方法,否则不会启动新线程)。

3.3 代码示例

// 自定义线程类:继承Thread
class MyThread extends Thread {
    @Override
    public void run() {
        // 线程任务:循环打印线程名称
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            try {
                // 休眠100ms,模拟耗时任务
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ThreadExtendsDemo {
    public static void main(String[] args) {
        // 创建两个线程对象
        MyThread thread1 = new MyThread();
        MyThread thread2 = new MyThread();
        
        // 设置线程名称
        thread1.setName("线程A");
        thread2.setName("线程B");
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

3.4 注意事项

  • 继承 Thread 类后,类无法再继承其他类(Java 单继承特性)。
  • run() 方法是线程的任务入口,start() 方法会启动新线程并执行 run()

多线程 04:网图下载(线程实战)

4.1 实战需求

使用多线程批量下载网络图片,通过 Thread 类实现,模拟真实场景中的多线程文件下载。

4.2 核心依赖

Java 网络编程需使用 URLURLConnection 类,IO 流需使用 FileOutputStreamInputStream 类。

4.3 代码示例

import java.io.FileOutputStream;
import java.io.InputStream;
import java.net.URL;
import java.net.URLConnection;

// 下载线程类
class DownloadThread extends Thread {
    // 网络图片地址
    private String imageUrl;
    // 本地保存路径
    private String savePath;

    public DownloadThread(String imageUrl, String savePath) {
        this.imageUrl = imageUrl;
        this.savePath = savePath;
    }

    @Override
    public void run() {
        try {
            URL url = new URL(imageUrl);
            URLConnection connection = url.openConnection();
            // 设置请求头,模拟浏览器
            connection.setRequestProperty("User-Agent", "Mozilla/5.0");
            
            // 获取输入流
            InputStream inputStream = connection.getInputStream();
            // 写入本地文件
            FileOutputStream outputStream = new FileOutputStream(savePath);
            
            byte[] buffer = new byte[1024];
            int len;
            while ((len = inputStream.read(buffer)) != -1) {
                outputStream.write(buffer, 0, len);
            }
            
            // 关闭流
            outputStream.close();
            inputStream.close();
            System.out.println(Thread.currentThread().getName() + ":图片下载完成 -> " + savePath);
        } catch (Exception e) {
            e.printStackTrace();
            System.out.println(Thread.currentThread().getName() + ":图片下载失败");
        }
    }
}

// 测试类
public class ImageDownloadDemo {
    public static void main(String[] args) {
        // 测试用的网图地址(可替换为真实地址)
        String url1 = "https://img-blog.csdnimg.cn/202401011200001.png";
        String url2 = "https://img-blog.csdnimg.cn/202401011200002.png";
        
        // 创建下载线程
        DownloadThread thread1 = new DownloadThread(url1, "image1.png");
        DownloadThread thread2 = new DownloadThread(url2, "image2.png");
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

4.4 运行说明

  • 需替换 imageUrl 为真实的网络图片地址。
  • 图片会保存到项目根目录下,可直接查看。

多线程 05:实现 Runnable 接口

5.1 核心优势

实现 Runnable 接口是创建线程的 推荐方式,解决了继承 Thread 类的单继承限制,同时可实现资源共享。

5.2 实现步骤

  1. 定义类实现 Runnable 接口。
  2. 重写 run() 方法,编写线程任务逻辑。
  3. 创建 Runnable 实现类对象,将其作为参数传入 Thread 构造器。
  4. 调用 Thread 对象的 start() 方法启动线程。

5.3 代码示例

// 自定义任务类:实现Runnable
class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class RunnableDemo {
    public static void main(String[] args) {
        // 创建任务对象
        MyRunnable myRunnable = new MyRunnable();
        
        // 创建线程对象,绑定任务
        Thread thread1 = new Thread(myRunnable, "线程A");
        Thread thread2 = new Thread(myRunnable, "线程B");
        
        // 启动线程
        thread1.start();
        thread2.start();
    }
}

5.4 与继承 Thread 的对比

方式 优点 缺点
继承 Thread 代码简单,直接获取当前线程 单继承限制,无法继承其他类
实现 Runnable 无单继承限制,支持资源共享 需通过 Thread 包装,稍繁琐

多线程 06:初识并发问题

6.1 什么是并发问题

多线程同时操作 共享资源 时,因线程执行的随机性(CPU 时间片切换),导致数据结果不符合预期,即并发安全问题。

6.2 典型案例:卖票

多个窗口同时卖票,若未处理并发,会出现超卖、重复卖票问题。

6.3 代码示例(问题演示)

class TicketRunnable implements Runnable {
    // 共享票源
    private int ticketNum = 100;

    @Override
    public void run() {
        while (ticketNum > 0) {
            try {
                // 模拟售票耗时
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 售票
            System.out.println(Thread.currentThread().getName() + ":售出第" + ticketNum + "张票");
            ticketNum--;
        }
    }
}

public class ConcurrencyProblemDemo {
    public static void main(String[] args) {
        TicketRunnable ticketTask = new TicketRunnable();
        
        // 创建三个窗口线程
        Thread window1 = new Thread(ticketTask, "窗口1");
        Thread window2 = new Thread(ticketTask, "窗口2");
        Thread window3 = new Thread(ticketTask, "窗口3");
        
        window1.start();
        window2.start();
        window3.start();
    }
}

6.4 问题分析

  • 运行后会出现 重复售票(多个窗口同时售出同一张票)、超卖(ticketNum 变为负数)的问题。
  • 原因:多个线程同时读取、修改 ticketNum,未保证操作的原子性。

多线程 07:龟兔赛跑(线程控制实战)

7.1 实战需求

模拟龟兔赛跑场景,通过线程休眠控制线程执行速度,实现 “兔子跑得快但睡觉,乌龟慢但一直跑” 的效果。

7.2 代码示例

class RaceThread implements Runnable {
    // 胜利者
    private static String winner;

    @Override
    public void run() {
        String name = Thread.currentThread().getName();
        // 模拟赛跑:100步
        for (int step = 1; step <= 100; step++) {
            // 兔子线程:每走50步休眠100ms(睡觉)
            if (name.equals("兔子") && step % 50 == 0) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            // 乌龟线程:每走10步休眠20ms(慢走)
            if (name.equals("乌龟")) {
                try {
                    Thread.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(name + ":走了" + step + "步");
        }
        // 判断是否到达终点
        if (winner == null) {
            winner = name;
            System.out.println("\n===== 比赛结果 =====");
            System.out.println("胜利者:" + winner);
        }
    }
}

public class TortoiseRabbitRace {
    public static void main(String[] args) {
        RaceThread raceTask = new RaceThread();
        
        Thread rabbit = new Thread(raceTask, "兔子");
        Thread tortoise = new Thread(raceTask, "乌龟");
        
        rabbit.start();
        tortoise.start();
    }
}

7.3 运行结果

  • 兔子中途睡觉,最终乌龟获胜(或兔子获胜,取决于线程调度)。
  • 直观展示了线程休眠对执行节奏的影响。

多线程 08:实现 Callable 接口

8.1 核心优势

Callable 接口相比 Runnable 支持 有返回值 的线程任务,可抛出异常,是实现带返回值线程的标准方式(通常配合线程池使用)。

8.2 核心方法

Callable 接口仅定义一个方法:V call() throws Exception,线程任务逻辑写在 call() 中,返回值为任务执行结果。

8.3 实现步骤

  1. 定义类实现 Callable<V> 接口,指定返回值类型。
  2. 重写 call() 方法,编写带返回值的任务逻辑。
  3. 创建 FutureTask 对象,包装 Callable 任务(FutureTask 实现了 RunnableFuture 接口)。
  4. FutureTask 传入 Thread 构造器,启动线程。
  5. 调用 FutureTask.get() 获取线程执行结果(会阻塞等待线程完成)。

8.4 代码示例

import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

// 带返回值的线程任务:计算1~100的和
class SumCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
            Thread.sleep(10); // 模拟耗时
        }
        return sum;
    }
}

// 测试类
public class CallableDemo {
    public static void main(String[] args) {
        // 创建Callable任务
        SumCallable sumTask = new SumCallable();
        // 包装为FutureTask
        FutureTask<Integer> futureTask = new FutureTask<>(sumTask);
        
        // 创建线程并启动
        Thread thread = new Thread(futureTask, "计算线程");
        thread.start();
        
        try {
            // 获取线程执行结果(阻塞等待)
            int result = futureTask.get();
            System.out.println("1~100的和为:" + result);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}

8.5 关键特性

  • get() 方法会阻塞当前线程,直到任务执行完成并返回结果。
  • 可通过 isDone() 判断任务是否执行完成。
  • 支持抛出异常,便于处理任务执行中的错误。

多线程 09:静态代理模式

9.1 什么是静态代理

静态代理是代理模式的基础实现,指在编译期就确定代理关系,由代理类完全代理真实对象的行为,在不修改真实对象代码的前提下,对方法进行增强。

9.2 核心角色

  1. 抽象角色:定义统一的接口(如Runnable),约束真实对象和代理对象的行为。
  2. 真实角色:实现抽象接口,编写核心业务逻辑。
  3. 代理角色:实现抽象接口,持有真实对象的引用,在调用真实对象方法前后添加增强逻辑。

9.3 代码示例(结合线程场景)

// 1. 抽象角色:定义线程任务接口
interface Task {
    void execute();
}

// 2. 真实角色:核心业务逻辑
class RealTask implements Task {
    @Override
    public void execute() {
        System.out.println("执行核心业务:线程任务处理");
    }
}

// 3. 代理角色:静态代理,增强真实对象的方法
class ProxyTask implements Task {
    // 持有真实对象的引用
    private final Task realTask;

    public ProxyTask(Task realTask) {
        this.realTask = realTask;
    }

    @Override
    public void execute() {
        // 前置增强:任务执行前的操作
        System.out.println("【代理增强】任务开始前,初始化资源、记录日志");
        // 调用真实对象的核心方法
        realTask.execute();
        // 后置增强:任务执行后的操作
        System.out.println("【代理增强】任务结束后,释放资源、统计耗时");
    }
}

// 测试类
public class StaticProxyDemo {
    public static void main(String[] args) {
        // 创建真实任务对象
        Task realTask = new RealTask();
        // 创建代理对象,包装真实任务
        Task proxyTask = new ProxyTask(realTask);
        // 执行代理方法(自动触发增强逻辑)
        proxyTask.execute();
    }
}

9.4 与线程的关联

Java 中Thread类本质就是静态代理的典型应用:

  • Runnable是抽象角色,定义run()方法;
  • 自定义任务类是真实角色,实现run()的核心逻辑;
  • Thread是代理角色,持有Runnable引用,在start()中调用run(),同时封装线程启动、调度的底层逻辑。

9.5 优缺点

✅ 优点:不修改真实对象代码,符合开闭原则,逻辑简单易实现。❌ 缺点:每个真实对象都需要对应一个代理类,类数量爆炸,维护成本高。


多线程 10:Lambda 表达式

10.1 什么是 Lambda 表达式

Lambda 表达式是 Java 8 引入的函数式编程特性,用于简化只有一个抽象方法的接口(函数式接口) 的实现,大幅减少冗余代码,在多线程中常用于简化RunnableCallable的实现。

10.2 核心语法

(参数列表) -> { 方法体 }
  • 简化规则:参数类型可省略、单参数可省略括号、单语句可省略大括号和return
  • 前提:接口必须是函数式接口(仅含一个抽象方法,如RunnableCallableComparator)。

10.3 代码示例(线程场景)

传统方式(匿名内部类)
// 传统方式:匿名内部类实现Runnable
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("传统方式:线程任务执行");
    }
}).start();
Lambda 简化方式
// Lambda简化:一行代码实现线程任务
new Thread(() -> System.out.println("Lambda方式:线程任务执行")).start();
带参数的 Lambda 示例
// 函数式接口:自定义带参数的任务接口
@FunctionalInterface
interface CalcTask {
    int calculate(int a, int b);
}

// Lambda实现接口
CalcTask add = (a, b) -> a + b;
System.out.println("1+2=" + add.calculate(1, 2)); // 输出3

10.4 多线程中的应用

Lambda 表达式完美适配多线程场景,可大幅简化线程创建代码:

// 多线程Lambda实战:同时启动3个线程
for (int i = 0; i < 3; i++) {
    new Thread(() -> System.out.println(Thread.currentThread().getName() + "执行任务"))
            .start();
}

10.5 注意事项

  • Lambda 只能用于函数式接口,不能用于普通类或多方法接口。
  • Lambda 中访问外部变量时,变量默认是final(不可修改)。

多线程 11:线程停止

11.1 线程停止的核心原则

Java 中不推荐使用stop()suspend()等过时方法强制停止线程(会导致资源泄漏、数据不一致),推荐使用标志位中断法,让线程安全、优雅地退出。

11.2 推荐实现方式:标志位中断

通过自定义volatile标志位,控制线程的执行与停止,保证线程在退出前完成资源清理。

代码示例
class StopThread implements Runnable {
    // 1. 定义volatile标志位(保证多线程间的可见性)
    private volatile boolean flag = true;

    @Override
    public void run() {
        // 2. 循环判断标志位,控制线程执行
        while (flag) {
            System.out.println(Thread.currentThread().getName() + ":线程正在执行");
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
                // 捕获中断异常后,可选择退出线程
                Thread.currentThread().interrupt(); // 重新设置中断状态
                return;
            }
        }
        // 3. 线程退出前的资源清理
        System.out.println(Thread.currentThread().getName() + ":线程安全退出,资源已清理");
    }

    // 4. 对外提供停止方法,修改标志位
    public void stopThread() {
        this.flag = false;
    }
}

// 测试类
public class ThreadStopDemo {
    public static void main(String[] args) throws InterruptedException {
        StopThread stopThread = new StopThread();
        new Thread(stopThread, "线程A").start();

        // 主线程休眠3秒后,停止线程A
        Thread.sleep(3000);
        stopThread.stopThread();
    }
}

11.3 中断方法interrupt()的使用

interrupt()用于中断线程,但不会直接停止线程,仅会设置线程的中断状态:

  • 若线程处于阻塞状态(sleep()wait()join()),会抛出InterruptedException,并清除中断状态;
  • 若线程处于运行状态,仅设置中断标志位,需通过isInterrupted()判断并手动退出。
代码示例
Thread thread = new Thread(() -> {
    // 循环判断中断状态
    while (!Thread.currentThread().isInterrupted()) {
        System.out.println("线程执行中...");
    }
    System.out.println("线程因中断退出");
});

thread.start();
Thread.sleep(1000);
thread.interrupt(); // 发送中断信号

11.4 注意事项

  • 绝对禁止使用stop()方法,会强制终止线程,导致锁未释放、数据损坏等严重问题。
  • 标志位必须用volatile修饰,保证多线程间的可见性,避免出现死循环。

多线程 12:线程休眠_sleep

12.1 什么是sleep()

sleep(long millis)Thread类的静态方法,用于让当前线程进入阻塞状态,暂停执行指定的毫秒数,时间结束后自动恢复到就绪状态,等待 CPU 调度。

12.2 核心特性

  1. 属于当前线程Thread.sleep()只会让调用该方法的线程休眠,不会影响其他线程。
  2. 不释放锁:线程休眠时,会持有已获取的锁,其他线程无法获取该锁。
  3. 可中断:休眠期间若调用interrupt(),会抛出InterruptedException,并提前结束休眠。

12.3 代码示例

public class ThreadSleepDemo {
    public static void main(String[] args) {
        // 启动线程,模拟倒计时
        new Thread(() -> {
            for (int i = 10; i >= 0; i--) {
                System.out.println("倒计时:" + i);
                try {
                    // 线程休眠1秒
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("倒计时结束!");
        }, "倒计时线程").start();
    }
}

12.4 常见应用场景

  • 模拟耗时任务,测试多线程并发效果;
  • 实现定时任务、倒计时功能;
  • 控制线程执行节奏,避免 CPU 空转。

12.5 注意事项

  • sleep()是静态方法,调用时会自动绑定当前线程,不要通过线程对象调用(如thread.sleep()无意义,实际休眠的是当前线程)。
  • 休眠时间结束后,线程不会立即执行,需等待 CPU 分配时间片。

多线程 13:线程礼让_yield

13.1 什么是yield()

yield()Thread类的静态方法,用于让当前线程主动让出 CPU 执行权,从运行态回到就绪态,让同优先级或更高优先级的线程优先执行。

13.2 核心特性

  1. 礼让不保证yield()只是建议CPU 调度器让出执行权,调度器可能忽略该建议,线程可能立即再次获得 CPU 执行。
  2. 不释放锁:线程礼让时,不会释放已持有的锁。
  3. 仅礼让同优先级 / 高优先级线程:低优先级线程无法通过yield()获得执行权。

13.3 代码示例

public class ThreadYieldDemo {
    public static void main(String[] args) {
        // 线程A:执行时主动礼让
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
                // 主动礼让CPU
                Thread.yield();
            }
        }, "线程A");

        // 线程B:正常执行
        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            }
        }, "线程B");

        // 同时启动两个线程
        threadA.start();
        threadB.start();
    }
}

13.4 运行效果

线程 A 会主动礼让,线程 B 的执行机会会明显增加,但无法保证线程 A 完全礼让完再执行线程 B,因为yield()不具备强制性。

13.5 与sleep()的区别

特性 yield() sleep()
线程状态 从运行态→就绪态 从运行态→阻塞态
执行恢复 立即参与 CPU 调度 休眠时间结束后参与调度
锁持有 不释放锁 不释放锁
强制性 无,仅为建议 有,强制阻塞指定时间

多线程 14:线程强制执行_join

14.1 什么是join()

join()Thread类的实例方法,用于让当前线程阻塞,等待调用join()的线程执行完成后,再继续执行,实现线程的 “插队” 效果。

14.2 核心方法

  • join():无限等待,直到目标线程执行完成;
  • join(long millis):等待指定毫秒数,超时后自动恢复执行。

14.3 代码示例

public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("主线程开始执行");

        // 创建子线程
        Thread subThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println("子线程执行完成");
        }, "子线程");

        // 启动子线程
        subThread.start();

        // 子线程插队:主线程阻塞,等待子线程执行完成后再继续
        subThread.join();

        System.out.println("主线程继续执行,程序结束");
    }
}

14.4 运行效果

主线程启动子线程后,会被join()阻塞,直到子线程完全执行完毕,主线程才会继续执行,实现 “子线程优先执行” 的效果。

14.5 常见应用场景

  • 多线程任务中,需要等待某个子线程执行完成后,再执行后续逻辑(如数据加载完成后再渲染页面);
  • 线程间的执行顺序控制,保证任务的依赖关系。

14.6 注意事项

  • join()会让当前线程(调用join()的线程) 阻塞,而非目标线程。
  • join()会释放锁,阻塞期间其他线程可获取锁资源。

多线程 15:观测线程状态

15.1 线程的 6 种核心状态

Java 中线程的状态由Thread.State枚举定义,共 6 种状态,线程在生命周期中会在这些状态间切换:

状态 说明
NEW(新建) 线程已创建,但未调用start(),尚未启动
RUNNABLE(可运行) 线程正在执行,或等待 CPU 分配时间片(就绪态 + 运行态合并)
BLOCKED(阻塞) 线程等待获取锁,被阻塞在同步块 / 方法前
WAITING(无限等待) 线程无限等待,需被其他线程唤醒(如wait()join()
TIMED_WAITING(计时等待) 线程等待指定时间,时间到后自动唤醒(如sleep()wait(long)
TERMINATED(终止) 线程执行完成,生命周期结束

15.2 观测线程状态的方法

通过Thread.getState()方法,可获取线程当前的状态,用于调试、监控线程执行流程。

15.3 代码示例

public class ThreadStateDemo {
    public static void main(String[] args) throws InterruptedException {
        // 1. 新建状态:线程创建后未启动
        Thread thread = new Thread(() -> {
            try {
                // 3. 计时等待状态:线程休眠
                Thread.sleep(1000);
                // 线程执行完成,进入终止状态
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "测试线程");

        // 观测新建状态
        System.out.println("1. 新建状态:" + thread.getState()); // NEW

        // 2. 启动线程,进入可运行状态
        thread.start();
        System.out.println("2. 启动后状态:" + thread.getState()); // RUNNABLE

        // 3. 主线程休眠200ms,观测线程的计时等待状态
        Thread.sleep(200);
        System.out.println("3. 休眠中状态:" + thread.getState()); // TIMED_WAITING

        // 4. 等待线程执行完成,观测终止状态
        thread.join();
        System.out.println("4. 执行完成状态:" + thread.getState()); // TERMINATED
    }
}

15.4 状态切换流程

NEWRUNNABLEBLOCKED/WAITING/TIMED_WAITINGRUNNABLETERMINATED

15.5 注意事项

  • RUNNABLE包含了 “就绪” 和 “运行” 两种状态,Java 中不区分这两个状态。
  • 线程一旦进入TERMINATED状态,无法再次启动。

多线程 16:线程的优先级

16.1 什么是线程优先级

线程优先级用于告诉 CPU 调度器,哪些线程更优先执行,优先级越高,获得 CPU 执行权的概率越大(不保证绝对优先,仅为概率性)。

16.2 优先级范围

Java 中线程优先级范围为1~10Thread类定义了 3 个常量:

  • MIN_PRIORITY = 1:最低优先级
  • NORM_PRIORITY = 5:默认优先级(所有线程默认优先级为 5)
  • MAX_PRIORITY = 10:最高优先级

16.3 核心方法

  • setPriority(int newPriority):设置线程优先级;
  • getPriority():获取线程当前优先级。

16.4 代码示例

public class ThreadPriorityDemo {
    public static void main(String[] args) {
        // 线程1:最高优先级
        Thread highPriority = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            }
        }, "高优先级线程");
        highPriority.setPriority(Thread.MAX_PRIORITY);

        // 线程2:最低优先级
        Thread lowPriority = new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                System.out.println(Thread.currentThread().getName() + ":执行第" + i + "次");
            }
        }, "低优先级线程");
        lowPriority.setPriority(Thread.MIN_PRIORITY);

        // 同时启动两个线程
        lowPriority.start();
        highPriority.start();
    }
}

16.5 运行效果

高优先级线程获得 CPU 执行权的概率更高,执行次数会明显多于低优先级线程,但无法保证高优先级线程先执行完,因为 CPU 调度具有随机性。

16.6 注意事项

  1. 优先级不保证执行顺序:仅为概率性提升,不能依赖优先级控制线程执行顺序。
  2. 优先级继承:子线程的优先级默认继承父线程的优先级。
  3. 不要过度依赖优先级:优先级仅用于优化性能,不能作为业务逻辑的控制手段。
  4. 优先级设置需在start()之前:启动线程后设置优先级无效。

多线程 17:守护线程

17.1 什么是守护线程

守护线程(Daemon Thread)是服务于用户线程的后台线程,核心特点是:当所有用户线程执行完毕后,守护线程会自动终止,不会阻止 JVM 退出

17.2 核心特性

  • 生命周期依赖用户线程:只要有一个用户线程在运行,守护线程就会运行;所有用户线程结束,守护线程立即终止。
  • 优先级低:默认优先级低于用户线程,仅在 CPU 空闲时执行。
  • 典型应用:垃圾回收线程(GC)、后台日志记录、JVM 监控线程、文件自动保存线程。

17.3 核心方法

  • setDaemon(boolean on):设置线程为守护线程(true)或用户线程(false),必须在start()之前调用
  • isDaemon():判断线程是否为守护线程。

17.4 代码示例

public class DaemonThreadDemo {
    public static void main(String[] args) {
        // 1. 创建守护线程:模拟后台日志记录
        Thread daemonThread = new Thread(() -> {
            int count = 0;
            while (true) {
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("【守护线程】后台日志记录中... " + count++);
            }
        }, "守护线程-日志");

        // 2. 设置为守护线程(必须在start()前调用)
        daemonThread.setDaemon(true);
        // 启动守护线程
        daemonThread.start();

        // 3. 创建用户线程:模拟主业务
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("【用户线程】主业务执行中... " + i);
            }
            System.out.println("【用户线程】主业务执行完成");
        }, "用户线程-主业务");

        // 启动用户线程
        userThread.start();
    }
}

17.5 运行效果

用户线程执行 5 次后结束,JVM 中无活跃用户线程,守护线程会自动终止,程序退出。

17.6 注意事项

  • 守护线程中不要执行读写文件、数据库操作等需要保证完成的任务,因为 JVM 退出时会直接终止守护线程,可能导致数据丢失。
  • 守护线程创建的子线程默认也是守护线程。

多线程 18:线程同步机制

18.1 为什么需要线程同步

多线程并发操作共享资源时,会出现线程安全问题(如超卖、数据不一致),线程同步机制的核心作用是:保证同一时间只有一个线程操作共享资源,避免并发冲突

18.2 核心原理

通过锁机制实现同步:线程操作共享资源前,必须先获取锁;获取锁后执行操作,操作完成后释放锁;其他线程需等待锁释放后才能获取锁并执行,保证操作的原子性、可见性、有序性。

18.3 同步的三大特性

  • 原子性:操作不可分割,要么全部执行,要么全部不执行。
  • 可见性:一个线程修改了共享变量,其他线程能立即看到最新值。
  • 有序性:禁止指令重排序,保证代码按预期顺序执行。

18.4 常见同步实现方式

  • synchronized关键字(同步方法、同步块)
  • Lock接口(ReentrantLock等)
  • 原子类(AtomicInteger等)
  • 并发容器(CopyOnWriteArrayList等)

多线程 19:三大不安全案例

19.1 案例 1:多线程卖票(超卖问题)

多个窗口同时卖票,未同步导致超卖、重复卖票。

class Ticket implements Runnable {
    // 共享票源
    private int ticket = 10;

    @Override
    public void run() {
        while (true) {
            if (ticket <= 0) break;
            try {
                // 模拟售票耗时
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
            ticket--;
        }
    }
}

public class UnsafeTicketDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        // 3个窗口同时卖票
        new Thread(ticket, "窗口1").start();
        new Thread(ticket, "窗口2").start();
        new Thread(ticket, "窗口3").start();
    }
}

问题:出现同一张票被多个窗口售出、票号为负数的情况。


19.2 案例 2:多线程取钱(余额不一致)

多个账户同时取钱,未同步导致余额计算错误。

class Account {
    // 账户余额
    private int balance = 1000;

    // 取钱方法
    public void withdraw(int amount) {
        if (balance >= amount) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            balance -= amount;
            System.out.println(Thread.currentThread().getName() + "取钱" + amount + ",余额:" + balance);
        } else {
            System.out.println(Thread.currentThread().getName() + "余额不足");
        }
    }
}

public class UnsafeAccountDemo {
    public static void main(String[] args) {
        Account account = new Account();
        // 两个人同时取钱
        new Thread(() -> account.withdraw(800), "小明").start();
        new Thread(() -> account.withdraw(800), "小红").start();
    }
}

问题:两人同时取钱,余额变为负数,出现透支。


19.3 案例 3:多线程操作集合(并发修改异常)

多线程同时操作ArrayList,导致ConcurrentModificationException

public class UnsafeListDemo {
    public static void main(String[] args) {
        List<String> list = new ArrayList<>();
        // 1000个线程同时向集合添加元素
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        // 等待所有线程执行完成
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("集合大小:" + list.size());
    }
}

问题:集合大小小于 1000,甚至抛出并发修改异常。


多线程 20:同步方法及同步块

20.1 synchronized关键字

synchronized是 Java 内置的同步机制,可修饰方法代码块,自动加锁、释放锁,是最常用的线程安全实现方式。

20.2 同步方法

修饰实例方法,锁对象是当前对象(this;修饰静态方法,锁对象是类的 Class 对象

代码示例:卖票问题修复
class Ticket implements Runnable {
    private int ticket = 10;

    // 同步方法:锁对象是this
    @Override
    public synchronized void run() {
        while (true) {
            if (ticket <= 0) break;
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
            ticket--;
        }
    }
}

20.3 同步块

synchronized(锁对象),可自定义锁对象,粒度更灵活,推荐使用。

  • 锁对象可以是任意对象,但必须是共享对象(所有线程使用同一个锁)。
  • 常用锁对象:this(实例对象)、类.class(静态同步)、共享资源对象。
代码示例:取钱问题修复
class Account {
    private int balance = 1000;

    public void withdraw(int amount) {
        // 同步块:锁对象为当前账户对象
        synchronized (this) {
            if (balance >= amount) {
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                balance -= amount;
                System.out.println(Thread.currentThread().getName() + "取钱" + amount + ",余额:" + balance);
            } else {
                System.out.println(Thread.currentThread().getName() + "余额不足");
            }
        }
    }
}

20.4 同步方法 vs 同步块

特性 同步方法 同步块
锁对象 自动绑定this/类.class 可自定义锁对象
粒度 粗粒度(整个方法) 细粒度(仅同步核心代码)
灵活性
性能 相对较低(锁范围大) 相对较高(锁范围小)

20.5 注意事项

  • 锁对象必须唯一,否则无法实现同步。
  • synchronized是可重入锁,线程获取锁后可再次获取,不会死锁。
  • 同步会降低程序性能,尽量缩小同步范围。

多线程 21:CopyOnWriteArrayList

21.1 什么是CopyOnWriteArrayList

CopyOnWriteArrayList是 Java 并发包提供的线程安全的 List 实现,核心原理是写时复制:修改集合时,复制原集合的副本,在副本上修改,修改完成后替换原集合,读操作无锁,写操作加锁。

21.2 核心特性

  • 读操作完全无锁:读性能极高,适合读多写少的场景。
  • 写操作加锁 + 复制:写操作会加锁,复制原集合,避免读写冲突。
  • 解决并发修改异常:读操作基于原集合,写操作基于副本,不会出现ConcurrentModificationException

21.3 代码示例:集合安全修复

import java.util.concurrent.CopyOnWriteArrayList;

public class SafeListDemo {
    public static void main(String[] args) {
        // 使用CopyOnWriteArrayList替代ArrayList
        CopyOnWriteArrayList<String> list = new CopyOnWriteArrayList<>();
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                list.add(Thread.currentThread().getName());
            }).start();
        }
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("集合大小:" + list.size()); // 输出1000,无异常
    }
}

21.4 适用场景

  • 读多写少的场景:如缓存、配置列表、白名单列表。
  • 不适合写频繁的场景:写操作需要复制数组,内存开销大,性能低。

21.5 与ArrayList的对比

特性 ArrayList CopyOnWriteArrayList
线程安全 不安全 安全
读性能 极高(无锁)
写性能 低(复制 + 加锁)
内存开销 高(写时复制)
并发修改异常 会出现 不会出现

多线程 22:死锁

22.1 什么是死锁

死锁是指两个或多个线程互相持有对方需要的锁,同时等待对方释放锁,导致所有线程永久阻塞,程序无法继续执行的状态。

22.2 死锁产生的四大必要条件(缺一不可)

  1. 互斥条件:资源只能被一个线程持有,其他线程只能等待。
  2. 请求与保持条件:线程持有一个锁的同时,请求另一个锁,不释放已持有的锁。
  3. 不剥夺条件:线程已持有的锁不能被强制剥夺,只能主动释放。
  4. 循环等待条件:多个线程形成循环等待链,每个线程等待下一个线程持有的锁。

22.3 代码示例:死锁模拟

// 锁对象A
class LockA {}
// 锁对象B
class LockB {}

public class DeadLockDemo {
    // 两个共享锁对象
    static LockA lockA = new LockA();
    static LockB lockB = new LockB();

    public static void main(String[] args) {
        // 线程1:先拿A锁,再拿B锁
        new Thread(() -> {
            synchronized (lockA) {
                System.out.println("线程1持有A锁,等待B锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockB) {
                    System.out.println("线程1持有A、B锁");
                }
            }
        }, "线程1").start();

        // 线程2:先拿B锁,再拿A锁
        new Thread(() -> {
            synchronized (lockB) {
                System.out.println("线程2持有B锁,等待A锁");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (lockA) {
                    System.out.println("线程2持有A、B锁");
                }
            }
        }, "线程2").start();
    }
}

运行效果:两个线程互相等待,永久阻塞,程序无法结束。

22.4 死锁的避免与解决

  • 破坏四大条件
    1. 破坏请求与保持:一次性申请所有锁,不分步申请。
    2. 破坏循环等待:按固定顺序获取锁(如统一先拿 A 再拿 B)。
    3. 破坏不剥夺:获取锁失败时,释放已持有的锁。
  • 使用定时锁Lock.tryLock(long timeout),超时自动释放锁。
  • 避免嵌套同步:尽量减少同步块嵌套,降低锁竞争。

多线程 23:Lock 锁

23.1 什么是 Lock 锁

Lock是 Java 5 提供的显式锁接口,相比synchronized隐式锁,提供了更灵活的锁控制,常用实现类是ReentrantLock(可重入锁)。

23.2 synchronized vs Lock

特性 synchronized Lock
锁类型 隐式锁(自动加锁、释放锁) 显式锁(手动加锁、释放锁)
可中断 不可中断 可中断(lockInterruptibly()
定时锁 有(tryLock(long timeout)
公平锁 非公平 支持公平 / 非公平
锁绑定 绑定线程 不绑定线程
性能 JDK1.6 后优化,性能接近 Lock 高并发下性能更优

23.3 核心方法

  • lock():加锁,阻塞直到获取锁。
  • unlock():释放锁,必须在 finally 中执行,避免锁泄漏。
  • tryLock():尝试获取锁,立即返回结果,不阻塞。
  • lockInterruptibly():可中断加锁,线程可被中断。

23.4 代码示例:卖票问题 Lock 锁修复

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

class Ticket implements Runnable {
    private int ticket = 10;
    // 创建可重入锁
    private final Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 加锁
            lock.lock();
            try {
                if (ticket <= 0) break;
                Thread.sleep(10);
                System.out.println(Thread.currentThread().getName() + "售出第" + ticket + "张票");
                ticket--;
            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                // 释放锁:必须在finally中,保证锁一定被释放
                lock.unlock();
            }
        }
    }
}

public class LockDemo {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();
        new Thread(ticket, "窗口1").start();
        new Thread(ticket, "窗口2").start();
        new Thread(ticket, "窗口3").start();
    }
}

23.5 注意事项

  • unlock()必须在finally中执行,否则程序异常时锁无法释放,导致死锁。
  • ReentrantLock是可重入锁,支持同一线程多次获取锁。
  • 公平锁:按线程申请顺序分配锁,性能低;非公平锁:随机分配,性能高,默认非公平。

多线程 24:生产者消费者问题

24.1 什么是生产者消费者问题

生产者消费者是经典的多线程同步问题:生产者线程生产数据,消费者线程消费数据,生产者生产满后等待,消费者消费空后等待,通过同步机制协调两者的执行

24.2 核心需求

  • 生产者:缓冲区满时停止生产,等待消费者消费。
  • 消费者:缓冲区空时停止消费,等待生产者生产。
  • 保证线程安全,避免数据丢失、重复消费。

24.3 实现方式:synchronized + wait() + notify()

  • wait():线程释放锁,进入等待状态。
  • notify():唤醒一个等待的线程。
  • notifyAll():唤醒所有等待的线程。

24.4 代码示例:生产者消费者实现

// 缓冲区:存储数据
class Buffer {
    private int data;
    // 缓冲区是否有数据
    private boolean hasData = false;

    // 生产者生产数据
    public synchronized void produce(int data) {
        // 缓冲区有数据,生产者等待
        while (hasData) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 生产数据
        this.data = data;
        hasData = true;
        System.out.println("生产者生产:" + data);
        // 唤醒消费者
        notifyAll();
    }

    // 消费者消费数据
    public synchronized int consume() {
        // 缓冲区无数据,消费者等待
        while (!hasData) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        // 消费数据
        hasData = false;
        System.out.println("消费者消费:" + data);
        // 唤醒生产者
        notifyAll();
        return data;
    }
}

// 生产者线程
class Producer implements Runnable {
    private Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.produce(i);
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 5; i++) {
            buffer.consume();
            try {
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class ProducerConsumerDemo {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        // 启动生产者和消费者线程
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}

24.5 核心注意事项

  • wait()必须在synchronized块中调用,否则抛出IllegalMonitorStateException
  • 使用while循环判断等待条件,避免虚假唤醒(线程被唤醒后,条件仍不满足)。
  • notifyAll()优先于notify(),避免唤醒错误线程导致死锁。

多线程 25:管程法

25.1 什么是管程法

管程(Monitor)是一种同步机制的抽象,核心思想是:将共享资源和操作共享资源的方法封装在一个对象内部,同一时间只允许一个线程进入管程执行方法,其他线程阻塞等待。Java 中synchronized关键字的底层就是基于管程实现的,管程法是生产者消费者问题的经典解决方案之一。

25.2 核心原理

  1. 封装共享资源:将缓冲区、数据等共享资源封装在管程类中,对外仅暴露同步方法。
  2. 互斥访问:管程内的方法默认互斥,同一时间只有一个线程能执行,保证线程安全。
  3. 条件变量:通过wait()/notify()实现线程的等待与唤醒,协调生产者和消费者的执行节奏。

25.3 代码示例:管程法实现生产者消费者

// 管程类:封装缓冲区和同步方法
class SynchroBuffer {
    // 缓冲区:最多存储10个数据
    private final Object[] buffer = new Object[10];
    // 缓冲区当前元素数量
    private int count = 0;

    // 生产者生产方法(同步,互斥访问)
    public synchronized void put(Object data) throws InterruptedException {
        // 缓冲区满,生产者等待
        while (count == buffer.length) {
            wait();
        }
        // 生产数据:放入缓冲区
        buffer[count] = data;
        count++;
        System.out.println("生产者生产:" + data + ",缓冲区剩余:" + (buffer.length - count));
        // 唤醒消费者
        notifyAll();
    }

    // 消费者消费方法(同步,互斥访问)
    public synchronized Object get() throws InterruptedException {
        // 缓冲区空,消费者等待
        while (count == 0) {
            wait();
        }
        // 消费数据:从缓冲区取出
        count--;
        Object data = buffer[count];
        System.out.println("消费者消费:" + data + ",缓冲区剩余:" + count);
        // 唤醒生产者
        notifyAll();
        return data;
    }
}

// 生产者线程
class Producer implements Runnable {
    private final SynchroBuffer buffer;

    public Producer(SynchroBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                buffer.put(i);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private final SynchroBuffer buffer;

    public Consumer(SynchroBuffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                buffer.get();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class MonitorDemo {
    public static void main(String[] args) {
        SynchroBuffer buffer = new SynchroBuffer();
        // 启动生产者和消费者线程
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}

25.4 核心优势

  • 封装性强:将共享资源和同步逻辑封装在管程内部,对外隐藏实现细节,代码更易维护。
  • 安全性高:自动保证互斥访问,避免手动加锁的失误。
  • Java 原生支持:基于synchronized实现,无需额外依赖,兼容性好。

多线程 26:信号灯法

26.1 什么是信号灯法

信号灯法(Semaphore)是另一种经典的同步机制,核心是通过信号量(计数器) 控制线程的执行:

  • 信号量 > 0:线程可执行,执行后信号量 - 1(P 操作)
  • 信号量 = 0:线程阻塞等待,其他线程执行后信号量 + 1(V 操作)Java 中Semaphore类是信号灯法的标准实现,常用于控制并发线程数、生产者消费者问题。

26.2 核心原理

  1. 信号量初始化:设置初始许可数(如生产者消费者中,初始许可数为缓冲区大小)。
  2. P 操作(申请许可):线程执行前申请许可,许可不足则阻塞。
  3. V 操作(释放许可):线程执行完成后释放许可,唤醒等待线程。

26.3 代码示例:信号灯法实现生产者消费者

import java.util.concurrent.Semaphore;

// 缓冲区类
class Buffer {
    private final Object[] buffer = new Object[10];
    // 空缓冲区信号量:初始10个许可(可生产10个数据)
    private final Semaphore empty = new Semaphore(10);
    // 满缓冲区信号量:初始0个许可(无数据可消费)
    private final Semaphore full = new Semaphore(0);
    // 互斥锁:保证缓冲区操作的原子性
    private final Semaphore mutex = new Semaphore(1);

    private int in = 0; // 生产者写入位置
    private int out = 0; // 消费者读取位置

    // 生产者生产
    public void put(Object data) throws InterruptedException {
        // 申请空缓冲区许可
        empty.acquire();
        // 申请互斥锁
        mutex.acquire();

        // 写入数据
        buffer[in] = data;
        in = (in + 1) % buffer.length;
        System.out.println("生产者生产:" + data);

        // 释放互斥锁
        mutex.release();
        // 释放满缓冲区许可(通知消费者有数据可消费)
        full.release();
    }

    // 消费者消费
    public Object get() throws InterruptedException {
        // 申请满缓冲区许可
        full.acquire();
        // 申请互斥锁
        mutex.acquire();

        // 读取数据
        Object data = buffer[out];
        out = (out + 1) % buffer.length;
        System.out.println("消费者消费:" + data);

        // 释放互斥锁
        mutex.release();
        // 释放空缓冲区许可(通知生产者有空间可生产)
        empty.release();

        return data;
    }
}

// 生产者线程
class Producer implements Runnable {
    private final Buffer buffer;

    public Producer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                buffer.put(i);
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 消费者线程
class Consumer implements Runnable {
    private final Buffer buffer;

    public Consumer(Buffer buffer) {
        this.buffer = buffer;
    }

    @Override
    public void run() {
        for (int i = 1; i <= 20; i++) {
            try {
                buffer.get();
                Thread.sleep(200);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 测试类
public class SemaphoreDemo {
    public static void main(String[] args) {
        Buffer buffer = new Buffer();
        new Thread(new Producer(buffer)).start();
        new Thread(new Consumer(buffer)).start();
    }
}

26.4 核心应用场景

  • 控制并发线程数:如接口限流,限制同时访问的线程数。
  • 生产者消费者问题:通过信号量协调生产和消费的节奏。
  • 资源池管理:如数据库连接池,控制同时使用的连接数。

26.5 管程法 vs 信号灯法

特性 管程法(synchronized) 信号灯法(Semaphore)
实现方式 基于对象锁,封装同步逻辑 基于信号量计数器,灵活控制
并发控制 同一时间仅一个线程访问 可控制多个线程同时访问
适用场景 简单同步、生产者消费者 复杂并发控制、限流、资源池
灵活性

多线程 27:线程池

27.1 什么是线程池

线程池是管理线程的容器,核心思想是提前创建一批线程,放入池中复用,避免频繁创建 / 销毁线程的开销,同时控制线程的最大并发数,提升程序性能和稳定性。

27.2 线程池的核心优势

  • 降低资源消耗:复用线程,避免频繁创建 / 销毁线程的开销。
  • 提高响应速度:任务到达时,直接使用池中的线程执行,无需等待创建。
  • 提高线程可管理性:统一管理线程,避免无限制创建线程导致的系统崩溃。

27.3 线程池的核心参数(ThreadPoolExecutor)

public ThreadPoolExecutor(
    int corePoolSize,       // 核心线程数:常驻线程,即使空闲也不会销毁
    int maximumPoolSize,    // 最大线程数:线程池允许的最大线程数
    long keepAliveTime,     // 空闲线程存活时间:非核心线程空闲后销毁的时间
    TimeUnit unit,          // 时间单位
    BlockingQueue<Runnable> workQueue, // 任务队列:存储等待执行的任务
    ThreadFactory threadFactory,        // 线程工厂:创建线程
    RejectedExecutionHandler handler    // 拒绝策略:任务队列满、线程数满时的处理策略
)

27.4 4 种拒绝策略

策略 说明
AbortPolicy 默认策略,直接抛出异常,拒绝任务
CallerRunsPolicy 由调用线程(主线程)执行任务
DiscardPolicy 直接丢弃任务,不抛出异常
DiscardOldestPolicy 丢弃队列中最老的任务,执行新任务

27.5 常用线程池工具类(Executors)

Java 提供Executors工具类,快速创建常用线程池:

  1. newFixedThreadPool(int nThreads):固定大小线程池,核心线程数 = 最大线程数
  2. newCachedThreadPool():缓存线程池,线程数无上限,空闲线程 60 秒后销毁
  3. newSingleThreadExecutor():单线程线程池,保证任务顺序执行
  4. newScheduledThreadPool(int corePoolSize):定时任务线程池,支持定时 / 周期执行

27.6 代码示例:线程池使用

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

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

        // 2. 提交10个任务到线程池
        for (int i = 1; i <= 10; i++) {
            int taskId = i;
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务:" + taskId);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 3. 关闭线程池(不再接受新任务,等待现有任务执行完成)
        pool.shutdown();
    }
}

27.7 自定义线程池(推荐)

Executors创建的线程池存在 OOM 风险(如newCachedThreadPool无上限),生产环境推荐自定义线程池:

import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;

public class CustomThreadPoolDemo {
    public static void main(String[] args) {
        // 自定义线程工厂:自定义线程名称,方便排查问题
        ThreadFactory threadFactory = new ThreadFactory() {
            private final AtomicInteger count = new AtomicInteger(1);
            @Override
            public Thread newThread(Runnable r) {
                return new Thread(r, "自定义线程-" + count.getAndIncrement());
            }
        };

        // 自定义线程池
        ThreadPoolExecutor pool = new ThreadPoolExecutor(
                3,                          // 核心线程数
                5,                          // 最大线程数
                60L,                        // 空闲线程存活时间
                TimeUnit.SECONDS,
                new LinkedBlockingQueue<>(10), // 任务队列,容量10
                threadFactory,               // 线程工厂
                new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
        );

        // 提交任务
        for (int i = 1; i <= 15; i++) {
            int taskId = i;
            pool.execute(() -> {
                System.out.println(Thread.currentThread().getName() + "执行任务:" + taskId);
            });
        }

        pool.shutdown();
    }
}

27.8 注意事项

  • 生产环境禁止使用Executors创建线程池,推荐自定义ThreadPoolExecutor,避免 OOM。
  • 线程池使用完成后,必须调用shutdown()关闭,否则程序无法退出。
  • 合理设置核心线程数:CPU 密集型任务设置为CPU核心数+1,IO 密集型任务设置为2*CPU核心数

多线程 28:总结

28.1 全 28 章知识体系梳理

一、基础概念(01-02)
  • 进程、线程、多线程的核心定义与区别
  • 多线程的执行原理与应用场景
二、线程创建(03-08)
  • 3 种线程创建方式:继承Thread类、实现Runnable接口、实现Callable接口
  • 实战案例:网图下载、龟兔赛跑
三、线程控制(09-16)
  • 静态代理模式、Lambda 表达式简化线程创建
  • 线程生命周期控制:停止、休眠、礼让、强制执行
  • 线程状态观测、线程优先级、守护线程
四、线程安全与同步(17-24)
  • 线程同步机制、三大不安全案例
  • 同步方法 / 同步块、CopyOnWriteArrayList并发容器
  • 死锁、Lock锁、生产者消费者问题
五、高级同步与线程池(25-27)
  • 管程法、信号灯法两种同步方案
  • 线程池的原理、参数、自定义实现

28.2 核心知识点总结

  1. 线程创建:推荐实现Runnable接口,避免单继承限制,支持资源共享。
  2. 线程安全:多线程操作共享资源必须加锁,常用synchronizedReentrantLock
  3. 线程同步:管程法(synchronized)适合简单场景,信号灯法(Semaphore)适合复杂并发控制。
  4. 线程池:生产环境必须自定义线程池,避免Executors的 OOM 风险,合理设置核心参数。
  5. 死锁避免:破坏四大必要条件,按固定顺序获取锁,避免嵌套同步。
  6. 守护线程:依赖用户线程,用户线程结束后自动终止,适合后台任务。

28.3 多线程最佳实践

  • 避免共享资源:尽量使用局部变量,减少共享资源的竞争。
  • 缩小同步范围:同步块粒度越小越好,避免整个方法同步,提升性能。
  • 使用并发容器:优先使用CopyOnWriteArrayListConcurrentHashMap等并发容器,替代普通集合。
  • 合理使用线程池:复用线程,避免频繁创建 / 销毁,控制并发数。
  • 避免死锁:统一锁的获取顺序,使用定时锁,减少嵌套同步。
  • 优雅停止线程:使用标志位中断法,禁止使用stop()等过时方法。

28.4 学习路线建议

  1. 掌握基础概念:进程、线程、多线程原理
  2. 熟练线程创建与控制:3 种创建方式、线程生命周期方法
  3. 攻克线程安全:同步机制、锁、死锁、生产者消费者
  4. 学习高级特性:信号灯法、线程池、并发容器
  5. 实战项目:多线程下载、并发接口、定时任务等
Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐