欢迎光临
我们一直在努力

Java 多线程初阶通关指南:从核心概念到实战案例,一篇吃透

一、认识线程:到底什么是线程?

1.1 线程的核心概念

线程是轻量级进程,为了解决创建进程的开销比较大的问题

  • 这就是多线程的核心思想:将一个大任务拆解为多个小任务,交给不同的执行流并行处理
  • 其中张三是主线程(Main Thread),李四、王五是子线程

1.2 为什么我们需要多线程?

  • 硬件层面刚需:单核 CPU 的发展遇到瓶颈,多核 CPU 成为主流,多线程能充分利用多核 CPU 的算力资源,大幅提升程序执行效率
  • 业务场景刚需:程序在 IO 等待场景(网络请求、文件读写、数据库操作)中会阻塞,多线程可以在等待 IO 的空闲时间,去执行其他工作,最大化利用 CPU 资源
  • 性能优势显著:虽然多进程也能实现并发编程,但线程比进程更轻量,创建、销毁、调度的开销远小于进程,是更高效的并发实现方式
  • 1.3 进程和线程的核心区别(面试高频)

    这是多线程入门最核心的概念,也是面试必问的基础题,核心区别如下:

    1、进程包含线程,2、进程是操作系统资源分配的基本单位,3、线程是操作系统调度执行的基本单位,4、同一个进程中的多个线程之间,共用一份资源(内存文件)

     

    1.4 Java 线程与操作系统线程的关系

    线程是操作系统内核实现的机制,操作系统会对用户层提供线程操作的 API(比如 Linux 的 pthread 库);而 Java 标准库中的Thread类,本质上是对操作系统原生线程 API 的进一步封装和抽象,每一个 Java 线程,都对应着操作系统中的一个原生线程。线程调度是操作系统内核进行控制的,程序员在应用程序这个层次,无法感知,无法干预

    二、线程的创建:5 种方式

    Java 中创建线程有多种常用方式,各有适用场景。

    2.1 方式 1:继承 Thread 类

    核心逻辑:继承Thread类,重写run()方法(定义线程的执行逻辑),调用start()方法启动线程。

    // 1. 继承Thread类,重写run方法
    class MyThread extends Thread {
    @Override
    public void run() {
    System.out.println("线程执行逻辑:" + Thread.currentThread().getName());
    }
    }

    // 2. 创建实例,启动线程
    public class ThreadCreateDemo {
    public static void main(String[] args) {
    MyThread t = new MyThread();
    t.start(); // 真正启动线程,在操作系统底层创建新的原生线程
    }
    }

    核心注意点:start () 和 run () 的本质区别

    • run()只是定义了线程的执行逻辑,直接调用run()不会创建新线程,只是在主线程中普通执行方法
    • 只有调用start(),才会真正在操作系统中创建新线程,触发并发执行(JVM调用操作系统的API完成线程创建操作)

    2.2 方式 2:实现 Runnable 接口

    实现Runnable接口,重写run()方法,将Runnable实例传入Thread构造方法,再启动线程。这种方式解耦了「线程执行逻辑」和Thread类本身,扩展性更强,是更推荐的基础写法。

    // 1. 实现Runnable接口,重写run方法
    class MyRunnable implements Runnable {
    @Override
    public void run() {
    System.out.println("线程执行逻辑:" + Thread.currentThread().getName());
    }
    }

    // 2. 创建Thread实例,传入Runnable,启动线程
    public class ThreadCreateDemo {
    public static void main(String[] args) {
    Thread t = new Thread(new MyRunnable());
    t.start();
    }
    }

    2.3 方式 3:匿名内部类创建 Thread 子类

    简化代码书写,无需单独定义子类,适合一次性使用的场景。

    Thread t1 = new Thread() {
    @Override
    public void run() {
    System.out.println("匿名内部类创建Thread子类");
    }
    };
    t1.start();

    2.4 方式 4:匿名内部类创建 Runnable 子类

    Thread t2 = new Thread(new Runnable() {
    @Override
    public void run() {
    System.out.println("匿名内部类创建Runnable子类");
    }
    });
    t2.start();

    2.5 方式 5:Lambda 表达式创建(日常开发最常用)

    Lambda 表达式可以极简地简化Runnable的书写,代码简洁优雅,是日常开发中最主流的写法。

    // 最简写法
    Thread t3 = new Thread(() -> System.out.println("Lambda创建线程"));
    t3.start();

    // 带复杂逻辑的写法
    Thread t4 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
    System.out.println("线程执行:" + i);
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    t4.start();

    三、Thread 类核心方法:玩转线程操作

    Thread类是 JVM 管理线程的核心类,提供了大量实用的 API,这里讲解开发中最常用的核心方法。

    3.1 常用构造方法

    表格

    构造方法功能说明
    Thread() 创建基础的线程对象
    Thread(Runnable target) 基于 Runnable 对象创建线程
    Thread(String name) 创建线程并指定线程名称(方便调试排查问题)
    Thread(Runnable target, String name) 基于 Runnable 创建线程,并指定线程名称

    用下面这个写法必须要重写Thread的run方法

    // 创建线程并指定名称,方便jconsole等工具调试
    Thread t = new Thread(() -> {
    System.out.println(Thread.currentThread().getName());
    }, "订单处理线程");
    t.start();

    3.2 核心属性与获取方法

    表格

    线程 ID getId() Java给每个运行的线程分配id,标识线程身份效果,类似pid
    线程名称 getName() 获取线程名称,主要用于调试排查
    线程状态 getState() 获取线程当前的生命周期状态
    调度优先级 getPriority() 获取线程的调度优先级,优先级越高理论上越容易被 CPU 调度
    是否后台线程 isDaemon() 判断线程是否为守护(后台)线程
    是否存活 isAlive() 判断线程是否处于运行中(run 方法是否执行完成)
    是否被中断 isInterrupted() 判断线程是否被设置了中断标志

    前台线程就是能够影响到进程继续存在的线程,前台线程和后台线程可以有多个,但是有多个前台线程,只有所有前台线程结束,才会终止整个进程。上面有一些是JVM自带的线程,JVM提供这些线程有特殊功能,跟随整个进程的执行,比如垃圾回收,start和run,start是调用系统api,run是线程入口方法,不需要手动调用

    3.3 线程中断:停止线程的正确方式

    public class Demo10{
    private static boolean isFinisher=false;

    public static void main(String []args){
    Thread t=new Thread(()->{
    while(!isFinished){

    System.out.println("hello thread");
    try{
    Thread.sleep(1000)
    }catch (INterruptedException e){
    throw new RuntimeException(e);=
    }

    System.out.println("结束");
    }
    });
    t.start();
    Thread.sleep(1000);
    isFinisher=true;

    }

    }

    线程启动后,我们有时需要提前终止它的运行,Java 提供了两种安全的中断方式,不推荐使用已废弃的stop()方法(会强制终止线程,导致数据不一致)。

    方式 1:自定义 volatile 标志位

    通过共享的volatile变量作为中断标志,线程在循环中判断该标志,决定是否终止运行。

    public class ThreadInterruptDemo1 {
    private static class MyRunnable implements Runnable {
    // 用volatile保证多线程间的内存可见性
    public volatile boolean isQuit = false;

    @Override
    public void run() {
    while (!isQuit) {
    System.out.println(Thread.currentThread().getName() + ":正在处理业务");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    System.out.println(Thread.currentThread().getName() + ":业务终止完成");
    }
    }

    public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "业务线程");
    thread.start();

    // 10秒后触发中断
    Thread.sleep(10000);
    System.out.println("主线程:通知业务线程终止运行");
    target.isQuit = true;
    }
    }

    方式 2:调用 interrupt () 方法

    Thread类内部自带了中断标志位,通过interrupt()方法设置中断标志,配合interrupted()/isInterrupted()判断。这种方式更强大,即使线程处于sleep/wait阻塞状态,也能被唤醒并处理中断。主线程中调用interrupt,会使抛出异常,离开循环终止线程

    public class ThreadInterruptDemo2 {
    private static class MyRunnable implements Runnable {
    @Override
    public void run() {
    // isInterrupted()判断中断标志,调用后不会清除标志位
    while (!Thread.currentThread().isInterrupted()) {
    System.out.println(Thread.currentThread().getName() + ":正在处理业务");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    // 阻塞状态被中断会抛出异常,在这里处理终止逻辑
    System.out.println(Thread.currentThread().getName() + ":收到中断通知,终止业务");
    break; // 跳出循环,终止线程
    }
    }
    }
    }

    public static void main(String[] args) throws InterruptedException {
    MyRunnable target = new MyRunnable();
    Thread thread = new Thread(target, "业务线程");
    thread.start();

    // 10秒后触发中断
    Thread.sleep(10000);
    System.out.println("主线程:调用interrupt()中断线程");
    thread.interrupt();
    }
    }

    两个中断判断方法的核心区别:

    • Thread.interrupted():静态方法,判断当前线程的中断标志,调用后会清除标志位
    • Thread.currentThread().isInterrupted():实例方法,判断指定线程的中断标志,调用后不会清除标志位
    • 同时有个细节,如何catch里面没有break,那么线程就不会终止,interrupt方法会把isInterrupted修改为true,但是由于上诉代码把sleep唤醒,那么sleep就会把isInterrupted修改为false

    3.4 线程等待:join () 方法

    有时我们需要等待一个线程执行完成后,再执行后续逻辑,就可以使用join()方法,它会让调用方线程阻塞,直到目标线程执行完成。

    public class ThreadJoinDemo {
    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 10; i++) {
    System.out.println("t1正在执行");
    try {
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    System.out.println("t1执行完成");
    }, "t1");

    t1.start();
    t1.join(); // 主线程阻塞,等待t1执行完成后,才会往下执行
    System.out.println("主线程:t1已经执行完,继续执行后续逻辑");
    }
    }

    join()还有带超时时间的重载方法join,可以指定最多等待的毫秒数,避免线程无限阻塞。

    3.5 其他高频核心方法

    • Thread.currentThread():静态方法,获取当前正在执行代码的线程对象的引用,是日常开发中最高频使用的方法之一
    • Thread.sleep(long millis):静态方法,让当前线程休眠指定毫秒数,进入TIMED_WAITING状态,会释放 CPU 资源,但不会释放持有的锁

    四、线程的生命周期:6 种状态与流转

    Java 线程的生命周期分为 6 种状态,定义在Thread.State枚举中,理解状态流转是排查多线程问题的关键,也是面试高频考点。

    4.1 6 种状态详解

  • NEW(新建):创建了Thread对象,但还没调用start()方法,只是定义了任务,还没在系统底层创建线程,也没有开始执行。
  • RUNNABLE(可运行):线程处于可被 CPU 调度的状态,分为两个子状态:
    • 就绪(READY):线程已经准备好,正在等待 CPU 调度
    • 运行中(RUNNING):线程正在 CPU 上执行代码
  • BLOCKED(阻塞):线程等待获取synchronized锁,被阻塞挂起,只有成功拿到锁之后,才能回到 RUNNABLE 状态。
  • WAITING(无限等待):线程调用了无超时参数的wait()/join()等方法,进入无限等待状态,必须被其他线程主动唤醒(notify/notifyAll),否则会一直等待。
  • TIMED_WAITING(超时等待):线程调用了sleep(long)、wait(long)、join(long)等带超时参数的方法,等待指定时间后会自动唤醒,不会无限阻塞。
  • TERMINATED(终止):线程工作完成了,但是Thread对象还在
  • 4.2 核心状态流转

    五、线程安全:多线程的核心痛点与解决方案

    线程安全是多线程开发中最核心、最容易出问题的部分,也是面试的绝对重点。

    5.1 什么是线程安全?

    如果多线程环境下,代码的运行结果和单线程环境下的预期结果完全一致,就说明这个程序是线程安全的;反之,就是线程不安全。

    我们先看一个线程不安全的典型案例:

    public class ThreadUnsafeDemo {
    // 多个线程共享的计数变量
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
    // 线程1:对count自增5万次
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
    count++;
    }
    });

    // 线程2:对count自增5万次
    Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
    count++;
    }
    });

    t1.start();
    t2.start();
    // 等待两个线程执行完成
    t1.join();
    t2.join();

    // 预期结果是100000,实际运行结果几乎永远小于100000
    System.out.println("最终count值:" + count);
    }
    }

    运行后会发现,最终的 count 值几乎永远达不到 100000,这就是典型的线程不安全问题。

    5.2 线程不安全的 5 大根本原因

  • 线程抢占式调度(根本原因):操作系统对线程的调度是随机的、抢占式的,线程的执行顺序无法预知,导致多线程执行时,操作可能被中途打断。
  • 多线程修改共享可变数据:多个线程同时修改同一个共享变量,才会引发安全问题;如果是线程私有数据、只读不写的共享数据,不会有线程安全问题。
  • 操作不具备原子性:上面的count++看似一行代码,实际分为 3 步 CPU 指令:
  • 从内存读取 count 的值到 CPU 寄存器
  • CPU 中对 count 的值进行更新
  • 把更新后的值写回内存这 3 步执行过程中,可能被其他线程打断,导致数据覆盖、结果错误。
  • 内存可见性问题:Java 内存模型(JMM)中,线程有自己的工作内存(CPU 寄存器 / 高速缓存),共享变量存储在主内存。线程修改共享变量时,先修改工作内存的副本,再同步回主内存;如果修改没有及时同步,其他线程读到的就是旧数据,引发问题。
  • 指令重排序:编译器 / CPU 会在保证单线程逻辑不变的前提下,对指令执行顺序进行优化重排序,提升执行效率;但在多线程环境下,重排序可能导致逻辑错乱,引发线程安全问题。
  • 5.3 解决方案 1:synchronized 关键字(监视器锁)

    synchronized是解决原子性问题的核心,通过加锁实现互斥执行,保证同一时间只有一个线程能执行被锁定的代码,从根源上避免操作被中途打断。

    5.3.1 synchronized 的核心特性
  • 互斥性:当一个线程执行到某个对象的synchronized代码块时,其他线程执行同一个对象的synchronized代码块,会阻塞等待,直到锁被释放。进入synchronized代码块相当于加锁,退出代码块相当于解锁。
  • 可重入性:同一个线程可以多次对同一把锁加锁,不会出现自己把自己锁死的情况。锁内部维护了线程持有者和计数器,同一个线程加锁时计数器自增,解锁时递减,计数器为 0 时才真正释放锁。
  • 可重入锁原理,1、在锁内部记录当前是哪个线程持有的锁,后续每次加锁,都进行判定

    2、通过计数器记录当前加锁的次数,从而确定何时真正解锁

    死锁场景

    public class {
    public static void main( String []args){

    Object locker1 =new Object();
    Object locker2 =new Object();
    Thread t1=new Thread(()->{
    synchronized(locker1){
    try{
    }
    Thread.sleep(1000)
    }catch(){
    。。。。。
    }
    synchronized(locker2){
    }

    });

    Thread t2=new Thread(()->{
    synchronized(locker2){
    try{

    }
    Thread.sleep(1000)
    }catch(){
    。。。。。
    }

    }
    synchronized(locker1){
    }

    });

    }

    }

    5.3.2 synchronized 的 3 种使用方式
  • 修饰代码块,手动指定锁对象
  • public class SynchronizedDemo {
    // 锁对象,必须是多个线程共享的同一个对象
    private final Object locker = new Object();
    private int count = 0;

    public void add() {
    // 对locker对象加锁
    synchronized (locker) {
    count++;
    }
    }
    }

  • 修饰普通成员方法,锁对象是当前 this 对象
  • public class SynchronizedDemo {
    private int count = 0;

    // 等价于 synchronized (this) 包裹整个方法体
    public synchronized void add() {
    count++;
    }
    }

  • 修饰静态方法,锁对象是当前类的 Class 对象
  • public class SynchronizedDemo {
    private static int count = 0;

    // 等价于 synchronized (SynchronizedDemo.class) 包裹整个方法体
    public synchronized static void add() {
    count++;
    }
    }

    用synchronized修复之前的线程不安全案例:

    public class ThreadSafeDemo {
    private static int count = 0;
    // 多个线程共享的锁对象
    private static final Object locker = new Object();

    public static void main(String[] args) throws InterruptedException {
    Thread t1 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
    synchronized (locker) {
    count++;
    }
    }
    });

    Thread t2 = new Thread(() -> {
    for (int i = 0; i < 50000; i++) {
    synchronized (locker) {
    count++;
    }
    }
    });

    t1.start();
    t2.start();
    t1.join();
    t2.join();

    // 最终结果一定是100000,实现线程安全
    System.out.println("最终count值:" + count);
    }
    }

    5.4 解决方案 2:volatile 关键字

    volatile的核心作用是保证内存可见性,同时禁止指令重排序,但不保证原子性,不能解决count++这类操作的线程安全问题。

    5.4.1 volatile 的核心作用
  • 保证内存可见性:被volatile修饰的变量,线程修改后会立即同步回主内存,线程读取时会直接从主内存读取最新值,不会使用工作内存的旧副本。
  • 禁止指令重排序:volatile修饰的变量,编译器 / CPU 不会对其相关指令进行重排序,保证执行顺序和代码逻辑完全一致。
  • 5.4.2 典型使用场景

    我们先看一个内存可见性引发的 bug:

    public class VolatileDemo {
    static class Counter {
    // 没有volatile修饰,会出现内存可见性问题
    public int flag = 0;
    }

    public static void main(String[] args) {
    Counter counter = new Counter();

    // 线程1:循环判断flag,非0则结束循环
    Thread t1 = new Thread(() -> {
    while (counter.flag == 0) {
    // 空循环
    }
    System.out.println("t1循环结束");
    });

    // 线程2:读取用户输入,修改flag的值
    Thread t2 = new Thread(() -> {
    Scanner scanner = new Scanner(System.in);
    System.out.println("请输入一个整数:");
    counter.flag = scanner.nextInt();
    });

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

    运行后会发现,即使输入了非 0 的值,t1 的循环也不会结束,因为 t1 读取的是自己工作内存中的旧 flag 值,感知不到 t2 的修改。给 flag 加上volatile修饰,就能彻底解决这个问题:

    static class Counter {
    // 加上volatile,保证多线程间的内存可见性
    public volatile int flag = 0;
    }

    5.4.3 volatile 和 synchronized 的核心区别
    特性synchronizedvolatile
    核心作用 保证原子性、可见性、禁止指令重排序 保证可见性、禁止指令重排序,不保证原子性
    线程阻塞 会造成线程阻塞等待 不会造成线程阻塞
    适用场景 多线程修改共享变量,保证操作原子性 一个线程写、多个线程读的场景,保证可见性
    修饰范围 代码块、成员方法 成员变量

    六、线程间通信:wait/notify/notifyAll

    多线程之间需要协调执行顺序时,就需要用到线程间通信,核心是Object类的wait()、notify()、notifyAll()三个方法。

    6.1 核心方法详解

  • wait():让当前线程进入等待状态,释放当前持有的锁,直到被其他线程唤醒、超时、或被中断。必须在 synchronized 代码块中使用,否则会直接抛出异常。
  • notify():随机唤醒一个在当前对象上等待的线程,唤醒后的线程需要重新竞争锁。同样必须在 synchronized 代码块中使用。
  • notifyAll():唤醒所有在当前对象上等待的线程,所有线程一起竞争锁。
  • 6.2 典型使用示例

    public class WaitNotifyDemo {
    // 锁对象,必须和wait/notify调用的对象一致
    private static final Object locker = new Object();

    // 等待任务
    static class WaitTask implements Runnable {
    @Override
    public void run() {
    synchronized (locker) {
    System.out.println(Thread.currentThread().getName() + ":开始等待");
    try {
    locker.wait(); // 进入等待,同时释放锁
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    System.out.println(Thread.currentThread().getName() + ":被唤醒,继续执行");
    }
    }
    }

    // 唤醒任务
    static class NotifyTask implements Runnable {
    @Override
    public void run() {
    synchronized (locker) {
    System.out.println(Thread.currentThread().getName() + ":开始唤醒");
    locker.notifyAll(); // 唤醒所有在locker对象上等待的线程
    System.out.println(Thread.currentThread().getName() + ":唤醒完成");
    }
    }
    }

    public static void main(String[] args) throws InterruptedException {
    // 启动3个等待线程
    Thread t1 = new Thread(new WaitTask(), "等待线程1");
    Thread t2 = new Thread(new WaitTask(), "等待线程2");
    Thread t3 = new Thread(new WaitTask(), "等待线程3");
    t1.start();
    t2.start();
    t3.start();

    // 1秒后启动唤醒线程
    Thread.sleep(1000);
    Thread notifyThread = new Thread(new NotifyTask(), "唤醒线程");
    notifyThread.start();
    }
    }

    6.3 面试高频:wait () 和 sleep () 的区别

    这是面试中几乎必问的问题,核心区别如下表:

    表格

    特性wait()sleep()
    所属类 Object 类的成员方法 Thread 类的静态方法
    锁处理 会释放当前持有的锁 不会释放任何锁
    使用前提 必须在 synchronized 代码块中使用 无前置要求,任何地方都能使用
    唤醒方式 被其他线程 notify/notifyAll 唤醒、超时、中断 超时时间到达、被中断
    核心用途 线程间通信、协调执行顺序 让线程休眠指定时间

    七、多线程经典实战案例

    掌握了核心知识点后,我们来看几个多线程最经典的实战案例,也是面试的高频考点。

    7.1 单例模式

    单例模式保证某个类在程序中只存在唯一的实例,分为饿汉模式和懒汉模式,其中懒汉模式需要重点处理多线程安全问题。

    1. 饿汉模式

    类加载时就创建实例,天生线程安全,实现简单,缺点是会提前占用内存。

    class SingletonHungry {
    // 类加载时就创建唯一实例
    private static final SingletonHungry instance = new SingletonHungry();

    // 私有构造方法,禁止外部通过new创建实例
    private SingletonHungry() {}

    // 对外提供获取实例的唯一方法
    public static SingletonHungry getInstance() {
    return instance;
    }
    }

    2. 懒汉模式(双重校验锁 + volatile,线程安全版)

    第一次使用时才创建实例,实现延迟加载,节省内存资源,同时通过双重校验锁保证线程安全,是最经典的实现方式。

    class SingletonLazy {
    // 加volatile,禁止指令重排序,保证内存可见性
    private static volatile SingletonLazy instance = null;

    // 私有构造方法
    private SingletonLazy() {}

    // 双重校验锁实现线程安全的懒加载
    public static SingletonLazy getInstance() {
    // 第一层判断:实例已创建,直接返回,无需竞争锁,大幅提升性能
    if (instance == null) {
    // 加锁,保证同一时间只有一个线程进入
    synchronized (SingletonLazy.class) {
    // 第二层判断:防止多个线程竞争锁时,重复创建实例
    if (instance == null) {
    instance = new SingletonLazy();
    }
    }
    }
    return instance;
    }
    }

    7.2 阻塞队列与生产者消费者模型

    阻塞队列是线程安全的队列,核心特性是:队列满时,入队操作会阻塞;队列空时,出队操作会阻塞。它的典型应用就是生产者消费者模型,可以实现业务解耦和流量削峰填谷。

    1. Java 标准库的阻塞队列使用

    Java 标准库提供了BlockingQueue接口,常用实现类是LinkedBlockingQueue,核心方法是put()(阻塞式入队)和take()(阻塞式出队)。

    public class ProducerConsumerDemo {
    public static void main(String[] args) {
    // 创建阻塞队列,容量为10
    BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);

    // 生产者线程:生产随机数,放入阻塞队列
    Thread producer = new Thread(() -> {
    Random random = new Random();
    while (true) {
    try {
    int num = random.nextInt(100);
    System.out.println("生产元素:" + num);
    queue.put(num);
    Thread.sleep(1000);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }, "生产者");

    // 消费者线程:从队列取出元素,进行消费
    Thread consumer = new Thread(() -> {
    while (true) {
    try {
    int num = queue.take();
    System.out.println("消费元素:" + num);
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    }, "消费者");

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

    2. 手动实现简易阻塞队列

    基于循环数组 + synchronized + wait/notify,实现一个简易的阻塞队列:

    public class MyBlockingQueue {
    // 底层存储数组
    private final int[] items = new int[1000];
    // 队列元素个数
    private volatile int size = 0;
    // 队头指针
    private volatile int head = 0;
    // 队尾指针
    private volatile int tail = 0;

    // 阻塞式入队
    public void put(int value) throws InterruptedException {
    synchronized (this) {
    // 队列满了,进入等待
    while (size == items.length) {
    wait();
    }
    // 放入元素
    items[tail] = value;
    tail = (tail + 1) % items.length;
    size++;
    // 唤醒等待出队的线程
    notifyAll();
    }
    }

    // 阻塞式出队
    public int take() throws InterruptedException {
    synchronized (this) {
    // 队列空了,进入等待
    while (size == 0) {
    wait();
    }
    // 取出元素
    int ret = items[head];
    head = (head + 1) % items.length;
    size–;
    // 唤醒等待入队的线程
    notifyAll();
    return ret;
    }
    }

    // 获取队列当前元素个数
    public synchronized int size() {
    return size;
    }
    }

    7.3 定时器

    定时器类似闹钟,到达设定的时间后,执行预设的代码,是后端开发中非常常用的组件。

    1. Java 标准库的 Timer 使用

    public class TimerDemo {
    public static void main(String[] args) {
    Timer timer = new Timer();
    // 3000毫秒后执行定时任务
    timer.schedule(new TimerTask() {
    @Override
    public void run() {
    System.out.println("定时任务执行");
    }
    }, 3000);
    }
    }

    2. 手动实现简易定时器

    核心是基于优先级队列(按任务执行时间排序)+ 扫描线程,实现定时任务的管理和执行。

    import java.util.PriorityQueue;

    // 任务类,实现Comparable接口,按执行时间排序
    class MyTask implements Comparable<MyTask> {
    // 要执行的任务逻辑
    public Runnable runnable;
    // 任务执行的绝对时间戳
    public long time;

    public MyTask(Runnable runnable, long delay) {
    this.runnable = runnable;
    this.time = System.currentTimeMillis() + delay;
    }

    @Override
    public int compareTo(MyTask o) {
    // 执行时间早的任务排在前面
    return (int) (this.time – o.time);
    }
    }

    // 自定义定时器
    class MyTimer {
    // 优先级队列,存储定时任务
    private final PriorityQueue<MyTask> queue = new PriorityQueue<>();
    // 锁对象
    private final Object locker = new Object();

    // 注册定时任务
    public void schedule(Runnable command, long after) {
    synchronized (locker) {
    MyTask task = new MyTask(command, after);
    queue.offer(task);
    // 唤醒扫描线程
    locker.notify();
    }
    }

    // 构造方法,启动任务扫描线程
    public MyTimer() {
    Thread worker = new Thread(() -> {
    while (true) {
    try {
    synchronized (locker) {
    // 队列为空,等待新任务
    while (queue.isEmpty()) {
    locker.wait();
    }
    // 取出队首任务,判断是否到执行时间
    MyTask task = queue.peek();
    long curTime = System.currentTimeMillis();
    if (curTime >= task.time) {
    // 时间到,执行任务
    queue.poll();
    task.runnable.run();
    } else {
    // 时间没到,等待到任务执行时间
    locker.wait(task.time – curTime);
    }
    }
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    worker.start();
    }
    }

    7.4 线程池

    线程池是管理线程的容器,核心好处是减少线程频繁创建、销毁的开销,提升程序性能,是高并发开发中最常用的工具。

    1. Java 标准库的线程池使用

    Executors工具类提供了快速创建线程池的方法,常用的有:

    • newFixedThreadPool:创建固定线程数的线程池
    • newCachedThreadPool:创建线程数动态增长的线程池
    • newSingleThreadExecutor:创建单线程的线程池
    • newScheduledThreadPool:支持定时任务的线程池

    使用示例:

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

    // 提交1000个任务到线程池
    for (int i = 0; i < 1000; i++) {
    int taskId = i;
    pool.submit(() -> {
    System.out.println(Thread.currentThread().getName() + " 执行任务" + taskId);
    });
    }

    // 关闭线程池
    pool.shutdown();
    }
    }

    2. 手动实现简易线程池

    核心是阻塞队列存储任务,固定数量的工作线程不断从队列中取出任务执行。

    import java.util.concurrent.LinkedBlockingQueue;

    class MyThreadPool {
    // 阻塞队列,存储待执行的任务
    private final LinkedBlockingQueue<Runnable> queue = new LinkedBlockingQueue<>();

    // 构造方法,n为线程池的核心线程数量
    public MyThreadPool(int n) {
    for (int i = 0; i < n; i++) {
    // 创建工作线程
    Thread worker = new Thread(() -> {
    while (true) {
    try {
    // 从队列取出任务并执行
    Runnable task = queue.take();
    task.run();
    } catch (InterruptedException e) {
    e.printStackTrace();
    }
    }
    });
    worker.start();
    }
    }

    // 提交任务到线程池
    public void submit(Runnable task) throws InterruptedException {
    queue.put(task);
    }
    }

    // 测试自定义线程池
    public class MyThreadPoolDemo {
    public static void main(String[] args) throws InterruptedException {
    MyThreadPool pool = new MyThreadPool(4);
    for (int i = 0; i < 100; i++) {
    int taskId = i;
    pool.submit(() -> {
    System.out.println(Thread.currentThread().getName() + " 执行任务" + taskId);
    });
    }
    }
    }

    八、总结

    本文系统讲解了 Java 多线程初阶的全部核心知识点,从线程的基本概念、创建方式,到 Thread 核心 API、线程生命周期,再到线程安全的核心问题与解决方案,线程间通信,最后通过 4 个经典实战案例完成知识点落地。

    最后,我们再总结一下保证线程安全的核心思路:

  • 避免共享可变数据:使用线程私有数据,或共享只读数据,从根源上避免线程安全问题
  • 保证操作原子性:通过synchronized加锁,让多线程互斥执行共享数据的修改
  • 保证内存可见性:通过volatile修饰共享变量,让多线程能及时感知数据的修改
  • 禁止指令重排序:volatile和synchronized都能禁止指令重排序,保证多线程环境下的逻辑正确性
  • 多线程是 Java 开发的核心技能,初阶知识是后续高并发编程的基础,只有把这些核心知识点吃透,才能在后续的开发中写出高效、安全的并发代码。

    赞(0)
    未经允许不得转载:171主机测评 » Java 多线程初阶通关指南:从核心概念到实战案例,一篇吃透
    分享到: 更多 (0)

    评论 抢沙发

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