常常在网上看一些大牛们的博客,从中收获到一些东西的同时会产生一种崇拜感,从而萌发了本身写写博客的念头.然而已经有这个念头好久,却始终不敢下手开始写.今天算是迈出了人生的一大步^_^!java
定义:若是一个字段被声明成volatile,那么java线程内存模型将确保全部线程看到的这个变量的值都是一致的.缓存
从它的定义当中我们也能够了解到volatile具备可见性的特性.但它具体是如何保证其可见性的呢?安全
先看一段JIT编译器生成的汇编指令并发
//Java代码以下 instance = new Singleton(); //这里instance是volatile变量 //反汇编后 0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: lock add1 $0x0,(%esp);
有volatile修饰的变量在进行写操做时会出现第二行反汇编代码,重点在lock这个指令.它有两个目的:ide
这里你们也许会有疑问,有没有可能存在多个cpu一块儿回写数据?测试
答案是不会的.虽然cpu鼓励多个处理器能够有竞争,可是总线会对竞争作出裁决,只会有一个cpu获取优先权.其余处理器会被总线禁止,处于阻塞状态.以下图:线程
对于第二点,其余cpu缓存该地址的数据失效后想要再次使用的话就必须得从主内存中从新读取,这样就能保证再次执行计算时所获取的值是最新的,也能够认为全部CPU的缓存是一致的,这也就证实了volatile修饰的字段是可见的.3d
这里我们先引进一段代码:code
/** * volatile 变量自增运算 * * @author mars */ public class VolatileTest { public static volatile int count = 0; public static void increase() { count++; } private static final int THREAD_COUNTS = 20; public static void main(String[] args) throws InterruptedException { CountDownLatch latch = new CountDownLatch(THREAD_COUNTS); Thread[] threads = new Thread[THREAD_COUNTS]; for (int j = 0; j < THREAD_COUNTS; j++) { threads[j] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } latch.countDown(); } }); threads[j].start(); } //等待全部的线程执行结束 latch.await(); System.out.println(count); } }
这段代码供发起了20个线程,对count变量进行了10000次自增操做,若是volatile修饰的字段在并发下是安全的话,讲道理最终结果都会是200000,但通过测试发现,每次的输出结果都会不同.但具体是什么缘由形成的?blog
其实最主要的问题是出在increase()这个自增方法上,这个操做不是一个原子操做,也就是否是一步就能操做完成的,其中会经历count值入栈,add,出栈,到操做线程缓存,最终到内存等等一系列步骤.当A线程其执行这些指令时,B线程正好将数据同步到了主内存中,此时A线
程栈顶的数据就会变成过时数据,而后A线程就会将较小的值同步到主内存中.
要想运用好volatile修饰符,须要保证运用场景符合下述规则:
例如使用volatile变量来控制并发就很合适:
volatile boolean shutdownWork; public void shutdowm(){ shutdownWork = true; } public void doWork(){ while (!shutdownWork){ //execute task } }
上面这段代码运行结果并没有需依赖shutdownWork的值,可是只要shutdownWork的值一旦通过改变,便会当即被其余全部线程所感知,而后中止执行任务.
在多处理器下,为了保证各个处理器的缓存是一致的,处理器会使用嗅探技术来保证它的内部缓存,系统内存和其余处理器的缓存的数据在总线上保持一致.若是经过嗅探一个处理器来检测其余处理器打算写内存地址,而这个地址当前处于共享状态,那么正在嗅探的处理器将使它的缓存无效,在下次访问相同的内存地址时,强制执行缓存行填充,也就是从内存中从新读取该内存地址指向的值.