CAS自旋锁浅析

CAS自旋锁浅析

前提:

了解 JMM(Java内存模型),链接:JMM(Java内存模型)浅记

了解volatile和synchronized关键字

volatile关键字:

  • 保证了可见性
  • 不保证原子性
  • 保证了有序性(通过屏障来防止指令重排,来实现 JMM 规范的有序性)

synchronized关键字:

  • 保证了可见性
  • 保证了原子性
  • 保证了有序性(通过限制每次只能一个线程执行操作,来实现 JMM 规范的有序性)

注意:

虽然 volatile 和 synchronized 都实现了 JMM 规范的有序性,但是两者实现的方式有所不同,volatile通过禁止指令重排来保证有序性,synchronized 通过限制每次只能一个线程执行加锁的代码的操作来保证有序性。

CAS:

翻译过来就是,比较并交换。

介绍:

JMM规范中有主内存和工作内存(即线程的独占内存)之分。

工作内存在往主内存写回数据之前,根据期望值(之前获得主内存的数据)和现在主内存的数值进行比较,如果两者的值相同,则说明没有其他线程修改主内存中值,此时当前线程可以将自己的工作内存数据写回主内存。反之,期望值和现在主内存的值不相等,说明主内存数据被其他线程更改过了,所以更新期望值为主内存中的数值,在下一次写回主内存中,使用新的期望值和现在主内存中数据比较。往复循环,直至工作内存中的数据写回主内存中。

简单来说:

比较并交换,即是通过期望值当前值进行比较,如果比较两者相同,则修改主内存中的值,否则不修改,直至修改成功。

案例:

当我们多线程进行 自增操作时,会发生数据覆盖,导致得到的结果不正确,可以采用如下两种方式来解决:

  • 使用原子操作类AtomicInteger来进行自增(推荐),其底层就是使用rt.jar中的 Unsafe 类和 CAS 自旋。
  • 使用 synchronized (不推荐)

原因:

synchronized 是一把重型锁,影响性能,不适用这种场景。

补充:

volatile虽然是轻量级的同步机制,但是其不保证原子性,所以无法解决多线程自增,数据发生覆盖的问题。

代码演示如下:

多线程执行自增:

@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
    private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
	
    //正常的定义变量
    private int[] a = {0};
    private AtomicInteger atomic = new AtomicInteger();

    @Test
    public void test1() throws InterruptedException {

        for (int i = 0; i < 18; i++){
            new Thread(()->{
                //logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
                for (int j = 0; j < 1000; j++){
                     a[0]++;
                }
                logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
            }, String.valueOf(i)).start();
        }
        //休眠5秒,保证上面各线程执行完成
        TimeUnit.SECONDS.sleep(5);
        logger.info("得到的最终结果:{}", a[0]);
    }
}

得到的结果:
CAS自旋锁浅析_第1张图片
由于 i++ 不是原子性操作,会存在覆盖的情况,导致结果小于 18000 。

使用volatile

@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
    private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);
	
    //使用volatile
    volatile private int[] a = {0};
    private AtomicInteger atomic = new AtomicInteger();

    @Test
    public void test1() throws InterruptedException {

        for (int i = 0; i < 18; i++){
            new Thread(()->{
                //logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
                for (int j = 0; j < 1000; j++){
                     a[0]++;
                }
                logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
            }, String.valueOf(i)).start();
        }
        //休眠5秒,保证上面各线程执行完成
        TimeUnit.SECONDS.sleep(5);
        logger.info("得到的最终结果:{}", a[0]);
    }
}

得到的结果
CAS自旋锁浅析_第2张图片

可见 volatile 关键字不能保证原子性。

使用synchronized

@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
    private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);

    private int[] a = {0};
    //原子操作类,不传入参数,默认是0
    private AtomicInteger atomic = new AtomicInteger();

     @Test
    public void test2() throws InterruptedException {

        for (int i = 0; i < 18; i++){
            new Thread(()->{
                //logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
                for (int j = 0; j < 1000; j++){
                    
                    //使用synchronized
                    synchronized (this){
                        a[0]++;
                    }
                }
                logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
            }, String.valueOf(i)).start();
        }
        //休眠5秒,保证上面各线程执行完成
        TimeUnit.SECONDS.sleep(5);
        logger.info("得到的最终结果:{}", a[0]);
    }
}

