深入理解Java中的volatile关键字-HollisChuang’s Blog
volatile
volatile这个关键字,不仅仅在Java语言中有,在很多语言中都有的,而且其用法和语义也都是不尽相同的。尤其在C语言、C++以及Java中,都有volatile关键字。都可以用来声明变量或者对象。
volatile关键字在C与java语言作用
C语言
(1)阻止编译器为了提高速度将- -个变量缓存到寄存器内而不写回。
(2)阻止编译器调整操作volatile变量的指令顺序。
注意:无法阻止CPU动态调度换序。
以下是Java语言中的volatile关键字功能
::(1)volitle能保证可见性,也就是说只要一个线程对共享变量修改了,其他线程都能立即看到。::
::(2)volitle的第二条语义:禁止指令重排序(注意包含编译器和cpu)::
可见性
Java代码:
volatile Singleton instance = new Singleton();//instance是volatile变量
汇编代码:
0x01a3de1d: movb 0x0,(%esp);
有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
- 将当前处理器缓存行的数据会写回到系统内存。
- 这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。
指令重排
阻止编译时(编译器)和运行时(cpu)的指令重排
编译时JVM编译器遵循内存屏障的约束,运行时依靠CPU屏障指令来阻止重排。
CPU屏障指令即内存屏障
内存屏障(Memory Barrier)是一种CPU指令
并发之volatile底层原理 - 左手指月 - 博客园
概念
[image:090F27BD-407F-42DB-80CC-5AEAB99BA3C2-1267-0002F5AC3DB6C4AF/20161111081903485.png]
Cache Line
Cache Line可以简单的理解为CPU Cache中的最小缓存单位。目前主流的CPU Cache的Cache Line大小都是64Bytes。假设我们有一个512字节的一级缓存,那么按照64B的缓存单位大小来算,这个一级缓存所能存放的缓存个数就是512/64 = 8个。
false sharing(伪共享)
当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
伪共享(False sharing)和cpu缓存行;网上关于这两个方面的文章也很多,如:
关于CPU Cache — 程序猿需要知道的那些事 ,
7个示例科普CPU CACHE ,
What is @Contended and False Sharing ?
Padding
填充数据结构可以减少”false sharing”,使得线程操作的变量落入到不同的cache line中。
Java 8中引入了一个新注解 @Contented,主要是用来减少“False sharing”,在你需要避免“false sharing”的字段上标记注解,这可以暗示虚拟机“这个字段可以分离到不同的cache line中”。
实现
为了解决cache line带来的false sharing问题,并没有使用AtomicLong,在高并发的单调自增场景,LongAdder提供了比AtomicLong更好的性能。Hystrix和Sentinel都没有直接使用JDK中的LongAdder,copy过来做了简单修改。
不同实现:
sentinel:
com.alibaba.csp.sentinel.slots.statistic.base.LongAdder
com.alibaba.csp.sentinel.slots.statistic.base.Striped64
Hystrix:
com.netflix.hystrix.util.LongAdder
com.netflix.hystrix.util.Striped64
Java8:
java.util.concurrent.atomic.LongAdder
java.util.concurrent.atomic.Striped64
Striped64中使用了一个叫Cell的类,是一个普通的二元算术累积单元,线程也是通过hash取模操作映射到一个Cell上进行累积。为了加快取模运算效率,也把Cell数组的大小设置为2^n,同时大量使用Unsafe提供的底层操作。基本的实现桶1.7的ConcurrentHashMap非常像,而且更简单。
com.alibaba.csp.sentinel.slots.statistic.base.Striped64中Cell实现
static final class Cell {
volatile long p0, p1, p2, p3, p4, p5, p6;
volatile long value;
volatile long q0, q1, q2, q3, q4, q5, q6;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = getUnsafe();
Class> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
说明:这里本质上是让上面的value单独拥有一个缓存行,因为一个缓存行的大小一般是64字节,而value是long类型,占用8个字节,所以在其前后分别加7个Long类型的填充数,那么value一定会单独占用一个缓存行,本质是采用了空间换时间。
但是这种方式毕竟不通用,例如32、64位操作系统的缓存行大小不一样,因此JAVA8中就增加了一个注@sun.misc.Contended解用于解决这个问题,由JVM去插入这些变量。
java.util.concurrent.atomic.Striped64Cell实现
@sun.misc.Contended static final class Cell {
volatile long value;
Cell(long x) { value = x; }
final boolean cas(long cmp, long val) {
return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
}
// Unsafe mechanics
private static final sun.misc.Unsafe UNSAFE;
private static final long valueOffset;
static {
try {
UNSAFE = sun.misc.Unsafe.getUnsafe();
Class> ak = Cell.class;
valueOffset = UNSAFE.objectFieldOffset
(ak.getDeclaredField("value"));
} catch (Exception e) {
throw new Error(e);
}
}
}
从AtomicLong到LongAdder,LongAdder的原理 - 进化的深山猿 - CSDN博客
引用
Java多线程——AtomicLong LongAdder源码解析-云栖社区-阿里云
【转载】从LongAdder看更高效的无锁实现 -
图文CPU Cache and Memory Ordering(修改版) - 百度文库