JUC(17)java中有哪些原子操作

一、什么是原子操作

  • 原子操作:一个或多个操作在CPU执行过程中不被中断的特性

  • 当我们说原子操作时,需要分清楚针对的是CPU指令级别还是高级语言级别。

    • 比如:经典的银行转账场景,是语言级别的原子操作;而当我们说volatile修饰的变量的复合操作,其原子性不能被保证(这里指的是CPU指令级别)。二者的本质是一致的。
  • “原子操作”的实质其实并不是指“不可分割”,这只是外在表现,本质在于多个资源之间有一致性的要求,操作的中间态对外不可见

    • 比如:在32位机器上写64位的long变量有中间状态(只写了64位中的32位);银行转账操作中也有中间状态(A向B转账,A扣钱了,B还没来得及加钱)

二、Java中原子操作的实现方式

  • 除了long和double之外的基本类型的赋值操作,因为long和double类型是64位的,所以它们的操作在32位机器上不算原子操作,而在64位的机器上是原子操作。
  • 所有引用reference的赋值操作
  • java.concurrent.Atomic *包中所有类的原子操作
  • Java使用锁和自旋CAS实现原子操作

2.1使用锁实现原子操作

  • 锁机制保证只有拿到锁的线程才能操作锁定的内存区域。
  • JVM内部实现了多种锁,偏向锁、轻量锁、互斥锁。不过轻量锁、互斥锁(即不包括偏向锁),实现锁时还是使用了CAS,即:一个线程进入同步代码时用自CAS拿锁,退出块的时候用CAS释放锁。
  • synchronized锁定的临界区代码对共享变量的操作是原子操作。

2.2使用CAS实现原子操作

  • 利用CAS实现原子操作,其实我们在用的时候,是使用java.util.concurrent.atomic包下的各种原子类,这些原子类里面的各种方法底层使用的就是CAS。下面是该包中的类:

    • AtomicBoolean – 原子布尔
    • AtomicInteger – 原子整型
    • AtomicIntegerArray – 原子整型数组
    • AtomicLong – 原子长整型
    • AtomicLongArray – 原子长整型数组
    • AtomicReference – 原子引用
    • AtomicReferenceArray – 原子引用数组
    • AtomicMarkableReference – 原子标记引用
    • AtomicStampedReference – 原子戳记引用
    • AtomicIntegerFieldUpdater – 用来包裹对整形 volatile 域的原子操作
    • AtomicLongFieldUpdater – 用来包裹对长整型 volatile 域的原子操作
    • AtomicReferenceFieldUpdater – 用来包裹对对象 volatile 域的原子操作
    • AtomicBoolean – 原子布尔
    • AtomicInteger – 原子整型
    • AtomicIntegerArray – 原子整型数组
    • AtomicLong – 原子长整型
    • AtomicLongArray – 原子长整型数组
    • AtomicReference – 原子引用
    • AtomicReferenceArray – 原子引用数组
    • AtomicMarkableReference – 原子标记引用
    • AtomicStampedReference – 原子戳记引用
    • AtomicIntegerFieldUpdater – 用来包裹对整形 volatile 域的原子操作
    • AtomicLongFieldUpdater – 用来包裹对长整型 volatile 域的原子操作
    • AtomicReferenceFieldUpdater – 用来包裹对对象 volatile 域的原子操作
  • 在这一点可以参考:

  • 一个案例:

package com.wlw.cas;

import java.util.concurrent.atomic.AtomicInteger;

public class CASDemo {
    //CAS :compareAndSet() 这个方法的缩写 比较并交换!
    public static void main(String[] args) {
        //原子类的底层运用了CAS
        AtomicInteger atomicInteger = new AtomicInteger(2020);

        // public final boolean compareAndSet(int expect, int update)
        //如果我期望的值达到了,那么就更新,否则,就不更新,CAS是CPU的并发原语!
        System.out.println(atomicInteger.compareAndSet(2020, 2021)); //true
        System.out.println(atomicInteger.get()); //2021 ,atomicInteger的值更新到了2021

        System.out.println(atomicInteger.compareAndSet(2020, 2021)); //false
        System.out.println(atomicInteger.get()); //2021,此时atomicInteger的值是2021,不更新
    }
}
2.2.1 CAS实现原子操作的问题

