存在的问题就是,主内存的运行速度严重限制
了CPU的效率
CPU缓存的运行速度非常快,比主内存的运行速度快上了好几个级别
java内存模型,其实称之为 java线程内存模型
更为合适;更多的是在多线程并发
时需要重点考虑的
java线程内存模型与CPU缓存模型类似;是基于CPU缓存模型来建立
的
java线程内存模型是标准化
的,屏蔽掉了底层不同计算机的区别
上述代码中,使用 Thread
运行了两个线程;
线程一:如果 initFlag
不被修改,就会死循环
,不输出 “=====================seccess”
线程二:修改 initFlag
的值
运行了之后,会出现下图情况;说明线程一在死循环,也就是说 initFlag 没有被修改
很明显,线程二修改了initFlag的值,但是线程一却没被修改
;说明上面的java线程内存模型是对的
run 方法执行完毕后
才会进行store操作 总线: 其实就跟电线差不多,只是电线传输的是电信息,总线传输的是CPU之间的信息
lock与unlock解析:
线程二在read操作
的时候,为主内存的变量加上了lock锁
,线程一无法read
;在write操作
的时候进行unlock解锁
,再让线程一read
。这样就可以保证数据一致性(当然,前提是线程二比线程一先read;但上述案例中并不是,故而即使加lock锁也不行)
注意: lock操作是加锁,导致其他线程无法使用线程变量;这也就变成了多线程串联运行
,这并不符合多线程的初衷
。故而,该方法也不能用
多个CPU中主内存中读取同一个数据到各自的高速缓存,当其中某个CPU修改了缓存里的数据,该数据会马上同步回主内存
,其他的CPU通过 总线嗅探机制
可以感知(监听)到数据的变化,从而将自己缓存里的数据失效
,使其重新读取
在很多时候,我们需要的就是一个线程的操作影响到另一个线程;这时候,很明显,上述的 java 线程内存模型是无法满足要求的
这时候就可以使用关键字 volatile
;将上述代码修改成如下形式:
private static volatile boolean initFlag = false;
volatile关键字:底层实现主要是通过汇编lock前缀指令
;这个指令的作用就是采用了MESI缓存一致性协议
,主要有三个操作。马上同步回主内存
(无需等待run方法执行完);其他线程通过总线嗅探机制监听并使缓存中数据失效
;在store操作时为主内存加上lock锁,等待write操作执行完才允许其他线程进行read操作
(这就保证了,在其他线程监听并使数据失效时,重新获取数据的时候是修改后的数据)
注意: 对于多线程中只有一个线程进行修改变量时,是可用
的;但是对于 多线程中有多个线程(一个以上) 修改变量而言,就会出现问题
了
在并发编程中,我们通常会遇到以下三个问题:原子性问题,可见性问题,有序性问题。我们先看具体看一下这三个概念:
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断;要么就都不执行
一个很经典的例子,就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,假如从账户A减去1000元之后,操作突然中止。这样就会导致账户A减去了1000元,但是账户B没有收到1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
同样地反映到并发编程中会出现什么结果呢?
举个最简单的例子,大家想一下假如;为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。
指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性的问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
即程序执行的顺序按照代码的先后顺序执行
举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定
,为什么呢?这里可能会发生指令重排序(InstructionReorder)
指令重排序: 一般来说,处理器为了 提高程序运行效率,可能会对输入代码进行 优化,它 不保证 程序中各个语句的执行先后顺序同代码中的顺序一致
,但是它会 保证 程序最终执行结果和代码顺序执行的结果是一致的
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行,而语句1后执行
但是要注意:虽然处理器会对指令进行重排序,靠什么保证结果不被影响?
再看下面一个例子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
这段代码有4个语句,那么可能的一个执行顺序是:
那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3
不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction2之前执行。
虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢?下面看一个例子:
//线程1:
context = loadContext(); //语句1
inited = true; //语句2
//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序
。假如发生了重排序,在线程1执行过程中先执行语句2;而此时线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法;而此时context并没有被初始化,就会导致程序出错。
从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性
也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确
保证了并发编程的可见性和有序性;却无法保证原子性
保证可见性,就是一个线程修改了共享变量,其他线程能够监听到
保证有序性,就是其他线程重新获取共享变量时,肯定是新的数据值(store步骤时的lock锁)
对于原子性,volatile关键字无法保证;故而需要synchronized锁机制
当有多个线程都需要修改共享变量时,有可能出现原子操作中断的情况
具体如下:
其中,t.join()
方法是指:等待 t 线程执行完成,回并到主线程
上
上述代码运行10个线程,每个线程都对num共享变量(用volatile
修饰了)进行1000次的累加操作
上述代码,如果满足原子性的话,最终结果应该是输出10000
运行后,得到的结果:
这只是其中一个结果,还有其他多种结果;但全部满足小于等于10000,说明程序有问题
具体原因是:
从上图中,可以看出;两个线程,明明执行了3次的累加操作,原本结果应该是3的;却由于线程二中第一次操作的中断(原子性未被保证),而丢失了一次操作数据,导致最终结果只有2
原子性问题的源头是 线程切换(在多核CPU情况下,就是线程间的互相影响)
(1)单核CPU与多核CPU的差异: 以 32 位 CPU 上执行 long 型变量的写操作为例,long 型变量是 64 位,在 32 位 CPU 上执行写操作会被拆分成两次写操作
在单核 CPU 场景下:同一时刻只有一个线程执行,禁止 CPU 中断,意味着操作系统不会重新调度线程,也就是禁止了线程切换,获得 CPU 使用权的线程就可以不间断地执行,所以两次写操作一定是:要么都被执行,要么都没有被执行,具有原子性
在多核场景下:同一时刻,有可能有两个线程同时在执行,一个线程执行在 CPU-1 上,一个线程执行在 CPU-2 上,此时禁止 CPU 中断,只能保证 CPU 上的线程连续执行,并不能保证同一时刻只有一个线程执行,如果这两个线程同时写 long 型变量高 32 位的话,那就有可能出现Bug
(2)单核CPU情况下(线程切换)的解决方案:
如果能够禁用线程切换不就能解决这个问题了吗?而操作系统做线程的切换是依赖CPU中断的,所以禁用CPU发生中断就能够禁止线程切换
(3)多核CPU情况下(线程间互相影响)的解决方案:
保证同一时刻只有一个线程执行,称之为 互斥
。如果我们能够保证对共享变量的修改是互斥的,那么无论是单核 CPU 还是多核 CPU,就都能保证原子性
;这就可以使用锁机制
如:将上述代码修改为:
public static synchronized void increase() {
num++;
}
参考文献:并发编程的三个概念(原子性、可见性和有序性)存在的问题及其解决方案
并发编程原子性解决方案-----互斥