cas内外网同时访问_「Java」 - 多线程五 & CAS

一、CAS

Java并发处理中锁非常重要,但是使用锁会导致线程上下文的切换和重新调度的开销。虽然Java提供了非阻塞的volatile关键字来解决共享变量的可见性问题,但是volatile不能保证原子性操作。

synchronized为悲观锁,始终认为会发生并发冲突,将同步代码块上锁;同时JDK也提供了乐观锁CAS(Compare And Swap),假设不会发生并发冲突,只在提交操作时检查是否违反数据完整性,如果数据不一致提交失败,会进行重试,保证了非阻塞原子性操作(Lock-Free)。

CAS通过硬件保证了比较-更新操作的原子性,JDK里面的Unsafe类提供的compareAndSwap方法。函数签名为boolean compareAndSwapLong(Object obj, long valueOffset, long expect, long update)。支持原子更新操作,适用于计数器,序列发生器等场景。

compareAndSwap是比较并交换,CAS有四个操作数分别为:对象内存位置V、对象中变量的偏移量Offset、变量预期值expect和新的值update。操作含义是如果对象obj中内存偏移量为valueOffset位置的变量值为expect则使用新的值update替换旧的值expect,这个是处理器提供的一个原子性指令。

CAS多数情况下对开发者来说是透明的,JUC的atomic包提供了常用的原子性数据类型以及引用、数组等相关原子类型和更新操作工具。Unsafe类虽提供CAS服务,但因能够操纵任意内存地址读写而有安全隐患,Java 9以后,可以使用Variable Handle API来替代Unsafe。

A、CAS缺点

  • 若循环时间长,则开销很大
  • 只能保证一个共享变量的原子操作
  • ABA问题
    • 解决ABA问题 - AtomicStampedReference

二、AtomicLong

AtomicLong就是使用无锁CAS算法实现的线程安全的计数器类,保证了多线程下更新内部的long型变量值的原子性与访问变量的内存可见性。

AtomicLong本质是使用Unsafe类提供了一些列的CAS方法来实现线程安全的原子变量。

public class AtomicLong extends Number implements java.io.Serializable 
{
    private static final long serialVersionUID = 1927816293512124184L;
    
    // 1.创建unsafe实例
    private static final Unsafe unsafe = Unsafe.getUnsafe();

    // value在AtomicLong中的偏移量
    private static final long valueOffset;
    
    static 
    {
        try 
        {
            // 2.获取变量value在AtomicLong中的偏移量
            valueOffset = unsafe.objectFieldOffset(AtomicLong.class.getDeclaredField("value"));
        } 
        catch (Exception ex) 
        { 
            throw new Error(ex); 
        }
    }
    
    // 3.计数器值
    private volatile long value;
    
    // 4.获取计数值
    public final long get() 
    {
        return value;
    }
    
    // 5.设置计数值
    public final void set(long newValue) 
    {
        value = newValue;
    }
    
    // 6.递增/递减 然后返回递增/递减后的值
    public final long incrementAndGet() 
    {
        return unsafe.getAndAddLong(this, valueOffset, 1L) + 1L;
    }
    
    public final long decrementAndGet() 
    {
        return unsafe.getAndAddLong(this, valueOffset, -1L) - 1L;
    }
    
    // 7.递增/递减 然后返回递增/递减前的值
    public final long getAndIncrement() 
    {
        return unsafe.getAndAddLong(this, valueOffset, 1L);
    }
    
    public final long getAndDecrement() 
    {
        return unsafe.getAndAddLong(this, valueOffset, -1L);
    }
    
    // 8.CAS替换原子变量的值
    public final boolean compareAndSet(long expect, long update) 
    {
        return unsafe.compareAndSwapLong(this, valueOffset, expect, update);
    }
}

代码1,创建Unsafe实例,因为AtomicLong中本质是使用Unsafe提供的CAS方法来实现计数安全的,需要注意的是,开发人员自己使用代码1获取的Unsafe实例是不可以用的,时候会抛出错误。

Exception in thread "main" java.lang.ExceptionInInitializerError
Caused by: java.lang.SecurityException: Unsafe
        at sun.misc.Unsafe.getUnsafe(Unsafe.java:90)

因为Unsafe类的某些方法是直接可以操作内存的,这是不安全的,所以JDK提供方限制只有使用BoostrapClassLoader加载的JDK的核心类里面才可以使用代码1方式获取Unsafe的实例,而AtomicLong类是rt.jar里面的(使用BootStrapClassloader加载),而自己main函数的类加载器为AppClassloader,所以抛出了异常,当然如果确实需要使用,使用万能的反射就可以获取可以使用的Unsafe实例。

代码2,获取value变量在AtomicLong对象内存分布中的偏移量,并保存到valueOffset中。
代码3,是记录计数的变量,这里被声明为volatile是为了避免内存可见性问题,比如线程调用代码4调用get()方法获取变量value值的时候,不会去线程的本地变量而是去主内存里获取;线程调用代码5设置value的值后,会把本地变量的值刷新到主内存,以便其他线程通过方法4获取最新的值。

