volatile关键字使用

volatile关键字主要是使变量在多线程间可见,它与java内存模型有着密不可分的关系,因此本文需要先简单介绍一下内存内存模型相关的知识。

一、内存模型的相关概念

计算机在执行程序时,每条指令都是在CPU中执行的,在这个过程中,存在着数据的读取与写入。由于程序运行过程中的临时数据是存放在主存(物理内存)当中的,这时就存在一个问题,由于CPU执行速度很快,而从内存读取数据和向内存写入数据的过程跟CPU执行指令的速度比起来要慢的多,因此如果任何时候对数据的操作都要通过和内存的交互来进行,会大大降低指令执行的速度。因此在CPU里面就有了高速缓存。

  也就是,当程序在运行过程中,会将运算需要的数据从主存复制一份到CPU的高速缓存当中,那么CPU进行计算时就可以直接从它的高速缓存读取数据和向其中写入数据,当运算结束之后,再将高速缓存中的数据刷新到主存当中。

例如这段代码 
  i = i + 1;

当线程执行这个语句时,会先从主存当中读取i的值,然后复制一份到高速缓存当中,然后CPU执行指令对i进行加1操作,然后将数据写入高速缓存,最后将高速缓存中i最新的值刷新到主存当中。

        这个代码在单线程中运行是没有任何问题的,但是在多线程中运行就会有问题。假设同时有两个线程在这行代码,可能会存在这样一种情况:初始化时,两个线程都将i值读入各自的高速缓存中,线程1对变量i进行+1操作,然后把新值写入主存;此时,线程2中高速缓存中的i值为初始值,进行+1操作,再把新值写入主存。这种情况下,i值最终只被进行了一次+1操作,即最终结果并不符合预期,这就是缓存不一致的问题。

        硬件层面上解决办法通常有以下两种:

        1、CPU和其他部件进行通信都是通过总线来进行的,在某个线程访问CPU时,对总线进行加锁,阻止其它CPU对变量读取,直到加锁代码段执行完毕之后,其它CPU才能对变量所在内存进行读取。该方法效率比较低。

        2、通过缓存一致性协议。最出名的就是Intel 的MESI协议,MESI协议保证了每个缓存中使用的共享变量的副本是一致的。它核心的思想是:当CPU写数据时,如果发现操作的变量是共享变量,即在其他CPU中也存在该变量的副本,会发出信号通知其他CPU将该变量的缓存行置为无效状态,因此当其他CPU需要读取这个变量时,发现自己缓存中缓存该变量的缓存行是无效的,那么它就会从内存重新读取。

二、并发编程的三个概念

        1、原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

        假设操作不具备原子性,若某线程中32位赋值操作进行到一半被中断,其它进程去读取改变量的值时,就会出现错误。

        2、可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。在第一部分中列举的例子就是不满足可见性的后果,导致变量赋值不符合预期目标。

        3、有序性:即程序执行的顺序按照代码的先后顺序进行。

int i = 0;               
boolean flag = false;
i = 1;                //语句1   
flag = true;          //语句2

        从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

        指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性。

//线程1:
context = loadContext();   //语句1
inited = true;           //语句2
 
//线程2:
while
(!inited ){  
    sleep() 
}
doSomethingwithconfig(context);

        由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

        也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

三、java内存模型

        在Java虚拟机规范中试图定义一种Java内存模型(Java Memory Model,JMM)来屏蔽各个硬件平台和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致的内存访问效果。

 Java内存模型规定所有的变量都是存在主存当中(类似于前面说的物理内存),每个线程都有自己的工作内存(类似于前面的高速缓存)。线程对变量的所有操作都必须在工作内存中进行,而不能直接对主存进行操作。并且每个线程不能访问其他线程的工作内存。

        

        1、原子性:Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

        2、可见性:java使用volatile关键字实现变量的可见性。当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

  而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

  另外,通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

        3、有序性:虚拟机可能会对程序代码进行指令重排序。虽然进行重排序,但是最终执行的结果是与程序顺序执行的结果一致的,它只会对不存在数据依赖性的指令进行重排序。因此,在单个线程中,程序执行看起来是有序执行的,这一点要注意理解。事实上,这个规则是用来保证程序在单线程中执行结果的正确性,但无法保证程序在多线程中执行的正确性。

