volatile底层实现原理

volatile

定义

  • Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获取这个变量。

线程的可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
public class VolatileApplication {

// 当使用volatile时,主线程修改完内存中的flag后,对等线程会从内存中读取到flag被修改后的值,正常结束!
private static /*volatile*/ boolean flag = true;

public static void main(String[] args) throws InterruptedException {

new Thread() {
@Override
public void run() {
while (flag) {

}
System.out.println("对等线程执行完毕");
}
}.start();

Thread.sleep(1000);

flag = false;

System.out.println("主线程执行完毕");
}
}

原理

java源码:

1
instance = new Singleton();		//instance是volatile变量

汇编:

1
0x01a3deld:movb $0✖️0,0✖️1104800(%esi);oxo1a3de24:lock addl $0✖️0,(%esp);

lock指令干了两件事情:

  1. 将当前处理器缓存行的数据写会到系统内存
  2. 这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效

缓存一致性

  • 由于CPU的运行速度远远大于向内存读取的速度,所以为了提高效率,加入了高速缓存机制,即处理器不会直接和内存进行通信,而是先将物理内存的数据读到内部缓存(L1,L2,L3或其他),再进行操作 。保存的一致性就是保证多个缓存中共享数据的一致性,目前主要是通过MESI协议来保证的。

MESI 协议

  • Modified、Exclusive、 Share or Invalid状态而命名的,用来标识每个缓存行的状态(由两个状态位表示),每个缓存行(缓存的数据基本单位)都处于M、E、S和I这四种状态之一。

伪共享

  • 当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
public class T02_CacheLinePadding {

// 不对齐的结果耗时:227ms,对齐的结果耗时:79ms
private static class Padding {
/*volatile*/ long p0, p1, p2, p3, p4, p5, p6;
}

private static class T extends Padding {
volatile long x = 0L;
}


public static T[] arr = new T[2];

static {
arr[0] = new T();
arr[1] = new T();
}

public static void main(String[] args) {
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000L; i++) {
arr[0].x = i;
}
}
});

Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
for (int i = 0; i < 10000000L; i++) {
arr[1].x = i;
}
}
});
long startTime = System.currentTimeMillis();

thread1.start();
thread2.start();
try {
thread1.join();
thread2.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("总耗时:" + (System.currentTimeMillis() - startTime) + "ms");
}
}

避免伪共享

  • 缓存行隔离

    • 利用缓存行在现代计算机中占8字64字节的特性,为避免多线程缓存一致性带来的性能影响,引入了缓存行对齐,在定义对象时,如果对象在内存中不占64字节,就补齐64字节,保证对象中的数据缓存在不同的缓存行中。
  • 缓存行64个字节是CPU同步的基本单位,缓存行隔离会比伪共享效率要高

参考:杂谈 什么是伪共享(false sharing)

Concurrency with LMAX Disruptor – An Introduction

指令重排序

定义

  • CPU内部的一个优化,当CPU同时先后执行两条指令,一条是从内存中读指令,一条是执行操作数据指令,CPU由于读指令更耗时,会对指令进行重新排序,先执行操作数据的指令,再执行读指令,当然是这两条指令的变量必须是没有关联,不会互相影响的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
public class T04_Disorder {

private static int x = 0, y = 0;
private static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {

int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;

Thread one = new Thread(new Runnable() {
@Override
public void run() {

shortWait(100L);
a = 1;
x = b;
}
});

Thread two = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
one.start();
two.start();
one.join();
two.join();

String result = "第" + i + "次 (" + x + "," + y + ")";

if (x == 0 && y == 0) {
System.err.println(result);
break;
} else {
// System.out.println(result);
}
}
}

private static void shortWait(long interval) {
long startTime = System.currentTimeMillis();
long endTime;
do {
endTime = System.currentTimeMillis();
} while (endTime - startTime <= interval);
}
}

问题

  • 对象的半初始化
1
2
3
4
5
6
7
public class T {

int m = 8;
public static void main(String[] args) {
T t = new T();
}
}
  • DCL单例到底需不需要加volatile修饰符

这里的 volatile 关键字主要是为了防止指令重排。原理如下:

singleton = new Singleton();这段代码其实是分为三步:

  • 分配内存空间,t.m=0
  • 初始化对象,执行构造函数,t.m=8
  • singleton 对象指向分配的内存地址

加上 volatile 是为了让以上的三步操作顺序执行,反之有可能第二步在第三步之前被执行就有可能某个线程拿到的单例对象是还没有初始化的,以致于报错。

JSR内存屏障

  • 两条指令间如果加了内存屏障,CPU运行的时候就不能对指令进行重排序。

volitile如何解决指令重排序

  1. volatile i
  2. ACC_VOLATILE
  3. JVM的内存屏障
  4. hotspot实现

查看虚拟机中的C++源码文件如下:

bytecodeinterpreter.cpp

1
2
3
4
5
int field_offset = cache->f2_as_index();
if (cache->is_volatile()) {
if (support_IRIW_for_not_multiple_copy_atomic_cpu) {
OrderAccess::fence();
}

orderaccess_linux_x86.inline.hpp

1
2
3
4
5
6
7
8
9
10
inline void OrderAccess::fence() {
if (os::is_MP()) {
// always use locked addl since mfence is sometimes expensive
#ifdef AMD64
__asm__ volatile ("lock; addl $0,0(%%rsp)" : : : "cc", "memory");
#else
__asm__ volatile ("lock; addl $0,0(%%esp)" : : : "cc", "memory");
#endif
}
}

参考:你应该知道的 volatile 关键字