JUC-volatile -13

93 阅读2分钟

内存可见性

我们如何保证多线程下共享变量的可见性呢?也就是当一个线程修改了某个值后,对其他线程是可见的。

这里有两种方案:加锁 和 使用 volatile 关键字


 public static void main(String[] args) {
      MyThread myThread = new MyThread();
      // 开启线程
      myThread.start();

      // 主线程执行
      for (; ; ) {
          synchronized (myThread) {
              if (myThread.isFlag()) {
                  System.out.println("主线程访问到 flag 变量");
                }
          }
      }
  }

这里大家应该有个疑问是,为什么加锁后就保证了变量的内存可见性了? 因为当一个线程进入 synchronizer 代码块后,线程获取到锁,会清空本地内存,然后从主内存中拷贝共享变量的最新值到本地内存作为副本,执行代码,又将修改后的副本值刷新到主内存中,最后线程释放锁。

除了 synchronizer 外,其它锁也能保证变量的内存可见性。

有序性

什么是重排序?

为了提高性能,在遵守 as-if-serial 语义(即不管怎么重排序,单线程下程序的执行结果不能被改变。编译器,runtime 和处理器都必须遵守。)的情况下,编译器和处理器常常会对指令做重排序。

x = 2;        //语句1

y = 0;        //语句2

flag = true;  //语句3

x = 4;         //语句4

y = -1;       //语句5 先后执行哪一个对结果么有影响!

一般重排序可以分为如下三种类型:

  • 编译器优化重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。\

  • 指令级并行重排序。现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。\

  • 内存系统重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。\

数据依赖性:如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑。

image.png

image.png

不保证原子性