java多线程开发04——内存模型
并发编程
线程同步
即当有一个线程在对内存进行操作时,其他线程都不可以对这个内存地址进行操作,直到该线程完成操作, 其他线程才能对该内存地址进行操作,而其他线程又处于等待状态
**临界区(线程维护):**通过对多线程的串行化来访问公共资源或一段代码,速度快,适合控制数据访问。在任意时刻只允许一个线程对共享资源进行访问,如果有多个线程试图访问公共资源,那么在有一个线程进入后,其他试图访问公共资源的线程将被挂起,并一直等到进入临界区的线程离开,临界区在被释放后,其他线程才可以抢占。
**互斥对象(内核维护):**互斥对象和临界区很像,采用互斥对象机制,只有拥有互斥对象的线程才有访问公共资源的权限。因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程同时访问。当前拥有互斥对象的线程处理完任务后必须将线程交出,以便其他线程访问该资源。
**信号量:**有时被称为信号灯,是在多线程环境下使用的一种设施,是可以用来保证两个或多个关键代码段不被并发调用。在进入一个关键代码段之前,线程必须获取一个信号量;一旦该关键代码段完成了,那么该线程必须释放信号量。其它想进入该关键代码段的线程必须等待直到第一个线程释放信号量。
事件对象: 通过通知操作的方式来保持线程的同步,还可以方便实现对多个线程的优先级比较的操作
同步是指程序中用于控制不同线程间操作发生相对顺序的机制
——java并发编程的艺术
同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都能看到同一个锁保护的之前所有的修改效果
——高性能java
问题
在并发编程中有两个关键问题:
- 线程间如何同步
- 线程间如何通信
同步指的是,程序中用于控制不同线程间操作发生相对顺序的机制。所谓相对顺序,我是这样理解的,假设某个线程发生了操作A和操作B,其他线程见到了操作B的发生,那么必然可见操作A也发生了。
通信即数据传输和共享,java中通过共享内存实现的通信。
java内存模型
如上图所示,java内存模型中,主内存保存了所有的共享变量,每个线程从主内存中获取一份共享变量的副本,在每个线程中进行计算,计算后的结果在jmm的控制下写入主内存中。但是共享变量的副本什么时候更新,线程处理完成后什么时候写入主内存,这两个过程是不确定的。
重排序
在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型。
- 编译器优化重排序,编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序
- 指令级并行重排序,我的理解的一段代码分配到多个cpu执行
- 内存系统的重排序,如果按照程序的执行顺序,每个变量读取时都从主存中读取,每个变量写入时都写入到主存中,那么效率是很低的。因此每个cpu都有缓存区,支持批量地写入和读取。那么就可能存在着某个cpu对变量进行了修改,但是其他的cpu不可见的问题。
重排序和内存屏障
为了保证内存可见性,java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。JMM把重排序和对应的内存屏障指令分为4类:
屏障类型 | 指令示例 | 说明 |
---|---|---|
LoadLoad屏障 | Load1; LoadLoad; Load2 | 确保Load1数据的装载先于Load2及所有后续装载指令的装载 |
StoreStore屏障 | Store1; StoreStore; Store2 | Store2存储代码进行写入操作执行前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见 |
LoadStore屏障 | Load1; LoadStore; Store2 | Store2存储代码进行写入操作执行前,保证Load1加载代码要从主内存里面读取的数据读取完毕。 |
StoreLoad屏障 | Store1; StoreLoad; Load2 | 在Load2加载代码在从主内存里面读取的数据之前,保证Store1的写入操作已经把数据写入到主内存里面,确认Store1的写入操作对其它处理器可见。 |
LoadLoad
载入重排序。可能某个变量的载入顺序发生了更改,例如以下代码:
a = 0;
flag = false;
public void reader(){
if(flag){ //1
int i = a * a; //2
}
}
public void writer(){
a = 1; //3
flag = true; //4
}
在发生loadload重排序时,reader的两个操作1,2的执行顺序可能发生更改。如下所示,先进行了2操作,读取了a的值,此时a的值为0,那么a*a为0。这时到线程B执行完writer整个方法,a变成了1,flag变成了true。然后回到线程A读取flag进行判断,在把temp的值赋给i。因此发生了异常:writer中的a=1赋值先于flag=true的赋值。执行到i = a * a时说明flag已经为true,那么a的值理应为1,然而由于读取重排序,a的读取先于flag的读取,导致程序发生异常。
由此可见,虽然禁止数据依赖重排序可以使得部分具有数据上依赖的共享数据不会被重排序执行,但是有一些指令存在逻辑上的依赖,比如对某个值判断之后,才对某个值进行操作。
loadload内存屏障通过保证后续部分的读取指令,一定发生在屏障之后,从而避免了逻辑上依赖的问题。
StoreStore
存储重排序,某个值的写入发生了打乱。
同样是上述代码例子,发生了如下图的情况:操作3和操作4在线程B中发生了重排序,线程B执行了flag的赋值,然后调度到了线程A执行读取flag进行判断,再对读取a进行了操作。a的赋值本应该在flag的赋值之前,由于发生了重排序导致出现了不一致的情况。
LoadStore
loadstore重排序,某个值的读取和写入发生了重排序。例如以下代码:
a = 0;
flag = false;
public void method1(){
i=a*a; //1
flag = true; //2
}
public void method2(){
if(flag){ //3
a=1; //4
}
}
如下图所示,线程A在执行method1的过程中,操作1和操作2发生了重排序,操作2先进行了处理,导致flag已经赋值了true。然后另一个线程基于flag对变量a进行了更改。回到线程A时,基于改变的a值进行了运算取得结果。
StoreLoad
StoreLoad重排序,写入和载入发生更改。思路和上述一样,不详细叙述。