JUC并发编程02 - 线程方法(01)

笔记依旧是有参考:JavaNote/Prog.md at main · Seazean/JavaNote

线程方法

API

Thread 类 API

方法说明
public void start()启动一个新线程,Java虚拟机调用此线程的 run 方法
public void run()线程启动后调用该方法
public void setName(String name)给当前线程取名字
public void getName()获取当前线程的名字
线程存在默认名称:子线程是 Thread-索引,主线程是 main
public static Thread currentThread()获取当前线程对象,代码在哪个线程中执行
public static void sleep(long time)让当前线程休眠多少毫秒再继续执行
Thread.sleep(0) : 让操作系统立刻重新进行一次 CPU 竞争
public static native void yield()提示线程调度器让出当前线程对 CPU 的使用
public final int getPriority()返回此线程的优先级
public final void setPriority(int priority)更改此线程的优先级,常用 1 5 10
public void interrupt()中断这个线程,异常处理机制
public static boolean interrupted()判断当前线程是否被打断,清除打断标记
public boolean isInterrupted()判断当前线程是否被打断,不清除打断标记
public final void join()等待这个线程结束
public final void join(long millis)等待这个线程死亡 millis 毫秒,0 意味着永远等待
public final native boolean isAlive()线程是否存活(还没有运行完毕)
public final void setDaemon(boolean on)将此线程标记为守护线程或用户线程

1. CPU 竞争是什么?

CPU 就像一个“超级服务员”,它一次只能服务一个“顾客”(线程)。但系统里有成百上千个线程都想让 CPU 服务自己。所以这些线程就得“抢”这个服务员——这就是 CPU 竞争。当你调用 Thread.sleep(0)Thread.yield(),你是在说:“我先不忙了,让别人也来抢一下服务员吧!

sleep(0):虽然睡0秒,但会立刻触发一次“重新排队抢CPU”的机会。
yield():是礼貌地“让贤”:“我先歇会儿,你们谁想上就上。”

根据以上内容可以写一个demo:

public class CPUSleepZeroDemo {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("小弟我在干活:" + i);
                if (i == 2) {
                    System.out.println(">> 我让出CPU,大家快来抢!");
                    try {
                        Thread.sleep(0); // 主动触发一次CPU竞争
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();

        // 主线程也打印点东西,看谁抢到CPU
        for (int i = 0; i < 5; i++) {
            System.out.println("   主线程在打印:" + i);
        }
    }
}

代码输出如下:

2. 打断标记是什么?有什么用?

每个线程都有一个“小红点”(打断标记),表示“有人想让你停下来”。

  • 调用 thread.interrupt() → 给那个线程贴上“小红点”(打断标记设为 true)
  • 线程自己可以通过 isInterrupted() 查看有没有小红点
  • 如果发现有小红点,就可以优雅地收工:“哦,领导让我下班,那我收拾东西走人。”

注意:贴小红点 ≠ 强制停止线程!只是“通知”你该停了。

根据以上内容可以写一个demo:

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                System.out.println("程序员正在写第 " + i + " 行代码...");

