JAVA CAS、自旋、乐观锁

CAS全称compare and swap,是“比较并替换”的意思。CAS有3个重要的操作数:内存值(a)、预期值(aExpe)、更新值(aNew),当且仅当预期值与内存值相同,才将内存值改为修改值,否则什么都不做,最后返回成功或失败。下图是“比较并替换”的执行流程。

使用synchronized来模拟“比较并替换”的代码是这样的

/**
 * 使用代码模拟CAS
 */
class SimulatedCAS{
    private volatile int value;

    public synchronized int compareAndSwap(int expectedValue, int newValue){
        int oldValue = value;
        if (oldValue == expectedValue){
            value = newValue;
        }
        return oldValue;
    }
}

CAS与synchronized不同之处在于,synchronized是一种悲观锁,悲观锁假设了多个线程在同一时刻修改共享数据的情况一定会发生,所以给访问共享数据的代码加上锁。而CAS能实现乐观锁,乐观锁认为每次去拿共享数据的时候认为其他线程不会修改此数据,所以不会上锁,在更新共享数据的时候判断在此期间别的线程有没有修改共享数据,数据被修改,则失败,数据未被修改则更新成功。

如果“比较并替换”失败了,该怎么处理呢?以上图中的线程2位例子,线程2需要重新获取数据并执行“比较并替换”操作,可使用一个do while循环实现,这个do while循环也称为自旋,比方说因为while的条件在短时间内都是true,线程反复执行do while的代码,类似于自我旋转。

大家应该都知道,在Java中,i++不是原子操作,在多线程下是不安全的,可以使用AtomicInteger的getAndAdd(int delta)实现线程安全的数字自增。AtomicInteger#getAndAdd(int delta)调用了Unsafe.getAndAddInt(Object var1, long var2, int var4)方法,以下是Unsafe.getAndAddInt(Object var1, long var2, int var4)方法源码的源码,使用了CAS和自旋。

    /**
     * Unsafe#getAndAddInt(java.lang.Object, long, int)方法源码
     *
     * @param var1 可能是原子操作类对象,例如AtomicInteger
     * @param var2 共享变量在内存中的偏移地址
     * @param var4 自增的数值
     * @return
     */
    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            // 通过内存偏移地址取共享变量的值
            var5 = this.getIntVolatile(var1, var2);

            /**
             * 在一次原子操作中完成“比较并替换”
             * 再次通过var1、var2获取共享变量在内存中的值,如果仍然等于var5,则将共享变量设置为var5+var4
             * “比较并替换”失败,则循环重试,直到成功
             */
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

        return var5;
    }

Unsafe是Java实现CAS的核心类,此类提供了硬件级别的原子操作,很多方法是native方法,即方法不是Java语言实现,可能是由C/C++实现,Java可调用这些方法。

AtomicInteger实现线程安全的自增代码演示

class AtomicIntegerDemo{
    private static AtomicInteger atomicInteger = new AtomicInteger(0);
    private static int num = 0;

    private static Runnable runnable = () -> {
        for (int i = 0; i < 10000; i++) {
            incrementAtomic();
            increment();
        }
    };

    /**
     * 原子类递增
     */
    private static void incrementAtomic(){
        atomicInteger.getAndAdd(1);
    }

    /**
     * ++ 方式递增
     */
    private static void increment(){
        num++;
    }

    public static void main(String[] args) throws Exception{
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        Thread t3 = new Thread(runnable);
        Thread t4 = new Thread(runnable);
        Thread t5 = new Thread(runnable);

        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();

        t1.join();
        t2.join();
        t3.join();
        t4.join();
        t5.join();

        System.out.println("原子类结果:"+atomicInteger.get());
        System.out.println("普通变量结果:"+ num);
    }
}

CAS有以下缺点:

1、线程1获取到共享变量值是“A”,进行一系列操作,在此期间,线程2将变量改成“B”,线程3再将变量改为“A”,线程1执行到设置值阶段,认为值没有被修改,还是A。这也称为ABA问题。JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

2、循环时间长开销大。如果CAS失败,则会进行自旋,长时间自旋很消耗CPU。可以设置一个自旋阀值N,尝试N次失败后转为阻塞锁,并且N能够在程序运行过程中动态调整。这称为自适应自旋。

悲观锁适合场景:
    1、并发写请求多的情况
    2、适用于临界区持锁时间比较长的情况,悲观锁可以避免大量无用自旋等的消耗。典型场景:临界区有IO操作、临界区代码复杂或者循环量大。(注:同一时刻只有一个线程能执行的的代码片段叫做临界区)

乐观锁适合场景:
    1、大部分是读请求的场景,不加锁能让读取性能大幅度提高。

CAS在ConcurrentHashMap中的应用

在JDK8之前,ConcurrentHashMap使用分段加锁的方式实现,每一段称为一个segment,默认分成16段。到了JDK8后,使用HashMap、synchronized、cas实现线程安全。

class ConcurrentHashMapDemo{
    private static ConcurrentHashMap<String, Integer> scores = new ConcurrentHashMap<>();

    private static Runnable runnable = () -> {
        for (int i = 0; i < 1000; i++) {

            // 线程安全的写法。自旋 + CAS
            Integer score, newScore;
            do {
                score = scores.get("小明");
                newScore = score + 1;
            }while (!scores.replace("小明", score, newScore));


            ///**
            // * 组合操作,线程不安全
            // * ConcurrentHashMap只是源码内部保证线程安全,使用者要保证自己的代码也是线程安全的
            // */
            //Integer score = scores.get("小明");
            //Integer newScore = score + 1;
            //scores.put("小明", newScore);

        }
    };

    public static void main(String[] args) throws Exception{
        scores.put("小明", 0);
        Thread t1 = new Thread(runnable);
        Thread t2 = new Thread(runnable);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println(scores);
    }

}
 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值