原子操作的硬件与java实现

Posted 顧棟

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原子操作的硬件与java实现相关的知识,希望对你有一定的参考价值。

文章目录

原子操作

定义:不可被中断的一个或一系列操作。

现代计算机系统中,使用了高速缓存进行数据的交互解决了处理器和内存之间速度的矛盾。但是在这个种共享内存多核系统(每个处理器有自己的高速缓存,又共享同一主存的系统)中,对共享变量值操作时,操作完之后期望值与结果值不一致。这带来了缓存一致性的问题,如何保证内存操作的原子性,硬件和语言层都尝试做了一致性的处理。

原子操作的实现

硬件

32位IA-32处理器使用缓存锁定和总线锁定的方式来实现多处理器之间的原子操作。

总线锁定

采用总线锁:使用处理器提供一个LOCK#信号,当一个处理器在总线上输入此信号时,其他处理器的请求将被阻塞,那么这个处理器就可以独占这个共享内存。这个锁的是CPU与内存之间的通信,就等于将其他处理器与内存的所有交互都锁住了。

缓存锁定

是对总线锁的优化,主要思想是只锁住部分内存,来降低总线锁的代价。主要实现是 如果内存区域被缓存到了处理器的缓存行中,并在Lock操作期间被锁定,那么当它执行操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并依赖缓存一致性机制(阻止同时修改被两个及上处理器缓存的内存区域数据,其他处理器回写已被锁定的缓存行的数据时,会是缓存行无效)来保证原子性。应该是有遵守一些协议来实现缓存一致性机制的,有哪些,怎么做的,没调查,有兴趣的读者可以去搜搜。

不能使用缓存锁定的场景

  1. 操作的数据不能缓存在处理器中
  2. 操作的数据跨多个缓存行
  3. 处理器本身不支持缓存锁定

这三种情况只能使用总线锁。

java

java中通过循环CAS和锁实现原子操作。

使用循环CAS实现原子操作

JVM中的CAS操作是利用处理器的CMPXCHG指令实现的,在JDK的并发包中的atomic包中的原子类中可以支持一些原子操作。自旋CAS实现的基本思路就是不停的CAS知道成功为止。

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

1. ABA问题

如果一个值原来是A,然后变成了B,最后有变成了A。使用CAS的时候,只直达值还是A,满足替换条件,将A替换成别的值。但是值的变化(A->B->A),无法感知。

场景举例:账户中原来有30元,现在要对账户进行充值100元。线程A执行充值操作(期望值是30),由于一些影响,线程A没有快速完成这个操作,线程B也发起了充值操作,并很快也充值成功了,此时账户(130)。在充值成功后,有快速的消费了100,此时账户30。线程A发现账户中的值是30,满足CAS条件,于是继续完成充值操作,账户此时为130。这样账户中就多出来100。在多线程高并发的场景下会出现这种情况。

简单的解决是通过在变量之前增加一个版本号,每次变量发生变化时版本号也随之变化,需要同时比较满足版本号、值都与期待值一致才能操作。

2. 循环时间开销大

如果自旋CAS长时间不成功的话,会造成CPU的空转,白白浪费执行资源。可以给自旋增加次数限制来处理。

寄希望于JVM可以支持处理器的pause指令。用来延迟流水线指令,避免CPU消耗过多的资源,同时避免退出循环时,发生内存顺序冲突(memory order violation)导致CPU流水线被清空(CPU pipeline flush),提高CPU使用效率。

3. 只能保证一个共享变量的原子操作

当对一个共享变量操作时,可以使用循CAS的方式保证原子操作,但是对多个共享变量操作是,循环CAS无法保证操作的原子性,这时可以用锁,或者将多个变量合并成一个变量,类似AtomicReference的实现。

锁机制保证了只有获得了锁的线程才能操作锁定的内存区域。比如synchronized。

主要来自《Java并发编程的艺术》 方腾飞 魏鹏 程晓明版

以上是关于原子操作的硬件与java实现的主要内容,如果未能解决你的问题,请参考以下文章

JMM与并发三大特性

volatile为啥不能保证原子性

java的原子性操作有哪些

自己动手写把”锁”---原子性操作

volatile实现原理

Java的CAS乐观锁原理解析