四、volatile关键字

1、如何保证可见性?

假如线程1先执行,线程2后执行:

//线程1
boolean stop = false;
while
(!stop){    
    doSomething();
}
//线程2
stop = true;

 (1)程序启动之后,线程1正常执行循环,线程对stop变量赋值之后,volatile关键字会强制将修改的值立即写入主存;

(2)当线程2进行修改时,会导致线程1的工作内存中缓存变量stop的缓存行无效

  (3)由于线程1的工作内存中缓存变量stop的缓存行无效,所以线程1再次读取变量stop的值时会去主存读取

2、有序性

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2
 .......			//语句3
//线程2:
while(!inited ){
  sleep()
}
doSomethingwithconfig(context);

        由于inited变量为volatile变量,那么在进行指令重排序的过程的时候,不会将语句2放到语句1前面,也不会放到语句3后面。但是要注意语句3之后和语句1之前部分的顺序是不作任何保证的;并且volatile关键字能保证,执行到语句2时,语句1必定是执行完毕了的,且语句1的执行结果对语句2/3是可见的。

        前面提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

3、原子性

volatile只能保证可见性,但是不能保证原子性。如下代码:

例如这段代码 
  i = i + 1;

        这段操作属于增操作,当两个线程同时执行此段代码时,线程1首先读取到该变量,然后被阻塞;线程2开始读取该变量,由于线程1只是对变量i进行读取操作,而没有对变量进行修改操作,所以不会导致线程2的工作内存中缓存变量i的缓存行无效,所以线程2会直接去主存读取i的值,然后进行加1操作,并把(初始值+1)写入工作内存,最后写入主存。

  然后线程1接着进行加1操作,如果,线程1尚未读取变量的值,那么它就能看到线程2对变量i做的最新修改,由于已经读取了i的值,此时在线程1的工作内存中inc的值仍然为初始值, 所以线程1对i进行加1操作后,i的值为(初始值+1),然后将它写入工作内存,最后写入主存。

  那么两个线程分别进行了一次自增操作后,i变量只增加了1。

4、关键字原子性小试验

public class VolatileNoAtomic extends Thread{
	//private static volatile int count;				/**volatile使用***/
	private static AtomicInteger count = new AtomicInteger(0);	/**AtomicInteger使用***/
	private static void addCount(){
		for (int i = 0; i < 1000; i++) {
			//count++ ;					/**volatile使用***/
			count.incrementAndGet();			/**AtomicInteger使用***/
		}
		System.out.println(count);
	}
	public void run(){
		addCount();
	}
	
	public static void main(String[] args) {
		
		VolatileNoAtomic[] arr = new VolatileNoAtomic[100];
		for (int i = 0; i < 10; i++) {
			arr[i] = new VolatileNoAtomic();
		}
  //模拟10个线程对变量进行操作		
		for (int i = 0; i < 10; i++) {
			arr[i].start();
		}
	}
}

        正常来说,cout最终值应该10000,但是使用volatile关键字打印结果可能如下

2372
4372
3372
2372
6620
2372
7304
5372
8304
9304

        由上可以看出,volatile关键字并不能保证操作的原子性,使用AtomicInteger类一般可避免此问题。当然,使用synchronized和ReentrantLock显示锁同样能够保证操作的原子性。

参考文章如下:

Java并发编程:volatile关键字解析 - Matrix海子 - 博客园

http://jiangzhengjun.iteye.com/blog/652532http://blog.sina.com.cn/s/blog_7bee8dd50101fu8n.html聊聊并发(一)深入分析Volatile的实现原理 | 并发编程网 – ifeve.com     深入理解Java内存模型之系列篇_gate1001的博客-优快云博客

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

皓月星辰_w

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值