JAVA—多线程与高并发之CAS

CAS

在了解CAS之前,我们要先要了解一下atomic(java.util.concurrent.atomic),为什么在jdk1.5开始引入了atomic包呢?

Atomic

1、为什么要使用Atomic类

在多线程和高并发环境中,我们经常会对一个int型的共享变量值进行+1或-1操作,例如:

int count = 0;
// ...
count ++;

但是这种写法,在多线程共同修改共享变量的情况下,会出现问题,导致实际值和预期值不符,即上面的代码是线程不安全的。因此我们需要实现线程的同步。通常的做法就是使用synchronized关键字修饰相关方法,或对代码块加锁。

按理来说,使用synchroized已经能满足功能需求了。为什么还会有AtomicXXX这些类呢?

那是因为在jdk1.6之前,synchroized是重量级锁。其最初的实现方式是“阻塞或唤醒一个Java线程需要操作系统切换CPU状态来完成,(synchronized关键字会让没有得到锁资源的线程进入BLOCKED状态,而后在争夺到锁资源后恢复为RUNNABLE状态,这个过程中涉及到操作系统用户模式和内核模式的转换,代价比较高。) 这种状态切换需要耗费处理器时间,如果同步代码块中内容过于简单,这种切换的时间可能比用户代码执行的时间还长”。

synchronized属于悲观锁的一种实现方式,即操作被锁的变量前就对对象加锁,不管此对象会不会产生资源竞争。

在JDK1.6以后,对synchronized进行了优化,提出了锁升级的概念。引入了偏向锁,轻量级锁,其中也采用了CAS这种思想,效率有了很大的提升。但是在最终转变为重量级锁之后,性能仍然较低。

由于synchronized在性能方面的问题以及对共享变量的值进行修改的用法太常见,jdk1.5开始引入了JUC的atomic包,提供了一组【原子操作类】。

所谓原子操作类,指的是java.util.concurrent.atomic包下,一系列以Atomic开头的包装类

jdk1.5的atomic包下提供的原子操作类

  • 标量类:AtomicBoolean,AtomicInteger,AtomicLong,AtomicReference
  • 数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
  • 更新器类:AtomicLongFieldUpdater,AtomicIntegerFieldUpdater,AtomicReferenceFieldUpdater
  • 复合变量类:AtomicMarkableReference,AtomicStampedReference

jdk1.8之后又添加了下面的四个类

  • LongAdder DoubleAdder 高并发情况下替代AtomicLong
  • LongAccumulator DoubleAccumulator

2、Atomic原子操作类实现原理

既然atomic类在某些情况下有比synchronized更好的性能,那它又是怎么做到的呢?这就要说到CAS

CAS是英文单词Compare And Swap的缩写,翻译过来就是比较并替换。sun.misc.Unsafe类提供底层实现,Unsafe类可以直接操作虚拟机内存,直接申请内存,通过偏移量寻址,可以直接修改内存地址中对应的变量值

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A(expect),要修改的新值B(update)。

更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS是一种无锁算法。即在不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步。

在JDK1.5 中新添加的 java.util.concurrent (J.U.C) 就是建立在 CAS 之上的。对于 synchronized 这种阻塞算法,CAS是非阻塞算法的一种实现。所以J.U.C在性能上有了很大的提升。

3、CAS的缺点

(1)CPU开销较大

在并发量比较高的情况下,如果许多线程反复尝试更新某一个变量,却又一直更新不成功,循环往复,会给CPU带来很大的压力。

atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当然,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。

(2)不能保证代码块的原子性

CAS机制所保证的只是一个变量的原子性操作,而不能保证整个代码块的原子性。比如需要保证3个变量共同进行原子性的更新,就不得不使用Synchronized了。

因为它本身就只是一个锁住总线的原子交换操作啊。两个CAS操作之间并不能保证没有重入现象。

(3)ABA问题

这是CAS机制最大的问题所在。那到底什么是ABA呢?具体来数就是对于一个旧的变量值A,线程2将A的值改成B又改成A,此时线程1通过CAS看到A并没有变化,但实际A已经发生了变化,这就是ABA问题。

解决这个问题的方法很简单,记录一下变量的版本就可以了,在变量的值发生变化时对应的版本也做出相应的变化,然后CAS操作时除了比较和预期值是否一致外,再比较一下版本,就知道变量有没有发生过改变了。

atomic包下AtomicStampedReference类实现了这种思路。Mysql中Innodb的多版本并发锁也是这个原理。

ABA问题对于数值类型,例如Integer、Long等没有什么影响,但是对于引用类型变量就可能存在问题。表面上看不出区别,但是其内部结构可能发生过变化。

Unsafe

(1)简介

Unsafe类是整个java并发包底层实现的核心。

Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,一旦能够直接操作内存,这也就意味着

  • 不受jvm管理,也就意味着无法被GC,需要我们手动GC,稍有不慎就会出现内存泄漏。
  • Unsafe的不少方法中必须提供原始地址(内存地址)和被替换对象的地址,偏移量要自己计算,一旦出现问题就是JVM崩溃级别的异常,会导致整个JVM实例崩溃,表现为应用程序直接宕掉。
  • 直接操作内存,也意味着其速度更快,在高并发的条件之下能够很好地提高效率。

因此,从上面三个角度来看,虽然在一定程度上提升了效率但是也带来了指针的不安全性。

(2)方法归纳

1、初始化

public final class Unsafe {

    // 注册native方法,使Unsafe方法可以调用C语言
    private static native void registerNatives();
    static {
        // 调用JVM本地方法registerNatives()和sun.reflect.Reflection
        registerNatives();
        sun.reflect.Reflection.registerMethodsToFilter(Unsafe.class, "getUnsafe");
    }

