volatile关键字和synchronized一样都能够保证线程的同步。
Java语言规范第三版中对volatile的定义如下:
java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。Java语言提供了volatile,在某些情况下比锁更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有线程看到这个变量的值是一致的。
volatile被称为轻量级的 synchronized。同时,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。
一、volatile的特性
补充:
1. Java内存模型(Java Memory Model,JMM)
JMM是由Java虚拟机规范定义的,用来屏蔽掉Java程序在各种不同的硬件和操作系统对内存的访问的差异,这样就可以实现java程序在各种不同的平台上都能达到内存访问的一致性。
在Java中,Java堆内存是存在数据共享的,这些共享数据的通信就是通过JMM来控制的。
JMM决定一个线程对共享数据的写入何时对另一个线程可见。
JMM是一个抽象的结构,它定义了线程和主内存的关系:
- 线程之间的共享变量存储在主内存(Main Memory)中
- 每一个线程都有一个私有的本地内存(Local Memory)
- 本地内存中储存了该线程可以读写变量的副本
由此,我们可以得出如下结论:
- 只有存放在Java堆和方法区中的数据,才会被线程共享,对于其他区是属于线程私有的数据不受JMM的影响
- 线程之间的数据,是不能直接进行数据传递的,一定要经过主内存进行传递
- A线程更新数据 -> 刷新主内存数据 -> B线程读取主线程数据
但是为什么需要内存模型?直接读写内存不可以吗?
主要是因为下面两个原因:
(1)CPU缓存一致性
CPU与内存读写和运算速度不在一个量级,CPU效率会比内存高的多。
为了解决CPU和内存效率差异问题,引入了高速缓存(Cache)和写缓冲区(Write Buffer)等,来作为CPU和内存的传输媒介。但是使用缓冲中读写可能造成数据不一致的问题,为了保证CPU缓存的一致性而引入了JMM。
(2)处理器优化和指令重排
处理器优化:处理器为了优化执行效率,可能会将输入的代码进行乱序执行处理
指令重排:JIT编译过程也可能会对指令进行乱序处理
为了解决这两个问题,需要引入JMM,而不是直接操作内存变量。
2. JMM并发的是三个特性:
(1)原子性
表示不可被中断的一个或一系列操作。一旦开始,就一直运行到结束,中间不会有任何线程切换。
(2)可见性
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
(3)有序性
由于指令的执行,会经过编译器和处理的重排序。有序性是指从指令上的执行结果上看,指令的执行顺序是有序的。
有了上述补充的知识背景,下面我们看一下volatile具备哪些特性。
volatile的特性:
- 互斥性:同一时刻只允许一个线程对变量进行操作。(互斥锁的特点)
- 可见性:线程修改变量的值对其他线程立即可见。
- 有序性:禁止指令的重排序。
注:volatile不保证原子性,但是synchronized、lock可以保证。
(synchronized具有互斥性、原子性、可见性,但是无法保证有序性。它无法保证编译器优化和指令重排)
比如 a=0;(a非long和double类型) 这个操作是不可分割的,那么我们说这个操作时原子操作。再比如:a++; 这个操作实际是a = a + 1;是可分割的,所以他不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(synchronized)来让它变成一个原子操作。
证明可见性代码:
public class VolatileTest {
volatile int a = 1;
volatile int b = 2;
public void change(){
a = 3;
b = a;
}
public void print(){
System.out.println("b="+b+";a="+a);
}
public static void main(String[] args) {
while (true){
final VolatileTest test = new VolatileTest();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.change();
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.print();
}
}).start();
}
}
}
证明不保证原子性代码:
public class VolatileTest {
volatile int i;
public void addI(){ //修改 -> synchronized
i++;
}
public static void main(String[] args) throws InterruptedException {
final VolatileTest test = new VolatileTest();
for (int n = 0; n < 1000; n++) {
new Thread(new Runnable() {
@Override
public void run() {
try {
Thread.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
test.addI();
}
}).start();
}
Thread.sleep(10000); //等待10秒,保证上面程序执行完成
System.out.println(test.i);
}
}
二、volatile的作用
- 线程修改变量的值对其他线程立即可见
- 禁止指令的重排序
三、volatile的原理
(1)可见性实现:
- 修改volatile变量时,会强制将修改后的值刷新到主内存中
- 修改volatile变量后,会导致其他线程工作内存中对应的变量值失效
(2)有序性实现:
通过内存屏障对内存的操作顺序进行限制
四、volatile和synchronized的比较
- 关键字volatile是线程同步的轻量级实现,所以volatile性能肯定比synchronized要好。
- volatile只能修饰变量,而synchronized可以修饰方法、代码块等。
- 多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
- volatile能保证数据的可见性,但不能保证数据的原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公共内存中的数据做同步处理。
- 关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。