Java 多线程标志位的使用

在 Java 多线程编程中,标志位(Flag) 是一种常见的线程协作与控制手段,用于通知线程"是否继续运行"、"是否停止任务"等。但它的使用有严格的前提条件,否则会导致可见性问题、响应延迟甚至死循环。

标志位通常是一个 boolean 类型的共享变量,一个线程通过修改它来通知另一个线程改变行为。

以下提供 4 种代码,来体现使用标志位时的注意事项

错误代码一:程序卡住!"子线程结束" 永远不打印!

此时,几乎 100% 会死循环!因为 JVM 会优化这个循环:

  • 第一次读取 running 到寄存器

  • 后续直接使用寄存器中的 true,永不重新读主内存

为什么后续 JVM直接使用寄存器中的 true,永不重新读主内存:

步骤 1:JVM 分析代码

  • JVM 发现:在这个线程的执行路径中,没有任何地方修改 running

  • 而且 running 不是 volatile,也没有被 synchronized 保护。

  • 所以 JVM 合理推断:running 的值在本线程中永远不会改变。

步骤 2:激进优化 ------ "提升为常量" 或 "缓存到寄存器"

步骤 3:后果 ------ 多线程下失效

  • 主线程修改了 running = false,写入主内存。

  • 但子线程早已不再访问主内存,它只看寄存器或本地缓存中的旧值 true

  • 结果:死循环,永远看不到变化。

java 复制代码
public class Demo9_3 {
    private static boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(running) {
                //空循环
            }
            System.out.println("子线程结束");
        });

        t.start();

        Thread.sleep(1000);
        running = false;
        System.out.println("主线程已设 running = false");

        //你会发现:程序卡住!"子线程结束" 永远不打印!

    }
}

错误代码二:表面上看,它"似乎能正常结束"------但这只是"偶然正确",本质上是存在严重并发 bug 的!

为什么 Thread.sleep() 有时能"救"回来?

虽然 sleep() 不是 JMM 规范定义的同步点(不像 volatile),但在实际 JVM 实现中:

  • Thread.sleep() 是一个 native 方法,会触发线程状态切换(RUNNABLE → TIMED_WAITING)。

  • 在进入/退出内核态、上下文切换时,CPU 缓存可能被刷新,或 JVM 保守地重新加载内存状态。

  • 所以从 sleep() 返回后,再次读取 running 时,可能碰巧读到主内存的新值。

⚠️ 但这不是规范保证的行为!不同 JVM、不同 OS、不同 CPU 架构下表现可能不同。

java 复制代码
public class Demo9_1 {
    private static boolean running = true;

    public static void main(String[] args) {
        Thread t = new Thread(() -> {
            while(running) {
                System.out.println("hello");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
            System.out.println("thread 线程结束");
        });

        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("输入任意内容, 控制 t 线程结束: ");
        scanner.next();
        running = false; 
    }
}

解决方案一:使用 volatile

保证可见性(Visibility)

  • 当一个线程写入 volatile 变量时,强制将该值刷新到主内存。

  • 当其他线程读取该 volatile 变量时,强制从主内存重新加载最新值,跳过 CPU 缓存和寄存器缓存。

类比理解(现实例子)

想象两个办公室:

  • 主内存 = 公司中央公告栏

  • CPU 缓存/寄存器 = 员工自己的笔记本

没有 volatile(无通知机制):

