欢迎光临
我们一直在努力

Java多线程基础实战:从进程与线程到并发通信,7课时吃透线程创建、安全与生产者消费者模型

第1章:进程与线程 + 继承Thread类

1.1 为什么学多线程?

先搞清楚,项目里什么时候用多线程:

场景

说明

Web服务器

每个用户请求分配一个线程(Tomcat就是这么干的)

日志异步写入

不阻塞主业务,后台线程写日志

批量数据导入

多线程并行处理大数据

定时任务

后台线程定时检查(订单超时取消)

一句话理解:只要你的程序需要"同时做多件事",就需要多线程。

1.2 进程与线程

进程:操作系统资源分配的基本单位,每个运行的程序就是一个进程

线程:CPU调度的基本单位,进程中的一个执行单元

┌─────────────────────────────────────┐
│        Java程序(一个进程)            │
│  ┌─────────────────────────────┐    │
│  │       共享的堆内存空间        │    │
│  │   对象、数组、静态变量…     │    │
│  └─────────────────────────────┘    │
│  ┌──────┐ ┌──────┐ ┌──────┐        │
│  │线程1 │ │线程2 │ │主线程│        │
│  │栈独立│ │栈独立│ │栈独立│        │
│  └──────┘ └──────┘ └──────┘        │
│  线程共享堆内存,各自有独立的栈        │
└─────────────────────────────────────┘

对比项

进程

线程

内存

独立内存空间

共享进程内存

开销

创建和切换开销大

创建和切换开销小

通信

需要IPC,复杂

共享内存,简单但需同步

项目理解:启动Tomcat → 一个进程;收到100个请求 → 100个线程处理;共享堆内存 → 所以才有线程安全问题。

�� 面试考点:进程和线程的区别?

标准话术:"进程是操作系统资源分配的基本单位,线程是CPU调度的基本单位。一个进程可以包含多个线程,线程之间共享进程的堆内存,但各自有独立的栈空间。线程创建和切换的开销比进程小得多,线程间通信更方便(共享内存),但也因此需要同步机制来保证线程安全。"

1.3 创建线程方式1:继承Thread类

// java
// 第1步:继承Thread类,重写run()
public class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(getName() + " 执行第" + i + "次");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

// 第2步:创建对象,调用start()启动
public class Demo {
    public static void main(String[] args) {
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        t1.start(); // ✅ 启动新线程
        t2.start();
    }
}

1.4 start() vs run()(面试必考!)

// java
t.start(); // ✅ 创建新线程,新线程执行run()
t.run();   // ❌ 没有新线程,只是在主线程中调用run()方法

// start():主线程继续往下走,新线程并行执行run()
// run():主线程等run()执行完才继续,等于单线程串行

�� 面试考点:start()和run()的区别?

标准话术:"start()会创建一个新的线程,由JVM在新线程中调用run()方法,实现多线程并发;run()只是普通方法调用,在当前线程中执行,不会创建新线程。start()只能调用一次,重复调用会抛出IllegalThreadStateException。"

1.5 线程只能启动一次

// java
MyThread t = new MyThread();
t.start();
// t.start(); // ❌ 报错!IllegalThreadStateException
// 要再执行同样的任务,必须创建新对象
new MyThread().start(); // ✅

1.6 项目实战:异步日志写入

场景:用户操作(登录、下单)需要记日志,日志写入不能拖慢主业务。

// java
public class AsyncLoggerThread extends Thread {
    private String logMessage;

    public AsyncLoggerThread(String logMessage) {
        this.logMessage = logMessage;
    }

    @Override
    public void run() {
        String time = new java.text.SimpleDateFormat("HH:mm:ss")
            .format(new java.util.Date());
        System.out.println("[" + time + "] " + getName() + " 写入日志: " + logMessage);
        try {
            Thread.sleep(200); // 模拟写文件耗时
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

// 使用
public class ECommerceMain {
    public static void main(String[] args) {
        System.out.println("用户张三提交订单 #1001");
        new AsyncLoggerThread("用户张三提交订单 #1001").start(); // 异步记日志,不阻塞

        System.out.println("订单 #1001 库存扣减完成");
        new AsyncLoggerThread("订单 #1001 库存扣减完成").start();

        System.out.println("订单 #1001 支付成功");
        new AsyncLoggerThread("订单 #1001 支付成功").start();

        System.out.println("=== 订单主流程结束 ===");
    }
}

学了能做什么:任何"后台处理不阻塞主流程"的场景 —— 日志异步写入、邮件异步发送、文件异步上传。

1.7 第1章小结

知识点

项目用途

进程vs线程

理解Tomcat为什么能同时处理多个请求

继承Thread

创建独立后台线程(日志、监控等)

start() vs run()

面试必考、项目必踩的坑

第2章:实现Runnable接口 + 两种方式对比

2.1 为什么需要第二种方式?

Java只支持单继承!类已经继承了别的类,就无法再继承Thread。

// java
// ❌ Java不支持多继承
public class AsyncLogger extends BaseLogger, Thread { } // 编译错误!

2.2 创建线程方式2:实现Runnable接口

// java
// 第1步:实现Runnable接口
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + " 第" + i + "次");
        }
    }
}

// 第2步:创建Runnable对象,传给Thread构造方法
MyRunnable r = new MyRunnable();
Thread t1 = new Thread(r, "线程A"); // 第二个参数是线程名
Thread t2 = new Thread(r, "线程B");
t1.start();
t2.start();

2.3 匿名内部类写法(一次性任务常用)

// java
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("一次性任务执行");
    }
}, "任务线程").start();

2.4 两种方式对比

对比项

继承Thread

实现Runnable

多继承限制

❌ 无法再继承其他类

✅ 还可以继承其他类