得到的结果正常:
CAS自旋锁浅析_第3张图片

原子操作类AtomicInteger

@RunWith(SpringRunner.class)
@SpringBootTest
public class AtomicIntegerTest {
    private Logger logger = LoggerFactory.getLogger(AtomicIntegerTest.class);

    private int[] a = {0};
    //原子操作类,不传入参数,默认是0
    private AtomicInteger atomic = new AtomicInteger();

     @Test
    public void test3() throws InterruptedException {

        for (int i = 0; i < 20; i++){
            new Thread(()->{
                //logger.info("当前线程的名称:{},开始",Thread.currentThread().getName());
                for (int j = 0; j < 1000; j++){
                    
                    //使用原子操作类的方法进行自增
                    atomic.getAndIncrement();
                }
                logger.info("当前线程的名称:{},结束",Thread.currentThread().getName());
            }, String.valueOf(i)).start();
        }
        //休眠5秒,保证上面各线程执行完成
        TimeUnit.SECONDS.sleep(5);
        logger.info("得到的最终结果:{}", atomic.get());
    }
}

得到的结果:
CAS自旋锁浅析_第4张图片

结果正常,和使用 synchronized 效果相同。

AtomicInteger源码浅析:

AtomicInteger对象调用自增的方法

atomic.getAndIncrement();

AtomicInteger中的源码

// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

/**
* Atomically increments by one the current value.
*
* @return the previous value
*/
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

进入到 rt.jar 下的 Unsafe 类

//var1:传入的对象,var2:偏移地址, var4:1
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        //通过传入的对象和偏移地址,获得主内存的当前数据,作为线程的工作内存的预期值
        //方法名中有volatile,具有可见性。
        var5 = this.getIntVolatile(var1, var2);
        
        /**
        * 当工作内存回写到主内存时,通过 var1 和 var2 获取主内存中 当前数据,判断主内存的当前数据
        * 和之前线程的工作内存中预期值 var5 是否相同,如果相同,则把预期值 var5 + 1 (var4),
        * 返回true,取反后得到FALSE,循环终止。如果通过var1和var2取得的主内存当前值和预期值不同,
        * 返回false,取反之后得到true,继续循环,直至线程写回主内存成功。
        */
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
	//返回 + var4 (1) 的最终结果 
    return var5;
}

AtomicInteger底层通过不断自旋,保证得到的结果正确。

CAS 自旋锁和 synchronized 锁优缺点:

CAS:

  • 并发性更好,没有限制只有一个线程执行加锁的操作
  • CAS 不断循环占用CPU较多的时间
  • 无法解决 ABA 问题

synchronized:

  • 并发性较差,为了保证JMM规范的有序性,只允许一个线程执行加锁的操作。
  • 不会占用太多CPU的时间

ABA问题

概述:

一个值从A变成B,然后再变成A,此时两个A虽然数值一样,但是从实质来说,两者不是同一个A。在自旋的过程中,会将ABA中的后一个A当做前一个A。这个问题在一些情况是没问题的,在一些情况是会造成问题的。

问题:

  • 导致CAS不一定是原子性的

解决:

添加版本号,类似于乐观锁的版本号。

拓展:

谈到 JMM 需要想到 volatile 和 synchronized 关键字,volatile 要想到 单例模式 、 DCL (双重检查锁)和 原子操作类(比如 AtomicInteger),通过原子操作类要想到其底层 使用了 UnSafe 类(在类中大多数据都是用native修饰,使用了C语言来操作底层硬件系统)和 CAS 自旋锁,CAS 底层用到 CAS 原语以此来保证操作的原子性。 通过原子操作类要想到原子引用。通过 CAS 要想到 ABA 问题,通过ABA问题要想到乐观锁。

注意:

即使我们 java 的一个语句对应 一个编译语言,也不能保证该操作就是原子性操作,因为编译语言还要转换为机器语言,要保证是原子操作,必须要对应的是编译语言的原语(原语要保证操作不被中断,直到操作执行完成)。

注意:

DCL的优缺点待补充。

你可能感兴趣的:(java,java,开发语言,后端)