  • 老板(主线程)在公告栏贴了"今天下班"(running = false

  • 员工 A(子线程)早上抄了一条"今天上班"(running = true)到笔记本

  • 之后他再也不看公告栏,只看笔记本 → 一直加班到死 💀

volatile(强制看公告栏):

  • 老板贴通知时,广播:"所有人立刻看公告栏!"

  • 员工 A 下次看 running 时,必须去公告栏看最新内容 → 看到"下班",就走了 ✅

java 复制代码
public class Demo9_4 {
    private static volatile boolean running = true;

    public static void main(String[] args) throws InterruptedException {
        Thread t = new Thread(() -> {
            while(running) {
                //空循环
            }
            System.out.println("子线程结束");
        });

        t.start();

        Thread.sleep(1000);
        running = false;
        System.out.println("主线程已设置 running = false");


    }
}

解决方法二:使用 AtomicBoolean(更现代)

使用 AtomicBoolean 能解决多线程中标志位的可见性与安全性问题,核心原因在于它内部通过 volatile + CAS(Compare-And-Swap)机制,既保证了内存可见性,又提供了原子操作语义。

一、为什么 AtomicBoolean 能解决问题?

1. 底层使用 volatile 修饰值

AtomicBoolean.get()set() 操作都作用于一个 volatile int,因此天然具备 内存可见性 ------ 主线程调用 set(false) 后,子线程调用 get() 一定能读到最新值。

2. 支持原子的"比较并设置"操作(CAS)

虽然你的场景只用到 get()set(),但 AtomicBoolean 还提供:

java 复制代码
public final boolean compareAndSet(boolean expect, boolean update)
  • 这个操作是 原子的(通过 CPU 的 CAS 指令实现)
  • 即使多个线程同时尝试修改,也能保证"读-改-写"不被干扰:当多个线程同时对同一个共享变量执行"先读取当前值 → 根据值做计算 → 写回新值"这一系列操作时,使用 CAS(如 AtomicBoolean.compareAndSet)能确保整个过程不会被其他线程打断,从而避免数据错误。

二、与 volatile boolean 对比

特性 volatile boolean AtomicBoolean
可见性 ✅(靠 volatile ✅(内部用 volatile
原子性(单次读/写) ✅(读/写本身是原子的)
复合操作原子性(如"如果为 true 则设为 false") ✅(用 compareAndSet
语义清晰度 一般 ✅(明确表达"这是个原子布尔标志")
可扩展性 ✅(支持更多原子操作)

三、为什么它能避免"死循环"问题?

✅ 关键点:

  • running.get() → 读取 volatile int value → 强制从主内存加载

  • running.set(false) → 写入 volatile int value → 立即刷新到主内存

  • 因此,子线程下一次调用 get() 就能看到 false,循环退出

🚫 不会出现"缓存旧值导致死循环"的问题!

总结:为什么 AtomicBoolean 能解决问题?

原因 说明
1. 内部使用 volatile 保证 get()/set() 的内存可见性,解决"看不到修改"的问题
2. 对象引用在线程间共享 作为堆对象,所有线程访问的是同一个实例(不像局部变量被捕获副本)
3. 语义明确、不易误用 明确表达"这是一个线程安全的布尔状态"
4. 支持未来扩展 如需原子条件更新,直接用 compareAndSet,无需重构
java 复制代码
import java.util.Scanner;
import java.util.concurrent.atomic.AtomicBoolean;

public class Demo9_3 {
    public static void main(String[] args) {
        AtomicBoolean running = new AtomicBoolean(true);

        Thread t = new Thread(() -> {
            while (running.get()) {
                System.out.println("hello thread");
                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    break;
                }
            }
            System.out.println("thread 结束");
        });

        t.start();

        Scanner scanner = new Scanner(System.in);
        System.out.println("输入任意内容, 控制 t 线程结束: ");
        scanner.next();
        running.set(false); // 原子操作,线程安全
    }
}

总结

使用标志位并不能唤醒等待,要是想唤醒等待,必须使用 t.interrupt(); 来终止线程

使用标志位注意事项:

✅ 1. 必须保证内存可见性

问题 :一个线程修改标志位,另一个线程可能永远看不到更新。
原因:CPU 缓存、JVM 优化导致变量值未同步到主内存。

✅ 正确做法(任选其一):
  • 使用 volatile 修饰标志位:

    java 复制代码
    private static volatile boolean running = true;
  • 使用 AtomicBoolean

    java 复制代码
    private final AtomicBoolean running = new AtomicBoolean(true);
为什么要加 final:

✅ 1. 防止引用被意外修改(保证"对象不变性")

  • final 表示:running 这个引用变量不能再指向其他 AtomicBoolean 对象。

  • 你仍然可以调用 running.set(false) 修改其内部状态(因为 AtomicBoolean 本身是可变的)。

  • 但你不能做:

    java 复制代码
    running = new AtomicBoolean(false); // ❌ 编译错误!

为什么这很重要?

  • 如果允许多个线程重新赋值 running 引用,会导致:

    • 不同线程看到不同的 AtomicBoolean 实例

    • 原来的原子状态丢失

    • 线程间失去同步基础 → 严重并发 bug

java 复制代码
public class UnsafeTask {
    // ❌ 没有 final,引用可变!
    private AtomicBoolean running = new AtomicBoolean(true);

    public void startWorker() {
        Thread t1 = new Thread(() -> {
            // t1 拿到当前 running 引用(指向 instance A)
            AtomicBoolean localRef = this.running;
            while (localRef.get()) {
                System.out.println("T1: running = " + localRef.get());
                try { Thread.sleep(1000); } catch (InterruptedException e) { break; }
            }
        });

        Thread t2 = new Thread(() -> {
            try { Thread.sleep(2000); } catch (InterruptedException e) {}
            // ⚠️ 危险操作:创建新实例,并赋值给 running!
            this.running = new AtomicBoolean(false); // ← 指向 instance B
            System.out.println("T2: 已替换 running 为新对象");
        });

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

❌ 结果:

  • t1 在启动时捕获了 this.running 的引用(假设叫 instance A)。

  • t2 后来执行 this.running = new AtomicBoolean(false),让 running 指向 instance B。

  • t1 仍然在检查 instance A 的值(它一直是 true!)。

  • t1 永远不会退出循环!死锁式 bug!

💥 这就是"不同线程看到不同 AtomicBoolean 实例"的真实场景!
🔑 final 保证所有线程操作的是同一个 AtomicBoolean 对象,这是线程安全的前提。

✅ 2. 表达设计意图:这是一个不可变的引用

  • final 向阅读代码的人(包括未来的你)明确传达:

    "这个字段在对象创建后就不会再变了,所有线程都共享同一个状态容器。"

  • 这符合 "不可变引用 + 可变状态" 的经典并发设计模式:

    • 引用不可变(final)→ 安全共享

    • 内部状态可变(AtomicBoolean)→ 支持并发更新

✅3. 避免低级错误

想象你不小心写了:

java 复制代码
private AtomicBoolean running = new AtomicBoolean(true);

public void reset() {
    running = new AtomicBoolean(true); // 重置?看似合理...
}

但问题来了:

  • 如果有线程正在使用旧的 running 对象,它们永远不会收到新对象的状态变化。

  • 旧对象可能还在被某些线程使用,导致逻辑混乱。

而如果 runningfinal,这种错误在编译期就被阻止了。

相关推荐
qq_12498707531 天前
基于Java Web的城市花园小区维修管理系统的设计与实现(源码+论文+部署+安装)
java·开发语言·前端·spring boot·spring·毕业设计·计算机毕业设计
h7ml1 天前
查券返利机器人的OCR识别集成:Java Tesseract+OpenCV优化图片验证码的自动解析方案
java·机器人·ocr
野犬寒鸦1 天前
从零起步学习并发编程 || 第五章:悲观锁与乐观锁的思想与实现及实战应用与问题
java·服务器·数据库·学习·语言模型
Volunteer Technology1 天前
Sentinel的限流算法
java·python·算法
岁岁种桃花儿1 天前
SpringCloud从入门到上天:Nacos做微服务注册中心
java·spring cloud·微服务
jdyzzy1 天前
什么是 JIT 精益生产模式?它与传统的生产管控方式有何不同?
java·大数据·人工智能·jit
Chasmれ1 天前
Spring Boot 1.x(基于Spring 4)中使用Java 8实现Token
java·spring boot·spring
froginwe111 天前
Python 条件语句
开发语言
汤姆yu1 天前
2026基于springboot的在线招聘系统
java·spring boot·后端
七夜zippoe1 天前
Python统计分析实战:从描述统计到假设检验的完整指南
开发语言·python·统计分析·置信区间·概率分布