如何实现并发的原子性 可见性 有序性

在JAVA多线程并发中有三个性质,分别为原子性、可见性、有序性,接下来将会介绍java多线程并发时是如何满足这三个性质的。

JMM是什么

首先介绍JMM(java内存模型)是什么,JMM定义了一种内存模型来屏蔽各个硬件平台和操作系统的内存访问差异,已实现java程序在各个平台下都能达到一致的内存访问效果。

在java中,所有的实例域、静态域和数组元素都存储在堆内存中,堆内存在线程之间共享,这被称为共享内存。线程之间的共享变量存储在主内存之中,每个线程的工作内存存储了共享变量的副本。

就如下图所示一样,每个线程的工作内存是相互隔离的,当线程对自己的工作内存中的共享变量进行修改时,其他线程是如何感知这个变量的值已经被修改了呢?这个问题就是多线程并发编程的可见性的问题。
在这里插入图片描述

如何解决可见性?

  1. 通过同步互斥的方式实现,也就是我们所说的锁。当加锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须从主内存中读取共享内存;当释放锁时,JMM会把线程对应的本地内存中的共享内存刷新到主内存中。
  2. 如果觉得加锁的方式过于“重”了,也可以选择使用volatile修饰变量。在写一个volatile变量时,JMM会把线程对应的本地内存中的共享变量刷新到主内存中;在读一个volatile变量时,JMM会把线程对应的本地内存置为无效,线程接下来从主内存中读取共享变量。

volatile的底层实现

volatile通过汇编中的lock指令将当前处理器缓存行当数据写回到系统内存,这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。为了提高处理器的速度,处理器不直接和内存通信,而是先将系统内存写入处理器缓存再进行操作。当对一个volatile变量进行写操作时,会对处理器发送一条lock指令,处理器就将处理器缓存中的数据写回到系统内存。在多处理器下,为保证多处理器中的缓存的数据都是一致的,会使用内存一致性协议,每个处理器嗅探总线上传播的数据来检查自己的数据是否被更改,当处理器发现自己缓存行对应的内存地址被修改,就将这个内存地址置为无效,当处理器要对这个数据进行操作时,就从系统内存中读取这个数据到自己的缓存中。

如何解决原子性?

线程的原子操作是不可被中断的一个或一系列操作,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

通过总线锁定保证原子性

使用处理器提供的lock#信号,当处理器在总线上输出这个信号时,其他处理器的请求会被阻塞住,那么该处理器可以独占共享内存。

通过缓存锁定保证原子性

通过总线锁定原子性开销比较大,当共享内存被锁定时,其他处理器不能操作其他内存地址的数据。所以处理器在某些场合下使用缓存锁定代替总线锁定。缓存锁定就是处理器不输出lock#信号,而是更改内部的内存地址,使用缓存一致性原则保证操作的原子性。

java中通过锁和循环CAS保证原子性

使用循环CAS实现原子操作

自旋CAS的基本思路就是循环进行CAS操作知道成功为止。CAS需要两个数值,一个旧值,一个新值,在操作前先比较旧值有无发生变化,若无变化则交换成新值,若有变化则不交换。

CAS实现原子操作的三大问题

  1. ABA问题
    因为CAS需要比较旧值是否发生变化,如果没有变化则更新,如果一个变量值为A,若A线程B线程同时操作这个变量,线程A将这个值写为B,再写为A,线程B操作这个变量时会认为这个变量并没有发生变化,但是实际上发生了变化,这就是ABA问题。
    在Atomic包中的AtomicStampedReference解决了ABA问题,这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。
  2. 循环时间长开销大
    如果自旋CAS长时间不成功,会给CPU带来非常大的执行开销。
  3. 只能保证一个共享变量的原子操作
    对多个共享变量进行操作时,循环CAS就无法保证操作的原子性。JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里进行CAS操作。

如何解决有序性?

什么是有序性?

在单线程下,编译器和处理器会在不改变程序执行结果的前提下,对程序和指令执行顺序进行重新排序,以此优化程序执行效率(这遵守了as-if-serial语义)。但在多线程下,这种重排序会影响程序执行的结果。