资源共享

每个线程各自一份资源

✅ 多线程共享同一个Runnable对象

扩展性

较差

✅ 符合面向接口编程

推荐度

⭐⭐

⭐⭐⭐⭐⭐

企业开发规范:优先用Runnable。原因:避免单继承限制、任务和线程分离、方便用线程池。

�� 面试考点:线程有几种创建方式?推荐哪种?

标准话术:"有两种基本方式:继承Thread类和实现Runnable接口。推荐实现Runnable接口,原因有三:1)避免Java单继承限制,类还可以继承其他类;2)任务与线程解耦,Runnable只负责定义任务,Thread负责执行;3)方便使用线程池,线程池接受的是Runnable/Callable对象;4)多个线程可以共享同一个Runnable对象,便于资源共享。"

2.5 资源共享对比——抢票场景

// java
// ❌ 继承Thread:3个窗口各卖10张票,总共卖30张(应该只卖10张)
class TicketWindow extends Thread {
    private int tickets = 10; // 每个线程各自一份,不共享!
    public void run() {
        while (tickets > 0) {
            System.out.println(getName() + " 售出第" + tickets– + "张票");
        }
    }
}
// 3个对象 = 3份tickets = 30张票(错误)

// ✅ 实现Runnable:3个线程共享同一个Runnable对象,共享10张票
class TicketRunnable implements Runnable {
    private int tickets = 10; // 只有一份,3个线程共享
    public void run() {
        while (tickets > 0) {
            System.out.println(Thread.currentThread().getName() + " 售出第" + tickets– + "张票");
        }
    }
}
// 1个Runnable对象传给3个线程 = 共享10张票(正确)
TicketRunnable tr = new TicketRunnable();
new Thread(tr, "窗口1").start();
new Thread(tr, "窗口2").start();
new Thread(tr, "窗口3").start();

注意:Runnable版虽然共享了数据,但可能出现"重票"问题,这就是线程安全问题,第5章解决。

2.6 项目实战:用户请求处理器

场景:Web服务器为每个用户请求分配一个线程处理。

// java
public class RequestHandler implements Runnable {
    private String userName;
    private String requestType; // LOGIN / ORDER / PAY
    private int processingTime;

    public RequestHandler(String userName, String requestType, int processingTime) {
        this.userName = userName;
        this.requestType = requestType;
        this.processingTime = processingTime;
    }

    @Override
    public void run() {
        String tName = Thread.currentThread().getName();
        System.out.println("[" + tName + "] 处理 " + userName + " 的" + requestType + "请求…");
        try {
            Thread.sleep(processingTime);
        } catch (InterruptedException e) {
            return;
        }
        System.out.println("[" + tName + "] " + userName + " " + requestType + "完成");
    }
}

// 使用:每个请求一个线程
new Thread(new RequestHandler("张三", "LOGIN", 800), "req-1").start();
new Thread(new RequestHandler("李四", "ORDER", 1500), "req-2").start();
new Thread(new RequestHandler("王五", "PAY", 1200), "req-3").start();

学了能做什么:理解Web服务器的基本原理 —— 每个HTTP请求分配一个线程,这就是多用户能同时访问网站的原因。

2.7 第三种方式:实现Callable接口

Thread和Runnable都有一个限制:run()方法没有返回值,也不能抛出受检异常。Callable接口解决了这个问题。

// java
import java.util.concurrent.Callable;

/**
 * Callable<String>中的泛型表示线程执行完之后的返回值类型
 * 与具体业务相关,比如:执行结果状态码、计算结果、处理报告等
 */
public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        System.out.println("线程执行了….");
        // call()方法可以有返回值,也可以抛出异常
        return "执行成功…..";
    }
}

// java
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.FutureTask;
import java.util.concurrent.TimeUnit;

public class CallableDemo {
    public static void main(String[] args) {
        // 第1步:创建Callable对象
        Callable<String> callable = new MyCallable();

        // 第2步:使用FutureTask包装(FutureTask实现了Runnable接口)
        FutureTask<String> futureTask = new FutureTask<>(callable);

        // 第3步:传给Thread启动
        new Thread(futureTask).start();

        // 第4步:获取线程执行结果
        try {
            // get()会阻塞,直到线程执行完返回结果
            String result = futureTask.get();
            // 带超时的获取:最多等10秒
            String result2 = futureTask.get(10, TimeUnit.SECONDS);
            System.out.println("线程返回结果:" + result);
        } catch (Exception e) {
            System.out.println("获取结果异常:" + e.getMessage());
        }
    }
}

Callable vs Runnable 对比

对比项

Runnable

Callable

返回值

❌ 无(void)

✅ 有(泛型指定)

异常

不能抛受检异常

✅ 可以抛出异常

配套类

直接传给Thread

需要FutureTask包装

使用场景

不需要返回值的任务

需要返回结果的任务

�� 面试考点:Callable和Runnable的区别?

标准话术:"Runnable的run()方法没有返回值且不能抛出受检异常,Callable的call()方法有泛型返回值且可以抛出异常。Callable需要配合FutureTask使用,FutureTask实现了Runnable接口,所以可以传给Thread。Callable适合需要获取执行结果的场景,比如异步计算、批量任务汇总等。"

2.8 第2章小结

知识点

项目用途

实现Runnable

企业开发首选,避免单继承限制

资源共享

多线程共享同一数据源(卖票、库存)

两种方式对比

面试高频题

实现Callable

需要返回值的线程任务

第3章:线程常用API

3.1 获取线程信息

// java
Thread current = Thread.currentThread();
current.getName();      // 线程名称
current.getId();        // 线程ID
current.getPriority();  // 优先级
current.isAlive();      // 是否存活
current.isDaemon();     // 是否守护线程

