以前的文章,咱们讲到volatile的一些做用数组
今天咱们来分析一下volatile的具体用法和内存可见性/读写原子性的实现原理缓存
咱们先来看看volatile的使用场景安全
可使用volatile的状况包括:并发
咱们来经过2个例子说明这些状况性能
class VolatileFeatures { long vl = 0L; // 64位的long型普通变量 //对单个的普通 变量的写用同一个锁同步 public synchronized void set(long l) { vl = l; } public void getAndIncrement () { //普通方法调用 long temp = get(); //调用已同步的读方法 temp += 1L; //普通写操做 set(temp); //调用已同步的写方法 } public synchronized long get() { //对单个的普通变量的读用同一个锁同步 return vl; } }
上面的例子中对值的修改须要依赖当前值,可是当前值可能会同时修改,从而出错测试
public class NumberRange { private volatile int lower = 0; private volatile int upper = 10; public int getLower() { return lower; } public int getUpper() { return upper; } public void setLower(int value) { if (value > upper) throw new IllegalArgumentException(...); lower = value; } public void setUpper(int value) { if (value < lower) throw new IllegalArgumentException(...); upper = value; } }
上述代码中,上下界初始化分别为0和10,假设线程A和B在某一时刻同时执行了setLower(8)和setUpper(5), 且都经过了不变式的检查,设置了一个无效范围(8, 5),优化
因此在这种场景下是没法保证线程安全的,须要经过sychronize保证方法setLower和setUpper在每一时刻只有一个线程可以执行。this
简单的来讲写入volatile 变量的这些有效值须要独立于任何程序的状态,包括变量的当前状态.net
常见的使用场景线程
public class ServerHandler { private volatile isopen; public void run() { if (isopen) { } else { } } public void setIsopen(boolean isopen) { this.isopen = isopen } }
在并发场景中经过volatile来控制isopen在控制线程的执行逻辑
当只有一个线程能够修改字段的值,其它线程能够随时读取,那么把字段声明为volatile也是合理的。
须要注意的是声明一个引用变量为volatile,不能保证经过该引用变量访问到的非volatile变量的可见性。同理,声明一个数组变量为volatile不能确保数组内元素的可见性。volatile的特性不能在数组内传递,由于数组里的元素不能被声明为volatile
下面咱们来分析一下volatile的实现原理,如何保证内存的可见性和读写的原子性
咱们经过观察volatile变量和普通变量所生成的汇编代码能够发现,操做volatile变量会多出一个lock前缀指令:
Java代码: private volatile Singleton instance = new Singleton(); 汇编代码: 0x01a3de1d: movb $0x0,0x1104800(%esi); 0x01a3de24: **lock** addl $0x0,(%esp);
在这里这个lock前缀指令,提供了如下保证:
CPU为了提升处理性能,并不直接和内存进行通讯,而是将内存的数据读取到内部缓存再进行操做,但操做完并不能肯定什么时候写回到内存,但对volatile变量进行写操做,当CPU执行到Lock前缀指令时,会将这个变量所在缓存行的数据写回到内存,但其它CPU缓存的仍是旧值,因此为了保证各个CPU的缓存一致性,每一个CPU经过检测在总线上传播的数据来检查本身缓存的数据有效性,当发现本身缓存行对应的内存地址的数据被修改,就会将该缓存行设置成无效状态,当CPU读取该变量时,发现所在的缓存行被设置为无效,就会从新从内存中读取数据到缓存中。
这里能够参考咱们以前的文章对cpu的原子性实现的分析
在使用volatile的时候咱们还会遇到伪共享的问题
那么什么是伪共享问题
咱们首先要知道,cpu缓存加载的时候一次性最少会加载64个字节(64位处理器),这意味着若是一个队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每一个处理器都会缓存一样的头尾节点,当一个处理器试图修改头接点时会将整个缓存行锁定,那么在缓存一致性机制的做用下,会致使其余处理器要从新加载缓存,而队列的入队和出队操做是须要不停修改头接点和尾节点,因此在多处理器的状况下将会严重影响到队列的入队和出队效率。咱们可使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头接点和尾节点加载到同一个缓存行,使得头尾节点在修改时不会互相锁定,从而解决伪共享的问题。
下面咱们经过一个例子来讲明这个问题
/** * 伪共享优化 * */ public final class FalseSharing implements Runnable { public static int NUM_THREADS = 4; // change public final static long ITERATIONS = 500L * 1000L * 1000L; private final int arrayIndex; private static VolatileLong[] longs; public FalseSharing(final int arrayIndex) { this.arrayIndex = arrayIndex; } public static void main(final String[] args) throws Exception { Thread.sleep(1000); System.out.println("starting...."); if (args.length == 1) { NUM_THREADS = Integer.parseInt(args[0]); } longs = new VolatileLong[NUM_THREADS]; for (int i = 0; i < longs.length; i++) { longs[i] = new VolatileLong(); } final long start = System.currentTimeMillis(); runTest(); System.out.println("duration = " + (System.currentTimeMillis() - start)); } private static void runTest() throws InterruptedException { Thread[] threads = new Thread[NUM_THREADS]; for (int i = 0; i < threads.length; i++) { threads[i] = new Thread(new FalseSharing(i)); } for (Thread t : threads) { t.start(); } for (Thread t : threads) { t.join(); } } public void run() { long i = ITERATIONS + 1; while (0 != --i) { longs[arrayIndex].value = i; } } @Contended //JDK8 默认支持自动填充加上这个注解 而且加上虚拟机参数-XX:-RestrictContended public final static class VolatileLong { public volatile long value = 0L; //64位系统默认对象头12字节(开启压缩) 补充10个字节的无用对象让缓存行共享失效 public long p1, p2, p3, p4, p5, p6,p7,p8,p9,p10; // 这行代码注释掉速度就慢很多 } }
分别测试添加一些无用字节来填充缓存行,和不填充,发现速度上差了很多,这就是伪共享带来的问题
那么是否是在使用Volatile变量时都应该追加到64字节呢?在两种场景下不该该使用这种方式。
第一:缓存行非64字节宽的处理器,如P6系列和奔腾处理器高速缓存行是32个字节宽。
第二:共享变量不会被频繁的写。由于使用追加字节的方式须要处理器读取更多的字节到高速缓冲区,这自己就会带来必定的性能消耗,共享变量若是不被频繁写的话,锁的概率也很是小,就不必追加字节
本文咱们探讨了volatile的具体用法和 volatile经过CPU的Lock指令来保证内存可见性/读写原子性的实现原理 咱们还讨论了缓存行引发的伪共享问题和解决方案,
其中省略了volatile引发的重排序内容等咱们将在后面的JVM内存模型文章中继续探讨