还在为Volatile烦恼吗?那么请移步Java并发编程数据安全之Volatile篇

本文摘要:
1 Volatile 关键字
2 Volatile 关键字特点
3 Volatile 实现
4 常见面试题

1 Volatile 关键字
并发编程中volatile 可以保证多个线程下 ,某个线程在对共享的变量进行修改后,其它线程可以在使用到改变量时可以获得修改后的变量值,及保证之前线程的操作对之后的线程都可见;

2 Volatile 关键字特点:
Volatile 确保了数据对于线程的可见性:
并发编程下,类似于cpu 的高校缓存,每个线程都有自己私有的工作内存,不同线程的工作内存互相隔离,但是他们都公用一个主内存(JMM模型),Java 内存模型并没有限制执行引擎使用处理器的寄存器或者高速缓存来提升指令执行的速度,也没有限制编译器对指令的重排序,这就可能产生前一个 线程对一个共享变量修改后,对其它线程造成不可见性从而产生数据安全问题。
看下面例子:

public class MyThreadSafe5 implements Runnable{
	static boolean flag= false;
	@Override
	public void run() {
		int i = 0;
		while(! flag) {
			i++;
		}
		System.out.println("thread end i="+i);
	}
	public static void main(String[] args) throws InterruptedException {
		MyThreadSafe5 thread = new MyThreadSafe5();
		new Thread(thread).start();
		Thread.sleep(1000);
		flag =  true;
		
	}
}

上面代码中,很多人中断线程时可能采用这种标记方法,但是这种标记方法真的一定回使得线程中断吗?不一定,也许在大多数情况下这个代码都可以中断线程,但是也有可能线程进入死循环无法中断;
线程无法中断原因:由于每个线程都自己的工作内存,线程1 在执行的时候回将 flag 的值拷贝一份放入自己的工作内存中,当线程2 修改了flag 的值但是还没来得及写入主内存就去做其它事情,那么线程1不知道线程2 修改的flag 值则会一直循环下去;
使用volatile修饰后:
当线程2 修改flag 值后,回立即更新至主内存中,并且使线程1的flag 值失效,当线程1发现flag 生效后回在从主内存中读取flag 的值,这样就保证线程2 对flag 修改的可见性;

volatile 保证了可见性,但是保证了原子性吗?看下面代码案例:

public class MyThreadSafe6 extends Thread{
	volatile static int num = 0  ;
	@Override
	public void run(){
		num ++ ;
	}
	public static void main(String[] args) throws InterruptedException {
		MyThreadSafe6 thread = null ;
		for (int i = 0; i < 1000; i++) {
			thread =  new MyThreadSafe6();
			new Thread(thread).start();
		}
		Thread.sleep(2000);
		System.out.println(num);
	}
}

正常情况下最后输出的num 值应该为 1000,但是多次运行会发现num 的值出现小于1000
在这里插入图片描述
说明 volatile 并没有解决原子性问题:可能有的朋友会有疑问,我使用了volatile 保证了,每次对num 进行修改后对使得线程可见,1000个线程最终的执行结果num 应该回是1000,这里就有一个误区,由于num++ 的操作在jvm中是需要从主内存中获取到值,在进行自增后,在写回到主内存中,这里如果当某两个线程都拿到了num 的值 假设此时num 为100,当其中一个线程如 线程1 在读取100 后进行了阻塞,并没有进行自增操作,所以也不会使得其它线程中的num 失效,然后另外一个线程,线程2对100 增加了1 变为101 ,并写入到主内存,接着阻塞的线程1 执行对num 自增操作,注意此时线程1的num 值仍为100 所以对其操作后变成101 并同步至主内存中i给你,此时这两个线程操作后实际上对num 只加了1 ;根源就出现在自增操作并不是原子操作。

3 Volatile 实现:
(1)volatile 关键字能禁止编译器和cpu 指令的重排序问题;
(2)volatile 关键字强制将工作内存中的变量刷新至主内存中以保证可见性;
Volatile 在jvm 底层是采用内存屏障来实现的,通过汇编代码可以发现,加入volatile 声明的关键字,回多出一个lock 前缀指令,lock 前准指令相当于一个内存屏障,内存屏障会提供3个功能:
(1). 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
(2). 它会强制将对缓存的修改操作立即写入主存;
(3). 如果是写操作,它会导致其他CPU中对应的缓存行无效。

4 常见面试题:
4.1 说说 synchronized 关键字和 volatile 关键字的区别
synchronized关键字和volatile关键字比较
volatile关键字是线程同步的轻量级实现,所以volatile性能肯定比synchronized关键字要好。但是volatile关键字只能用于变量而synchronized关键字可以修饰方法以及代码块。synchronized关键字在JavaSE1.6之后进行了主要包括为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁以及其它各种优化之后执行效率有了显著提升,实际开发中使用 synchronized 关键字的场景还是更多一些。
多线程访问volatile关键字不会发生阻塞,而synchronized关键字可能会发生阻塞
volatile关键字能保证数据的可见性,但不能保证数据的原子性。synchronized关键字两者都能保证。
volatile关键字主要用于解决变量在多个线程之间的可见性,而 synchronized关键字解决的是多个线程之间访问资源的同步性。

4.2 既然CPU有了MESI协议可以保证cache的一致性,那么为什么还需要volatile这个关键词来保证可见性(内存屏障)?或者是只有加了volatile的变量在多核cpu执行的时候才会触发缓存一致性协议?
多核情况下,所有的cpu操作都会涉及缓存一致性的校验,只不过该协议是弱一致性,不能保证一个线程修改变量后,其他线程立马可见,也就是说虽然其他CPU状态已经置为无效,但是当前CPU可能将数据修改之后又去做其他事情,没有来得及将修改后的变量刷新回主存,而如果此时其他CPU需要使用该变量,则又会从主存中读取到旧的值。而volatile则可以保证可见性,即立即刷新回主存,修改操作和写回操作必须是一个原子操作;
正常情况下,系统操作并不会进行缓存一致性的校验,只有变量被volatile修饰了,该变量所在的缓存行才被赋予缓存一致性的校验功能。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值