    // 构造方法是私有的,除了自己,其他类都不可以通过new来实例化
    private Unsafe() {}
    
    // 出初始化方法 单例模式
    private static final Unsafe theUnsafe = new Unsafe();


    @CallerSensitive
    public static Unsafe getUnsafe() {
        Class<?> caller = Reflection.getCallerClass();
        if (!VM.isSystemDomainLoader(caller.getClassLoader()))
            throw new SecurityException("Unsafe");
        return theUnsafe;
    }
    /* 通过Reflection的getCallerClass判断当前调用的类是否是主类加载器(BootStrap classLoader)加载的,
    否则的话抛出一个SecurityException。这也证明了一个问题,那就是只有由主类加载器(BootStrap classLoader)加载的类才能调用这个类中的方法。*/
    // ....
}

2、操作属性方法

(1)public native Object getObject(Object o, long offset);

通过给定的Java变量获取引用值。这里实际上是获取一个Java对象o中,获取偏移地址为offset的属性的值,此方法可以突破修饰符的抑制,也就是无视private、protected和default修饰符。类似的方法有getInt、getDouble等等。同理还有putObject方法。

(2)public native Object getObjectVolatile(Object o, long offset);

强制从主存中获取属性值。类似的方法有getIntVolatile、getDoubleVolatile等等。同理还有putObjectVolatile。

(3)public native void putOrderedObject(Object o, long offset, Object x);

设置o对象中offset偏移地址offset对应的Object型field的值为指定值x。这是一个有序或者有延迟的putObjectVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被修改的时候使用才会生效。类似的方法有putOrderedInt和putOrderedLong。

(4)public native long staticFieldOffset(Field f);

返回给定的静态属性在它的类的存储分配中的位置(偏移地址)。

(5)public native long objectFieldOffset(Field f);

返回给定的非静态属性在它的类的存储分配中的位置(偏移地址)。

(6)public native Object staticFieldBase(Field f);

返回给定的静态属性的位置,配合staticFieldOffset方法使用。

3、操作数组

(1)public native int arrayBaseOffset(Class arrayClass);

返回数组类型的第一个元素的偏移地址(基础偏移地址)。

(2)public native int arrayIndexScale(Class arrayClass);

返回数组中元素与元素之间的偏移地址的增量。

这两个方法配合使用就可以定位到任何一个元素的地址。

4、内存管理

(1)public native int addressSize();

获取本地指针的大小(单位是byte),通常值为4或者8。常量ADDRESS_SIZE就是调用此方法。

(2)public native int pageSize();

获取本地内存的页数,此值为2的幂次方。

(3)public native long allocateMemory(long bytes);

分配一块新的本地内存,通过bytes指定内存块的大小(单位是byte),返回新开辟的内存的地址。

(4)public native long reallocateMemory(long address, long bytes);

通过指定的内存地址address重新调整本地内存块的大小,调整后的内存块大小通过bytes指定(单位为byte)。

(5)public native void setMemory(Object o, long offset, long bytes, byte value);

将给定内存块中的所有字节设置为固定值(通常是0)。

5、线程挂起和恢复

(1)public native void unpark(Object thread);

释放被park创建的在一个线程上的阻塞。由于其不安全性,因此必须保证线程是存活的。

(2)public native void park(boolean isAbsolute, long time);`

阻塞当前线程,一直等道unpark方法被调用。

6、内存屏障

(1)public native void loadFence();

在该方法之前的所有读操作,一定在load屏障之前执行完成。

(2)public native void storeFence();

在该方法之前的所有写操作,一定在store屏障之前执行完成

(3)public native void fullFence();

在该方法之前的所有读写操作,一定在full屏障之前执行完成,这个内存屏障相当于上面两个(load屏障和store屏障)的合体功能。

7、CAS机制

(1)public final native boolean compareAndSwapObject(Object o, long offset, Object expected, Object x)

(2)public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x);

(3)public final native boolean compareAndSwapLong(Object o, long offset,long expected,long x);

不需要记住每一个方法,只需记住大的分类,就能涵盖Unsafe的大部分全部功能

(3)怎么使用

(Although the class and all methods are public, use of this class is limited because only trusted code can obtain instances of it.)
可见官方并不推荐我们使用,也就是说我们无法直接new出来一个Unsafe类出来,通过getUnsafe()方法进行调用也直接抛出异常。那我们该如何使用呢?

可以通过反射机制来获取Unsafe。

public class HelloUnsafe {
    static class M {
        private M() {}
        int i =0;
    }

   public static void main(String[] args) throws Exception {
       // 通过反射得到theUnsafe对应的Field对象
       Field field = Unsafe.class.getDeclaredField("theUnsafe");
       // 设置该Field为可访问
       field.setAccessible(true);
       // 通过Field得到该Field对应的具体对象,传入null是因为该Field为static的
       Unsafe unsafe = (Unsafe) field.get(null);
       //  创建对象实例
       M m = (M)unsafe.allocateInstance(M.class);
       m.i = 9;
       System.out.println(m.i);

       // Unsafe unsafe = Unsafe.getUnsafe();
       // M m = (M)unsafe.allocateInstance(M.class);
       // m.i = 9;
       // System.out.println(m.i);
    }
}

参考文章:

  • https://baijiahao.baidu.com/s?id=1648712942552745701&wfr=spider&for=pc(Unsafe部分内容摘自该文章)
  • https://blog.csdn.net/qq_35571554/article/details/82892806 程序员小灰
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值