风也温柔

计算机科学知识库

java关键字过滤算法 带你彻底理解JAVA关键字之volatile

  提示:关于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层面看来,两次下单扣库存,库存的数量却没有扣减正确。这就是多线程安全问题。

  至此,我们知道了,为什么多线程环境下会出现线程安全问题了(线程安全问题其实也很好理解),接下来,我们今天的主角就要闪亮登场了。

  java协同过滤推荐算法_java关键字过滤算法_java实现协同过滤算法

   英文翻译过来就是挥发性的;不稳定的;爆炸性的;反复无常的,为什么用这个英文来作为关键字呢?后面我会说一下我的理解,我们先来看一下,《深入JVM虚拟机》这本书对它的解释:

  Java关键字,可以说是Java虚拟机提供的最轻量级的同步机制,当一个变量被定义为修饰之后,这个变量就具备了以下两个属性,

  (1)保正变量对所有线程的立即可见性。

  (2)禁止jvm指令重排

  下面我们来对这两点进行解释与分析

  1、线程之间可见性

  线程之间如何通信?

  根据上面的Java内存模型,线程之间工作内存中的变量是相互独立,无法相互自可见的,必须通过与主内存交互,才能完成线程之间的通信的,而关键字,就可以保证,当变量在线程a中被修改后,其他线程工作内存中的该变量的值立即可以得知改变(这里并没有改变线程之间通信的方式:工作内存——主内存——工作内存),这是怎么实现的呢?

  简单的来说,关键字修饰变量的时候,当变量发生改变的时候,这个时候它会保证变量的修改被安全的同步到主内存当中,同时,使得其他线程的工作内存中对该变量的缓存失效,这样的话,其他线程必须重新去主内存中获取变量的最新值,即表现为对其他线程立即可见。

  但是据此很多人就草率地得出结论,基于修饰的变量,在并发下的运算一定是线程安全的。这个结论是不正确的,因为在Java里面的运算是非原子的,下面我通过一个简单例子,来说明一下这种情况:

  //变量自增并发下的问题

   class {

   int i=0;

   void (){

  i++;

  java实现协同过滤算法_java关键字过滤算法_java协同过滤推荐算法

  }

   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();

  java协同过滤推荐算法_java关键字过滤算法_java实现协同过滤算法

  }

  //打印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实现协同过滤算法_java协同过滤推荐算法_java关键字过滤算法

  我们知道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版权协议,转载请附上原文出处链接及本声明。

  原文链接:

  文章来源:https://zhuanlan.zhihu.com/p/157831583