3.2 线程命名(项目里必须做!)

为什么要命名? 线程出问题时,名字就是最直接的排查线索。

// java
// 方式1:构造方法(推荐)
Thread t = new Thread(runnable, "订单处理线程-001");

// 方式2:setName()
t.setName("库存扣减线程");

// 企业命名规范:模块-功能-序号
// order-process-001  inventory-sync-001  log-writer-001

�� 面试考点:为什么要给线程命名?

标准话术:"给线程命名是为了方便排查问题。线上环境出现线程死锁、CPU飙高时,通过线程dump可以看到线程名,有意义的命名能快速定位问题模块和功能。推荐命名格式:模块-功能-序号,如order-process-001。"

3.3 线程优先级

// java
Thread.MIN_PRIORITY = 1;     // 最低
Thread.NORM_PRIORITY = 5;    // 默认
Thread.MAX_PRIORITY = 10;    // 最高

t.setPriority(Thread.MAX_PRIORITY);
// ⚠️ 优先级只是"建议",OS不保证一定按优先级执行

3.4 Thread.sleep() —— 线程休眠

// java
Thread.sleep(1000); // 休眠1秒

// 项目场景1:轮询检查(订单超时、库存同步)
while (running) {
    checkTimeoutOrders();
    Thread.sleep(5000); // 每5秒检查一次
}

// 项目场景2:模拟耗时操作
Thread.sleep(200); // 模拟数据库/网络耗时

3.5 Thread.yield() —— 让出CPU

// java
Thread.yield(); // 当前线程主动让出CPU,回到就绪状态
// 执行后可能立即又获得CPU,只是"建议"调度器

先理解两个重要概念

并发:把一段时间分成一个一个时间片,每个时间片对应一个线程的执行,按照时间片交替执行

并行:多个线程真正同时执行(需要多核CPU)

// java
public class YieldExample {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("线程1: " + i);
                Thread.yield(); // 让出CPU,给线程2机会
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 1; i <= 5; i++) {
                System.out.println("线程2: " + i);
            }
        });

        t1.start();
        t2.start();
    }
}

Thread.yield() 只是一个提示:当前线程愿意让出CPU,线程调度器可以忽略这个提示(所以不是每次都会让成功)。加上yield()后,线程1在每次输出后更可能被切换出去,让线程2执行。

�� 面试考点:并发和并行的区别?

标准话术:"并发是指多个线程在同一时间段内交替执行,CPU通过时间片轮转实现,单核CPU也能实现并发;并行是指多个线程在同一时刻真正同时执行,需要多核CPU支持。并发是'看起来同时',并行是'真正同时'。Java的多线程在单核上是并发,在多核上既有并发也有并行。"

3.6 守护线程

// java
// 守护线程:为其他线程服务的后台线程,所有非守护线程结束后自动销毁
t.setDaemon(true); // 必须在start()之前设置

// 典型应用:GC垃圾回收线程就是守护线程
// 项目场景:心跳检测线程 —— 主业务结束,心跳自动停止

// java
// 守护线程示例
Thread heartbeat = new Thread(new Runnable() {
    @Override
    public void run() {
        while (true) {
            System.out.println("[心跳] 系统正常运行");
            try { Thread.sleep(1000); } catch (InterruptedException e) { return; }
        }
    }
}, "心跳线程");
heartbeat.setDaemon(true); // 设为守护线程
heartbeat.start();

// 主业务处理3个订单后退出,守护线程自动销毁
for (int i = 1; i <= 3; i++) {
    System.out.println("处理订单 #" + i);
    Thread.sleep(800);
}
System.out.println("主业务结束,程序退出");

�� 面试考点:什么是守护线程?有什么用?

标准话术:"守护线程是为其他线程提供后台服务的线程,当所有非守护线程结束后,守护线程会自动销毁。典型应用是GC垃圾回收线程。使用setDaemon(true)设置,必须在start()之前调用。项目里常用于心跳检测、监控等辅助功能——主业务结束,这些辅助线程没有存在的意义就自动退出。"

3.7 第3章小结

API

作用

项目用途

setName()/getName()

线程命名

调试和日志追踪(必须做)

setPriority()

设置优先级

关键任务优先(不保证)

sleep()

休眠

轮询检查、模拟耗时

yield()

让出CPU

协作式调度

setDaemon(true)

守护线程

后台服务(心跳、GC)

第4章:线程控制方法 + 生命周期

4.1 join() —— 等待线程完成

// java
thread.join();       // 一直等到thread执行完
thread.join(1000);   // 最多等1秒,超时继续

项目场景:数据导入 —— 多线程并行导入,主线程等全部完成后再做校验

// java
public class DataImportRunner {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== 数据迁移开始 ===");

        Thread productImport = new Thread(new Runnable() {
            public void run() {
                System.out.println("导入商品数据…");
                try { Thread.sleep(3000); } catch (InterruptedException e) {}
                System.out.println("商品导入完成");
            }
        }, "商品导入");

        Thread userImport = new Thread(new Runnable() {
            public void run() {
                System.out.println("导入用户数据…");
                try { Thread.sleep(2000); } catch (InterruptedException e) {}
                System.out.println("用户导入完成");
            }
        }, "用户导入");

        Thread orderImport = new Thread(new Runnable() {
            public void run() {
                System.out.println("导入订单数据…");
                try { Thread.sleep(4000); } catch (InterruptedException e) {}
                System.out.println("订单导入完成");
            }
        }, "订单导入");

        long start = System.currentTimeMillis();
        productImport.start();
        userImport.start();
        orderImport.start();

        // 主线程等待所有导入完成
        productImport.join();
        userImport.join();
        orderImport.join();

        long end = System.currentTimeMillis();
        System.out.println("=== 全部完成,耗时:" + (end – start) + "ms ===");
        // 3个线程并行执行,总耗时约4秒(最慢的那个),不是3+2+4=9秒
    }
}

4.2 join超时等待

// java
// 项目场景:导入不能无限等,超时执行降级方案
importThread.join(30000); // 最多等30秒
if (importThread.isAlive()) {
    System.out.println("导入超时,执行降级方案");
    importThread.interrupt();
}

企业经验:任何join都要考虑超时,线上环境不能让一个线程无限等待另一个线程。

4.3 interrupt() —— 中断线程

核心理解:interrupt()不是强制停止线程!只是设置一个"中断标志",线程需要主动检查。

// java
/**
 * 可中断的任务
 */
public class InterruptibleTask implements Runnable {
    @Override
    public void run() {
        int count = 0;
        while (!Thread.currentThread().isInterrupted()) { // ✅ 循环检查中断标志
            count++;
            System.out.println("处理第" + count + "条数据…");
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                // sleep期间被中断会抛异常,并清除中断标志
                System.out.println("任务被中断,已处理" + count + "条");
                return; // 退出run(),线程结束
            }
        }
    }
}

// 使用
Thread task = new Thread(new InterruptibleTask());
task.start();
Thread.sleep(2000);
task.interrupt(); // 发送中断信号

4.4 ❌ 已废弃的停止方式(面试要知道,代码不能用)

// java
thread.stop();    // ❌ 强制停止,可能导致数据不一致
thread.suspend(); // ❌ 挂起,容易死锁
// ✅ 正确做法:interrupt() + 检查中断标志

�� 面试考点:如何正确停止一个线程?

标准话术:"不要用stop(),它已被废弃,强制停止可能导致数据不一致。正确做法是调用interrupt()设置中断标志,线程在run()方法中通过isInterrupted()主动检查,收到中断信号后优雅退出。如果线程在sleep/wait中被中断,会抛出InterruptedException,在catch中做清理后return即可。"

4.5 线程生命周期

什么是生命周期? 线程从创建到销毁的整个过程就是线程的生命周期。

                    start()
  ┌────────┐     ┌──────────────────┐
  │ 新建   │────→│ 就绪/运行         │
  │ (New)  │     │ (Runnable)       │
  └────────┘     └──────────────────┘
                       │    ↑     │
           sleep()     │    │     │ run()执行完
           join()      ↓    │     ↓
                    ┌────────┐ ┌──────────┐
                    │ 阻塞/  │ │ 死亡     │
                    │ 等待   │ │(Terminated)│
                    └────────┘ └──────────┘
                        │
                        └──→ 条件满足 → 回到就绪状态

状态

说明

触发条件

NEW

对象已创建,未调start()

new Thread()

RUNNABLE

等待CPU调度或正在执行

start()

BLOCKED

等待获取锁

synchronized锁被其他线程持有

WAITING

无限期等待

wait() / join()

TIMED_WAITING

有时限等待

sleep(ms) / join(ms)

TERMINATED

执行完毕

run()执行完或抛出未捕获异常

// java
// 查看线程状态
Thread t = new Thread(() -> {});
System.out.println(t.getState()); // NEW
t.start();
System.out.println(t.getState()); // RUNNABLE

�� 面试考点:线程有几种状态?生命周期是怎样的?

标准话术:"Java线程有6种状态:NEW(新建)、RUNNABLE(就绪/运行)、BLOCKED(阻塞,等待获取锁)、WAITING(无限期等待)、TIMEDWAITING(超时等待)、TERMINATED(终止)。调用start()从NEW变为RUNNABLE,获取不到锁进入BLOCKED,调用wait()进入WAITING,sleep()进入TIMEDWAITING,run()执行完或抛出未捕获异常进入TERMINATED。"

4.6 第4章小结

方法

作用

项目用途

join()

等待线程完成

批量任务并行后等待汇总

join(ms)

超时等待

任务超时降级

interrupt()

发送中断信号

优雅停止后台任务

isInterrupted()

检查中断标志

循环中判断是否停止

第5章:线程安全问题 + synchronized同步方法

5.1 什么是线程安全问题?

多线程同时访问共享数据且至少一个线程执行写操作,不做同步控制就会出现数据不一致。

5.2 问题演示

// java
class UnsafeCounter {
    private int count = 0;
    public void increment() {
        count++; // ⚠️ 非原子操作!实际是3步:读取→加1→写回
    }
    public int getCount() { return count; }
}

// 100个线程各加1000次,期望100000,实际结果小于100000
UnsafeCounter counter = new UnsafeCounter();
for (int i = 0; i < 100; i++) {
    new Thread(new Runnable() {
        public void run() {
            for (int j = 0; j < 1000; j++) counter.increment();
        }
    }).start();
}
Thread.sleep(3000);
System.out.println(counter.getCount()); // 每次结果不同,且小于100000

5.3 count++为什么不是原子操作?

count++ 的底层执行过程:
第1步:读取count的值(假设为0)
第2步:计算count + 1(得到1)
第3步:将1写回count

线程A和线程B同时执行:
A: 读取count=0
B: 读取count=0  ← A还没写回,读到的还是0
A: 写回count=1
B: 写回count=1  ← 应该是2,但变成了1!数据丢失!

�� 面试考点:什么是线程安全问题?怎么产生的?

标准话术:"当多个线程同时访问共享数据且至少有一个线程执行写操作时,如果不做同步控制,就会出现数据不一致,这就是线程安全问题。根本原因是三个:1)原子性——操作不可分割,但count++实际是读-改-写三步;2)可见性——一个线程的修改对其他线程不可见;3)有序性——指令重排导致执行顺序变化。解决方式包括synchronized、Lock锁、volatile、原子类等。"

