原文链接:
一、简介
volatile是Java语言的关键字,用来修饰可变变量(即该变量不能被final
修饰),且必须是至少类内可见。所以它是可以修饰带static
的变量。这我自己下定义。
它是被设计用来修饰被不同线程访问和修改的变量。来自
二、功能
volatile提供了一个高效的同步机制,她在某些情况下可以代替synchronized实现更轻量和高效的同步机制,同时也更为脆弱,更难于掌控。被volatile修饰的变量具有内存可见性,但不具有原子性。至于什么是可见性,前面已经做过简单介绍,接下来我们进一步来看什么是可见性。
1. 内存可见性
首先为什么会出现内存可见性问题呢?
想完全理解这个问题,请自行阅读《深入理解计算机系统》吧!这里简单说一下,每个线程都有它自己的线程上下文,包括栈、栈指针、程序计数器、通用目的寄存器和条件码。所有的运行在一个进程里的共享该进程的整个虚拟地址空间。——来自《深入理解计算机系统》
下面这个说法可能并不严谨,甚至是有误,但对我们理解这个问题有帮忙。
如你所知,所有计算都发生CPU,然而它直接操作主存的效果比较远,不如CPU的缓存区,更远不如寄存器。其次,如上面所有的系统会为每个线程分配自己的线程上下文。在这两个大提前下,可能简化的理解为线程有自己的高速cache,即所有线程操作变量时,都不会直接操作主存。当发生cache miss时,从主存拷贝到cache,这些都是你懂的啦。跟所有的cache一样,都存在一致性的问题。即是正常情况下什么时候发生cache冲刷回主存并不可控。
不正常情况下,退出临界区时即刻强制更新主存。另一种情况,即我们要讨论的volatile。被volatile修饰的变量比较特殊,表示直接操作主存,不需要通过cache。直接要用时直接从主存取(注意取出来还是会把值放在自己的上下文,这点后面需要用到),用完写直接回主存。这就是内存可见性。2. 可不完全替代synchronized
之前整理synchronized的时候忘了讲synchronized怎么实现同步的,在这里顺便带出来吧。
synchronized是通过临界区实现同步的,临界区的同步方式是同一个时间只有最多一个线程进入临界区,也就是说只能保证原临界区具有原子性。这是什么意思呢,先来看一下面例子吧。void barfoo() { new Thread(() -> { for(int v=0; v<100; v++) bar(); }).start(); new Thread(() -> { for(int v=0; v<100; v++) foo(); }).start(); } } int v = 0; void bar() { final int t = v + 1; v++; try { TimeUnit.MILLISECONDS.sleep(RandomUtils.nextInt(10)); } catch (InterruptedException e) { } if(t != v)System.out.println("not match"); } synchronized void foo() { final int t = v + 1; v++; try { TimeUnit.MILLISECONDS.sleep(RandomUtils.nextInt(10)); } catch (InterruptedException e) { } if(t != v)System.out.println("not match"); }
执行barfoo()
的结果打印了not match
。
v
而言不具备原子性,更无法保证能够一致性。 volatile可部分替代synchronized,也就是说在特定条件或者场景下可以替代synchronized。上面我们提到过volatile具有内存可见性,但不具有原子性,而synchronized实际是上能够实现原子性的。这一点是volatile做不到的,也是这种场景下volatile无法代替synchronized。
这一点就不举例了,主要知道什么是原子性和非原子性即可自行实验了。如:a += b
就一个非原子性操作。 三、总结
- 简单的了解了volatile的用法;
- 进一步了解内存可见性和synchronized实现原理;
- volatile与synchronized的差异,以及可代替场景;
- volatile通过内存可见性实现同步,即线程A操作了被volatile修饰的变量之后,线程B立马可能读到线程A的修改结果。
我的另外一篇文章也有介绍: