volatile结合cpu多级缓存的资料整理

文章目录
  1. 1. 参考资料
  2. 2. volatile和cpu多级缓存有什么关系
  3. 3. volatile如何保证多线程在多级缓存中拿到相同的值的
  4. 4. volatile 能使得一个非原子操作变成原子操作吗?为什么?
  5. 5. 举几个在日常开发中使用volatile的场景,并分析一下使用volatile的原因
  6. 6. 详细说一下volatile关键字在底层所做的事情,然后分析一下volatile的读屏障和写屏障是什么

参考资料

volatile和cpu多级缓存有什么关系

在多级缓存的计算机体系结构中,CPU拥有多层缓存存储器来提高处理速度。 缓存是快速且昂贵的访问内存的一种形式。 据此,当使用volatile关键字声明某个变量时,不保证它将直接从主存取出数据,而是可能会从高速缓存读取旧数据。

具体来说,volatile关键字可以将编译器优化代码的行为禁用,以确保对于任何写入该变量的代码,在读取之前都不会被删除。因为编译器在优化指令时可能会选择将变量值暂存在寄存器或高速缓存中,以减少与内存的交互次数,从而提高代码执行速度。 但是,如果不正确使用缓存同步,这可能会导致问题,例如:

  1. 在多线程环境下,一个线程所做出的修改并不一定会立即反映在另一个线程中。
  2. 如果某个存储器位置在一个线程中更新,而另一个线程在相同存储器位置上尝试读取数据,则读取操作可能返回已过时的值。

因此,在使用volatile关键字声明变量时,需要使用硬件内存栅栏命令以确保对缓存和内存操作的同步。 这将确保所有线程都使用相同的值来访问存储器位置,从而避免了多线程并发访问时可能出现的问题。

volatile如何保证多线程在多级缓存中拿到相同的值的

在多线程环境下,一个线程所作出的修改并不一定会立即反映在另一个线程中,这可能导致多线程程序存在数据竞争或可见性问题。如果变量没有使用volatile关键字进行声明,编译器有可能将该变量的值暂存在CPU缓存或寄存器中,以加快访问速度。当另一个线程读取同一变量时,它可能会从缓存或寄存器中获取过时的值。

而使用volatile关键字声明变量后,可以禁止编译器对该变量的优化,保证每次访问该变量都是从内存中获取,避免了缓存失效的情况。同时,在对volatile变量进行读写操作时,会直接和内存交互,而不是通过缓存,从而保证了多个线程在处于多级缓存中拿到相同的数据。

当一个线程需要修改volatile变量的值时,会首先更新到自己的缓存中,并向处理器发送一条锁总线的命令,这个命令会通知所有其他CPU核心,要求它们把该变量的缓存行设置为无效状态。然后,本根据新的值更新内存,并在总线上发送指示缓存行已被更改的信号。此外,其他线程访问volatile变量时也必须将该变量的缓存行设置为无效状态,并从主内存中重新加载变量的值,从而确保多线程程序中所有线程都能够获得同一个可见的volatile值。

因此,volatile在多线程高并发的情况下,可以使用硬件内存栅栏命令来确保对于缓存和内存操作的同步,从而保证了多个线程在处于多级缓存中拿到相同的数据,避免了出现数据竞争或可见性问题。

volatile 能使得一个非原子操作变成原子操作吗?为什么?

使用volatile关键字仅能保证变量在读写操作时的内存可见性,不能实现原子操作。volatile只提供了线程中对于变量的读写同步,以避免缓存的问题。但是,如果需要多个线程同时修改同一个变量的值,并且确保这些修改不会相互干扰或写入竞争,则需要使用原子操作。

如果两个线程同时访问一个非原子操作的volatile变量,可能会造成写入竞争和读取的情况,导致数据出现异常,使得程序出现意外情况。

例如,假设变量i声明为volatile int类型,并有如下代码:

i = i + 1;

如果两个线程同时执行该代码,它们可能会同时从内存中获取i的值(假设为n),然后将其加1,再将结果写回内存。 如果操作是真正原子的,则两个线程最终都会将该变量增加2,但这是不可能实现的,因为在每个线程更新i的值期间,计算机可能会在寄存器中保持副本,而不是直接访问主内存。 因此,可能会发生数据竞争或其他异常。

