一、volatile概念:
关键字volatile可以说是java虚拟机提供的最轻量级的同步机制,但是它并不容易完全被正确、完整地理解。遇到需要处理多线程数据竞争问题的时候一律使用synchronized来进行同步。
volatile关键字的主要作用是使变量在多个线程间可见。多个线程间可见指的是当一条线程修改了这个变量的值,会立即通知到其他的线程。
二、内存模型原子性、可见性、有序性
1、原子性
原子是世界上最小的单位,具有不可分割性。比如i=0(i 非long和double类型)这个操作是不可分割的,那么我们认为这个;操作是原子操作。在比如i++;这个操作实际是a=a+1;是可分割的,所以它不是一个原子操作。非原子操作都会存在线程安全问题,需要我们使用同步技术(sychronized)来让它变成一个原子操作。一个操作是原子操作,那么我们称它具有原子性。java的concurrent包下提供了一些原子类,如AtomicInteger、AtomicLong等。
2、可见性
可见性,是指线程之间的可见性,当一个线程操作了被volatile修饰的共享变量,其他线程能够立即得知这个修改。而普通变量并不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。例如线程A修改了一个普通变量的值,然后向主内存进行回写,另一个线程B在线程A会写完成之后再从主内存进行读取操作,新变量值才会对线程B可见。也就是说会存在“工作内存与主内存同步延迟的线程”。
比如用volatile修饰的变量,就具有可见性,但不具有原子性。
3、有序性
Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”,后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。
Java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字禁止指令重排序,而synchronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。
三、示例
package volatileTest;
public class RunThread extends Thread{
/*volatile*/
private boolean isRunning = true;
private void setRunning(boolean isRunning){
this.isRunning = isRunning;
}
public void run(){
System.out.println("进入run方法..");
int i = 0;
while(isRunning == true){
//一直空轮询
}
System.out.println("线程停止");
}
public static void main(String[] args) throws InterruptedException {
RunThread rt = new RunThread();
rt.start();
Thread.sleep(1000);
rt.setRunning(false);
System.out.println("isRunning的值已经被设置了false");
Thread.sleep(1000);
System.out.println(rt.isRunning);
}
run方法中判断isRunning是否等于true,如果等于true,一直空轮询着,如果等于false,线程停止。输出结果如下:
System.out.println(rt.isRunning);已经变为false;线程陷入了死循环(右上角红色正方形红色正方形一直存在),代码System.out.println("线程停止");从未被执行。这是为何?
分析:
在启动RunThread线程时,变量private boolean isRunning =true;存在于公共堆栈及线程的私有堆栈中。当JVM为server模式时为了线程运行的效率,线程一直在私有堆栈中取得isRunning的值是true。而代码rt.setRunning(false)虽然被执行,更新的却是公共堆栈中的isRunning变量值false,所以一直处于死循环的状态。内存结构如下图所示:
这个问题其实就是私有堆栈中的值和公共堆栈中的值不同步造成的。解决这样问题就要使用volatile关键字了,它的主要作用是当线程访问isRunning这个变量时,强制从公共堆栈中进行读取。
isRunning变量被volatile关键字修饰之后再次运行程序,结果如下:
不再出现死循环的情况,线程停止。此时的内存结果如下图所示
使用volatile关键字增加了实例变量在多个线程之间的可见性。(在这个例子中存在两个线程一个main主线程,一个RunThread线程)。但volatile关键字最致命的缺点是不支持原子性。
四、关键词synchronized和volatile对比
1、关键字volatile是线程同步的轻量级实现,所以volatile性能比synchronized要好,并且volatile只能修饰变量,而synchronized可以修饰方法以及普通代码块。
2、多线程访问volatile不会发生阻塞,而synchronized会出现阻塞。
3、volatile能保证数据的可见性,但不能保证原子性;而synchronized可以保证原子性,也可以间接保证可见性,因为它会将私有内存和公有内存中的数据做同步。
4、关键字volatile解决的是变量在多个线程之间的可见性;而synchronized关键字解决的是多个线程之间访问资源的同步性。
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这连个方面来确保线程安全的。
五、关键字volatile和ThreadLocal对比
比如成员变量a默认为0,如果没有volatile关键字修饰变量a,当线程1把a set为1,其他线程get(a)之后的结果仍为0。当使用volatile修饰a之后,线程1把a set为1,其他线程get(a)之后的结果为 1,这是因为volatile关键字保证了线程的可见性。当使用ThreadLocal,线程1把a set为1,线程2把a set为2。线程1 get(a)为1.线程2 get(a)为2.这是因为ThreadLocal保证了线程隔离性。