可见性:
指一个线程修改了共享变量,其他线程可以马上看到这个修改。
主存与工作内存
Java内存模型中,有cpu,工作内存(高速缓存或寄存器),主存(RAM),主存即Java运行时数据结构(Java堆,java栈,方法区,本地方法区,本地方法栈),每个cpu对应一个工作内存。
cpu执行指令时,需要某个操作数时,会从工作内存的变量副本中读取数据(use指令)。当cpu执行指令时,需要输出一个值到变量中,把值写到工作内存的变量副本中(assign指令)。默认情况下工作内存会不定时的跟主存同步(read+load指令),但是不会每次写和读时都会去跟主存同步。假设线程A对变量x写1,接着线程B对变量x写入2,然后线程A读取变量x,就会产生线程A读到的值为1的情况,有可能是线程B写入的2没有同步到主存,也可能是线程A读取x时,没有从主存刷新,直接读取了工作内存中的x值。
就是以上模型产生了变量可见性的问题,而且这个问题只有在多线程并发时才会发生,因为同一线程使用的是同一个cpu,也就是读写在同一个工作内存,也就是操作同一个变量副本,就不存在可见性问题了。
volatile,final,synchronized皆可实现可见性。
原子性
数据操作指令的原子性,如Java内存模型的即在使用这个指令操作某个变量期间,不会有其他指令操作这个变量。
使用synchronize保证原子性,这个原子性是指对于使用同一个object作为锁的同步块,各同步块都是原子性的,即一个同步块正在进行时,其他同步块不能开始执行。
CAS保证的原子性,如果这个CAS操作由硬件实现,则对一个数据的compare和赋值期间,没有其他指令可以去访问(包括读写)这个数据。
总结上面三种原子性,就是说在多线程并发情况下,在某种操作(原子操作)执行期间,没有其他会影响这个操作正确完成的操作。
Java内存模型中的8种原子性操作:
1.lock:把主存中的变量标志为一个线程独占的状态
2.unlock:把主存中一个处于锁定状态的变量释放出来
//从cpu把主存中的变量
3.assign:cpu把一个值输出到工作内存的变量副本中。
4.store:把工作内存中的变量副本传送到主内存中。
5.write:把上一步传送到主存中的值写入到主存中的变量
//从主存中的变量到cpu
6.read:把主存中的变量传输到工作内存
7.load:把上一步传输到工作内存的值写入到工作内存中的变量副本中
8.use:从工作内存中的变量副本读入cpu中。
有序性与重排序
在代码编译的时候,编译器会处于优化的目的,对代码产生的指令进行重排序,即指令不按在源码中的控制流顺序排序。默认情况下,编译器可在保证在本线程内观察都是有序的情况下,对指令进行重排序。这就导致了在多线程并发时的有序性问题。看如下代码:
int i = 1;
boolean flag = false;
//线程A中执行以下代码:
void main(){
int i = 2;
boolean flag = true;
}
//在线程B中执行以下代码
void method(){
while(!flag);
int j = i;
System.out.println("j: " + j);
}
如果main重排序后(因为单线程执行时,main中i和flag的赋值顺序时不影响结果的),可能先执行flag赋值,后执行i赋值,那么method中就会出现j==1的情况。
"先行发生"原则(happens-before)
在Java内存模型中定义的两个操作之间的偏序关系。如果说操作A先行发生于操作B,则操作A影响能被操作B看到;影响包括,修改共享变量的值,调用了方法,发送了消息。先行发生关系的确认,可以保证部分有序性,同volatile,final,synchronized一起保证有序性。
Java内存模型下的天然先行发生关系
程序次序规则:在一个线程内,按照程序代码的控制流顺序,前面的操作先行发生于后面的操作。
管程锁定规则:unlock操作先行发生于后面对同一个锁的lock操作。
volatile变量规则:对一个volatile变量的写操作先行发生于后面对该变量的读操作。
线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测。
线程中断规则:对线程interrupt()调用先行发生于对此线程的中断检测。
对象终结规则:对对象的构造方法的调用先行发生于它的finalize()的开始
传递性:如果操作A先行发生于操作B,B先行发生于操作C,则操作A先行发生于操作C。
先行发生关系与重排序
先行发生关系并不保证不进行重排序,只是限制了重排序。
volatile的内存语义
1.可见性:即保证volatile变量对所有线程的可见性。
2.重排序:禁止指令重排序优化。伪代码例子如下:
volatile int i = 1;//语句1
代码块A
i = 1;//语句2
代码块B
以上代码中,代码块A和代码块B中的代码指令可以进行重排序,但是必须保证对变量i的读写操作不被重排序,即操作i的指令的位置不变,从上述例子中则表现为编译后,语句1指令必须在代码块A产生的指令之前,语句2指令必须在代码块A和代码块B之间。
volatile的语义也可组织成以下3条:
1.每次使用工作内存的变量副本时,必须从主存中刷新。
2.每次修改工作内存中的变量副本时,必须立刻同步回主存中。
3.volatile修饰的变量不能被重排序优化。
final的内存语义:
1.final域的两个重排序规则
①在构造函数中对一个final变量赋值时,将构造对象的指针赋给一个引用变量和对final变量的写操作这个两个操作之间不能重排序。代码如下:
class FinalTest{
final int i;
int j;
FinalTest object;
public FinalTest(){
i = 1;
j = 1;}
}
public void method(){
object = new FinalTest();
}
执行method时,可能被重排序成:
i=1;
将FinalTest对象的引用赋给object
j=1;
所以重排序会导致this逃逸,即未完成初始化就被外界持有了引用,使得使用该引用访问对象时因对象为完成初始化而发生错误。
②初次读一个包含final域的对象的引用和初次读该对象的final域这两个操作之间不能重排序。假如method方法改为以下那样:
public void methodA(){
FinalTest obj = object;
int a = obj.i;
int b = obj.j;
}
则可能被重排序成:
1.读取i的值,并赋给a
2.FinalTest obj = object;
3.int b = obj.j;
2.可见性
final域一旦在构造函数中初始化完成(且在构造函数中没有把this引用传递出去,即this逃逸。但是可以保证的是,不会因为构造函数重排序导致的this逃逸而影响final域的可见性,因为final域第一个重排序规则),便可被其他线程看到。
synchronized
1.synchronized禁止重排序
2.synchronized保证可见性。如果对一个变量执行lock操作,则要清空该变量在工作内存中的值,然后cpu使用这个变量时,需要先从主存获取,即rea+load;如果对一个变量进行unlock操作前,则要先把该变量在工作内存中的值同步到主存中。