证明volatile 是线程可见的

本文深入探讨了Java中volatile关键字的作用,主要体现在线程可见性和禁止指令重排序。通过实例代码展示了未修饰和修饰后的变量在多线程环境中的行为差异,解释了线程如何从主内存和线程工作内存中读取变量值。同时,文章提到了即使没有volatile,通过延长代码执行时间也可能使线程重新从主内存获取变量,感知到变化。此外,还介绍了JMM内存模型对理解volatile的重要性。

1.介绍

Volatile关键字的作用主要有如下两个:
1.线程的可见性:当一个线程修改一个共享变量时,另外一个线程能读到这个修改的值。
2. 顺序一致性:禁止指令重排序。

本文主要介绍 java中 volatile 关键字是线程可见的,以前了解的是一个线程对 volatile 修饰的遍历修改之后,另一个线程再次获取这个变量的值的时候,就是修改之后的值了。如果变量没有 volatile 修饰,另一个线程就不会感知到便获。        其实,这就需要了解JMM内存模型。

下面介绍我对 volatile 关键字的部分理解,在了解JMM 内存模型之后,明白线程是在哪里取值的,才能进一步理解 volatile 的意思。我认为,一个变量 Boolean  flag 如果没有被 volatile 修饰,线程加载这个变量之后,会放入线程的内存中,在使用的时候,一直从线程内存中取值,对于主内存flag变量的变化,线程是不会感知的,也就是说,如果其他线程对flag进行操作,改变了他的值,那么这个线程也不会感知;但是也仅限于,线程内存flag值不会消失的情况,如果这个变量在线程内存中消失了,他再去主内存加载的时候,就是新的值,如果变量flag在线程内存中长时间不用,(这个时间也很短,大约几百毫秒的样子--我猜的,反正很短时间,因为线程内存也很小,对于不常用的变量就会丢弃,常用的就在里面存着方便下次使用)也可能会消失,线程再次取值的时候,只能去主内存取值,如果在取值之前,被另一个线程改变了,他取到的值就是改变后的值。

代码证明线程可见性

package com.example.demovalid;

public class test1 {

// 变量
    Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {

        test1 test1 = new test1();

// 线程1 取变量的值
        Thread t1 = new Thread(() -> {

            System.out.println("这是--"+ Thread.currentThread() + "---" + test1.flag);


            // 这里死循环,是为了让线程1不断的取值,这里是取的线程内存的值,为了不让变量在线程内存中消失,不会去加载主内存的值
            while (true){

                if (!test1.flag){
                    System.out.println("这是11--"+ Thread.currentThread() + "---" + test1.flag);
                    break;
                }

            }
            System.out.println("这是222--"+ Thread.currentThread() + "---" + test1.flag);

        }, "t1");

// 线程2取变量的值,并对其修改
        Thread t2 = new Thread(() -> {
            try {
// 等1秒,为了让线程1启动起来
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

// 打印值
            System.out.println("这是--"+ Thread.currentThread() + "---" + test1.flag);
            System.out.println("t2" + System.currentTimeMillis());
// 对变量修改
            test1.flag= false;

        }, "t2");

// 线程1和2设置为守护线程,当主线程结束的时候,他们也结束
        t1.setDaemon(true);
        t2.setDaemon(true);

// 启动线程
        t1.start();
        t2.start();

// 主线程等待10秒,为了看线程1和2打印变量的值
        Thread.sleep(10000L);

        System.out.println("结束");

    }
}

上边是没有修饰的变量执行后打印的结果

这是--Thread[t1,5,main]---true
这是--Thread[t2,5,main]---true
t21671368338074
结束

从上面结果可以看出线程1没有感知到线程2对变量的修改,一直在循环中。

修饰之后的代码

package com.example.demovalid;

public class test1 {

// 修饰的变量
    volatile Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {

        test1 test1 = new test1();

// 线程1 取变量的值
        Thread t1 = new Thread(() -> {

            System.out.println("这是--"+ Thread.currentThread() + "---" + test1.flag);


            // 这里死循环,是为了让线程1不断的取值,这里是取的线程内存的值,为了不让变量在线程内存中消失,不会去加载主内存的值
            while (true){

                if (!test1.flag){
                    System.out.println("这是11--"+ Thread.currentThread() + "---" + test1.flag);
                    break;
                }

            }
            System.out.println("这是222--"+ Thread.currentThread() + "---" + test1.flag);

        }, "t1");

// 线程2取变量的值,并对其修改
        Thread t2 = new Thread(() -> {
            try {
// 等1秒,为了让线程1启动起来
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

// 打印值
            System.out.println("这是--"+ Thread.currentThread() + "---" + test1.flag);
            System.out.println("t2" + System.currentTimeMillis());
// 对变量修改
            test1.flag= false;

        }, "t2");

// 线程1和2设置为守护线程,当主线程结束的时候,他们也结束
        t1.setDaemon(true);
        t2.setDaemon(true);

// 启动线程
        t1.start();
        t2.start();

// 主线程等待10秒,为了看线程1和2打印变量的值
        Thread.sleep(10000L);

        System.out.println("结束");

    }
}

