Java多线程

文章目录

  • 一、多线程概述
    • [1.1 并发与并行](#1.1 并发与并行)
    • [1.2 线程与进程](#1.2 线程与进程)
    • [1.3 多线程的核心优势](#1.3 多线程的核心优势)
  • 二、多线程的创建
    • [2.1 方式 1:继承 Thread 类](#2.1 方式 1:继承 Thread 类)
    • [2.2 方式 2:实现 Runnable 接口](#2.2 方式 2:实现 Runnable 接口)
    • [2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常)](#2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常))
    • [2.4 Thread 与 Runnable 的核心区别及使用场景](#2.4 Thread 与 Runnable 的核心区别及使用场景)
    • [2.5 方式 4:线程池创建](#2.5 方式 4:线程池创建)
  • [三、Thread 类的核心 API](#三、Thread 类的核心 API)
    • [3.1 构造方法](#3.1 构造方法)
    • [3.2 核心成员方法](#3.2 核心成员方法)
    • [3.3 线程调度相关方法](#3.3 线程调度相关方法)
  • 四、线程的生命周期
    • [4.1 生命周期概述](#4.1 生命周期概述)
    • [4.2 各状态核心说明](#4.2 各状态核心说明)
    • [4.3 线程状态转换](#4.3 线程状态转换)
  • 五、线程安全
    • [5.1 线程安全的定义](#5.1 线程安全的定义)
    • [5.2 线程安全问题的产生条件与解决策略](#5.2 线程安全问题的产生条件与解决策略)
    • [5.3 同步代码块](#5.3 同步代码块)
    • [5.4 同步方法](#5.4 同步方法)
    • [5.5 Lock 锁(显式锁)](#5.5 Lock 锁(显式锁))
  • 六、死锁
    • [6.1 死锁的定义](#6.1 死锁的定义)
    • [6.2 死锁的产生条件(四大必要条件)](#6.2 死锁的产生条件(四大必要条件))
    • [6.3 死锁的代码演示](#6.3 死锁的代码演示)
    • [6.4 死锁的解决策略](#6.4 死锁的解决策略)
    • [6.5 死锁的排查与避免](#6.5 死锁的排查与避免)
  • 七、线程间通信
    • [7.1 线程间通信的定义](#7.1 线程间通信的定义)
    • [7.2 线程间通信的核心机制:等待唤醒机制](#7.2 线程间通信的核心机制:等待唤醒机制)
    • [7.3 生产者 - 消费者案例](#7.3 生产者 - 消费者案例)

一、多线程概述

1.1 并发与并行

核心定义

  • 并发:指两个或多个事件在同一个时间段内发生,宏观上看似同时执行,微观上实际是分时交替执行。
  • 并行:指两个或多个事件在同一时刻发生,多个任务真正意义上的同时执行,需要多核 CPU 支持。

底层执行逻辑

  • 单核 CPU 系统:仅支持并发,CPU 通过时间片轮转的方式为多个任务分配执行时间,每个任务执行极短的时间后切换到下一个任务,由于切换速度极快,给用户 "同时执行" 的视觉感受。
  • 多核 CPU 系统:同时支持并发与并行,多个 CPU 核心可同时处理不同的任务,实现真正的并行;同时单个核心仍可通过时间片轮转处理多个任务,实现并发。

注意事项

单核处理器无法实现真正的并行,多线程在单核下本质是并发执行。

无论多核还是单核,Java 中线程的调度均由 JVM 控制,宏观上的并行不代表微观上的同时执行。

1.2 线程与进程

  • 进程(Process)

    定义:是内存中运行的应用程序,拥有独立的内存空间(堆、方法区等),是操作系统运行程序的基本单位。

    特征:一个应用程序可同时运行多个进程;进程的生命周期包含创建、运行、阻塞、消亡,系统运行一个程序即对应一个进程的完整生命周期。

    示例:打开电脑上的微信、IDEA、浏览器,每一个应用对应一个独立的进程。

  • 线程(Thread)

    定义:是进程中的一个执行单元,负责执行进程中的程序逻辑,是操作系统调度的最小单位。

    特征:一个进程中至少包含一个主线程(如 Java 程序的 main 方法对应主线程),也可创建多个子线程,拥有多线程的程序称为多线程程序;线程共享所属进程的内存空间(堆、方法区),但拥有自己独立的栈空间。

    进程与线程的核心区别

进程与线程的核心区别

对比维度 进程 线程
内存空间 拥有独立的内存空间,进程间相互隔离 共享所属进程的内存空间,线程间数据可直接交互
资源消耗 创建、销毁、切换的资源消耗大 创建、销毁、切换的资源消耗小,轻量级进程
独立性 进程间相互独立,一个进程崩溃不影响其他进程 线程间依赖进程,一个线程崩溃可能导致整个进程崩溃
调度单位 操作系统的基本运行单位 操作系统的最小调度单位

线程调度机制

JVM 采用抢占式调度(Java 默认),而非分时调度,核心规则:

优先级高的线程优先获取 CPU 执行权;

线程优先级相同时,JVM 随机选择一个线程执行(存在线程随机性);

一个线程获取 CPU 执行权后,会一直执行直到执行完毕、主动放弃、被高优先级线程抢占。

1.3 多线程的核心优势

  1. 提高CPU 的利用率:让 CPU 在等待某个任务(如 IO 操作、网络请求)时,可切换执行其他任务,避免 CPU 空闲;
  2. 提升程序的响应速度:如桌面应用中,主线程处理界面交互,子线程处理耗时操作(如文件下载、数据计算),避免界面卡死;
  3. 实现任务的并行处理:如多线程下载、多线程处理数据,提升任务处理效率。
    注意:多线程不会提高单个任务的执行速度,仅能提升多任务场景下的整体执行效率。

二、多线程的创建

Java 中所有线程对象均为java.lang.Thread类或其子类的实例,线程的执行逻辑封装在线程执行体(run()方法)中。核心创建方式有 3 种,拓展方式为线程池创建,共 4 种实现方案。

2.1 方式 1:继承 Thread 类

实现步骤

  1. 定义Thread类的子类,重写run()方法:run()方法的方法体为线程的执行逻辑,即线程执行体;
  2. 创建 Thread 子类的实例,即创建线程对象;
  3. 调用线程对象的 **start()方法 **:启动线程,JVM 会自动调用该线程的run()方法。

代码实现

java 复制代码
// 定义Thread子类,重写run方法
public class MyThread extends Thread {
    @Override
    public void run() {
        // 线程执行逻辑:打印10次HelloWorld
        for (int i = 0; i < 10; i++) {
            System.out.println("子线程:HelloWorld! " + i);
        }
    }
}

// 测试类
public class ThreadTest {
    public static void main(String[] args) {
        // 1. 创建自定义线程对象
        MyThread mt = new MyThread();
        // 2. 启动线程:必须调用start(),而非直接调用run()
        mt.start();
        // 主线程执行逻辑:打印10次main线程信息
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:执行中 " + i);
        }
    }
}

关键注意事项

  • 启动线程必须调用start()方法,而非直接调用run()方法:
    • 调用start():会创建新的线程,将线程状态从新建态转为就绪态,等待 JVM 调度后执行run();
    • 直接调用run():不会创建新线程,仅在当前线程(如主线程)中执行run()方法的代码,属于普通方法调用。
  • 一个线程对象的start()方法仅能调用一次,多次调用会抛出IllegalThreadStateException。

2.2 方式 2:实现 Runnable 接口

实现步骤

  1. 定义Runnable接口的实现类,重写run()方法:封装线程执行逻辑;
  2. 创建Runnable实现类的实例:该实例为线程任务对象,仅封装执行逻辑,并非线程对象;
  3. 创建Thread对象,将 Runnable 实例作为构造方法参数:Thread 对象才是真正的线程对象,关联线程任务;
  4. 调用 Thread 对象的start()方法,启动线程。

代码实现

java 复制代码
// 定义Runnable接口实现类,重写run方法
public class MyRunnable implements Runnable {
    @Override
    public void run() {
        // 线程执行逻辑:打印20次HelloWorld
        for (int i = 0; i < 20; i++) {
            System.out.println("子线程:HelloWorld! " + i);
        }
    }
}

// 测试类
public class RunnableTest {
    public static void main(String[] args) {
        // 1. 创建线程任务对象
        MyRunnable mr = new MyRunnable();
        // 2. 创建线程对象,关联任务对象
        Thread t = new Thread(mr);
        // 3. 启动线程
        t.start();
        // 主线程执行逻辑
        for (int i = 0; i < 20; i++) {
            System.out.println("main线程:执行中 " + i);
        }
    }
}

简化写法:匿名内部类

java 复制代码
public class RunnableAnonymousTest {
    public static void main(String[] args) {
        // 匿名内部类实现Runnable,直接创建线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++) {
                    System.out.println("匿名子线程:执行中 " + i);
                }
            }
        }).start();
        // 主线程逻辑
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程:执行中 " + i);
        }
    }
}

2.3 方式 3:实现 Callable 接口(有返回值 + 可抛异常)

核心优势

  • 相比Runnable,Callable接口的call()方法有返回值,可获取线程执行结果;
  • call()方法可抛出受检异常,无需在方法内部捕获,便于异常处理。

实现步骤

  1. 定义Callable接口的实现类,重写call()方法:V为返回值类型,方法体为线程执行逻辑;
  2. 创建Callable实现类的实例;
  3. 创建FutureTask对象,将 Callable 实例作为构造方法参数:FutureTask是RunnableFuture的实现类,既实现了Runnable(可作为 Thread 构造参数),又实现了Future(可获取返回值);
  4. 创建Thread对象,将FutureTask对象作为构造方法参数;
  5. 调用 Thread 对象的start()方法,启动线程;
  6. 调用FutureTask的 **get()方法 **:获取线程执行的返回值,该方法为阻塞方法,会等待线程执行完毕后返回结果。

代码实现

java 复制代码
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

// 定义Callable实现类,指定返回值类型为Integer
public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        // 线程执行逻辑:计算1-100的和
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum += i;
        }
        return sum; // 返回执行结果
    }
}

// 测试类
public class CallableTest {
    public static void main(String[] args) throws Exception {
        // 1. 创建Callable实例
        MyCallable mc = new MyCallable();
        // 2. 创建FutureTask对象,关联Callable
        FutureTask<Integer> ft = new FutureTask<>(mc);
        // 3. 创建Thread对象,关联FutureTask
        Thread t = new Thread(ft);
        // 4. 启动线程
        t.start();
        // 5. 获取返回值:get()为阻塞方法,等待线程执行完毕
        Integer result = ft.get();
        System.out.println("子线程执行结果:1-100的和为 " + result);
    }
}

2.4 Thread 与 Runnable 的核心区别及使用场景

本质区别

  • 继承 Thread:线程对象与线程任务耦合在一起,线程对象本身就是任务对象;
  • 实现 Runnable:线程对象与线程任务解耦,Runnable 实例封装任务,Thread 实例封装线程,一个任务可被多个线程执行。

实现 Runnable 接口的优势

  1. 避免 Java 单继承的局限性:一个类继承 Thread 后,无法再继承其他类;实现 Runnable 接口后,仍可继承其他类;
  2. 便于实现资源共享:多个线程可共享同一个 Runnable 任务对象,适合多线程操作同一资源的场景(如卖票、抢票);
  3. 提高程序健壮性:任务与线程分离,代码可被多个线程共享,便于维护和扩展;
  4. 适配线程池:线程池仅能接收Runnable或Callable类型的任务,无法直接接收继承 Thread 的类。

使用场景选择

  • 继承 Thread:适合简单的多线程场景,线程任务与线程对象一一对应,无需资源共享;
  • 实现 Runnable:适合多线程操作同一资源的场景,或需要继承其他类的场景,是实际开发中的首选方式;
  • 实现 Callable:适合需要获取线程执行结果或处理受检异常的场景。

2.5 方式 4:线程池创建

线程池是 Java 中管理线程的高级方式,可复用线程、减少资源消耗,是实际开发中最常用的多线程创建方式,后续单独讲解。也可以查看我另一篇文章Java多线程自定义线程池------线程池的七大参数和四大拒绝策略

三、Thread 类的核心 API

3.1 构造方法

Thread 类提供了多个重载的构造方法,核心常用构造方法如下:

构造方法 说明
public Thread() 创建一个无名称的线程对象
public Thread(String name) 创建一个指定名称的线程对象
public Thread(Runnable target) 创建一个关联 Runnable 任务的线程对象,使用默认名称
public Thread(Runnable target, String name) 创建一个关联 Runnable 任务且指定名称的线程对象

代码实现

java 复制代码
public class ThreadConstructorTest {
    public static void main(String[] args) {
        // 1. 无参构造
        Thread t1 = new Thread();
        // 2. 指定名称构造
        Thread t2 = new Thread("我的线程2");
        // 3. 关联Runnable任务
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println("线程任务执行");
            }
        };
        Thread t3 = new Thread(r);
        // 4. 关联Runnable任务并指定名称
        Thread t4 = new Thread(r, "任务线程4");
        
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

3.2 核心成员方法

线程名称相关方法

  • public String getName():获取当前线程的名称;
  • public void setName(String name):设置当前线程的名称;
  • 注意:主线程的默认名称为main,子线程的默认名称为Thread-0、Thread-1、Thread-2...

线程启动与执行方法

  • public void start():启动线程,将线程从新建态转为就绪态,JVM 自动调用run()方法;
  • public void run():线程执行体,封装线程的核心执行逻辑,由 JVM 调用,无需手动调用。

获取当前线程对象

  • public static Thread currentThread():静态方法,返回当前正在执行的线程对象的引用;
  • 适用场景:在任意代码位置获取当前执行的线程,如在 Runnable 的run()方法中获取线程对象。

代码演示

java 复制代码
public class ThreadMethodTest {
    public static void main(String[] args) {
        // 创建线程对象,未指定名称
        Thread t = new Thread(new Runnable() {
            @Override
            public void run() {
                // 获取当前线程对象(子线程)
                Thread current = Thread.currentThread();
                // 获取线程名称
                System.out.println("子线程原名称:" + current.getName());
                // 设置线程名称
                current.setName("业务处理线程");
                System.out.println("子线程新名称:" + current.getName());
            }
        });
        // 启动线程
        t.start();
        
        // 获取主线程对象并设置名称
        Thread mainThread = Thread.currentThread();
        mainThread.setName("主业务线程");
        System.out.println("主线程名称:" + mainThread.getName());
    }
}

start () 与 run () 方法的核心区别

对比维度 start () 方法 run () 方法
调用者 由开发人员手动调用 由 JVM 自动调用,无需手动调用
线程创建 会创建新的线程,分配独立的栈空间 不会创建新线程,仅在当前线程中执行代码
状态转换 将线程从新建态转为就绪态 将线程从就绪态转为运行态
执行次数 一个线程对象仅能调用一次, 多次调用抛异常 可被多次调用,属于普通方法
返回值 void,无返回值 void,无返回值(Callable 的 call () 有返回值)

3.3 线程调度相关方法

Thread 类提供了一系列用于线程调度的方法,用于控制线程的执行顺序、状态,核心方法如下:

方法名 修饰符 说明
public static void sleep(long millis) 静态 让当前线程休眠指定毫秒数(millis),休眠期间线程进入阻塞态,释放 CPU 执行权,不释放锁资源;休眠结束后转为就绪态,等待 JVM 调度;可抛出InterruptedException
public final void join() 非静态 等待该线程终止后,其他线程才能继续执行;调用该方法的线程进入阻塞态,直到目标线程执行完毕;可抛出InterruptedException
public final void join(long millis) 非静态 等待该线程终止,最多等待指定毫秒数,超时后不再等待
public static void yield() 静态 线程礼让,让当前正在执行的线程主动放弃 CPU 执行权,转为就绪态,与其他就绪态线程重新争夺 CPU;礼让是自愿的,JVM 可能忽略该请求
public final void setDaemon(boolean on) 非静态 将该线程标记为守护线程(on=true)或用户线程(on=false);必须在start()方法前调用;当 JVM 中所有运行的线程都是守护线程时,JVM 会自动退出
public final void setPriority(int newPriority) 非静态 设置线程的优先级,取值范围 1-10,默认 5;优先级越高,获取 CPU 的概率越大
public final int getPriority() 非静态 获取线程的优先级
public void interrupt() 非静态 中断线程,将线程的中断状态置为 true;若线程处于 sleep/join/wait 状态,会抛出InterruptedException并清除中断状态
public static boolean interrupted() 静态 判断当前线程是否被中断,会清除中断状态(连续调用两次,第二次返回 false)
public boolean isInterrupted() 非静态 判断当前线程是否被中断,不会清除中断状态

关键方法代码演示

  1. 线程休眠(sleep ())
java 复制代码
import java.util.Date;

public class ThreadSleepTest {
    public static void main(String[] args) {
        Thread t = new Thread("休眠线程") {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(getName() + ":" + i + ",当前时间:" + new Date());
                    try {
                        // 休眠1秒(1000毫秒)
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        t.start();
    }
}
  1. 线程加入(join ())
java 复制代码
public class ThreadJoinTest {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread("子线程1") {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(getName() + ":" + i);
                }
            }
        };
        t1.start();
        // 让主线程等待t1执行完毕后再执行
        t1.join();
        // 主线程逻辑
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
        }
    }
}
  1. 线程礼让(yield ())
java 复制代码
public class ThreadYieldTest {
    public static void main(String[] args) {
        Thread t1 = new Thread("礼让线程1") {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(getName() + ":" + i);
                    // 线程礼让
                    Thread.yield();
                }
            }
        };
        Thread t2 = new Thread("线程2") {
            @Override
            public void run() {
                for (int i = 0; i < 5; i++) {
                    System.out.println(getName() + ":" + i);
                }
            }
        };
        t1.start();
        t2.start();
    }
}
  1. 守护线程(setDaemon ())
java 复制代码
public class ThreadDaemonTest {
    public static void main(String[] args) {
        Thread t1 = new Thread("守护线程1") {
            @Override
            public void run() {
                // 死循环,若为守护线程,主线程结束后该线程会自动退出
                while (true) {
                    System.out.println(getName() + ":运行中");
                    try {
                        Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };
        // 设置为守护线程,必须在start()前
        t1.setDaemon(true);
        t1.start();
        
        // 主线程执行5次后结束
        for (int i = 0; i < 5; i++) {
            System.out.println("主线程:" + i);
            try {
                Thread.sleep(500);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        System.out.println("主线程执行完毕,JVM即将退出");
    }
}
  1. 线程停止
    stop():让线程停止,过时了,但是还可以使用。
    interrupt():中断线程。 把线程的状态终止,并抛出一个InterruptedException。
java 复制代码
public class ThreadStopTst {
    public static void main(String[] args) {
        // 匿名内部类实现Runnable,直接创建线程对象
        new Thread(new Runnable() {
            @Override
            public void run() {
	            System.out.println("开始执行:"+new Date());
        		try {
            		Thread.sleep(10000);//睡了10秒
        		} catch (InterruptedException e) {
            		e.printStackTrace();
        		}
        		System.out.println("结果执行:"+new Date());
            }
        }).start();

        try {
            //如果你睡3秒还不睡醒,就干掉你
            Thread.sleep(3000);
            // t1.stop();//已过时,但可以用。如果后面还有代码就不没去运行
            t1.interrupt();//判断当前线程中断,不影响后面的执行
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

四、线程的生命周期

4.1 生命周期概述

Java 中线程的生命周期包含6 种核心状态,状态之间可通过 Thread 类的方法相互转换


JVM 通过java.lang.Thread.State枚举定义了这 6 种状态:

  • NEW(新建态)
  • RUNNABLE(就绪态 / 运行态)
  • BLOCKED(阻塞态)
  • WAITING(等待态)
  • TIMED_WAITING(计时等待态)
  • TERMINATED(消亡态)

4.2 各状态核心说明

  1. NEW(新建态)
    状态描述:创建了线程对象,但未调用start()方法;
    示例:Thread t = new Thread();,此时 t 处于新建态;
    转换条件:调用start()方法,转为RUNNABLE。
  2. RUNNABLE(就绪态 / 运行态)
    状态描述:JVM 中将就绪态和运行态合并为 RUNNABLE,包含两种子状态:
    就绪态:调用start()方法后,线程等待 JVM 分配 CPU 执行权,具备执行资格,但无执行权;
    运行态:线程获取 CPU 执行权,正在执行run()方法的代码,具备执行资格和执行权;
    转换条件:
    就绪态 → 运行态:JVM 调度,分配 CPU 执行权;
    运行态 → 就绪态:线程礼让(yield ())、被高优先级线程抢占、时间片用完;
    运行态 → 其他状态:调用 sleep ()/join ()/wait ()、获取锁失败、线程执行完毕。
  3. BLOCKED(阻塞态)
    状态描述:线程因获取同步锁(synchronized)失败而进入的状态,等待其他线程释放锁;
    转换条件:
    进入:运行态线程尝试获取 synchronized 锁,锁已被其他线程持有;
    退出:其他线程释放 synchronized 锁,当前线程获取锁成功,转为RUNNABLE。
  4. WAITING(等待态)
    状态描述:线程进入无时间限制的等待,需依靠其他线程的显式唤醒才能退出等待;
    进入方式:调用无参的wait()、join()、LockSupport.park()方法;
    转换条件:
    唤醒:其他线程调用notify()/notifyAll()(对应 wait ())、目标线程执行完毕(对应 join ())、LockSupport.unpark()(对应 park ()),转为BLOCKED(需获取锁)或RUNNABLE;
    中断:其他线程调用interrupt(),抛出InterruptedException,转为RUNNABLE。
  5. TIMED_WAITING(计时等待态)
    状态描述:线程进入有时间限制的等待,到达指定时间后自动唤醒,也可被其他线程显式唤醒;
    进入方式:调用带参的sleep(long millis)、wait(long millis)、join(long millis)、LockSupport.parkNanos()、LockSupport.parkUntil();
    转换条件:
    自动唤醒:等待时间到达,转为BLOCKED或RUNNABLE;
    显式唤醒:其他线程调用notify()/notifyAll()等,转为BLOCKED或RUNNABLE;
    中断:其他线程调用interrupt(),抛出InterruptedException,转为RUNNABLE。
  6. TERMINATED(消亡态)
    状态描述:线程的run()方法执行完毕,或因异常导致线程终止;
    进入方式:
    正常终止:run()/call()方法执行完毕;
    异常终止:线程执行过程中抛出未捕获的异常,导致线程终止;
    注意:消亡态的线程无法再次启动,调用start()会抛出IllegalThreadStateException。

4.3 线程状态转换

java 复制代码
新建态(NEW) →[调用 start()]→ 就绪态(RUNNABLE)
就绪态(RUNNABLE) →[获得CPU时间片]→ 运行态(RUNNABLE)
运行态(RUNNABLE)
├─[yield()/时间片用完/高优先级抢占] → 回到 就绪态(RUNNABLE)
├─[获取 synchronized 锁失败] → 进入 阻塞态(BLOCKED)
├─[sleep(long)/wait(long)/join(long)] → 进入 计时等待(TIMED_WAITING)
├─[wait()/join()] → 进入 无限等待(WAITING)
└─[run()执行完毕/异常终止] → 进入 消亡态(TERMINATED)
阻塞态(BLOCKED) →[成功获取锁] → 回到 就绪态(RUNNABLE)
计时等待(TIMED_WAITING)
├─[时间到]
├─[被 notify()/notifyAll()]
└─[被 interrupt()]
→ 回到 就绪态/阻塞态
无限等待(WAITING)
├─[被 notify()/notifyAll()]
├─[join() 线程执行完毕]
└─[被 interrupt()]
→ 回到 就绪态/阻塞态

五、线程安全

5.1 线程安全的定义

当多个线程同时访问同一个共享资源,且至少有一个线程对该资源进行写操作时,若程序的执行结果与单线程执行结果一致,且共享资源的状态与预期一致,则称该程序是线程安全的;反之则为线程不安全。

线程不安全的本质

  • 共享资源:多个线程共同访问的变量、对象、文件等(如堆中的对象、静态变量);
  • 原子性缺失:对共享资源的写操作并非原子操作(即操作不可被分割,要么全部执行,要么全部不执行),多个线程的操作会相互干扰。

线程不安全案例:电影院卖票

电影院有 100 张票,3 个售票窗口(3 个线程)同时卖票,模拟卖票过程,观察线程安全问题。

代码实现(线程不安全版)

java 复制代码
// 卖票任务类:实现Runnable,共享票源
public class TicketRunnable implements Runnable {
    // 共享资源:100张票,多个线程共享同一个对象的该变量
    private int ticket = 100;

    @Override
    public void run() {
        // 死循环,持续卖票
        while (true) {
            // 判断票是否存在
            if (ticket > 0) {
                try {
                    // 休眠10毫秒,放大线程安全问题(模拟售票耗时)
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 卖票:打印当前售票窗口和票号,票号减1
                System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
                ticket--;
            }
        }
    }
}

// 测试类:创建3个线程模拟3个售票窗口
public class TicketTest {
    public static void main(String[] args) {
        // 创建共享的卖票任务对象
        TicketRunnable tr = new TicketRunnable();
        // 创建3个线程,关联同一个任务对象
        Thread t1 = new Thread(tr, "窗口1");
        Thread t2 = new Thread(tr, "窗口2");
        Thread t3 = new Thread(tr, "窗口3");
        // 启动线程
        t1.start();
        t2.start();
        t3.start();
    }
}

运行结果问题分析

运行代码后,会出现以下线程不安全现象:

  1. 重复卖票:同一张票被多个窗口卖出(如窗口 1 和窗口 2 同时卖出第 1 张票);
  2. 卖出负票:票号变为 0 或负数后,仍有窗口卖票(如窗口 3 卖出第 - 1 张票)。

问题原因

对共享资源ticket的操作并非原子操作,if (ticket > 0)和ticket--是两个独立的步骤,多个线程在执行过程中会相互干扰:

例如:线程 1 执行if (ticket > 0)后,ticket=1,此时 CPU 被线程 2 抢占;

  • 线程 2 也执行if (ticket > 0),ticket 仍 = 1,然后线程 2 休眠,CPU 被线程 1 抢占;
  • 线程 1 继续执行,卖出第 1 张票,ticket=0;
  • 线程 2 休眠结束,继续执行,卖出第 1 张票,ticket=-1,出现重复卖票和负票问题。

5.2 线程安全问题的产生条件与解决策略

线程安全问题必须同时满足以下 3 个条件,缺一不可:

  1. 多线程环境:存在两个或以上的线程同时执行;
  2. 共享资源:多个线程访问同一个共享资源(如全局变量、静态变量、堆对象);
  3. 写操作:至少有一个线程对共享资源进行写操作(修改、删除、新增),若仅为读操作,则不会出现线程安全问题。

线程安全的解决策略:同步机制

解决线程安全问题的核心思路是保证对共享资源的操作是原子操作,让多个线程串行执行对共享资源的写操作,避免相互干扰。Java 中提供了 3 种核心的同步机制:

  • 同步代码块(synchronized);
  • 同步方法(synchronized);
  • Lock 锁(java.util.concurrent.locks.Lock)。

核心原理

通过加锁的方式,让多个线程在访问共享资源时,必须先获取锁,只有获取锁的线程才能执行操作,其他线程需等待锁释放后再争夺,从而保证操作的原子性。

5.3 同步代码块

语法格式

java 复制代码
synchronized(同步锁对象) {
    // 需保证线程安全的代码(对共享资源的写操作)
    // 称为同步代码块
}

同步锁对象的要求

  1. 锁对象必须是引用类型:可以是 Object、自定义对象、类的 Class 对象等,不能是基本数据类型(int、long、boolean 等);
  2. 多个线程必须使用同一个锁对象:若多个线程的锁对象不同,则无法实现同步,仍会出现线程安全问题;
  3. 锁对象的选择:
    • 非静态方法 / 代码块:可使用this(当前对象)或自定义的共享对象;
    • 静态方法 / 代码块:必须使用类的 Class 对象(如TicketRunnable.class),因为静态方法属于类,不依赖对象。

代码实现(同步代码块版,解决卖票问题)

java 复制代码
public class TicketRunnable implements Runnable {
    private int ticket = 100;
    // 定义共享的锁对象:多个线程使用同一个对象
    private Object lock = new Object();

    @Override
    public void run() {
        while (true) {
            // 同步代码块:锁对象为lock
            synchronized (lock) {
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
                    ticket--;
                }
            }
        }
    }
}

// 测试类不变,同6.2

原理说明

  • 当线程 1 进入同步代码块时,会获取lock对象的锁,其他线程(线程 2、线程 3)尝试进入同步代码块时,因获取不到锁而进入BLOCKED(阻塞态);
  • 线程 1 执行完同步代码块后,会自动释放锁,此时线程 2、线程 3 争夺锁,获取锁的线程进入执行,其余线程继续阻塞;
  • 保证了对ticket的操作(if 判断 + ticket--)是原子操作,避免了多线程干扰。

5.4 同步方法

语法格式

将synchronized关键字修饰在方法上,该方法即为同步方法,整个方法体的代码均为同步代码,保证原子性。

java 复制代码
// 非静态同步方法
public synchronized 返回值类型 方法名(参数列表) {
    // 需保证线程安全的代码
}

// 静态同步方法
public static synchronized 返回值类型 方法名(参数列表) {
    // 需保证线程安全的代码
}

同步方法的锁对象

同步方法的锁对象由 JVM 自动指定,无需手动定义,规则如下:

  1. 非静态同步方法:锁对象为this(当前类的实例对象);
  2. 静态同步方法:锁对象为当前类的 Class 对象(如TicketRunnable.class),因为静态方法属于类,不依赖实例对象。

代码实现(同步方法版,解决卖票问题)

方式 1:非静态同步方法

java 复制代码
public class TicketRunnable implements Runnable {
    private int ticket = 100;

    @Override
    public void run() {
        while (true) {
            // 调用同步方法
            sellTicket();
        }
    }

    // 非静态同步方法:锁对象为this
    private synchronized void sellTicket() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

方式 2:静态同步方法(票源为静态变量时)

java 复制代码
public class TicketRunnable implements Runnable {
    // 静态共享资源:属于类,所有实例共享
    private static int ticket = 100;

    @Override
    public void run() {
        while (true) {
            sellTicket();
        }
    }

    // 静态同步方法:锁对象为TicketRunnable.class
    private static synchronized void sellTicket() {
        if (ticket > 0) {
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
            ticket--;
        }
    }
}

同步代码块与同步方法的区别

对比维度 同步代码块 同步方法
锁对象 手动指定,灵活多样 JVM 自动指定,非静态为 this,静态为 Class 对象
同步范围 可指定任意代码段,范围更小,效率更高 整个方法体,范围更大,效率相对较低
灵活性 高,可针对不同代码段使用不同锁对象 低,整个方法使用同一个锁对象
适用场景 仅需对方法中部分代码做同步时 需对整个方法的代码做同步时

5.5 Lock 锁(显式锁)

核心介绍

  • java.util.concurrent.locks.Lock是 Java 提供的显式锁接口,相比synchronized(隐式锁,自动加锁和释放),Lock 锁需要手动加锁和手动释放,灵活性更高;
  • 核心实现类:java.util.concurrent.locks.ReentrantLock(可重入锁),支持公平锁和非公平锁,默认非公平锁;
  • 核心优势:支持尝试获取锁、可中断获取锁、超时获取锁,解决了synchronized锁无法释放的问题。

Lock 接口的核心方法

方法名 说明
void lock() 获取锁:若锁已被持有,则阻塞,直到获取锁
boolean tryLock() 尝试获取锁:成功返回 true,失败返回 false,不阻塞
boolean tryLock(long time, TimeUnit unit) 超时尝试获取锁:在指定时间内尝试获取锁,成功返回 true,超时返回 false
void unlock() 释放锁:必须手动调用,建议在finally块中执行,保证锁一定被释放
Condition newCondition() 创建条件对象,用于线程间通信(替代 wait ()/notify ())

实现步骤

  1. 在成员位置创建ReentrantLock对象:保证多个线程共享同一个锁对象;
  2. 在需要同步的代码前调用lock()方法,手动加锁;
  3. 在需要同步的代码后调用unlock()方法,手动释放锁;
  4. 建议将unlock()方法放在finally块中,避免因异常导致锁未释放,造成死锁。

代码实现(Lock 锁版,解决卖票问题)

java 复制代码
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class TicketRunnable implements Runnable {
    private int ticket = 100;
    // 1. 成员位置创建ReentrantLock对象,默认非公平锁
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            // 2. 手动加锁
            lock.lock();
            try {
                // 需同步的代码
                if (ticket > 0) {
                    try {
                        Thread.sleep(10);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println(Thread.currentThread().getName() + ":正在卖第" + ticket + "张票");
                    ticket--;
                }
            } finally {
                // 3. 手动释放锁,放在finally中,保证锁一定被释放
                lock.unlock();
            }
        }
    }
}

synchronized 与 Lock 锁的核心区别

对比维度 synchronized(隐式锁) Lock(显式锁,ReentrantLock)
锁的性质 隐式锁,自动加锁、自动释放(代码块 / 方法执行完毕或异常时释放) 显式锁,手动加锁(lock ())、手动释放(unlock ()),需在 finally 中释放
灵活性 低,仅支持阻塞式获取锁 高,支持尝试获取锁(tryLock ())、超时获取锁、可中断获取锁
锁的类型 非公平锁(默认),不支持公平锁 非公平锁(默认),可通过构造方法指定为公平锁(new ReentrantLock (true))
线程间通信 通过 Object 类的 wait ()/notify ()/notifyAll () 通过 Condition 对象的 await ()/signal ()/signalAll (),支持多条件通信
可重入性 支持可重入(同一线程可多次获取同一把锁) 支持可重入
异常处理 异常时自动释放锁,不会造成死锁 异常时若未手动释放锁,会造成死锁,需在 finally 中释放
性能 低并发下性能较好,高并发下性能较差 高并发下性能优于 synchronized

六、死锁

6.1 死锁的定义

死锁是指两个或多个线程在执行过程中,因相互持有对方所需要的锁资源,而导致所有线程都处于等待状态,无法继续执行,且这种等待永远无法自行解除的现象。

死锁的核心特征

  1. 多个线程相互持有对方需要的锁;
  2. 所有线程都处于阻塞 / 等待状态,无法释放自己持有的锁;
  3. 无外部干预的情况下,线程永远无法继续执行。

6.2 死锁的产生条件(四大必要条件)

死锁的产生必须同时满足以下4 个必要条件,缺一不可,只要破坏其中任意一个条件,死锁即可避免:

  1. 互斥条件:锁资源为独占资源,同一时间仅能被一个线程持有,其他线程无法获取;
  2. 请求并持有条件:一个线程已经持有一个锁资源,又尝试请求获取另一个锁资源;
  3. 不可剥夺条件:线程持有的锁资源无法被其他线程强制剥夺,仅能由持有线程主动释放;
  4. 循环等待条件:多个线程形成一个循环等待链,每个线程都在等待链中下一个线程持有的锁资源。

6.3 死锁的代码演示

场景设计

创建两个锁对象LockA和LockB,创建一个线程任务类,让线程在执行时:

  • 当i%2==0时,先获取LockA,再尝试获取LockB;
  • 当i%2!=0时,先获取LockB,再尝试获取LockA;
  • 两个线程同时执行该任务,会形成循环等待,产生死锁。

代码实现

java 复制代码
// 定义锁对象A:单例,保证全局唯一
class LockA {
    private LockA() {}
    public static final LockA lockA = new LockA();
}

// 定义锁对象B:单例,保证全局唯一
class LockB {
    private LockB() {}
    public static final LockB lockB = new LockB();
}

// 死锁任务类:实现Runnable
class DeadLockRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                // 线程1:先获取lockA,再尝试获取lockB
                synchronized (LockA.lockA) {
                    System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
                    synchronized (LockB.lockB) {
                        System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
                    }
                }
            } else {
                // 线程2:先获取lockB,再尝试获取lockA
                synchronized (LockB.lockB) {
                    System.out.println(Thread.currentThread().getName() + ":获取了LockB,尝试获取LockA");
                    synchronized (LockA.lockA) {
                        System.out.println(Thread.currentThread().getName() + ":获取了LockA,执行完毕");
                    }
                }
            }
            i++;
        }
    }
}

// 测试类:创建两个线程,模拟死锁
public class DeadLockTest {
    public static void main(String[] args) {
        DeadLockRunnable dlr = new DeadLockRunnable();
        Thread t1 = new Thread(dlr, "线程1");
        Thread t2 = new Thread(dlr, "线程2");
        t1.start();
        t2.start();
    }
}

运行结果分析

运行代码后,控制台会输出以下内容,然后程序卡死,不再执行:

bash 复制代码
线程1:获取了LockA,尝试获取LockB
线程2:获取了LockB,尝试获取LockA
  • 线程 1 持有LockA,等待获取LockB;
  • 线程 2 持有LockB,等待获取LockA;
  • 两个线程相互持有对方需要的锁,形成循环等待,满足死锁的四大必要条件,产生死锁。

6.4 死锁的解决策略

解决死锁的核心思路是破坏死锁的四大必要条件中的任意一个,实际开发中最常用、最简单的方式是破坏循环等待条件,其次是破坏请求并持有条件。

策略 1:破坏循环等待条件(最常用)

统一锁的获取顺序:让所有线程在获取多个锁时,按照固定的顺序获取,避免形成循环等待链。

java 复制代码
// 锁对象A、LockB不变
class DeadLockRunnable implements Runnable {
    private int i = 0;

    @Override
    public void run() {
        while (true) {
            if (i % 2 == 0) {
                // 统一顺序:先获取LockA,再获取LockB
                synchronized (LockA.lockA) {
                    System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
                    synchronized (LockB.lockB) {
                        System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
                    }
                }
            } else {
                // 统一顺序:先获取LockA,再获取LockB(不再先获取LockB)
                synchronized (LockA.lockA) {
                    System.out.println(Thread.currentThread().getName() + ":获取了LockA,尝试获取LockB");
                    synchronized (LockB.lockB) {
                        System.out.println(Thread.currentThread().getName() + ":获取了LockB,执行完毕");
                    }
                }
            }
            i++;
        }
    }
}

原理:所有线程都先获取LockA,再获取LockB,不会形成循环等待,破坏了循环等待条件,避免死锁。

策略 2:破坏请求并持有条件

一次性获取所有需要的锁:让线程在执行前,一次性获取所有需要的锁资源,若无法获取全部锁,则放弃已获取的锁,重新尝试,避免持有部分锁并请求另一部分锁。

  • 实现方式:可通过Lock锁的tryLock()方法实现,尝试获取所有锁,若有一个获取失败,则释放已获取的锁。

策略 3:破坏不可剥夺条件

使用可中断的锁获取方式:让线程在获取锁的过程中,可被其他线程中断,若线程等待锁的时间过长,则中断线程,释放其持有的锁资源。

  • 实现方式:Lock锁的lockInterruptibly()方法,支持可中断的锁获取。

策略 4:破坏互斥条件

使用共享锁替代独占锁:将独占的锁资源改为共享资源,允许多个线程同时持有锁,避免互斥。

  • 适用场景:仅适用于对共享资源的读操作,若有写操作,则无法使用,因此实际开发中使用较少。

6.5 死锁的排查与避免

死锁的排查方式

  1. jps 命令:查看当前运行的 Java 进程的 PID;
  2. jstack PID 命令:查看该进程的线程堆栈信息,若存在死锁,会在堆栈信息中明确标注Deadlock,并显示死锁的线程和锁资源;
  3. IDE 工具:使用 IDEA/Eclipse 的 JVM 监控工具,查看线程状态,识别死锁线程。

死锁的避免原则

  1. 尽量减少锁的嵌套:避免在同步代码块 / 同步方法中嵌套另一个同步代码块 / 同步方法;
  2. 统一锁的获取顺序:多个线程获取多个锁时,按照固定的顺序获取;
  3. 控制锁的持有时间:尽量缩短线程持有锁的时间,执行完同步代码后立即释放锁;
  4. 使用显式锁(Lock) 替代 synchronized:Lock 锁支持尝试获取锁、超时获取锁,可有效避免死锁;
  5. 避免无限期等待锁:使用tryLock()设置超时时间,若超时则放弃获取锁,释放已持有的锁。

七、线程间通信

7.1 线程间通信的定义

线程间通信是指多个线程在执行过程中,通过一定的机制相互协调、相互配合,共同完成一个任务,实现对共享资源的有序访问,避免资源争夺,提高程序的执行效率。

线程间通信的核心场景

当多个线程处理同一个共享资源,但任务不同时,需要通过通信让线程有序执行,例如:

  • 生产者线程生产数据,消费者线程消费数据(生产者 - 消费者模型);
  • 线程 A 写入数据,线程 B 读取数据,保证先写后读。

7.2 线程间通信的核心机制:等待唤醒机制

等待唤醒机制是 Java 中线程间通信的核心机制,通过 Object 类的一组方法实现,让线程在满足特定条件时进入等待状态,在条件满足时被其他线程唤醒,实现有序执行。

核心方法(Object 类的方法,必须在同步代码块 / 同步方法中使用

方法名 说明
public final void wait() 让当前线程进入WAITING(无计时等待态),释放持有的锁资源,需其他线程调用notify()/notifyAll()唤醒
public final void wait(long timeout) 让当前线程进入TIMED_WAITING(计时等待态),释放持有的锁资源,超时自动唤醒,也可被显式唤醒
public final void notify() 唤醒在此对象监视器上等待的单个线程,被唤醒的线程需重新争夺锁资源
public final void notifyAll() 唤醒在此对象监视器上等待的所有线程,被唤醒的线程需重新争夺锁资源

方法使用的核心注意事项

  1. 必须在同步代码块 / 同步方法中使用:这些方法的调用需要依赖对象的监视器(锁),若在非同步环境中使用,会抛出IllegalMonitorStateException;
  2. 必须由同一个锁对象调用:等待的线程和唤醒的线程必须使用同一个锁对象调用 wait ()/notify ()/notifyAll (),否则无法唤醒;
  3. wait () 方法会释放锁资源:与 sleep () 不同,wait () 在等待时会释放持有的锁,让其他线程可以获取锁;sleep () 在休眠时不会释放锁;
  4. 被唤醒的线程需重新争夺锁:notify ()/notifyAll () 仅唤醒线程,不会直接让线程执行,被唤醒的线程会进入BLOCKED 态,等待争夺锁资源,获取锁后才能继续执行。

7.3 生产者 - 消费者案例

实现一个简单的生产者 - 消费者模型:

  1. 包子铺(生产者线程):生产包子,包子有state状态(true:有包子,false:无包子);
  2. 吃货(消费者线程):消费包子,若有包子则吃,若无包子则等待;
  3. 核心规则:
    • 无包子时,消费者线程等待,生产者线程生产包子,生产完成后唤醒消费者;
    • 有包子时,生产者线程等待,消费者线程消费包子,消费完成后唤醒生产者。
java 复制代码
// 包子类:共享资源,包含状态和名称
class BaoZi {
    // 包子状态:true-有包子,false-无包子
    public boolean state = false;
    // 包子名称
    public String name;
}

// 包子铺(生产者线程):实现Runnable
class BaoZiPu implements Runnable {
    private BaoZi bz; // 共享的包子对象

    public BaoZiPu(BaoZi bz) {
        this.bz = bz;
    }

    @Override
    public void run() {
        int count = 0; // 包子计数,模拟不同包子
        while (true) {
            // 同步代码块:锁对象为包子对象(共享资源)
            synchronized (bz) {
                // 有包子时,生产者等待
                if (bz.state) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 无包子时,生产包子
                if (count % 2 == 0) {
                    bz.name = "猪肉大葱包";
                } else {
                    bz.name = "韭菜鸡蛋包";
                }
                count++;
                System.out.println("包子铺:生产了" + bz.name);
                // 修改包子状态为有
                bz.state = true;
                // 唤醒消费者线程
                bz.notify();
            }
        }
    }
}

// 吃货(消费者线程):实现Runnable
class ChiHuo implements Runnable {
    private BaoZi bz; // 共享的包子对象

    public ChiHuo(BaoZi bz) {
        this.bz = bz;
    }

    @Override
    public void run() {
        while (true) {
            // 同步代码块:锁对象为包子对象(与生产者一致)
            synchronized (bz) {
                // 无包子时,消费者等待
                if (!bz.state) {
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 有包子时,消费包子
                System.out.println("吃货:吃了" + bz.name);
                // 修改包子状态为无
                bz.state = false;
                // 唤醒生产者线程
                bz.notify();
            }
        }
    }
}

// 测试类:创建生产者和消费者线程
public class ProducerConsumerTest {
    public static void main(String[] args) {
        // 创建共享的包子对象
        BaoZi bz = new BaoZi();
        // 创建生产者和消费者线程
        Thread pu = new Thread(new BaoZiPu(bz), "包子铺");
        Thread huo = new Thread(new ChiHuo(bz), "吃货");
        // 启动线程
        pu.start();
        huo.start();
    }
}

运行结果(有序执行)

bash 复制代码
包子铺:生产了猪肉大葱包
吃货:吃了猪肉大葱包
包子铺:生产了韭菜鸡蛋包
吃货:吃了韭菜鸡蛋包
包子铺:生产了猪肉大葱包
吃货:吃了猪肉大葱包
...

原理说明

  1. 初始时包子状态为false(无包子),消费者线程进入同步代码块后,调用bz.wait()进入等待态,释放锁;
  2. 生产者线程获取锁,生产包子,修改状态为true,调用bz.notify()唤醒消费者线程,然后继续循环,因状态为true,调用bz.wait()进入等待态,释放锁;
  3. 消费者线程被唤醒,争夺锁成功后,消费包子,修改状态为false,调用bz.notify()唤醒生产者线程,然后继续循环,因状态为false,调用bz.wait()进入等待态,释放锁;
  4. 如此循环,实现生产者和消费者的有序执行,完成线程间通信。

wait () 与 sleep () 的核心区别

wait () 和 sleep () 均能让线程进入等待状态,但二者存在本质区别,是面试高频考点,核心区别如下:

对比维度 wait() sleep()
所属类 Object 类的方法,所有对象均可调用 Thread 类的静态方法,仅能通过 Thread 调用
锁资源 等待时释放锁资源,让其他线程可获取锁 休眠时不释放锁资源,其他线程无法获取锁
使用环境 必须在同步代码块 / 同步方法中使用 可在任意环境中使用,无同步要求
唤醒方式 需其他线程调用notify()/notifyAll()唤醒,或中断; 无参 wait () 需显式唤醒 到达指定时间后自动唤醒,或被中断
异常处理 必须捕获InterruptedException,或抛出 必须捕获InterruptedException,或抛出
状态转换 运行态 → WAITING/TIMED_WAITING,释放锁 运行态 → TIMED_WAITING,不释放锁
用途 用于线程间通信,协调线程执行顺序 用于线程休眠,暂停执行指定时间
相关推荐
小胖java2 小时前
高校培养方案制定系统
java·spring
小陈工2 小时前
2026年3月28日技术资讯洞察:5G-A边缘计算落地、低延迟AI推理革命与工业智造新范式
开发语言·人工智能·后端·python·5g·安全·边缘计算
常利兵2 小时前
Spring项目新姿势:Lambda封装Service调用,告别繁琐注入!
java·数据库·spring
第二只羽毛2 小时前
C++ 高并发内存池1
大数据·开发语言·c++·开源
不想看见4042 小时前
C++/Qt 实习岗位深度解析【结合一次研发实习谈感受】
开发语言·c++·qt
智算菩萨2 小时前
【OpenGL】10 完整游戏开发实战:基于OpenGL的2D/3D游戏框架、物理引擎集成与AI辅助编程指南
人工智能·python·游戏·3d·矩阵·pygame·opengl
sjmaysee3 小时前
Java框架SpringBoot(一)
java·开发语言·spring boot
寒秋花开曾相惜3 小时前
(学习笔记)3.8 指针运算(3.8.3 嵌套的数组& 3.8.4 定长数组)
java·开发语言·笔记·学习·算法
想唱rap3 小时前
Linux线程
java·linux·运维·服务器·开发语言·mysql