程序是否线程安全,取决于哪些要素呢,主要是以下三个:

原子性,

可见性,

有序性。

今天先一起来学习可见性。

可见性问题的原因:

可见性的原因,是因为 java 内存模型中,主内存的数据需要在工作内存中有副本,才能被工作线程使用。如下图所示:

主内存与工作内存与线程

如数据 a,在两个线程中使用,即是在两个工作内存中存在 a 数据的副本,其中一个修改了 a,只是在工作内存中修改,修改后的数据何时同步到主内存,同步到另一个工作内存,无人可以预料。

因此,这里的可见性,是指线程处理的数据只是对应的工作内存的数据,主内存及其他工作内存中,该数据的值对它来说不可见。

可见性问题的后果:

请看如下代码:

Integer i=1000;// 语句 1

i++;// 语句 2

j=i;// 语句 3

j 的值是多少呢?

单线程的情况下,明显是 1001,多线程情况下,则不确定。

语句 1 和 2 在线程 A 中执行,语句 3 在线程 B 中执行,如果线程 A 执行完毕后,i 的数据并没有马上刷新回主存,或者线程 B 没从主存中重新取 i 的值,那么 j 的值可能是 1000。

可见性问题的解决方案:

1. 最简单的方案,就是将变量放在方法中,使用局部变量的方式。

public void f(){
Integer i=1000;

i++;

j=i;

}

使用这种方式,在不同的线程其实在主内存中申请了不同的空间,各用各的,不会有冲突的问题。这种方式的代码也叫 “可重入代码”。如下图所示:

可重入方式

2. 使用关键字保障数据的可见性,volatile,synchronized,final

这块的使用,涉及内存屏障的相关知识,先顺道提一下:

内存屏障 (Memory barrier),原本是硬件中,防止高速缓存与主内存是数据不一致使用的,jvm 为了跨平台使用,封装了这块操作,形成了 jvm 的内存屏障指令。

内存屏障的作用:

阻止屏障两侧的指令重排序,即使在多线程情况下,每个线程执行的顺序也必须遵守屏障的规则 (保证有序性);

强制把写缓冲区 / 高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效(在 jvm 中,就是将线程的工作内存的数据写回主内存,并让其他线程的工作内存中的数据失效,保证可见性)。

Java 内存屏障主要有以下几类:

LoadLoad 屏障:对于这样的语句 Load1; LoadLoad; Load2,即在 Load2 即后面的读取操作读取工作内存中的数据前,让各个工作内存中的数据失效,从主内存中重新读取数据。

StoreStore 屏障:对于这样的语句 Store1; StoreStore; Store2,即在 Store2 将工作内存中的数据操作前,保证 Store1 的写入刷新到主内存,并使各个工作内存中的相应数据失效。

LoadStore 屏障:对于这样的语句 Load1; LoadStore; Store2,即在 Store2 及后面的操作将工作内存中的数据写入到主内存前,保证 Load1 的要读取的数据已经读取完毕。

StoreLoad 屏障:对于这样的语句 Store1; StoreLoad; Load2,在 Load2 及后续所有读取工作内存并执行操作前,将 Store1 的写入刷新到主内存,并使各个工作内存中的相应数据失效。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

volatile 关键字执行的内存屏障策略:

  1. 在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障:

理解:

Store1 StoreStore volatile 写 StoreLoad Load1

即在 volatile 写之前,保证写的数据是最新的主内存刷新的数据,在 volatile 写之后,写完的数据马上刷新到主内存及各个工作内存。

  1. 在每个 volatile 读操作前插入 LoadLoad 屏障,在读操作后插入 LoadStore 屏障:

理解:

Load1 LoadLoad volatile 读 LoadStore Store1

即在 volatile 读之前,刷新主内存中的数据并使工作内存中的相应数据失效,保证 volatile 读的数据是最新的,在读取完毕后,才能进行 Store1 的数据写入主内存的操作。

synchronized 关键字的策略:

这个关键字,是根据在操作变量后,解除锁之前,变量的数据必须同步会主内存的规则进行可见性的策略,是在解锁前,加了 StoreLoad 及 StoreStore 屏障。

final 关键字的策略:

final 的作用是定义常量,只在初始化时赋值。只需在初始化后插入 StoreLoad 屏障,即可保证数据是最新的,且在初始化未结束前,禁止对该对象的调用(禁止重排序)。