1.定义
volatile是一个特征修饰符(type specifier).volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。
volatile用来确保将变量的更新操作通知到其他线程。当把变量声明为volatile类型后,编译器与运行时都会注意到这个变量是共享的,因此不会将该变量上的操作与其他内存操作一起重排序。volatile变量不会被缓存在寄存器或者对其他处理器不可见的地方,因此在读取volatile类型的变量时总会返回最新写入的值。
2.相关概念介绍
2.1 线程同步
线程同步:即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态。
2.2 线程同步的方式
2.2.1 临界区和互斥对象
通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。它并不是核心对象,不是属于操作系统维护的,而是属于进程维护的。
临界区的一种实现就是使用Synchronized关键字实现互斥代码块或者Lock关键字实现。
synchronized(syncObject) {
}
2.2.3 互斥量和事件对象
互斥量跟临界区很相似,只有拥有互斥对象的线程才具有访问资源的权限,由于互斥对象只有一个,因此就决定了任何情况下此共享资源都不会同时被多个线程所访问。当前占据资源的线程在任务处理完后应将拥有的互斥对象交出,以便其他线程在获得后得以访问资源。互斥量比临界区复杂。因为使用互斥不仅仅能够在同一应用程序不同线程中实现资源的安全共享,而且可以在不同应用程序的线程之间实现对资源的安全共享。
信号量对象对线程的同步方式与前面几种方法不同,信号允许多个线程同时使用共享资源 ,这与操作系统中的PV操作相同。它指出了同时访问共享资源的线程最大数目。它允许多个线程在同一时刻访问同一资源,但是需要限制在同一时刻访问此资源的最大线程数目。信号量是在多线程环境中,线程间传递信号的一种方式。
java.util.concurrent包中有Semaphore的实现,可以设置参数,控制同时访问的个数。
事件对象通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作。
2.3 指令重排
指令重新排序是指编译器和处理器为了优化程序性能而对指令序列进行排序的一种手段。重排序需要遵守一定规则:
a.重排序操作不会对存在数据依赖关系的操作进行重排序。
比如:a=1;b=a; 这个指令序列,由于第二个操作依赖于第一个操作,所以在编译时和处理器运行时这两个操作不会被重排序。
b.单线程下程序的执行结果不能被改变
比如:a=1;b=2;c=a+b这三个操作,第一步(a=1)和第二步(b=2)由于不存在数据依赖关系, 所以可能会发生重排序,但是c=a+b这个操作是不会被重排序的,因为需要保证最终的结果一定是c=a+b=3。
重排序在单线程下一定能保证结果的正确性,但是在多线程环境下,可能发生重排序,影响结果,下例中的1和2由于不存在数据依赖关系,则有可能会被重排序,先执行status=true再执行a=2。而此时线程B会顺利到达4处,而线程A中a=2这个操作还未被执行,所以b=a+1的结果也有可能依然等于2。
2.4 Java 内存模型中的可见性、原子性和有序性
可见性:是指线程之间的可见性,一个线程修改的状态对另一个线程是可见的。也就是一个线程修改的结果,另一个线程马上就能看到
在 Java 中 volatile、synchronized 和 final 实现可见性。
原子性:原子是世界上的最小单位,具有不可分割性。Java中指某次操作是不可被分割的。
java的concurrent包下提供了一些原子类,如:AtomicInteger、AtomicLong、AtomicReference等。在 Java 中 synchronized 和在 lock、unlock 中操作保证原子性。
有序性:持有同一个对象锁的两个同步块只能串行执行。
Java 语言提供了 volatile 和 synchronized 两个关键字来保证线程之间操作的有序性。
2.5 缓存一致性原理
为了解决缓存不一致的问题,我们需要一种机制来约束各个核,也就是缓存一致性协议。
可以参考MESI协议。
3. volatile原理
volatile有两种特性:保证此变量对所有的线程的可见性和禁止指令重排序优化。
在变量声明时增加volatile关键字,在编译器便以为机器指令的时候,会增加一个lock前缀的指令。lock前缀的指令在多核处理器下会引发了两件事情:
●将当前处理器缓存行的数据会写回到系统内存。(可见性)
●这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。(可见性)
●它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面。(禁止指令重排的有序性)
处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了volatile变量进行写操作,JVM就会向处理器发送一条lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。这也就保证了线程读取的变量是最新的值。
4. volatile的使用场景
synchronized关键字是防止多个线程同时执行一段代码,那么就会很影响程序执行效率,是重量级锁。而volatile关键字在某些情况下性能要优于synchronized,是轻量级锁。但是要注意volatile关键字是无法替代synchronized关键字的,因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:
1)对变量的写操作不依赖于当前值
2)该变量没有包含在具有其他变量的不变式中
volatile关键字适用的场景:
1.volatile最适用一个线程写,多个线程读的场合。
2.结合使用 volatile 和 synchronized 实现 “开销较低的读-写锁”
3.单例模式的双重检查锁