JAVA虚拟机--volatile详解

最近在阅读放腾飞的《并发编程艺术》这本书,里面讲的内容比较深入,觉得不错。不过排版上不太适合个人的思维习惯,故作一番整理,这次讲讲volatile。

volatile的定义与实现原理

Java规范3版对volatile定义如下:Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排她所单独获得这个变量。

这个定义就是说,如果线程A和B,如果某个字段被声明称volatile,对于线程A和B看到的变量值是一样的,即保证volatile变量在多个线程的可见性。

如何保证该可见性呢?这是通过Java线程内存模型确保的。

简单讲讲内存模型(以下称JMM)的抽象结构:


JMM决定一个线程对共享变量的写入何时对另一个线程可见。JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存中,每个线程都有一个私有的本地内存,本地内存中存储了该线程以读/写共享变量的副本。本地变量是什么?这个听起来比较虚哈哈,其实具体来说是缓存,写缓冲区,寄存器以及其他的硬件和编译器优化,那么缓存,写缓冲区关系又是啥呢?借用知乎一个答案,就是:

             

那么寄存器和缓存,缓冲区的关系是什么呢?其实这一切的一切都是为了CPU能够快速正确的运行服务的,CPU获取的数据都是从寄存器那里取来的,然鹅寄存器数量又很少(比如ARM v7,寄存器只有37个,每个的大小也就32bit),所以就要从内存里拿东西,我们都知道内存可以很大(4G 8G内存),但是CPU要通过总线去获取内存数据,这样导致速度就比较慢,为了加快速度,于是就来了缓存这个东西,所以寄存器会去缓存(L1,L2,L3 级)拿数据,然鹅缓存也不能存比较多的东西,一般也就是几兆,所以内核就搞了个叫做缓存命中的概念,先在缓存上拿数据,拿不到就去内存上拿。这样一来一去,一个变量就有可能有多个副本,在内存上,在缓存(有的级别缓存每个CPU有自己独立的,有的是多个CPU共享一个缓存)里,在寄存器里(每个CPU有自己的一组寄存器)。缓冲区就是内存的一块数据,目的就是为了处理速度的流平衡,使用Java编程什么时候会用到缓冲区呢,就是在用到NIO的时候会用到缓冲区(觉得不对的话可以在留言处说下,谢谢了)。对于硬件优化是什么鬼,我也不太明白,请各位大神讲讲哈哈。

讲了那么多,就想详细说下为什么要引入volatile。现在回过来继续说说volatile。


在X86处理器下,我们来看看volatile进行写操作,CPU会做什么事情。

Java代码:

instance = new Singleton();

汇编代码:

0x01a3de1d: movb $0x0, 0x1104800(%esi); 

0x01a3de24: lock add $0x0, (%esp);

我们可以知道volatile进行写操作时候多出了第二行汇编代码,Lock前缀的指令在多核处理器下会引发两件事:

1)将当前处理器缓存行数据写回系统内存(可以通过锁总线或者锁缓存行来保证操作)

2)协会操作会使得其他CPU里缓存了该内存地址的数据无效(保证一致)


volatile的使用优化

JDK7的并发包里增加了LinkedTransferQueue,它在使用volatile变量时候,以追加字节的方式优化出队入队的性能。为啥要追加字节呢?因为这样可以优化性能,因为对于很多芯片处理器的L1,L2或者L3的高速缓存行是64个字节宽,不支持部分填充缓存行,所以如果队列的首尾节点都不足64字节的话,处理器会将他们读到同一个缓存行中,那么如果被锁住的话就会很影响效率。


volatile内存语义

当声明了共享变量volatile后,这个变量的读写会比较特别。理解其特性可以把它当作是使用同一个锁对这些单个读写进行了同步操作。看代码:

class VolatileFeaturesExample {
	long v1 = 0L;
	public void set(long 1) {
		v1 = l;
	}
	
	public void getAndIncrement() {
		v1 ++;
	}
	
	public long get() {
		return vl;
	}
}
class VolatileFeatureExample {
	long vl = 0L;
	
	public synchronized void set(long l) {
		v1 = l;
	}
	
	public void getAndIncrement() {
		long temp = get();
		temp += 1L;
		set(temp);
	}
	
	public synchronized long get() {
		return v1;
	}
}

上面示例程序执行效果相同。多个volatile操作整体上不具有原子性。

volatile写-读建立的happens-before关系
class VolatileExample() {
	int a = 0;
	volatile boolean flag = false;
	
	public void writer() {
		a = 1;
		flag = true;
	}
	
	public void reader() {
		if(flag) {
			int i = a;
		}
	}
}

假设Thread A 执行writer(),Thread B执行reader()方法。根据happens-before(以下简称hb)规则,可以分为3类:

1)根据程序次序规则,1 hb 2; 3 hb 4

2)根据volatile规则,2 hb 3。

3)根据hb传递行,1 hb 4。

volatile 写读的内存语义

还是上面的代码,JMM的过程如下


volatile内存语义实现

JMM采取保守策略对volatile读写插入内存屏障:

1)每个 volatile 写前插入 StoreStore屏障

2)每个 volatile 写后插入 StoreLoad屏障

3)每个 volatile 读后插入 LoadLoad屏障

4)每个 volatile 读后插入 LoadStore屏障

由于volatile仅保证对单个volatile变量的读写具有原子性,而锁的互斥执行的特性可以确保对整个临界区代码的执行具有原子性。功能上,锁更强大;在可伸缩性和执行性能上,volatile更有优势。

接下来有时间会写写Java中的锁。。。




  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值