提示:关于Java关键字文章只是本人在看书学习的过程中的一些记录与思考,可能有理解不到位的地方,如果有不对的地方,欢迎评论区讨论
多线程安全
什么线程安全问题?
多线程安全问题,许多一线程序员谈之色变的问题。这往往是因为首先我们自己打心底里就对多线程安全问题很抗拒,觉得线程安全问题很复杂难以理解。克服恐惧最好的办法就是去面对恐惧。下面我们一起来探讨探讨(其实也不是想象中那么困难哦~~)
为了更好的理解线程安全问题,首先我们需要了解一下JAVA的内存模型,此处要与JVM内存模型区分开来,很多同学总是会把这两个内存模型混淆。(不要着急,磨刀不误砍柴工)
简单来说,Java内存模型规定了所有的变量都存储在主内存中,而每一条线程(比如我们的一次接口调用)都拥有自己的工作内存,线程的工作内存就是用来保存我们每次用到的一些变量(我们平时在类中定义的一下变量),而这些变量都是从主内存中拷贝过来的副本。在线程的后续的业务逻辑处理的过程中,所有涉及到对变量的修改,都是在每个线程自己的工作内存中完成的,然后会同步到主内存,线程与线程之间是无法直接访问对方的工作内存中的变量,只能通过与主内存交互,完成变量值的传递滴。可以通过下面一幅图来加深我们的理解。
通过上面简单的学习,相信大家已经对Java内存工作原理有了一些初步的认识,在此基础上,我来考虑一个问题,上述内存模型,多线程情况下会出现什么问题呢?显而易见,这就是我们前面说的线程安全问题——数据不一致。举个列子,当前主内存中有一个库存数量 =2,此时A线程请求扣减库存,则A线程的依次操作是:
1、去主内存中读取=2,并拷贝一份副本到自己的工作内存中
2、在自己的工作内存中,做自己的业务处理,并扣减扣库存, =-1
3、将自己对修改后的变量 =1,刷回到主内存当中。
经过上面三步操作,线程A圆满的完成了自己的任务。如果此时B线程,刚好在A线程完成上述三步后,请求扣减库存,则重复上述三个步骤,并完成自己的工作,此时,主内存中库存数量=0 皆大欢喜。但是事情往往不是想我们预想的那样完美的执行,试想一下,假如,线程B在线程A完成前两步操作,还没来的及将修改后的变量同步回主内存的情况下,请求扣减库存,这个时候发生的情形是这样的,
1、线程B去主内存读取库存 =2
2、在自己的工作内存中,做自己的业务处理,并扣减扣库存, =-1
3、将自己对修改后的变量 =1,刷回到主内存当中。
至此,不管是线程A先将自己工作内存中的修改后的库存刷回主存,还是线程B先把自己工作内存中修改的库存变量刷回主内存中,这时候,主存中的库存都是不正确,因为两次扣库存,数量没有变为0,在web层面看来,两次下单扣库存,库存的数量却没有扣减正确。这就是多线程安全问题。
至此,我们知道了,为什么多线程环境下会出现线程安全问题了(线程安全问题其实也很好理解),接下来,我们今天的主角就要闪亮登场了。
英文翻译过来就是挥发性的;不稳定的;爆炸性的;反复无常的,为什么用这个英文来作为关键字呢?后面我会说一下我的理解,我们先来看一下,《深入JVM虚拟机》这本书对它的解释:
Java关键字,可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义为修饰之后,这个变量就具备了以下两个属性,
(1)保正变量对所有线程的立即可见性。
(2)禁止jvm指令重排
下面我们来对这两点进行解释与分析
1、线程之间可见性
线程之间如何通信?
根据上面的Java内存模型,线程之间工作内存中的变量是相互独立,无法相互自可见的,必须通过与主内存交互,才能完成线程之间的通信的,而关键字,就可以保证,当变量在线程a中被修改后,其他线程工作内存中的该变量的值立即可以得知改变(这里并没有改变线程之间通信的方式:工作内存——主内存——工作内存),这是怎么实现的呢?
简单的来说,关键字修饰变量的时候,当变量发生改变的时候,这个时候它会保证变量的修改被安全的同步到主内存当中,同时,使得其他线程的工作内存中对该变量的缓存失效,这样的话,其他线程必须重新去主内存中获取变量的最新值,即表现为对其他线程立即可见。
但是据此很多人就草率地得出结论,基于修饰的变量,在并发下的运算一定是线程安全的。这个结论是不正确的,因为在Java里面的运算是非原子的,下面我通过一个简单例子,来说明一下这种情况:
//变量自增并发下的问题
class {
int i=0;
void (){
i++;
}
void main([] args){
for(int i = 0; i < 10; i++){
//每个线程对t进行1000次加1的操作
new (new (){
@
void run(){
for(int j = 0; j < 1000; j++){
();
}
}
}).start();
}
//等待所有累加线程都结束
while(.() > 1){
.yield();
}
//打印t的值
.out.(i);
}
}
这段代码最终执行结果会是我们想要的10000么?答案是否定的,而且每次运行的结果都不一样,都是一个小于10000的数字,(大家可以自行运行一下看看)这是为什么呢?不是说好的线程立即可见么?
网上很多人的解释是,线程1更新了i=0+1的值,还没有同步到主内存中,线程2,也读取了i=0的值,然后线程1同步回主内存,线程2,工作内存中还是i=0,然后执行i=0+1;然后同步到主内存中,这样就导致两次自增操作,结果只增加了1,这种解释其实是错误的,有很大的误导性,没有真正的理解的原理,及Java底层运算原理。
问题的关键在于,自增运算i++,我们可以通过Javap反编译一下这句话,会发现一行i++代码在Class文件中却产生了四条指令如下:
1
void ();
Code:
Stack =2,
0: //Field i
3:
4: iadd
5:
我们知道Java的方法的执行是在栈中进行的,每个方法的执行就是一个个栈桢的进栈与出栈,当线程1修改i=0+1=1的时候并保证同步到主内存后,此时线程2的工作内存里i值已经是i=1了,(之前的缓存已经失效,重新读取主内存中的值),即当线程执行 将变量取到栈顶的时候,已经保证了取到的是最新的i值,但是,在接下来执行,iadd这些指令时,其他线程可能又已经把i的值增到了,并刷回主存,这个时候线程2再执行指令,就可能把较小的值更新到主内存当中了,这就是问题的关键。
2、指令重排
首先,指令重排通俗地解释,就是我们写的一行行代码,在jvm生成相应的字节码去运行的时候java关键字过滤算法 带你彻底理解JAVA关键字之volatile,jvm为了提高效率,会在不改变运行结果的情况下对指令的执行顺序做出优化重排,如下
1、 int add (){
int i=3;
int j=5;
int s = i+j;
flag =true;
s;
}
在上面代码当中,jvm在执行的时候,会对方法生成的字节码进行指令优化重排, flag =true;可能会在int i=3;之前执行也能在int s = i+j;之前被执行,当然,不管如何指令重排,jvm都会保证,该方法最终的执行结果返回是正确的,这样的指令重排优化才有意义。
上述情况,在单线程情况下不会出现问题,不管如何,单线程最终运行方法得到的结果都是正确的,这就是Java内存模型中描述的所谓的“线程内表现串行的语义”,但是在多线程情况下,我们考虑下面情况,
//假设以下是线程A中的代码
Map = new ();
=();
= true
//假设以下是线程B中的代码
if(){
g()
}
上述伪代码中,如果线程A在执行的时候,jvm进行了指令重排, = true,排在了 =();之前被执行,此时线程B,从主内存中读取 = true,开始执行自己的逻辑,这时候初始化配置还没有完成,则B使用到配置信息时就会报错。这就是Java内存模型中描述的所谓的“线程外表现并行的语义”,
而当我们修改一下
Map = new ();
=();
= true
就不会出现上面的问题了,因为 关键字,可以起到禁止jvm指令重排优化操作,这样只有执行完 =();才会执行 = true。其原理就是java关键字过滤算法,关键字修饰的变量,生成字节码后会在变量前后生成内存屏障,指令重排时,不能把内存屏障后面的代码排到前面执行。
那么什么情况下,使用关键只可以保证线程安全呢,只有满足下面的情况,则可以保证线程的并发安全:
1、运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
2、变量不需要与其他状态变量共同参与不变约束。
总结: (开篇还提到的英文翻译:不稳定的;反复无常的,其实这就是告诉jvm我是不稳定的,对我的操作要及时地同步给其他使用者,并且不能把我随便排序,个人理解哈)码字不易,有理解不到位的地方,欢迎大家,评论区讨论指教,共同进步!
————————————————
版权声明:本文为CSDN博主「迈克尔.布莱恩特.杨」的原创文章java关键字过滤算法,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接: