那怎么保证程序里一个线程对共享变量的修改能立马被其他线程看到了?这时候有人会说了,加锁呀,前面不就是因为加锁成本太高才使用的 ThreadLocal的吗?怎么又说回去了?
其实CPU每个核心也都是有缓存的,今天要讲的volatile能保证变量在多线程间的可见性,本文我们会对变量可见性、指令重排、Happens Before 原则以及 Volatile 对这些特性提供的支持和在程序里的使用进行讲解,本文大纲如下:
变量的可见性
一个线程对共享变量的修改,另外一个线程能够立刻看到,称为变量的可见性。
在单核系统中,所有的线程都是在一颗 CPU 上执行,CPU 缓存与内存的数据一致性容易解决。但是多核系统中,每颗 CPU 都有自己的缓存,这时 CPU 缓存与内存的数据一致性就没那么容易解决了,当多个线程在不同的 CPU 上执行时,这些线程操作的是不同的 CPU 缓存。
比如下图中,线程 A 操作的是 CPU-1 上的缓存,而线程 B 操作的是 CPU-2 上的缓存,很明显,这个时候线程 A 对变量 V 的操作对于线程 B 而言不具备可见性。
Java 里可以使用 volatile 关键字修饰成员变量,来保证成员在线程间的可见性。读取 volatile 修饰的变量时,线程将不会从所在CPU的缓存,而是直接从系统的主存中读取变量值。同理,向一个 volatile 修饰的变量写入值的时候,也是直接写入到主存。
下面我们再来看一下,当不使用 volatile 时,多线程使用共享变量时的可见性问题。
Java 变量的可见性问题
Java 的 volatile 关键字能够保证变量更改的跨线程可见,在一个多线程应用程序中,为了提高性能,线程会把变量从主存拷贝到线程所在CPU信息的缓存上再操作。如果程序运行在多核机器上,多个线程可能会运行在不同的CPU 上,也就意味着不同的线程可能会把变量拷贝到不同的 CPU 缓存上。
因为CPU缓存的读写速度远高于主存,所以线程会把数据从主存读到 CPU 缓存,数据的更新也是是先更新CPU 缓存中的副本,再刷回主存,除非有(汇编指令)强制要求否则不会每次更新都把数据刷回主存。
对于非 volatile 修饰的变量,Java 无法保证 JVM 何时会把数据从主存读取到 CPU 缓存,或将数据从 CPU 缓存写入主内存。