 执行结果

这是--Thread[t1,5,main]---true
这是--Thread[t2,5,main]---true
t21671368435004
这是11--Thread[t1,5,main]---false
这是222--Thread[t1,5,main]---false
结束

从上面可以看出线程2对变量改变后,线程1也能感知道。

3. 我们对案例1做一些变化,虽然没没有volatile修饰,但是我们在循环加一些代码,让代码执行时间长一些,等线程再次从线程内存中取值的时候,娶不到,就会去主内存中取值,这样也能感知到变量的变化。

package com.example.demovalid;

public class test3 {
    Boolean flag = true;
//    volatile Boolean flag = true;

    public static void main(String[] args) throws InterruptedException {
        test3 test1 = new test3();

        long currentTimeMillis = System.currentTimeMillis();

        Thread t1 = new Thread(() -> {

            System.out.println("这是--" + Thread.currentThread() + "---" + test1.flag);

            while (true) {

                try{
                // 这里抛异常,为了增加执行时间,延长下次从内存中去变量的值
                    int a = 1/0;
                }catch (Exception e){
// 这段打印是需要的,本人测试,去掉之后,还是不能跳出循环
                    System.out.println("出错以后");
                }

                if (!test1.flag) {
                    System.out.println("这是11--" + Thread.currentThread() + "---" + test1.flag);
                    break;
                }

            }
            System.out.println("这是222--" + Thread.currentThread() + "---" + test1.flag);

        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("这是--" + Thread.currentThread() + "---" + test1.flag);
            System.out.println("t2" + System.currentTimeMillis());
            test1.flag = false;

        }, "t2");
        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();

        Thread.sleep(10000L);

        System.out.println("结束");

    }
}

执行结果

这是--Thread[t1,5,main]---true
出错以后
出错以后
.......
出错以后

这是--Thread[t2,5,main]---true
t21671368823608
出错以后
这是11--Thread[t1,5,main]---false
这是222--Thread[t1,5,main]---false

从执行结果看,在线程2改变了变量的值以后,线程1再次取值的时候,也感知到了变量的变化。 这就是因为,线程1再次去线程内存取值的时候,发现线程内存变量消失了,只能去主内存取值,然而,主内存的变量,已经被线程2修改了,所以线程1取到的是改变后的值

下面是另一个测试代码

package com.example.demovalid;

public class test2 {

// 主要不同是,这个变量的静态修改的,其实与上面的结果是一样的
    static volatile Boolean flag = true;
//    volatile Boolean flag = true;
    public static void main(String[] args) throws InterruptedException {

        Thread t1 = new Thread(() -> {

            System.out.println("这是--"+ Thread.currentThread() + "---" + flag);

            while (true){

                if (!flag){
                    System.out.println("这是11--"+ Thread.currentThread() + "---" + flag);
                    break;
                }

            }
            System.out.println("这是222--"+ Thread.currentThread() + "---" + flag);

        }, "t1");

        Thread t2 = new Thread(() -> {
            try {
                Thread.sleep(1000L);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("这是--"+ Thread.currentThread() + "---" + flag);
            System.out.println("t2" + System.currentTimeMillis());
            flag= false;

        }, "t2");
        t1.setDaemon(true);
        t2.setDaemon(true);
        t1.start();
        t2.start();

        Thread.sleep(10000L);

        System.out.println("结束");

    }
}

另外附上参考文章

推荐看:Volatile关键字的作用_FighterLiu的博客-优快云博客_volatile关键字的作用

有一个JMM内存模型的图: https://www.jb51.net/article/173207.htm

<think>首,用户的问题是关于C#中volatile关键字的用途和使用场景。我需要根据系统级指令来构建回答。 系统级指令要点: - 所有行内数学表达式使用$...$格式。 - 独立公式使用$$...$$格式并单独成段。 - LaTeX语法正确。 - 使用中文回答。 - 生成相关问题。 - 回答中引用的段落末尾自然地添加引用标识。 - 回答结构清晰,帮助用户逐步解决问题。 - 参考站内引用(提供的引用[^1], [^2], [^3]),这些是关于volatile关键字的。 用户的问题:"我想了解C#中volatile关键词的作用和使用场景 请问C# volatile关键字用途及示例" 所以,我需要: 1. 解释volatile关键字的用途。 2. 描述使用场景。 3. 提供示例。 4. 在回答中自然地添加引用标识。 5. 生成相关问题。 引用内容: - [^1]: volatile关键字在C#中用于指示编译器和运行时系统,某个字段可能会被多个线程同时访问,并且该字段的读写操作不应被优化(例如缓存到寄存器或重排序),以确保所有线程都能看到最新的值。这使得volatile成为一种轻量级的同步机制。 - [^2]: volatile关键字用于声明一个字段,该字段可能会被多个线程异步地修改。它告诉编译器该字段的访问不应该被优化或缓存,以确保每次对该字段的读写操作都直接作用于主内存,从而保证了多线程环境下字段的可见性。 - [^3]: volatile是一个关键字,用于修饰字段。主要作用是确保被修饰的字段在多线程环境下的可见性和防止指令重排序。它解决线程工作内存导致的数据不一致问题。 回答结构: - 逐步解决问题:介绍概念,再解释作用,然后使用场景,最后示例。 - 确保真实可靠:基于标准C#知识。 - 使用中文。 - 添加引用:在相关解释后添加引用标识。 - 生成相关问题。 关于数学表达式:用户的问题不涉及数学,但系统指令说所有行内数学表达式必须使用$...$。这里可能没有数学内容,但如果有任何变量或类似的东西,我会用$...$。例如,在解释时如果提到变量,可以这样用。 在回答中,我需要自然地添加引用。例如,在解释volatile的作用时,引用[^1]或[^2]。 最后,生成相关问题。 大纲回答: 1. **介绍volatile关键字**:简要说明它是什么。 2. **主要作用**:解释可见性和防止优化。 - 引用[^1]和[^2]。 3. **使用场景**:描述何时使用,如多线程共享变量。 - 引用[^3]。 4. **示例**:提供一个C#代码示例。 5. **注意事项**:简要提到限制或替代方案。 6. **生成相关问题**:在结尾添加。 示例代码:提供一个简单的C#代码展示volatile使用。 引用添加:例如,在解释作用时说:"...以确保所有线程都能看到最新的值[^1]。" 生成相关问题:类似示例中的格式,如: - 1. volatile关键字与lock的区别是什么? - 2. 在哪些情况下应该避免使用volatile? - 3. volatile在C#中的性能影响如何? 确保回答清晰、逐步。</think>### C# volatile 关键字的作用与使用场景 在 C# 中,`volatile` 关键字是一种轻量级的同步机制,主要用于解决多线程环境下的数据可见性和指令重排序问题。它修饰字段(成员变量),指示编译器和运行时系统:该字段可能被多个线程同时访问,因此其读写操作不应被优化(如缓存到寄存器或重排序)。这确保了所有线程都能看到该字段的最新值,从而避免潜在的数据不一致性[^1]。下面,我将逐步解释其用途、使用场景,并提供代码示例。 #### 1. **volatile 关键字的主要作用** - **确保可见性**:在多线程程序中,每个线程都有自己的工作内存(缓存),这可能导致一个线程修改了共享变量的值,但其他线程无法立即看到更新。`volatile` 强制所有读写操作直接作用于主内存,而不是缓存,从而保证字段值的实时可见性[^2]。 - **防止指令重排序**:编译器和处理器可能为了优化性能而重排指令顺序,这在多线程环境下可能引发意外行为(如读取后写入的顺序被打乱)。`volatile` 禁止这种重排序,确保操作按代码顺序执行[^3]。 - **轻量级同步**:相比 `lock` 或 `Monitor` 等重量级同步机制,`volatile` 开销更小,适合简单共享变量的场景。但它不提供原子性(即不能保证复合操作的线程安全),因此不适合复杂逻辑[^1]。 这些作用使得 `volatile` 成为处理多线程共享变量的有效工具,尤其在性能敏感的应用中[^2]。 #### 2. **使用场景** `volatile` 适用于以下常见场景: - **标志位共享**:当多个线程共享一个简单的布尔标志(如控制循环退出)时,使用 `volatile` 可确保标志的更新对所有线程立即可见。例如,后台线程检查一个 `volatile` 标志来终止任务。 - **只读共享变量**:如果共享变量只被频繁读取而很少写入(如配置值),`volatile` 能避免缓存问题,而不引入锁的开销。 - **轻量级计数器**:在简单计数器场景(如统计线程执行次数),`volatile` 可提供基本可见性,但需注意:对于复合操作(如递增),仍需使用 `Interlocked` 类保证原子性[^3]。 需要注意,`volatile` 不适合: - 需要原子操作的场景(如 `i++`)。 - 复杂数据结构(如集合)的同步,此时应使用 `lock` 或并发集合类。 - 单线程程序,因为无优化问题。 总之,`volatile` 是特定场景下的优化手段,而非万能解决方案[^1][^3]。 #### 3. **代码示例** 以下是一个简单的 C# 示例,展示 `volatile` 在多线程环境中的使用。场景:一个后台线程根据 `volatile` 标志退出循环,而主线程修改该标志。 ```csharp using System; using System.Threading; public class VolatileExample { // 声明 volatile 字段,确保可见性和防止重排序 private volatile bool _stopFlag = false; public void Run() { // 启动后台线程 Thread backgroundThread = new Thread(() => { Console.WriteLine("后台线程启动..."); while (!_stopFlag) // 正确读取最新值 { // 模拟工作 Thread.Sleep(100); } Console.WriteLine("后台线程退出。"); }); backgroundThread.Start(); // 主线程等待后修改标志 Thread.Sleep(1000); _stopFlag = true; // 修改后立即对所有线程可见 Console.WriteLine("主线程设置了停止标志。"); backgroundThread.Join(); // 等待后台线程结束 } public static void Main() { new VolatileExample().Run(); } } ``` **解释**: - 在此代码中,`_stopFlag` 被声明为 `volatile`。如果没有 `volatile`,后台线程可能因缓存而看不到主线程修改,导致无限循环。 - 运行后,主线程在 1 秒后设置 `_stopFlag = true`,后台线程会立即退出,证明可见性有效。 - 注意:`volatile` 确保了读写的可见性,但如果有多个写入操作,仍需其他同步机制[^2]。 #### 4. **注意事项** - **性能影响**:`volatile` 会轻微降低访问速度(因绕过缓存),但通常比锁高效。 - **替代方案**:对于原子操作,使用 `Interlocked` 类;对于复杂同步,使用 `lock` 或 `Monitor`。 - **.NET 版本兼容**:`volatile` 在所有 .NET 版本中支持,但需谨慎使用以避免误用[^1][^3]。 通过以上解释,`volatile` 关键字在 C# 中是多线程编程的有力工具,但应限于简单共享变量场景[^2]。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值