代码6,incrementAndGet方法原子性地递增变量value的值,然后返回递增后的值,这里是调用了unsafe对象的getAndAddLong方法来做到的,unsafe.getAndAddLong(this, valueOffset, 1L)保证了value值可以原子性递增1,其中this是AtomicLong对象的引用,valueOffset是value在AtomicLong中的偏移量,1L说明要原子性地把value的值在原来的基础上增加1。decrementAndGet则是原子性地把value的值递减1,然后返回递减后的值。
代码7,getAndIncrement与incrementAndGet的不同之处在于,前者递增后返回的是递增前的值,后者是返回地址后的值。
代码8,这个前面讲解过使用CAS替换原子变量的值,这里this是AtomicLong对象的引用,valueOffset是value在AtomicLong中的偏移量,如果当前value的值等于expect,则使用update原子性替换value的值。
JDK8 中 unsafe.getAndAddLong 的实现代码为:

public final long getAndAddLong(Object paramObject, long paramLong1, long paramLong2) 
{
    long l;
    
    do {
        l = getLongVolatile(paramObject, paramLong1);
    } while (!compareAndSwapLong(paramObject, paramLong1, l, l + paramLong2));
    
    return l;
}

可知这里是一个无限循环,当多个线程调用getAndAddLong方法更新同一个变量时,由于compareAndSwapLong是CAS操作,同时只有一个线程可以更新成功,剩余失败的线程则会循环一次获取变量的最新值,然后再次尝试CAS操作,直到CAS成功。这个操作一般称为CAS自旋操作,本质是乐观锁,是使用CPU资源来减少锁阻塞带来的开销。
通过CAS提供的非阻塞原子操作,相比使用阻塞算法的同步器来说性能已经很好了,但是在高并发下大量线程会同时去竞争更新同一个原子变量,但是由于同时只有一个线程的CAS操作会成功,这就造成了大量线程竞争失败后会通过无限循环不断进行自旋尝试CAS的操作,而这会白白浪费CPU资源。
JDK8新增的一个类LongAdder用来解决高并发下AtomicLong的缺点
LongAdder的核心思想是把一个原子变量分解为多个变量,让同样多的线程去竞争多个资源,这样竞争每个资源的线程数就被分担了下来。

7c2241552c1971c566f60227f964e1f6.png

AtomicLong是多个线程同时竞争同一个原子变量。

44e70985bb2a45cddb533988d40556a6.png

LongAdder内部维护多个Cell变量,在同等并发量的情况下,争夺单个变量更新操作的线程量会减少,这是变相减少了争夺共享资源的并发量。

@sun.misc.Contende
static final class Cell
{
    volatile long value;
    
    Cell(long x)
    {
        value = x;
    }
    
    final boolean cas(long cmp, long val)
    {
        return UNSAFE.compareAndSwapLong(this, valueOffset, cmp, val);
    }
    
    // Unsafe 机制
    private static final sun.misc.Unsafe UNSAFE;
    private static final long valueOffset;
    
    static
    {
        try
        {
            UNSAFE = sun.misc.Unsafe.getUnsafe();
            Class<?> ak = Cell.class;
            valueOffset = UNSAFE.objectFieldOffset(ak.getDeclaredField("value"));
        }
        catch (Exception e)
        {
            throw new Error(e);
        }
    }
}

类似AtomicLong的结构,Cell里面有一个long型的value,并且被volatile修饰以避免多线程内部不可见问题,另外该类使用@sun.misc.Contended修饰是为了避免伪共享问题。
LongAdder维护了一个延迟初始化的原子性更新数组(默认情况下Cell数组是null)和一个基值变量base,由于Cells占用内存是相对比较大的,所以一开始并不创建,而是在需要时再创建,也就是惰性加载。
当一开始判断Cell数组是null并且并发线程较少时所有的累加操作都是对base变量进行的,这时候就退化为了AtomicLong。Cell数组的大小保持是2的N次方大小,初始化时候Cell数组的中Cell的元素个数为2,数组里面的变量实体是Cell类型。
当多个线程在争夺同一个Cell原子变量时,如果失败并不是在当前Cell变量上一直自旋CAS重试,而是会尝试在其他Cell的变量上进行CAS尝试,这个改变增加了当前线程重试时CAS成功的可能性。
最后获取LongAdder当前值的时候是把所有Cell变量的value值累加后再加上base返回的。

public long sum()
{
    Cell[] as = cells;
    Cell a;
    long sum = base;
    
    if (as != null)
    {
        for (int i = 0; i < as.length; ++i)
        {
            if ((a = as[i]) != null)
            {
                sum += a.value;
            }
        }
    }
    return sum;
}

首先把base的值赋值给sum变量,然后通过循环把每个cell元素的value值累加到sum变量上,最后返回sum。
其实这是一种分而治之的策略,先把并发量分担到多个原子变量上,让多个线程并发地对不同的原子变量进行操作,然后获取计数时再把所有原子变量的计数和累加。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值