CAS是并发包的基石,但用CAS有三个问题:

  • ABA问题
    根源:CAS的本质是对变量的current value ,期望值 expected value 进行比较,二者相等时,再将 给定值 given update value 设为当前值。

    因此会存在一种场景,变量值原来是A,变成了B,又变成了A,使用CAS检查时会发现值并未变化,实际上是变化了。
    对于数值类型的变量,比如int,这种问题关系不大,但对于引用类型,则会产生很大影响。

    ABA问题解决思路:版本号。在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。
    JDK5之后Atomic包中提供了AtomicStampedReference#compareAndSet来解决ABA问题。

    public boolean compareAndSet(@Nullable V expectedReference,
                             V newReference,
                             int expectedStamp,
                             int newStamp)
    
    
  • 循环时间长则开销大
    自旋CAS若长时间不成功,会对CPU造成较大开销。不过有的JVM可支持CPU的pause指令的话,效率可有一定提升。

    pause作用:

    • 延迟流水线指令(de-pipeline),使CPU不至于消耗过多执行资源。
    • 可避免退出循环时因内存顺序冲突(memorey order violation )引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。
  • 只能保证一个共享变量的原子操作
    CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。

    另外,也可“取巧”,将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab。JDK5提供AtomicReference保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。

三、CPU如何实现原子操作

3.1对于单核CPU

  • 对于单核cpu,所有的事件都是串行,执行完第一才会去执行第二个。所以,单核CPU实现原子操作比较简单。
  • 在单核CPU中,每个指令都保证是原子的,即中断只会在指令之间发生。Intel x86指令集支持内存操作数的inc操作,将多条指令的操作在一条指令内完成。因为进程的上下文切换是在总是在一条指令执行完成后,所以不会写撕裂或者读撕裂等并发问题。

3.2对于多核CPU

  • 首先,CPU会自动保证基本的内存操作的原子性。CPU保证从内存中读写一个字节是原子的,即:当一个CPU读一个字节时,其他处理器不能访问这个字节的内存地址。
  • 但对于复杂的内存操作如跨总线跨度、跨多个缓存行的访问,CPU是不能自动保证的。不过,CPU提供总线锁定和缓存锁定

1、使用总线锁

  • 总线锁用来锁住某一个共享内存。当一个cpu要对内存进行操作时,会加上总线锁,限制其他cpu对共享内存操作。Intel x86指令集提供了指令前缀lock用于锁定前端串行总线(FSB),保证了指令执行时不会受到其他处理器的干扰。
  • 假如多个处理器同时读改写共享变量,这种操作(e.g. i++)不是原子的,操作完的共享变量的值会和期望的不一致。
  • 原因:多个处理器同时从各自缓存读i,分别 + 1,分别写入内存。要想保证读改写共享变量的原子性,必须保证CPU1读改写该变量时,CPU2不能操作缓存了该变量内存地址的缓存。
  • 总线锁就是解决此问题的。总线锁:利用LOCK#信号,当一个CPU在总线上输出此信号,其他CPU的请求会被阻塞,则该CPU可以独占共享内存。

2、使用缓存锁

  • 使用总线锁,会锁定cpu与内存的通信,所以开销很大。有的cpu架构提供开销更小的缓存锁。缓存锁在一个cpu进行回写时,会使用缓存一致性机制来保护内部内存,当其他处理器回写已被锁定的缓存行的数据时会使缓存行无效。
  • 同一时刻,其实只要保证对某个内存地址的操作是原子的即可,但总线锁定把CPU和内存间的通信锁住了。锁定期间,其他CPU不能操作其他内存地址的数据,所以总线锁定的开销比较大。目前CPU会在一些场景下使用缓存锁替代总线锁来优化。
  • 频繁使用的内存会被缓存到L1、L2、L3高速cache中,原子操作可直接在高速cache中进行,不需要声明总线锁。
  • 缓存锁是指:缓存一致性机制阻止同时修改由两个以上CPU缓存的内存区域数据,当其他CPU回写已被锁定的缓存行数据时,会使缓存行无效。

3、CAS

  • CAS(Compare and Swap),cas记录原来内存中的值old,和将要修改的值new。CAS会检测现在内存中的值now,如果now和old一致,则说明没有别的cpu进行了内存修改,执行new值的更新。如果new和old值不等,则说明值已被修改,丢弃new值。
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

悬浮海

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值