当前位置:   article > 正文

Java基础之并发理论基础_i += 1需要三条 cpu 指令

i += 1需要三条 cpu 指令


一、为什么需要多线程

  • CPU 增加了缓存,以均衡与内存的速度差异;// 导致 可见性问题
  • 操作系统增加了进程、线程,以分时复用 CPU,进而均衡 CPU 与 I/O 设备的速度差异;// 导致 原子性问题
  • 编译程序优化指令执行次序,使得缓存能够得到更加合理地利用。// 导致 有序性问题

二、线程不安全

如果多个线程对同一个共享数据进行访问而不采取同步操作的话,那么操作的结果是不一致的。
例如1000 个线程同时对 cnt 执行自增操作,操作结束之后它的值有可能小于 1000。

1、三要素之一可见性(CPU缓存引起)

可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到。

//线程1执行的代码
int i = 0;
i = 10;
 
//线程2执行的代码
j = i;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6

假若执行线程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修改的值。

2、三要素之一原子性(分时复用引起)

原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

举个简单的例子,看下面这段代码:

int i = 1;

// 线程1执行
i += 1;

// 线程2执行
i += 1;
  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7

这里需要注意的是:i += 1需要三条 CPU 指令

  1. 将变量 i 从内存读取到 CPU寄存器;
  2. 在CPU寄存器中执行 i + 1 操作;
  3. 将最后的结果i写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

由于CPU分时复用(线程切换)的存在,线程1执行了第一条指令后,就切换到线程2执行,假如线程2执行了这三条指令后,再切换会线程1执行后续两条指令,将造成最后写到内存中的i值是2而不是3。

3、三要素之一有序性(重排序引起)

有序性:即程序执行的顺序按照代码的先后顺序执行。
但在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。重排序分三种类型:

  • 编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
  • 指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
  • 内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从 java 源代码到最终实际执行的指令序列,会分别经历下面三种重排序:
在这里插入图片描述
上述的 1 属于编译器重排序,2 和 3 属于处理器重排序。这些重排序都可能会导致多线程程序出现内存可见性问题。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序(不是所有的编译器重排序都要禁止)。对于处理器重排序,JMM 的处理器重排序规则会要求 java 编译器在生成指令序列时,插入特定类型的内存屏障(memory barriers,intel 称之为 memory fence)指令,通过内存屏障指令来禁止特定类型的处理器重排序(不是所有的处理器重排序都要禁止)。

三、解决方案

1、原子性角度

Java内存模型只保证了基本读取和赋值是原子性操作,如果要实现更大范围操作的原子性,可以通过synchronized和Lock来实现。由于synchronized和Lock能够保证任一时刻只有一个线程执行该代码块,那么自然就不存在原子性问题了,从而保证了原子性。

2、可见性角度

Java提供了volatile关键字来保证可见性。

当一个共享变量被volatile修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。

而普通的共享变量不能保证可见性,因为普通共享变量被修改之后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。

通过synchronized和Lock也能够保证可见性,synchronized和Lock能保证同一时刻只有一个线程获取锁然后执行同步代码,并且在释放锁之前会将对变量的修改刷新到主存当中。因此可以保证可见性。

3、有序性角度

在Java里面,可以通过volatile关键字来保证一定的“有序性”。另外可以通过synchronized和Lock来保证有序性,很显然,synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。当然JMM是通过Happens-Before 规则来保证有序性的。


  • 平安夜到了,向唯美的星空许个愿,愿你生活甜蜜平静;向悠扬的钟声祈个祷,愿你事业路上顺利平坦;向亲爱的朋友问声好,愿你快乐无忧,幸福安康,一生平安。祝平安夜快乐!
  • 人格的完善是本,财富的确立是末。
  • 你的饭钱,你的零用钱,你所有所有未步入社会前的支出都是父母给你的,你有什么资格颓废?你还你爸妈养你的钱了吗?
  • 一直相信,会有一个高度,让我看到不一样的风景。
  • 祝你平安夜乐相伴,福相随。
  • 愿你幸福永健康,好运财运长伴你!
  • 如果你总是做自己擅长的事,那你将永远不会进步。
  • 祝你平安夜乐相伴,福相随。
  • 然万物好像逝去了,但是,你瞧!那火红的枫叶在树枝上摇摆着,就像是一大群顽皮的孩子在手拉手一起跳着欢快的舞蹈呢。有些枫叶因跳舞不慎,从树枝上跌了下来,瞬间化作了一只只飞舞的蝴蝶,在空中飞来飞去。
  • 平安夜我一个人过,圣诞节我一个人过,跨年夜我一个人过,元旦我还是一个人,快到的生日是不是也要一个人过。
声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/菜鸟追梦旅行/article/detail/523915
推荐阅读
相关标签
  

闽ICP备14008679号