5.4 synchronized同步方法

// java
// 在方法上加synchronized,同一时刻只有一个线程能执行此方法
class SafeCounter {
    private int count = 0;
    public synchronized void increment() {  // ✅ 加锁
        count++;
    }
    public synchronized int getCount() {
        return count;
    }
}

5.5 synchronized锁的原理

线程A ──获取锁──→ 执行方法 ──释放锁──→
线程B ──获取锁──→ 锁被A持有 → 阻塞等待 → A释放后获取 → 执行方法
线程C ──获取锁──→ 阻塞等待…

锁对象是this(当前对象),同一个对象的所有synchronized方法共用同一把锁

5.6 项目实战:抢票系统

// java
class TicketSystem {
    private int availableTickets;

    public TicketSystem(int total) {
        this.availableTickets = total;
    }

    // ✅ synchronized保证同一时刻只有一个线程能购票
    public synchronized boolean buyTicket(String userName) {
        if (availableTickets <= 0) {
            System.out.println("【" + userName + "】票已售罄!");
            return false;
        }
        try { Thread.sleep(50); } catch (InterruptedException e) {} // 模拟出票耗时
        availableTickets–;
        System.out.println("【" + userName + "】抢票成功!剩余:" + availableTickets);
        return true;
    }

    public synchronized int getAvailableTickets() {
        return availableTickets;
    }
}

// 测试:20个用户抢10张票
TicketSystem ts = new TicketSystem(10);
for (int i = 1; i <= 20; i++) {
    final String name = "用户" + i;
    new Thread(new Runnable() {
        public void run() { ts.buyTicket(name); }
    }).start();
}
Thread.sleep(3000);
System.out.println("剩余票数:" + ts.getAvailableTickets()); // 一定是0

5.7 synchronized注意事项

// java
// 1. 同一个对象的两个synchronized方法互斥(同一把锁this)
class Demo {
    public synchronized void methodA() { } // 线程1执行A时,线程2不能执行B
    public synchronized void methodB() { }
}

// 2. 不同对象不互斥
Demo d1 = new Demo();
Demo d2 = new Demo();
// 线程1执行d1.methodA(),线程2执行d2.methodB() —— 互不影响(不同锁)

// 3. synchronized是可重入锁:同一线程可以再次获取同一把锁
class ReentrantDemo {
    public synchronized void methodA() {
        methodB(); // ✅ 不会死锁,同一把锁可重入
    }
    public synchronized void methodB() { }
}

�� 面试考点:synchronized是可重入锁吗?什么是可重入锁?

标准话术:"synchronized是可重入锁。可重入锁是指同一线程获取到锁后,可以再次获取同一把锁而不会死锁。比如在synchronized方法A中调用synchronized方法B,因为锁对象都是this,同一线程可以再次获取,不会阻塞。如果不可重入,就会造成死锁——线程持有锁又等待自己释放锁,矛盾。"

5.8 第5章小结

知识点

项目用途

线程安全问题

理解并发场景下数据为什么会出错

synchronized同步方法

最基本的线程安全方案

抢票系统

秒杀、抢购、库存扣减场景

第6章:同步代码块 + 线程通信

6.1 synchronized同步代码块

为什么不用同步方法?

// java
// 同步方法锁整个方法 —— 锁范围太大
public synchronized void processOrder() {
    validateOrder();    // 1秒,不需要同步
    deductStock();      // 0.1秒,需要同步!
    sendNotification(); // 2秒,不需要同步
}
// 锁了3.1秒,但真正需要同步的只有0.1秒!

// 同步代码块 —— 只锁需要的部分
public void processOrder() {
    validateOrder();            // 不锁,多线程并行
    synchronized (this) {       // 只锁库存操作
        deductStock();
    }
    sendNotification();         // 不锁,多线程并行
}

语法

// java
synchronized (锁对象) {
    // 需要同步的代码
}

// 锁对象选择:
// 1. this —— 当前对象
// 2. 专用对象 —— Object lock = new Object();(推荐,更灵活)

专用锁对象

// java
class OrderManager {
    private int stock = 100;
    private double revenue = 0;
    private Object stockLock = new Object();   // 库存专用锁
    private Object revenueLock = new Object(); // 营收专用锁

    public void deductStock(int qty) {
        synchronized (stockLock) {  // 只锁库存
            if (stock >= qty) stock -= qty;
        }
    }

    public void addRevenue(double amount) {
        synchronized (revenueLock) { // 只锁营收
            revenue += amount;
        }
    }
    // 不同线程可以同时操作库存和营收,互不影响
}

企业经验:锁粒度越小越好,只锁必须同步的代码。细粒度锁能显著提高并发性能。

�� 面试考点:synchronized同步方法和同步代码块的区别?

标准话术:"同步方法锁整个方法体,锁对象是this(实例方法)或Class对象(静态方法);同步代码块只锁指定代码段,锁对象可自定义。同步代码块更灵活:1)锁粒度更小,只锁真正需要同步的代码,提高并发性能;2)可以用专用锁对象,不同资源用不同锁,减少不必要互斥。企业开发中推荐优先使用同步代码块。"

6.2 线程通信 —— wait() / notify()

为什么需要线程通信?

生产者-消费者模型:一个线程生产数据,另一个线程消费数据,需要协调。

基本用法

// java
synchronized (lock) {
    lock.wait();       // 释放锁,等待(必须在synchronized内)
    lock.wait(1000);   // 超时等待1秒
    lock.notify();     // 唤醒一个等待线程
    lock.notifyAll();  // 唤醒所有等待线程
}
// ⚠️ wait/notify必须在synchronized内调用,且必须是同一把锁

项目实战:订单处理流水线

