Java内存模型

可见性:

指一个线程修改了共享变量,其他线程可以马上看到这个修改。

主存与工作内存

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操作前,则要先把该变量在工作内存中的值同步到主存中。

 

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值