要保证多个线程同时修改同一变量的值安全、正确且不产生竞争的情况下,需要使用原子操作,而不是使用volatile关键字。其中的常见原子操作有CAS、Mutex、Semaphore等。

举几个在日常开发中使用volatile的场景,并分析一下使用volatile的原因

使用volatile关键字标识变量可以保证在多线程的并发环境下,对该变量读写操作具有内存可见性,即使在多个线程操作同一个volatile变量时,每个线程都可以看到其他线程对其的读写操作。这种原子性和可见性的特性,使得volatile成为在多线程开发中广泛应用的重要机制。以下是几个日常开发中使用volatile的场景:

  1. 多线程计数器

在多线程计数器中,多个线程同时对计数器进行读写操作,需要防止竞争条件出现而引发数据不一致的问题,此时可以使用volatile标识计数器变量,保证多个线程对该变量的读写操作的原子性和可见性。

  1. 双重检查锁定模式

双重检查锁定模式(Double Checked Locking Pattern)使用volatile提供本地缓存读取于系统内存之间的可见性,从而确保多线程下只有一个对象被创建。该模式先判断对象是否已经存在,如果不存在则对对象加锁并进一步确认,然后再创建并返回该实例对象。

  1. 状态标志位

在程序中,有许多情况下,需要使用状态标志位来表示某些事件/条件是否已经发生或是否应该执行某些操作。如果状态标志位不使用volatile关键字修饰,则并发访问该标志位的多线程可能会产生不可预料的结果。例如,在一个启动线程等待一些特殊条件时,当另一个运行线程修改这个状态标志的值时,没有使用volatile可能会导致程序退出循环而停止执行。

以上是几个日常开发中使用volatile的场景及原因分析。总的来说,涉及到多线程操作同一变量的情况,通常需要使用volatile关键字进行声明,以确保对该变量的读写操作具有原子性和可见性,从而避免多线程问题,保证代码的正确性和可靠性。

详细说一下volatile关键字在底层所做的事情,然后分析一下volatile的读屏障和写屏障是什么

volatile关键字在底层主要做两件事情:

  1. 禁止编译器重排代码:编译器可能会对代码进行优化,例如把一些计算操作放到表达式外面去执行,如果没有使用volatile修饰的变量,在多线程中读写可能会被优化放到最里面,这就导致了不同步的问题。所以,使用volatile可以告诉编译器必须按照程序的顺序执行代码,并禁止对代码进行任何优化操作。

  2. 强制从主存(内存)中读写数据:volatile关键字能保证所修饰变量的可见性,当一个线程修改volatile变量时,它会立即被刷回主存,并且其他所有线程都会立即收到通知,以便重新获取该变量的值。此外,volatile还能防止对象或变量实例在创建之后加入缓存等本地副本,而又不能及时与其他线程共享所修改的状态。

在Java虚拟机规范中,为了保障volatile关键字的原子性和有序性而引入了“内存屏障”(MemoryBarrier)概念。内存屏障是一种处理器级别的处理,用于指示编译器在生成指令时需按照内存屏障语义插入指令,以保证程序的正确执行顺序。在Java中,volatile变量的读屏障和写屏障就是靠这种内存屏障来实现的。

  1. 读屏障

volatile修饰的变量的读操作,在用数值前,总是先要从主内存中刷新最新的值到本地缓存,然后再读取本地缓存中的数据。这种从主内存刷新到缓存的操作称为“读屏障”。

  1. 写屏障

volatile变量的写操作,总是要先将改变后的值同步回主内存,再执行其他指令。这种把值同步回主内存的操作称为“写屏障”。

通过读屏障和写屏障的使用,能够保证读取的变量值总是当前变量的最新值,同时对修改的变量值也可以立即反应到所有线程中。 Moreover,对volatile变量进行读或写操作时,每一个线程都不会将它自己的线程缓存里的数据保存起来。而是直接去主存当中获取数据,并且立即更新相应线程的高速缓存,所以一旦线程修改了共享变量,其它线程能够马上得知并立即更新。