在JMM中,JMM天生具有一定的有序性,这是因为JMM中,如果一个程序的操作执行结果需要对另一个操作可见,那么这两个操作必须要存在happens-before关系(这两个操作可以在单个线程内,也可以在不同的线程之中)。

as-if-serial语义

as-if-serial语义的意思是:不管怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

happens-before

  1. 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作。
  2. 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
  3. volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作。
  4. 传递规则:如果操作A先行发生于操作B,操作B先行发生于操作C,那么操作A先行发生于操作C。
  5. 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
  6. 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。
  7. 线程终结规则:线程中所有的操作都先行发生于线程的终止检测。
  8. 对象终结规则:一个对象的初始化完成先行发生于它的finalize()方法的开始。

两个操作如果不能从happens-before中推断出执行顺序,那么就无法保证它们的有序性。

重排序

重排序也被分为

  1. 编译器优化的重排序:编译器在不改变单线程的程序语义的前提下,可以重新安排执行顺序。
  2. 指令级并行的重排序:如果不存在数据依赖性,处理器可以改变语句对应的机器指令都执行顺序。
  3. 内存系统的重排序:由于处理器使用缓存和缓冲区,使得加载和存储操作看上去可能是乱序执行。

后两种重排序都属于处理器重排序,这三种重排序导致多线程中数据不可见,程序结果不符合预期。我们可以使用volatile和锁这两种方式解决重排序问题,那么从底层开始看起,底层是如何解决重拍序的问题的呢?对于编译器重排序,编译器会禁止特定类型的编译器重排序;对于处理器重排序,会插入特定类型的内存屏障,通过内存屏障禁止特定类型的处理器重排序。

数据依赖性和控制依赖性

上述中指令级并行的重排序提到了数据依赖性,那么数据依赖性是什么呢?除了数据依赖性还有控制依赖性,那么控制依赖性又是什么?

数据依赖性

如果线程A写了一个变量为1,线程B需要读取这个变量的值,线程B都读需要依赖线程A的写,那么这个操作就被称作数据依赖性。要注意,数据依赖性仅仅针对单个线程中执行的操作,多个线程之间的数据依赖性不被编译器和处理器考虑。

控制依赖性

如下代码

if(flag) //1 
a=i++; //2

第二行的执行依赖于第一行的判断,所以第一行和第二行存在控制依赖性。控制依赖性会影响指令序列执行的并行度,为此编译器和处理器会采用猜测执行来克服控制相关性对并行度的影响。就像这个程序中,处理器会提前计算i++的值,并把值临时保存在一个名为重排序缓存的硬件缓存中,当第一行的判断结果为true时,会把值赋给a。在单线程程序中,控制依赖的重排序不会影响程序执行结果,但在多线程程序中,则可能会影响。

顺序一致性内存模型

接下来顺便讲一讲顺序一致性内存模型,这个模型是一个理想化的理论模型。在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参考。
顺序一致性内存模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行。
  2. (不管程序是否正确同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。
同步程序的顺序一致性效果

JMM中,临界区内的代码可以重排序并且JMM在退出和进入临界区时会做一些特别的处理,使得线程在这两个时间点具有与顺序一致性模型相同的内存视图。

未同步程序的执行特性

对于未同步或未正确同步的多线程程序,JMM只保证最小安全性,即线程执行时读取到的值要么是之前线程写入的值,要么是默认值,不会凭空产生。JMM不保证未正确同步程序的执行结果与该程序在顺序一致性模型中的执行结果一致。

顺序一致性内存模型和JMM的差异
  1. JMM不保证所有操作按照程序的顺序执行。
  2. JMM不保证所有线程能看到一致的操作执行顺序,因为JMM中不保证每个操作必须立即对任意线程可见。
    在顺序一致性模型中的执行结果一致。
顺序一致性内存模型和JMM的差异
  1. JMM不保证所有操作按照程序的顺序执行。
  2. JMM不保证所有线程能看到一致的操作执行顺序,因为JMM中不保证每个操作必须立即对任意线程可见。
  3. JMM不保证对64位变量(引用类型)的写操作具有原子性。因为在32位操作系统中,对64位变量的写操作是分为两个32位进行的,所以32位操作系统对64位变量的写不具有原子性,但是对64位变量的读操作是保证原子性的且对基本变量也是原子性的。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值