volatile
- 定义:
轻量级的线程同步(sychronized)关键字; - 特点:
1.内存可见性:对于线程共享变量,一个线程的修改,另一个线程可以直接获取到修改过的值。
2.禁止指令的重排序(有序性); - 优势:
如果volatile变量修饰符使用恰当的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。
Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的。 - volatile如何保证可见性???
例:
统计一秒内可以进行多少次加的操作。
public class VolatileDemo0409 {
static boolean flag = false;
static class subThread extends Thread{
@Override
public void run() {
System.out.println("子线程开始执行");
int i = 0;
while (!flag){
i ++;
}
System.out.println("子线程结束 i:"+ i);
}
}
public static void main(String[] args) {
System.out.println("主线程开始执行");
new subThread().start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
flag = true;
System.out.println("主线程结束");
}
}
output:
可见,程序一直没有结束,当程序运行时,JVM会给每个任务执行的线程分配一个独立的缓存空间,用于提高效率。
下面来阐述一下为什么程序中断,且运行一直没有停下来,每个线程在运行过程中都有自己的工作内存,那么主线程在运行的时候,会将stop变量的值拷贝一份放在内存当中,之后将启动子线程,将flag更改,但是子线程就没有接收到flag更改的信息,所以会一直循环下去。
使用volatile修饰flag后,有什么不一样??
static volatile boolean flag = false;
看程序的运行结果:
可以看到子线程接收到了flag的改变,并正确打印出我们要的结果,接着通过对代码的反编译:在.java编译生成的.class文件中,在字节码文件中加了volatile修饰的变量会加上flags:ACC_VOLATILE,在底层汇编语言上就是讲该变量前添加#Lock前缀。
如下:
Lock前缀的指令在多核处理器下会引发了两件事情。
1)将当前处理器缓存行的数据写回到系统内存。
2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。
拓展
在早期的CPU当中,是通过在总线上加LOCK#锁的形式来解决缓存不一致的问题。因为CPU和其他部件进行通信都是通过总线来进行的,如果对总线加LOCK#锁的话,也就是说阻塞了其他CPU对其他部件访问(如内存),从而使得只能有一个CPU能使用这个变量的内存。由于在锁住总线期间,其他CPU无法访问内存,导致效率低下。
所以就出现了缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。
具体实现:
- 使用volatile关键字会强制将修改的值立即写入主存;
- 使用volatile关键字的话,当主线程进行修改时,会导致子线程的工作内存中缓存变量flag的缓存行无效(反映到硬件层的话,就是CPU的L1或者L2缓存中对应的缓存行无效);
- 由于子线程的工作内存中缓存变量flag的缓存行无效,所以子线程会在主存中去再次读取变量flag的值。
- 总结volatile作用:
(1)写过程:
1.将修改后的内容写到cache(工作内存)中,当前变量是volatile修饰,会立即写入到主内存;
2.其他线程的工作内存检测总线(一致性协议)上变量有被修改,则置为无效;
(2)读过程:
1.其他线程在读取时,在自己的工作内存中先检测该变量是否有效,
有效时(当前变量其他线程没有修改),则直接使用工作内存中的内容,
无效时(其他线程对该变量做了修改),则直接在主内存中读取最新的内容到工作内存。
- volatile劣势:
1.频繁更改、改变或写入volatile字段有可能导致性能低下。
2.限制现代JVM的JIT编译器对这个字段优化(volatile字段必须遵守一定顺序,但这也是优点,或者说是特点吧!本来就是要保证happen-before,放置顺序指令重排导致bug 例如单例双检测bug)
—解决办法:
减少对volatile的写操作;重构避免使用volatile。