学过计算机组成原理的一定知道,为了解决内存速度跟不上CPU速度这个问题,在CPU的设计中加入了缓存机制,缓存的速度介于CPU和主存之间。在进行运算的时候,CPU将需要的数据映射一份在缓存中,然后直接操作位于缓存中的数据,操作完毕后再将缓存中的数据写回到主存。这在单线程环境中是没有任何问题的。但是在多线程环境中就大不同了。
假设现在有这样的一个场景:有两个线程thread1和thread2,他们都在操作位于主存上的一个数据int a=2(具体操作为读取a的值并执行一个自增操作)。逻辑上正确的结果:应当是最后a=4。但可能有这样的情况,thread1将a=2从主存映射到自己的工作内存上,自增后变成a=3,在将a=3从工作内存写回到主存之前,thread2也将a=2从从主存映射到自己的工作内存上,也自增后变成a=3。然后两个线程先后将a=3写回到主存上。显然,a=3不是我们想看到的。看,这就是一个常见的缓存一致性问题。两个线程对a的操作结果互不可见,thread1不知道thread2对a进行了自增,thread2也不知道thread1对a进行了自增。在多线程编程中就是会出现这样一致性的问题。(在JMM中,可以知道,内存分为主内存和工作内存,每个线程有自己 的工作内存,他们共享主内存)。
因此我们要办法让线程对共享变量的操作结果互相可见,java语言中的volatile关键字就干了一件这样的事。使用volatile修饰的共享变量,当有线程修改了他的值的时候,他会立即强制将修改的值写回到主存,并通知其他使用该共享变量的线程:他们的缓存区中关于此变量的值已经失效。请重新从主存中读取。
仔细阅读volatile干的事,一共有3点影响:
1 将修改的值强制刷新到主存
2 通知其他相关线程变量已经失效
3 其它线程再使用变量的时候就会重新从主存读取
这就解决了JAVA并发编程中的可见性问题。
可见性:当多个线程访问同一个共享变量的时候,一个线程对该共享变量的修改能够实时的被访问该共享变量的其他线程知晓。
继续说上面的那个例子,如果变量a被使用了volatile修饰,那么在thread1中,当a变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将a=2从从主存映射到缓存上,那么在对a进行自增操作以前,会重新到主存中读取a=3,然后自增到a=4,然后写回到主存。上面的过程很完美,但这样是否保证了a最终的结果一定是4呢?未必。
++这个操作非原子,a=a+1 同样非原子,先读a 然后再写 a
继续说上面的那个例子,如果变量a被使用了volatile修饰,那么在thread1中,当a变为3的时候,就会强制刷新到主存。如果这个时候,thread2已经将a=2从从主存映射到缓存上并且已经做完了自增操作,此时a=3,那么最终主存中a的值为3。
所以,如果我们想让a的最终值是4,仅仅保证可见性是不够的,还得保证原子性。也就是对于变量a的自增操作加锁,保证任意一个时刻只有一个线程对a进行自增操作。可以说volatile是一种“轻量级的锁”,它能保证锁的可见性,但不能保证锁的原子性。
volatile变量的一种典型用法,就是用于那些状态的标记,比如: (这是一个最典型的应用,主要用于单例模式)
- volatile boolean flag=false;
- while(!flag){
- doSomething();
- }
在其他线程中,可能会修改flag的值为true,代表退出循环。如果不使用volatile修饰flag,可能在flag被回收之后,主线程还没收到其值改变的消息。这是volatile的一种典型应用。