// java
/**
 * 订单仓库 —— 生产者消费者之间的缓冲区
 * 场景:用户下单(生产者)→ 仓库发货(消费者)
 */
class OrderWarehouse {
    private String[] orders;
    private int count = 0;
    private int capacity;

    public OrderWarehouse(int capacity) {
        this.capacity = capacity;
        this.orders = new String[capacity];
    }

    // 生产者调用:放入订单
    public synchronized void putOrder(String orderNo) {
        while (count >= capacity) {  // ⚠️ 用while不用if,防止虚假唤醒
            try {
                System.out.println("[仓库] 已满,等待出库…");
                this.wait();
            } catch (InterruptedException e) { return; }
        }
        orders[count++] = orderNo;
        System.out.println("[仓库] " + orderNo + " 入库,库存:" + count);
        this.notifyAll(); // 通知消费者
    }

    // 消费者调用:取出订单
    public synchronized String takeOrder() {
        while (count <= 0) {  // ⚠️ 用while不用if
            try {
                System.out.println("[仓库] 为空,等待入库…");
                this.wait();
            } catch (InterruptedException e) { return null; }
        }
        String orderNo = orders[–count];
        orders[count] = null;
        System.out.println("[仓库] " + orderNo + " 出库,库存:" + count);
        this.notifyAll(); // 通知生产者
        return orderNo;
    }
}

// java
// 生产者:用户下单
class OrderProducer implements Runnable {
    private OrderWarehouse warehouse;
    public OrderProducer(OrderWarehouse w) { this.warehouse = w; }
    public void run() {
        for (int i = 1; i <= 10; i++) {
            String no = "ORD-" + i;
            warehouse.putOrder(no);
            try { Thread.sleep((int)(Math.random() * 300)); } catch (InterruptedException e) {}
        }
    }
}

// 消费者:仓库发货
class OrderConsumer implements Runnable {
    private OrderWarehouse warehouse;
    public OrderConsumer(OrderWarehouse w) { this.warehouse = w; }
    public void run() {
        for (int i = 1; i <= 10; i++) {
            String no = warehouse.takeOrder();
            System.out.println("[发货] " + no + " 已发货");
            try { Thread.sleep((int)(Math.random() * 500)); } catch (InterruptedException e) {}
        }
    }
}

// 测试
OrderWarehouse wh = new OrderWarehouse(3); // 仓库容量3
new Thread(new OrderProducer(wh), "下单线程").start();
new Thread(new OrderConsumer(wh), "发货线程").start();

6.3 ⚠️ 为什么wait()用while不用if?

// java
// ❌ 用if:被唤醒后不再检查条件,条件可能已经不满足
if (count >= capacity) { wait(); } // 被唤醒后直接往下走

// ✅ 用while:被唤醒后再次检查条件
while (count >= capacity) { wait(); } // 被唤醒后重新检查

// 原因:1.虚假唤醒 2.notifyAll唤醒多个消费者,第一个消费后条件已变

�� 面试考点:wait()为什么要用while判断条件,而不是if?

标准话术:"用if判断存在两个风险:1)虚假唤醒——线程可能在没有被notify的情况下被唤醒,如果用if,唤醒后不再检查条件就直接执行,可能导致逻辑错误;2)notifyAll唤醒多个线程时,第一个线程执行后改变了条件,后续线程如果不再检查就会在条件不满足的情况下继续执行。用while可以保证被唤醒后重新检查条件,只有条件真正满足才继续执行。"

�� 面试考点:wait()和sleep()的区别?

标准话术:"四个核心区别:1)所属类不同——wait()是Object的方法,sleep()是Thread的方法;2)是否释放锁——wait()会释放锁,sleep()不释放锁;3)使用位置——wait()必须在synchronized内调用,sleep()任意位置可用;4)唤醒方式——wait()需要notify/notifyAll唤醒,sleep()超时自动唤醒。wait()用于线程间通信,sleep()用于线程暂停。"

6.4 第6章小结

知识点

项目用途

同步代码块

精确控制锁范围,提高并发性能

专用锁对象

不同资源用不同锁,减少不必要互斥

wait/notify

线程间通信,生产者-消费者模型

while检查条件

防止虚假唤醒

第7章:综合实战 — 电商系统多线程模块

7.1 实战目标

把前6课知识整合,构建电商系统多线程并发模块:

模块

知识点

异步日志

继承Thread + 守护线程 + wait/notify

库存管理

synchronized同步代码块 + 专用锁

订单处理

Runnable接口 + synchronized

请求分发

多线程启动 + join等待

7.2 模块1:异步日志管理器

// java
/**
 * 异步日志管理器(生产者-消费者模型)
 * 业务线程生产日志 → 日志线程消费写入
 */
class AsyncLogManager {
    private String[] logQueue;
    private int count = 0;
    private int capacity;

    public AsyncLogManager(int capacity) {
        this.capacity = capacity;
        this.logQueue = new String[capacity];
    }

    // 业务线程调用:添加日志
    public synchronized void addLog(String message) {
        while (count >= capacity) {
            try { this.wait(); } catch (InterruptedException e) { return; }
        }
        String time = new java.text.SimpleDateFormat("HH:mm:ss.SSS")
            .format(new java.util.Date());
        logQueue[count++] = "[" + time + "] " + message;
        this.notifyAll();
    }

    // 日志线程调用:取出日志
    public synchronized String takeLog() {
        while (count <= 0) {
            try { this.wait(); } catch (InterruptedException e) { return null; }
        }
        String log = logQueue[0];
        for (int i = 1; i < count; i++) logQueue[i – 1] = logQueue[i];
        logQueue[–count] = null;
        this.notifyAll();
        return log;
    }
}

// 日志写入线程(守护线程)
class LogWriterThread extends Thread {
    private AsyncLogManager logManager;
    private volatile boolean running = true; // volatile保证可见性

    public LogWriterThread(AsyncLogManager lm) {
        super("日志写入线程");
        this.logManager = lm;
        setDaemon(true); // 守护线程
    }

    public void run() {
        while (running) {
            String log = logManager.takeLog();
            if (log != null) System.out.println(getName() + " >>> " + log);
        }
    }

    public void shutdown() { running = false; this.interrupt(); }
}

7.3 模块2:库存管理器

// java
class InventoryManager {
    private java.util.Map<String, Integer> stockMap = new java.util.HashMap<>();
    private Object stockLock = new Object(); // 专用锁

    public InventoryManager() {
        stockMap.put("SKU001-手机", 50);
        stockMap.put("SKU002-耳机", 100);
        stockMap.put("SKU003-充电器", 200);
    }

    public boolean deductStock(String sku, int quantity, String operator) {
        synchronized (stockLock) {
            Integer current = stockMap.get(sku);
            if (current == null || current < quantity) {
                System.out.println("[" + operator + "] 库存不足!" + sku);
                return false;
            }
            stockMap.put(sku, current – quantity);
            System.out.println("[" + operator + "] " + sku + " 扣减" + quantity
                + ",剩余:" + (current – quantity));
            return true;
        }
    }

    public synchronized void printAll() {
        System.out.println("=== 当前库存 ===");
        for (java.util.Map.Entry<String, Integer> e : stockMap.entrySet()) {
            System.out.println("  " + e.getKey() + ":" + e.getValue());
        }
    }
}

7.4 模块3:订单处理器

// java
class OrderProcessor implements Runnable {
    private InventoryManager inventoryManager;
    private AsyncLogManager logManager;
    private String userName;
    private String sku;
    private int quantity;

    public OrderProcessor(InventoryManager im, AsyncLogManager lm,
                          String userName, String sku, int quantity) {
        this.inventoryManager = im;
        this.logManager = lm;
        this.userName = userName;
        this.sku = sku;
        this.quantity = quantity;
    }

    public void run() {
        String tName = Thread.currentThread().getName();
        System.out.println("[" + tName + "] " + userName + " 下单:" + sku + " × " + quantity);

        boolean success = inventoryManager.deductStock(sku, quantity, userName);
        if (!success) {
            logManager.addLog(userName + " 下单失败:" + sku + " 库存不足");
            return;
        }

        String orderNo = "ORD" + System.currentTimeMillis() % 100000;
        System.out.println("[" + tName + "] " + userName + " 下单成功!订单号:" + orderNo);
        logManager.addLog(userName + " 下单成功:" + sku + " × " + quantity
            + ",订单号:" + orderNo);
    }
}

7.5 整合测试

// java
public class ECommerceConcurrentTest {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("=== 电商系统多线程模块测试 ===");

        AsyncLogManager logManager = new AsyncLogManager(50);
        InventoryManager inventoryManager = new InventoryManager();

        // 启动日志线程
        LogWriterThread logWriter = new LogWriterThread(logManager);
        logWriter.start();

        inventoryManager.printAll();

        // 模拟多用户并发下单
        String[][] orders = {
            {"张三", "SKU001-手机", "3"},  {"李四", "SKU001-手机", "5"},
            {"王五", "SKU002-耳机", "10"}, {"赵六", "SKU003-充电器", "50"},
            {"钱七", "SKU001-手机", "20"}, {"孙八", "SKU002-耳机", "80"},
            {"周九", "SKU003-充电器", "100"}, {"吴十", "SKU001-手机", "25"},
            {"郑一", "SKU002-耳机", "15"}, {"冯二", "SKU003-充电器", "60"}
        };

        Thread[] threads = new Thread[orders.length];
        for (int i = 0; i < orders.length; i++) {
            threads[i] = new Thread(
                new OrderProcessor(inventoryManager, logManager,
                    orders[i][0], orders[i][1], Integer.parseInt(orders[i][2])),
                "订单线程-" + (i + 1)
            );
        }

        for (Thread t : threads) t.start();
        for (Thread t : threads) t.join(); // 等待所有订单处理完

        Thread.sleep(1000); // 等日志写完
        inventoryManager.printAll();
        logWriter.shutdown();
        System.out.println("=== 测试结束 ===");
    }
}

7.6 实战要点总结

模块

使用的知识点

核心设计思路

AsyncLogManager

wait/notify + synchronized

生产者-消费者模型,业务线程生产日志不阻塞

LogWriterThread

继承Thread + 守护线程 + interrupt

后台服务线程,主业务结束自动销毁

InventoryManager

同步代码块 + 专用锁

精确锁粒度,只锁库存操作

OrderProcessor

实现Runnable

任务与线程解耦,方便线程池管理

整合测试

join等待

并行执行 + 汇总等待

面试考点汇总

#

考点

关键词

难度

1

进程和线程的区别

资源分配/CPU调度/共享内存

⭐⭐⭐

2

start()和run()的区别

新线程/普通方法调用

⭐⭐⭐

3

线程创建方式及推荐

Runnable/单继承/线程池

⭐⭐⭐⭐

4

Callable和Runnable的区别

返回值/异常/FutureTask

⭐⭐⭐⭐

5

为什么要给线程命名

排查问题/线程dump

⭐⭐

6

并发和并行的区别

时间片/真正同时/多核

⭐⭐⭐

7

守护线程

GC/自动销毁/setDaemon

⭐⭐⭐

8

如何正确停止线程

interrupt/stop废弃/优雅退出

⭐⭐⭐

9

线程有几种状态

6种状态/生命周期转换

⭐⭐⭐⭐

10

什么是线程安全问题

原子性/可见性/有序性

⭐⭐⭐⭐

