JAVA多线程基础篇 4、可见性、有序性与Volatile

本文探讨了Java多线程中的可见性和有序性问题,通过volatile关键字如何确保线程间变量的可见性,并通过实验展示了volatile防止指令重排序的效果。文章还分析了处理器的内存模型和缓存一致性协议(MESI),并提供了示例代码来验证volatile的作用。最后,总结了volatile在保证内存可见性和禁止特定类型处理器重排序中的作用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

1. 可见性问题和有序性问题

在多线程开发中,可见性问题和有序性问题需要依托volatile来保证。

可见性,一个线程对共享变量的修改,另外一个线程能够立刻看到。
首先需要了解一个概念,堆和栈。
程序在运行的过程中,数据放在堆中,线程内部的缓存数据会放在栈中。
在堆中的数据是公共的,而在线程的栈帧里的数据是缓存各自线程的。
因此,当A、B两个线程,无论谁更新了线程内的数据,都及时同步并告知其他线程重新刷新读取缓存。java使用volatile 修饰的变量,通过操作系统底层的锁总线(LOCK#)或者缓存一致性协议(MESI)实现可见性。

  • Lock 会对总线进行锁定,其它 CPU 对内存的读写请求都会被阻塞,直到锁释放。LOCK的开销比较大,锁总线期间其他 CPU 没法访问内存。于是引入了缓存一致性协议(MESI)来实现。
  • MESI 当一个缓存代表它所属的处理器去读写内存时,其它处理器都会得到通知,它们以此来使自己的缓存保持同步。 只要某个处理器写内存,其它处理器马上知道这块内存在它们的缓存段中已经失效。

有序性,即程序执行的顺序按照代码的先后顺序执行。
在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

通过synchronized和Lock来保证有序性,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。
通过使用volatile来保证有序性,对一个 volatile 域的写,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

# 2. 可见性问题的实验

public class VolatileTest {
    private static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag) {
            }
            System.out.println("over");
        }).start();
    
        Thread.sleep(5);
    
        flag=false;
    }
}

此时,程序是不会有任何输出的,因为在更改flag为false后,线程栈帧内存储的变量没有被改变。

volatile3

2.1 volatile确保了可见性

如下程序,简单增加volatile修饰后可以达到效果。

public class VolatileTest {
    private volatile static boolean flag = true;
    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            while (flag) {

            }
            System.out.println("over");
        }).start();

        Thread.sleep(5);

        flag=false;
    }
}

运行结果:
volatile1

注意:volatile一般修饰基础类型,尽量不要去修饰引用类型。

3. 一个指令乱序的实验

如果没有乱序执行,则只可能x和y 分别同时为1 ,或者是1 0 组合 或者是 0 1 组合。

一定不能出现x和y同时为0.

public class VolatileTest2 {
        static int a;
        static int b;
        static int y;
        static int x;
        public static void main(String[] args) throws InterruptedException {
            int count = 0;
            while (true) {
                a = 0;
                b = 0;
                y = 0;
                x = 0;
                Thread t1 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        a = 1;
                        x = b;
                    }
                });
                Thread t2 = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        b = 1;
                        y = a;
                    }
                });

                t1.start();
                t2.start();
                t1.join();
                t2.join();
                count++;
                if (x == 0 && y == 0) {
                    System.out.println("第"+count+"次");
                    break;
                }
            }
        }
}    

运行结果显示,出现了x和y同时为0。说明程序命令a = 1;x = b;b = 1;y = a发生了乱序执行。

183383(0,0)

Process finished with exit code 0

总结

java使用volatile 修饰的变量,通过操作系统底层的锁总线(LOCK#)或者缓存一致性协议(MESI)实现可见性。

java对一个 volatile 域的写,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。

多线程系列在github上有一个开源项目,主要是本系列博客的实验代码。

https://github.com/forestnlp/concurrentlab

如果您对软件开发、机器学习、深度学习有兴趣请关注本博客,将持续推出Java、软件架构、深度学习相关专栏。

您的支持是对我最大的鼓励。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悟空学编程

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值