简单说说volatile关键字实现的机制吧?

本文探讨了Volatile变量在Java并发编程中的作用及其优化方式,解析了Volatile变量如何确保可见性和轻量级同步,并介绍了通过追加字节填充缓存行来提升并发效率的方法。

    volatile变量用来实现并发过程中遇到的读写原子性的实现,不同线程对申明volatile变量的写后,其他线程对该变量的读为最新的值,即可见性。但是volatile不保证复合操作(如i++类似的依赖原值得操作)的线程安全。在JSR133中定义了happends-before原则,对volatile变量的写happends-before于对该变量的读。volatile相对于synchronized实现锁的同步是属于轻量级的同步。

 

参考资料:

同步与Java内存模型(一)序言

Java内存模型没有具体讲述前面讨论的执行策略是由编译器,CPU,缓存控制器还是其它机制促成的。甚至没有用开发人员所熟悉的类,对象及方法来讨论。取而代之,Java内存模型中仅仅定义了线程和内存之间那种抽象的关系。众所周知,每个线程都拥有自己的工作存储单元(缓存和寄存器的抽象)来存储线程当前使用的变量的值。Java内存模型仅仅保证了代码指令与变量操作的有序性,大多数规则都只是指出什么时候变量值应该在内存和线程工作内存之间传输。这些规则主要是为了解决如下三个相互牵连的问题:

  1. 原子性:哪些指令必须是不可分割的。在Java内存模型中,这些规则需声明仅适用于-—实例变量和静态变量,也包括数组元素,但不包括方法中的局部变量-—的内存单元的简单读写操作。
  2. 可见性:在哪些情况下,一个线程执行的结果对另一个线程是可见的。这里需要关心的结果有,写入的字段以及读取这个字段所看到的值。
  3. 有序性:在什么情况下,某个线程的操作结果对其它线程来看是无序的。最主要的乱序执行问题主要表现在读写操作和赋值语句的相互执行顺序上。

聊聊并发(一)深入分析Volatile的实现原理

为什么要使用Volatile

Volatile变量修饰符如果使用恰当的话,它比synchronized的使用和执行成本会更低,因为它不会引起线程上下文的切换和调度。

Volatile的使用优化

著名的Java并发编程大师Doug lea在JDK7的并发包里新增一个队列集合类LinkedTransferQueue,他在使用Volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。

追加字节能优化性能?这种方式看起来很神奇,但如果深入理解处理器架构就能理解其中的奥秘。让我们先来看看LinkedTransferQueue这个类,它使用一个内部类类型来定义队列的头队列(Head)和尾节点(tail),而这个内部类PaddedAtomicReference相对于父类AtomicReference只做了一件事情,就将共享变量追加到64字节。我们可以来计算下,一个对象的引用占4个字节,它追加了15个变量共占60个字节,再加上父类的Value变量,一共64个字节。

/** head of the queue */
private transient final PaddedAtomicReference<QNode> head;

/** tail of the queue */
private transient final PaddedAtomicReference<QNode> tail;

static final class PaddedAtomicReference <T> extends AtomicReference <T> {

  // enough padding for 64bytes with 4byte refs
  Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;

  PaddedAtomicReference(T r) {

    super(r);

  }

}

public class AtomicReference <V> implements java.io.Serializable {

  private volatile V value;

  //省略其他代码

}

为什么追加64字节能够提高并发编程的效率呢? 因为对于英特尔酷睿i7,酷睿, Atom和NetBurst, Core Solo和Pentium M处理器的L1,L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,这意味着如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作是需要不停修改头接点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定。

那么是不是在使用Volatile变量时都应该追加到64字节呢?不是的。在两种场景下不应该使用这种方式。第一:缓存行非64字节宽的处理器,如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个字节宽。第二:共享变量不会被频繁的写。因为使用追加字节的方式需要处理器读取更多的字节到高速缓冲区,这本身就会带来一定的性能消耗,共享变量如果不被频繁写的话,锁的几率也非常小,就没必要通过追加字节的方式来避免相互锁定。

JSR133中文版.pdf 

它们的定义如下:

 某个管程 m 上的解锁动作 synchronizes-with 所有后续在 m 上的锁定动作 (这里的后续是根据同步顺序定义的)。

 对 volatile 变量 v 的写操作 synchronizes-with 所有后续任意线程对 v 的读操 作(这里的后续是根据同步顺序定义的)。

 用于启动一个线程的动作 synchronizes-with 该新启动线程中的第一个动作。

 线程 T1 的最后一个动作 synchronizes-with 线程 T2 中任一用于探测 T1 是否 终止的动作。T2 可能通过调用 T1.isAlive()或者在 T1 上执行一个 join 动作 来达到这个目的。

 如果线程 T1 中断了线程 T2,T1 的中断操作 synchronizes-with 任意时刻任 何其它线程(包括 T2)用于确定 T2 是否被中断的操作。这可以通过抛出 一个 InterruptedException 或调用 Thread.interrupted 与 Thread.isInterrupted 来实现。

 为每个变量写默认值(0,false 或 null)的动作 synchronizes-with 每个线程 中的第一个动作。 虽然在对象分配之前就为该对象中的变量写入默认值看起来有些奇怪,从 概念上看,程序启动创建对象时都带有默认的初始值。因此,任何对象的 默认初始化操作 happens-before 程序中的任意其它动作(除了写默认值的 操作)。

 调用对象的终结方法时,会隐式的读取该对象的引用。从一个对象的构造 器末尾到该引用的读取之间存在一个 happens-before 边缘。注意,该对象 的所有冻结操作(见 9.2 节)happen-before 前面那个 happens-before 边缘 的起始点。

转载于:https://my.oschina.net/u/914290/blog/801377

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值