11

synchronized同步方法vs代码块

锁粒度/锁对象/灵活性

⭐⭐⭐⭐

12

synchronized是可重入锁吗

同一线程/再次获取/不死锁

⭐⭐⭐

13

wait()和sleep()的区别

释放锁/Object/Thread/通信

⭐⭐⭐⭐⭐

14

wait()为什么用while不用if

虚假唤醒/notifyAll/条件检查

⭐⭐⭐⭐⭐

15

生产者-消费者模型的作用

解耦/缓冲/并发

⭐⭐⭐⭐

16

join()的作用和用法

等待线程完成/超时等待

⭐⭐⭐

17

专用锁对象的设计

细粒度锁/不同资源不同锁

⭐⭐⭐

18

count++为什么不是原子操作

读-改-写/数据丢失

⭐⭐⭐

附录:速查表

线程方法速查

方法

作用

释放锁

start()

启动线程

sleep(ms)

休眠

join()

等待线程完成

yield()

让出CPU

interrupt()

发送中断信号

wait()

等待通知

notify()

唤醒一个线程

notifyAll()

唤醒所有线程

synchronized速查

方式

锁对象

语法

同步实例方法

this

public synchronized void method()

同步静态方法

Class对象

public static synchronized void method()

同步代码块

指定对象

synchronized(lock) { }

线程安全问题解决思路

1. 识别共享数据 → 哪些变量被多线程访问?
2. 识别写操作   → 是否有线程修改共享数据?
3. 选择同步方式 → 同步方法 or 同步代码块?
4. 确定锁对象   → this or 专用锁对象?
5. 最小化锁范围 → 只锁必须同步的代码
6. 测试验证     → 多线程并发测试

知识体系总结

Java多线程基础(7课时完整体系)
├── 第1章:进程与线程 + 继承Thread类
│   ├── 进程vs线程(内存/开销/通信)
│   ├── 继承Thread类创建线程
│   ├── start() vs run()(面试必考)
│   └── 项目实战:异步日志写入
├── 第2章:实现Runnable接口 + 两种方式对比
│   ├── 实现Runnable接口创建线程
│   ├── 两种方式对比(单继承/资源共享/扩展性)
│   ├── 资源共享演示(抢票场景)
│   ├── 实现Callable接口(第三种方式)
│   └── 项目实战:用户请求处理器
├── 第3章:线程常用API
│   ├── 线程信息获取与命名
│   ├── 线程优先级(只是建议)
│   ├── sleep()休眠 / yield()让出CPU
│   ├── 并发vs并行
│   └── 守护线程(GC/心跳)
├── 第4章:线程控制方法 + 生命周期
│   ├── join()等待线程完成(超时等待/降级方案)
│   ├── interrupt()中断线程(优雅停止)
│   └── 线程生命周期6种状态
├── 第5章:线程安全问题 + synchronized同步方法
│   ├── 线程安全问题产生原因(原子性/可见性/有序性)
│   ├── synchronized同步方法
│   ├── synchronized锁原理与可重入性
│   └── 项目实战:抢票系统
├── 第6章:同步代码块 + 线程通信
│   ├── 同步代码块 vs 同步方法(锁粒度)
│   ├── 专用锁对象(细粒度锁设计)
│   ├── wait()/notify()线程通信
│   ├── 项目实战:订单流水线(生产者-消费者)
│   └── while vs if 防虚假唤醒
└── 第7章:综合实战 — 电商系统多线程模块
    ├── 异步日志管理器(生产者-消费者 + 守护线程)
    ├── 库存管理器(同步代码块 + 专用锁)
    ├── 订单处理器(Runnable + synchronized)
    └── 整合测试(join等待 + 并发下单)

课后作业

基础题

1. 分别用继承Thread和实现Runnable两种方式创建线程,打印1-100的偶数

2. 编写程序验证start()和run()的区别(观察执行线程名称)

3. 创建3个线程,分别命名为"查询线程-001"、"更新线程-001"、"删除线程-001",观察线程执行顺序

进阶题

1. 实现一个简易倒计时器:主线程启动后,每隔1秒打印剩余秒数,倒计时结束后主线程退出,用Thread.sleep()实现

2. 用Callable + FutureTask实现一个异步计算任务:计算1+2+…+100的和,在主线程获取结果

3. 创建一个守护线程模拟系统监控:每2秒打印"系统正常",主线程处理5个任务后退出,观察守护线程是否自动销毁

4. 用join()实现:主线程启动3个子线程分别模拟"查询商品"、"创建订单"、"扣减库存",等待全部完成后输出总耗时

5. 用synchronized同步代码块改造第7题中的库存扣减操作,确保库存不会超卖

挑战题

1. 异步日志管理器v1:用继承Thread实现异步日志写入器,主线程模拟10次用户操作(下单、支付),每次操作后异步写入日志,要求日志不阻塞主业务流程

2. 电商库存系统:模拟10个用户同时抢购同一商品(库存5件),要求:

用同步代码块 + 专用锁对象实现线程安全

每次扣减库存前打印当前库存

库存不足时提示"抢购失败"

最终库存一定为0,不会出现超卖

3. 生产者-消费者扩展版:实现一个订单处理系统:

2个下单线程(生产者)不断生成订单放入仓库

3个发货线程(消费者)从仓库取订单处理

仓库容量为5,满时生产者等待,空时消费者等待

用wait/notifyAll实现通信,while防止虚假唤醒

赞(0)
未经允许不得转载:171主机测评 » Java多线程基础实战:从进程与线程到并发通信,7课时吃透线程创建、安全与生产者消费者模型
分享到: 更多 (0)

评论 抢沙发

  • 昵称 (必填)
  • 邮箱 (必填)
  • 网址