notesArea/Java/多线程/内存屏障.md

内存屏障

在多线程中,由于指令重排序,会出现这种情况:在 Thread 1 中,步骤 3 被重排序到了步骤 1 和 2 的前面,导致 Thread 2 中的步骤 4 跳出了循环,开始执行步骤 5 和步骤 6,然后果不其然地报了空指针异常。

// Thread 1
dataSource = initDataSource(); // 步骤1
httpClient = initHttpClient(); // 步骤2
initOk = true; // 步骤3

// Thread 2
while(!initOk) { // 步骤4
// waiting...
}
Object data = dataSource.getData() // 步骤5
httpClient.request(data) // 步骤6

要解决上述问题,可以通过给 initOk 添加关键词 volatile 来禁止重排序。

为什么 volatile 能够禁止重排序?这是因为 volatile 通过内存屏障来保证代码执行的有序性。在解释具体的原理前,先介绍下几个常见的内存屏障:

  • LoadLoad:此屏障前的所有读操作必须在此屏障后的读操作前完成
  • LoadStore:此屏障前的所有读操作必须在此屏障后的写操作前完成
  • StoreStore:此屏障前的所有写操作必须在此屏障后的写操作前完成
  • StoreLoad:此屏障前的所有写操作必须在此屏障后的读操作前完成

在了解了上述四种常见的内存屏障之后,我们从 Thread 2 的视角来尝试利用内存屏障解决重排序的问题。对于 Thread 2 来说,当具有标记作用的变量 initOk 的值变为 true 之后,自己本身业务所需要的其余变量(dataSourcehttpClient),按照 Thread 1 中的代码顺序来说,应当全部完成了初始化。要完成这个“应当”,关键就是对于写操作 initOk = true 来说,其余的写操作必须排序在它之间。而想实现这个逻辑,就需要StoreStore屏障:

// Thread 1
dataSource = initDataSource(); // 步骤1
httpClient = initHttpClient(); // 步骤2

// StoreStore
initOk = true; // 步骤3

通过StoreStore屏障,确保了步骤 1 和步骤 2 一定排序在步骤 3 之前。同时,为了保证 volatile 的可见性,即后续可能的读操作能够读取到 volatile 修饰的变量的最新的值,步骤 3 之后的读操作不应该排序到步骤 3 之前,因此,步骤 3 后面还应该加一个StoreLoad屏障。完整的代码如下:

// Thread 1
dataSource = initDataSource(); // 步骤1
httpClient = initHttpClient(); // 步骤2

// StoreStore
initOk = true; // 步骤3
// StoreLoad

在理解了 volatile 如何通过内存屏障来保证对写操作的可见性和有序性的保证之后,对于内存屏障是如何保证读操作的可见性和有序性就不难理解了:

// Thread 2
while(!initOk) { // 步骤4
// LoadLoad
// LoadStore

// waiting...
}
Object data = dataSource.getData() // 步骤5
httpClient.request(data) // 步骤6

通过在 volatile 修饰的变量读操作后面加上LoadLoadLoadStore两个内存屏障,确保只有读取到了内存中最新的值之后才能进行后续的读和写操作。

大纲
文件