                // 每写一行都检查一下:老板喊我下班了吗?
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("收到下班通知!保存工作,准备走人...");
                    return; // 优雅退出
                }

                try {
                    Thread.sleep(100); // 写代码累了休息一下
                } catch (InterruptedException e) {
                    // 注意!sleep时被interrupt会抛异常,同时清除标记
                    System.out.println("休息时被通知下班了!");
                    return; // 直接退出
                }
            }
        });

        worker.start();

        Thread.sleep(500); // 让他干一会儿活
        worker.interrupt(); // 老板喊:下班了!
    }
}
  • isInterrupted():看有没有被打断(不擦掉小红点)
  • interrupted():看有没有被打断(会擦掉小红点
  • sleep() 中被打断会抛异常,并清除标记

输出的结果如下:

3. 守护线程与用户线程是什么?

大白话解释:

  • 用户线程:正经员工,公司靠他们干活。只要还有一个正经员工在上班,公司就不能关门。
  • 守护线程:保安、保洁。他们只在有人上班时才工作。一旦所有人都走了,他们也自动下班。

对应到Java:

  • 主线程、你自己创建的线程默认是用户线程
  • 守护线程(比如垃圾回收线程):当所有用户线程结束,JVM 就会关闭,不管守护线程干到哪了。

又可以来写一个demo来演示:

public class DaemonDemo {
    public static void main(String[] args) {
        Thread userThread = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("我是正式员工,还在工作:" + i);
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            System.out.println(">> 正式员工下班了!");
        });

        Thread daemonThread = new Thread(() -> {
            while (true) {
                System.out.println("我是保安,正在巡逻...");
                try {
                    Thread.sleep(300);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        });

        // 必须在 start() 之前设置为守护线程
        daemonThread.setDaemon(true);

        userThread.start();
        daemonThread.start();

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

输出结果如下:

概念大白话关键方法生活比喻
CPU 竞争多个线程抢CPU执行权sleep(0)yield()奶茶店排队,我让别人先买
打断标记别人想让我停下来interrupt()isInterrupted()老板说“可以下班了”
守护线程服务用户线程的“辅助人员”setDaemon(true)保安:人走灯灭,立刻下班
run与start
  • start():是“按下启动键”,系统会开一个新线程去执行 run() 里的代码。()
  • run():是“直接干活”,但它只是普通方法调用,不会开新线程,就在当前线程里一步一步执行。

举个做饭的例子吧。

想象你是一个人(主线程),要做三件事:

  1. 煮饭(需要10分钟)
  2. 炒菜(需要5分钟)
  3. 洗碗(需要3分钟)

效率不高的做法:直接调用 run()(顺序执行)

你一个人干完一件再干下一件:

class CookRice implements Runnable {
    public void run() {
        System.out.println("开始煮饭...");
        try {
            Thread.sleep(10000); // 模拟煮饭10秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("煮饭完成!");
    }
}

class StirFry implements Runnable {
    public void run() {
        System.out.println("开始炒菜...");
        try {
            Thread.sleep(5000); // 模拟炒菜5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("炒菜完成!");
    }
}

public class RunMain {
    public static void main(String[] args) {
        CookRice cookRice = new CookRice();
        StirFry stirFry = new StirFry();

        System.out.println("主线程开始");

        // 直接调用 run() —— 顺序执行,效率低
        cookRice.run(); // 先煮饭,等10秒
        stirFry.run();  // 再炒菜,等5秒

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

代码输出如下:

效率高的做法:使用 start()(并发执行)

使用 start 是启动新的线程,此线程处于就绪(可运行)状态,通过新的线程间接执行 run 中的代码。说明:线程控制资源类。相当于请了一个帮手(新线程)来煮饭,你自己炒菜:

public class StartMain {
    public static void main(String[] args) {
        CookRice cookRice = new CookRice();
        StirFry stirFry = new StirFry();

        Thread riceThread = new Thread(cookRice); // 把煮饭任务交给新线程
        Thread stirFryThread = new Thread(stirFry); // 把炒菜任务交给另一个线程

        System.out.println("主线程开始");

        // 使用 start() —— 启动新线程并发执行
        riceThread.start();    // 启动煮饭线程(帮手开始干活)
        stirFryThread.start(); // 启动炒菜线程(你自己开始炒菜)

        // 主线程可以干别的,比如等他们做完
        try {
            riceThread.join(); // 等煮饭完成
            stirFryThread.join(); // 等炒菜完成
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

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

这时的代码输出如下:

run() 方法中的异常不能抛出,只能 try/catch

  • 因为父类中没有抛出任何异常,子类不能比父类抛出更多的异常
  • 异常不能跨线程传播回 main() 中,因此必须在本地进行处理

更进一步解释:

run() 方法来自 Runnable 接口,它的定义是:

public void run();

没有 throws Exception。所以你在重写 run() 时,不能抛出受检异常。

public void run() throws IOException { // 编译报错!
    FileReader file = new FileReader("xxx.txt"); // 可能抛 IOException
}

这是错误的写法。

public void run() {
    try {
        FileReader file = new FileReader("xxx.txt");
        // 读文件...
    } catch (IOException e) {
        System.out.println("文件读取失败:" + e.getMessage());
        // 在这里处理异常,不能往外抛
    }
}

这是正确的写法。为什么?因为新线程是独立的,它的异常传不回主线程。就像你请的帮手在厨房炸锅了,你不处理,没人知道。所以必须在线程内部 try/catch

sleep与yield

Thread.sleep() —— “我先睡会儿,别叫醒我,除非出大事!”

  • 调用 sleep 会让当前线程从 Running 进入 Timed Waiting 状态(阻塞,就是在睡觉)
  • sleep() 方法的过程中,线程不会释放对象锁(睡觉期间抱着一个资源睡觉,别人拿不走)
  • 其它线程可以使用 interrupt 方法打断正在睡眠的线程,这时 sleep 方法会抛出 InterruptedException(如果有人强行叫醒你(interrupt()):你会惊醒,并抛出 InterruptedException
  • 睡眠结束后的线程未必会立刻得到执行,需要抢占 CPU

可以根据以上写一个demo:

public class SleepDemo {
    private static final Object lock = new Object();

    public static void main(String[] args) throws InterruptedException {
        Thread coder = new Thread(() -> {
            synchronized (lock) {
                System.out.println("(coder) 我拿到文档,开始写代码...");

                try {
                    System.out.println("(coder) 我要睡5秒...");
                    Thread.sleep(5000); // 睡5秒
                } catch (InterruptedException e) {
                    System.out.println("(coder) 被叫醒了!出大事了!");
                    return;
                }

                System.out.println("(coder) 睡醒了,继续写代码...");
            }
        });

        coder.start();

        Thread.sleep(2000); // 2秒后
        new Thread(() -> {
            synchronized (lock) {
                System.out.println("(tester) 我想测试,但文档被占着,只能等...");
            }
        }).start();

        Thread.sleep(1000);
        System.out.println(">> 主线程决定打断他!");
        coder.interrupt(); // 强行叫醒
    }
}

输出结果如下:

  • 建议用 TimeUnit 的 sleep 代替 Thread 的 sleep 来获得更好的可读性
// 不推荐
Thread.sleep(3600000); // 360万毫秒?这是几小时?

// 推荐
TimeUnit.HOURS.sleep(1);           // 睡1小时
TimeUnit.MINUTES.sleep(30);        // 睡30分钟
TimeUnit.SECONDS.sleep(5);         // 睡5秒

例子依旧可以见上。

Thread.yield() —— “我先让一下,你们谁想上就上!”

  • 调用 yield():当前线程主动放弃CPU,回到“就绪状态”(调用 yield 会让提示线程调度器让出当前线程对 CPU 的使用)
  • 是否让出成功?不保证! 完全看操作系统心情(具体的实现依赖于操作系统的任务调度器)
  • 会放弃 CPU 资源,锁资源不会释放

也可以写一个简单的demo:

public class YieldDemo {
    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("T1: 唱歌第 " + i + " 句");
                if (i == 2) {
                    System.out.println("T1: 我让一下,你们谁想唱?");
                    Thread.yield(); // 让一下
                }
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                System.out.println("    T2: 我来唱!第 " + i + " 句");
            }
        });

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

输出结果每次可能不相同。

线程t2先的输出:

线程t1先的输出:

这就说明yield() 只是建议,不保证一定让出成功。

实际开发中怎么选?

首先对两者进行对比:

特性sleep()yield()
是否阻塞是(进入 Timed Waiting)否(进入就绪状态)
是否释放锁不释放不释放
是否准时醒来是(时间到就醒)——
是否可被打断可被 interrupt 打断不会抛异常
是否让出CPU 一定会让出一段时间不一定成功(看系统)
下一秒能否继续执行需重新抢CPU可能立刻又抢到
推荐使用 TimeUnit.SECONDS.sleep(5) 很少用,效果不确定
  • 用 sleep(1) 或 sleep(0) 来让出CPU(比 yield() 更可靠)
  • 避免单独使用 yield(),因为效果不保证
  • 需要“暂停执行”就用 sleep
  • 需要“优雅等待”可以用 sleep(0) 触发一次调度竞争
// 让当前线程退出CPU,强制重新排队
// 相当于“我先下台,让别人也有机会上”
Thread.sleep(0); 

这比 yield() 更有效,因为 sleep(0) 一定会触发调度器重新决策。

join

t1.join() 的意思就是:“我(调用者线程)要等你(t1线程)干完活,我才继续往下走。”

就像你约朋友吃饭,你说:“你到了餐厅我再点菜。” —— 你就在那儿等着,直到朋友到。

再举一个做蛋糕的生活例子。假设你要做一个蛋糕,流程是:

  1. 烤蛋糕(需要5分钟)—— 由帮手 A 负责
  2. 装饰蛋糕(需要2分钟)—— 你自己来做

但你不能先装饰,必须等蛋糕烤好了才能开始装饰。

不用 join():你不管帮手,直接开始装饰(出错!)

public class CakeMaker {
    static String cake = "生面团";

    public static void main(String[] args) {
        Thread bakeThread = new Thread(() -> {
            System.out.println("帮手A:开始烤蛋糕...");
            try {
                Thread.sleep(5000); // 模拟烤5秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cake = "熟蛋糕";
            System.out.println("帮手A:蛋糕烤好了!");
        });

        bakeThread.start(); // 启动烤蛋糕线程

        // 没有等待,直接开始装饰
        System.out.println("我:开始装饰蛋糕...");
        if (cake.equals("熟蛋糕")) {
            System.out.println("装饰完成!可以吃了!");
        } else {
            System.out.println("失败!蛋糕还是生的,不能装饰!");
        }
    }
}

代码输出如下:

你没等帮手,蛋糕还是“生面团”你就开始装饰了,很明显这是不符合常理的。

想要等烤好了再装饰,就需要用到 join(),等帮手烤好后再装饰。

public class CakeMaker2 {
    static String cake = "生面团";

    public static void main(String[] args) throws InterruptedException {
        Thread bakeThread = new Thread(() -> {
            System.out.println("帮手A:开始烤蛋糕...");
            try {
                Thread.sleep(5000); // 模拟烤5秒
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            cake = "熟蛋糕";
            System.out.println("帮手A:蛋糕烤好了!");
        });

        bakeThread.start(); // 启动烤蛋糕线程

        // 加上这一句:我(主线程)要等 bakeThread 干完
        bakeThread.join();

        // 只有等烤好后,我才继续执行下面的代码
        System.out.println("我:开始装饰蛋糕...");
        if (cake.equals("熟蛋糕")) {
            System.out.println("装饰完成!可以吃了!");
        }
    }
}

用了 join()主线程被阻塞,一直等到 bakeThread 结束才继续,所以蛋糕一定是熟的!代码输出结果如下:

接下来再来探讨一下 join() 的底层原理:调用者轮询检查线程 alive 状态。你可以把 join() 想象成这样一段代码:

// 伪代码:join 的原理
public final void join() throws InterruptedException {
    while (isAlive()) {  // 只要这个线程还活着
        wait(0);        // 我(调用者)就在这等,啥也不干
    }
}
  • isAlive():检查那个线程还在不在运行。
  • wait(0):调用者线程进入等待状态,释放的是当前线程对象的锁(后面讲)。
  • 一旦 bakeThread 执行完,isAlive() 变成 false,跳出循环,join() 结束,主线程继续。

 注意:join() 方法是 synchronized 的,它用的是线程对象本身作为锁
比如 t1.join(),锁的是 t1 这个对象。wait() 释放的也是 t1 的锁,不会影响其他锁。

  • join 方法是被 synchronized 修饰的,本质上是一个对象锁,其内部的 wait 方法调用也是释放锁的,但是释放的是当前的线程对象锁,而不是外面的锁

  • 当调用某个线程(t1)的 join 方法后,该线程(t1)抢占到 CPU 资源,就不再释放,直到线程执行完毕

但是 join() 也有缺点:

问题说明
必须等线程结束不能设置超时(可以用 join(long millis) 超时等待)
不能配合线程池线程池里的线程是复用的,你没法 join() 一个池子里的线程
需要共享变量像上面的 cake 是全局变量,破坏封装性,容易出错
不够灵活只能等,不能取消、不能获取返回值

线程同步:

  • join 实现线程同步,因为会阻塞等待另一个线程的结束,才能继续向下运行
    • 需要外部共享变量,不符合面向对象封装的思想
    • 必须等待线程结束,不能配合线程池使用
  • Future 实现(同步):get() 方法阻塞等待执行结果
    • main 线程接收结果
    • get 方法是让调用线程同步等待

更好的替代方案:Future(支持返回值 + 更灵活)

ExecutorServiceFuture,可以拿到线程的执行结果,还能取消任务。接下来可以优化烤蛋糕代码:

import java.util.concurrent.*;

public class BetterCakeMaker {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(2);

        // 提交一个能返回结果的任务
        Future<String> future = pool.submit(() -> {
            System.out.println("帮手A:开始烤蛋糕...");
            Thread.sleep(5000);
            System.out.println("帮手A:蛋糕烤好了!");
            return "熟蛋糕"; // 可以返回结果!
        });

        // get() 就像 join(),但还能拿到返回值
        String cake = future.get(); // 阻塞等待,直到有结果

        System.out.println("我:开始装饰 " + cake);
        System.out.println("装饰完成!可以吃了!");

        pool.shutdown();
    }
}

代码输出如下:

优点:

  • 能拿到返回值
  • 可以设置超时:future.get(3, TimeUnit.SECONDS)
  • 可以取消:future.cancel(true)
  • 配合线程池,更高效
interrupt

线程打断 ≠ 停止线程!
它就像你给一个正在干活的人递了个“请尽快收工”的小纸条。
他看到了,可以选择:

  • 马上停下
  • 先把手头事干完再停
  • 甚至假装没看见(不推荐)

Java 不允许强制停止线程,只能“礼貌地请求”中断。

举一个生活例子,快递员送快递。

假设你是一个快递员(线程 t1),正在一条街一条街地送快递(循环干活)。

老板突然打电话说:“今天台风要来了,你尽快收工回家!”
这就是 “打断” —— 不是直接把你抓回来,而是告诉你:“该收工了。”

你听到后可以:

  • 马上回家(响应中断)
  • 把手上这单送完再走(合理)
  • 装听不见,继续送(危险,不推荐)
方法大白话解释是否清空标记
t1.interrupt()我要打断线程 t1!发个“中断请求”不清空,只是设置标记
Thread.interrupted()当前线程是否被中断过?是静态方法!会清除标记(只看一次)
t1.isInterrupted()线程 t1 是否被中断过?不清除标记(可反复看)

关键区别:interrupted() 是静态方法,看的是“当前线程”,而且看一眼就清空标记

写一个打断正在 sleep 的线程(会抛异常)

public class InterruptSleepDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            System.out.println("t1:我开始睡觉了,睡10秒...");
            try {
                Thread.sleep(10000); // 睡10秒
            } catch (InterruptedException e) {
                System.out.println("t1:啊!我睡着时被人打断了!异常来了!");
                System.out.println("t1:我的中断标记现在是:" + Thread.currentThread().isInterrupted());
                // 注意:sleep 被打断后,中断标记会被 JVM 自动清除!
            }
            System.out.println("t1:我醒了,继续干活...");
        }, "t1");

        t1.start();

        Thread.sleep(500); // 主线程等500毫秒,让t1进入sleep

        System.out.println("主线程:我来打断 t1!");
        t1.interrupt(); // 发出中断请求

        // 等t1执行完
        t1.join();

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

码输出如下:

  • sleep() 中被打断 → 抛出 InterruptedException
  • 抛出异常后,JVM 会自动清除中断标记(变 false
  • 所以你在 catch 里看到 isInterrupted() 是 false

这是 Java 的设计:阻塞方法(sleep/wait/join)被打断,会清空中断标记,并抛异常。

再写一个打断一个正在运行的线程(不会抛异常,靠手动检查):

package com.cg.jucproject.demo;

public class InterruptRunningDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread t2 = new Thread(() -> {
            int i = 0;
            while (true) {
                // 手动检查是否被中断
                // 只有当线程不在阻塞状态,而是在正常运行中被中断,并且手动检查了中断标记,走那 if 分支
                if (Thread.currentThread().isInterrupted()) {
                    System.out.println("t2:有人打断我了!我要退出了!");
                    System.out.println("t2:中断标记是:" + Thread.currentThread().isInterrupted());
                    break;
                }
                // 模拟干活
                i++;
                // 加个小小暂停,不然太快看不清
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    System.out.println("t2:我在干活时被 sleep 打断了!");
                    break;
                }
            }
            System.out.println("t2:任务结束,i=" + i);
        }, "t2");

        t2.start();

        Thread.sleep(500); // 等500毫秒

        System.out.println("主线程:我来打断 t2!");
        t2.interrupt(); // 设置中断标记为 true

        t2.join(); // 等t2结束
        System.out.println("主线程:任务结束");
    }
}

打断 park

LockSupport.park() 就像你按了“暂停键”,线程暂停运行;
别人调 interrupt() 就像按了“强制唤醒键”;
但和 sleep 不同:park 被打断后,中断标记不会被清空,而且下次 park 可能直接失效!

举一个遥控器暂停电视的例子。

想象你在看电视(线程运行),你按了“暂停”键(park()),画面停了。

这时候你妈进来,直接按了“关机”遥控键(interrupt()),电视被强制唤醒,自动退出暂停状态。但注意:

  • 电视知道是“被强行关的”,记录下这个事件(中断标记 = true
  • 你想再按“暂停”,发现:没反应! 因为系统说:“你已经被关过一次了,不能再暂停了。”

这就是 park 被打断后的“一次性”特性

继续来写一个demo:park 被打断,不会清空中断标记

import java.util.concurrent.locks.LockSupport;

public class ParkInterruptDemo {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            System.out.println("t1: 我要暂停了 (park...)");

            // 线程暂停,等待被 unpark 或 interrupt
            LockSupport.park();

            System.out.println("t1: 我被唤醒了 (unpark...)");

            // 检查中断状态
            System.out.println("t1: 打断状态:" + Thread.currentThread().isInterrupted());
            // 输出:true!因为 interrupt 不会清空中断标记
        }, "t1");

        t1.start();

        Thread.sleep(1000); // 主线程等1秒,让 t1 进入 park

        System.out.println("主线程:我来打断 t1!");
        t1.interrupt(); // 打断 t1
    }
}

代码输出如下:

  • t1.interrupt() 成功唤醒了 park()
  • 但 isInterrupted() 是 true → 中断标记没有被清空!
  • 对比 sleep():如果这里用 sleep(1000),被打断后标记会被清空(变 false

park()sleep() 最大区别之一:park 被打断不自动清标记

举一个例子:打断标记为 true 时,park() 会直接失效

public class ParkTwiceDemo {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            System.out.println("第一次 park...");
            LockSupport.park();
            System.out.println("第一次 unpark");

            System.out.println("第二次 park...(此时中断标记是 true)");
            LockSupport.park(); // 这里不会阻塞!直接跳过!
            System.out.println("第二次 unpark"); // 立刻执行
        }, "t1");

        t1.start();

        Thread.sleep(1000); // 主线程等1秒,让 t1 进入 park

        System.out.println("主线程:我来打断 t1!");
        
        t1.interrupt(); // 设置中断标记为 true,并唤醒第一次 park
    }
}

代码输出如下:

  • 第一次 park() 被 interrupt() 唤醒,中断标记保持 true
  • 第二次 park() 发现:“咦?中断标记已经是 true 了?” → 直接认为“你已经被唤醒过了”,所以不阻塞,直接跳过
  • 所以“第二次 unpark”立刻执行,没有停顿

这就是 park() 的“防重入”机制:如果中断标记为 truepark() 直接失效,不会阻塞。

解决方案:用 Thread.interrupted() 清空中断标记

如果你想让第二次 park() 有效,就必须手动清空中断标记

import java.util.concurrent.locks.LockSupport;

public class ParkFixedDemo {
    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            System.out.println("第一次 park...");
            LockSupport.park();
            System.out.println("第一次 unpark");

            System.out.println("第二次 park...(此时中断标记是 true)");
            LockSupport.park(); // 不会阻塞,直接跳过
            System.out.println("第二次 unpark"); // 立刻执行
        }, "t1");

        t1.start();

        Thread.sleep(1000); // 等1秒,确保 t1 进入 park

        System.out.println("主线程:我来打断 t1!");
        t1.interrupt(); // 打断 t1,唤醒第一次 park
    }
}

Thread.interrupted() 是静态方法,它会:

  1. 返回当前线程是否被中断过
  2. 自动清空中断标记
特性Thread.sleep()LockSupport.park()
被打断时是否抛异常抛 InterruptedException不抛异常
被打断后是否清空中断标记清空(变 false不清空(保持 true
中断标记为 true 时再次调用仍会阻塞直接失效,不阻塞
是否响应中断是,但方式不同
底层实现JVM 层面基于 Unsafe,更底层
  • ReentrantLockSemaphoreCountDownLatchFutureTask 等底层都用 park/unpark 来控制线程阻塞和唤醒。
  • 它比 wait/notify 更灵活:不需要 synchronized,也不需要锁对象。
  • unpark(thread) 可以提前发,park() 后来收到,不会丢(类似“许可证”机制)。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值