java高并发实战(三)——Java内存模型和线程安全

由于之前看的容易忘记,因此特记录下来,以便学习总结与更好理解,该系列博文也是第一次记录,所有有好多不完善之处请见谅与留言指出,如果有幸大家看到该博文,希望报以参考目的看浏览,如有错误之处,谢谢大家指出与留言。

一、原子性

原子性是指一个操作是不可中断的。即使是在多线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。

一般认为CPU指令是原子性的,要么不执行,要么全部执行完。

但写的程序就不一定,比如:i++ 至少包含两个操作,读和加两个操作,其实还有加完写到i中。所以这中间多个线程同时操作,就可能数据混乱。

二、有序性

在并发时,程序的执行可能就会出现乱序。

《java高并发实战(三)——Java内存模型和线程安全》

我们一般认为指令是一条一条执行的,但其实一条指令的执行是可以分为很多步骤(这里的指令是的是汇编语言的指令),如下:

  1. 读取(指令) IF  (用这个if代替该步骤,以下如是)
  2. 译码和取寄存器操作数 ID
  3. 执行或者有效地址计算 EX
  4. 存储器访问 MEM
  5. 写回 (寄存器中)WB

如下两条指令,如果分解成不成阶段(五个阶段),假如每一个阶段消耗一个cpu时钟周期,如果串行的方式执行,会消耗10个cpu时钟周期,所以,在一条执行时,当实行完if时,他的处理if指令的硬件是空闲,那么下一条就可以跟着同时执行,像流水线一样执行。如下图:A=B+C案例

《java高并发实战(三)——Java内存模型和线程安全》

中间在add的时候有个X阶段,是指在上一步MEM执行完后才能执行EX,因为X的时候R2值还没有,所以没法相加,当MEM步骤时就可以从硬件直接获取,不必等到写回到寄存器WB步骤。X其实可以理解为气泡。最后一步为什么还有一个气泡的原因是,如果去掉气泡EX前移会跟第三步中的EX重合相冲突,因为同一个操作,在两个不同指令之间不可以同时操作,会访问同一个硬件设备。

《java高并发实战(三)——Java内存模型和线程安全》

下面这个案例是,消掉气泡,操作。穿插操作,产生气泡原因是,LW步骤和ADD是依赖关系,那么在中间去操作另一个指令,就可以消掉气泡。提高了,气泡带来消费问题。因此这就是指令重排。

《java高并发实战(三)——Java内存模型和线程安全》

只要没有依赖关系,都可以指令重排,因此多线程执行时,出现乱序问题,就是指令重排产生的。指令重排只是其中一种优化方式,还会有其他优化方式同时操作。

《java高并发实战(三)——Java内存模型和线程安全》

可见性原因是,各种性能优化产生的,编译器,虚拟机级别的优化造成的,主要虚拟机的指令重排优化,造成可见性
多线程安全问题可以使用同步块阻塞解决,或者一些非阻塞方式

三、可见性

可见性是指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。

  1. 编译器优化层面可见性 :比如一个编译程序,一个线程把一个变量值优化写入寄存器中,另一个线程把这个变量给缓存起来cache,所以她俩同一时刻并不知道对方修改了这个值。并不知道在这个值真实值,无法一致。
  2. 硬件优化(如:写吸收、批操作)层面可见性:比如cpu,把数据写入内存,但他会通过硬件队列方式先存储到队列后批量的方式写入内存,因为单独写入比较慢。但过程中,他还会优化,因为最后获取最终值,所以他可能把最终值不写入队列中,直接写入内存,这样比较快,因此把队列中值丢弃所以另一个线程并看不到最终值,由此优化造成可见性问题。

可见性是由于不同级别优化产生的,不同环节产生,可以看做系统性问题。

《java高并发实战(三)——Java内存模型和线程安全》

   3.JVM层面的可见性:http://hushi55.github.io/2015/01/05/volatile-assembly

《java高并发实战(三)——Java内存模型和线程安全》

使用-server模式启动,因为他会执行更多的优化方案。-client主要是给用户,体验快。下面是打印执行步骤

《java高并发实战(三)——Java内存模型和线程安全》

介绍这么多可见性问题,主要是讲述,可见性问题成因比较复杂,产生的原因有好多种,基于不同程度优化方便导致的,总而言之是:可见性是一个线程中你可能看不到另一个线程对某个线程的修改。解决方法也比较简单,加个volatile。

下面也是表现可见性与指令重排现象:

《java高并发实战(三)——Java内存模型和线程安全》《java高并发实战(三)——Java内存模型和线程安全》

可见性:http://www.techweb.com.cn/network/system/2017-06-24/2539856.shtml

四、Happen-Before(先行发生规则)

  • 程序顺序原则:一个线程内保证语义的串行性 a=1;  b=a+1;(比如他俩是依赖性的,不能指令重排,不管是指令重排结果还是不重排的指令结果,结果都是必须一致的)
  • volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性
  • 锁规则:解锁(unlock)必然发生在随后的加锁(lock)前
  • 传递性:A先于B,B先于C,那么A必然先于C
  • 线程的start()方法先于它的每一个动作
  • 线程的所有操作先于线程的终结(Thread.join())
  • 线程的中断(interrupt())先于被中断线程的代码
  • 对象的构造函数执行结束先于finalize()方法

五、线程安全初识

指某个函数,函数库在多个线程环境中被调用时,能够正确的处理各个线程的局部变量,使程序功能正确完成。

下面就是不安全的操作举例:

《java高并发实战(三)——Java内存模型和线程安全》

解决线程安全也简单,通过阻塞的方式:

《java高并发实战(三)——Java内存模型和线程安全》

参考:http://www.techweb.com.cn/network/system/2017-06-24/2539856.shtml

学习:https://blog.csdn.net/ty_laurel/article/details/52403718

https://www.cnblogs.com/humc/p/5426351.html

http://ifeve.com/java%E9%94%81%E6%98%AF%E5%A6%82%E4%BD%95%E4%BF%9D%E8%AF%81%E6%95%B0%E6%8D%AE%E5%8F%AF%E8%A7%81%E6%80%A7%E7%9A%84/

    原文作者:java内存模型
    原文地址: https://blog.csdn.net/gududedabai/article/details/80816488
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