多线程之美6一CAS与自旋锁

1、什么是CAS

CAS 即 compare and swap 比较并交换, 涉及到三个参数,内存值V, 预期值A, 要更新为的值B, 拿着预期值A与内存值V比较,相等则符合预期,将内存值V更新为B, 不相等,则不能更新V。

为什么预期值A与内存值V不一样了呢?

在多线程环境下,对于临界区的共享资源,所有线程都可以访问修改,这时为了保证数据不会发生错误,通常会对访问临界区资源加锁,同一时刻最多只能让一个线程访问(独占模式下),这样会让线程到临界区时串行执行,加锁操作可能会导致并发性能降低,而循环CAS可以实现让多个线程不加锁去访问共享资源,却也可以保证数据正确性。 如 int share = 1,线程A获取到share的值1,想要将其修改为2,这时线程B抢先修改share = 3了,线程A这时拿着share =1 预期值与实际内存中已经变为3的值比较, 不相等,cas失败,这时就重新获取最新的share再次更新,需要不断循环,直到更新成功;这里可能会存在线程一直在进行循环cas,消耗cpu资源。

cas缺点:

1、存在ABA问题

2、循环cas, 可能会花费大量时间在循环,浪费cpu资源

3、只能更新一个值(也可解决,AtomicReference 原子引用类泛型可指定对象,实现一个对象中包含多个属性值来解决只能更新一个值的问题)

2、原子类 Atomic

原子类在JUC的atomic包下提供了 AtomicInteger,AtomicBoolean, AtomicLong等基本数据类型原子类,还有可传泛型的AtomicReference, 以及带有版本号的 AtomicStampedReference , 可实现对象的原子更新, 其具体是怎样保证在多线程环境下,不加锁的情况也可以原子操作, 是其内部借助了Unsafe类,来保证更新的原子性。

类图结构如下:

分别用AtomicInteger和 Integer 演示多个线程执行自增操作,是否能够保证原子性,执行结果是否正确

代码如下:

/**
 * @author zdd
 * 2019/12/22 10:47 上午
 * Description: 演示AtomicInteger原子类原子操作
 */
public class CasAtomicIntegerTest {
    static  final Integer THREAD_NUMBER = 10;
    static  AtomicInteger atomicInteger = new AtomicInteger(0);
    static  volatile Integer integer = 0;

    public static void main(String[] args) throws InterruptedException {
        ThreadTask task = new ThreadTask();
        Thread[] threads = new Thread[THREAD_NUMBER];
        //1,开启10个线程
        for (int j = 0; j < THREAD_NUMBER; j++) {
            Thread thread  = new Thread(task);
            threads[j]= thread;
        }
        for (Thread thread:threads) {
            //开启线程
            thread.start();
            //注: join 为了保证主线程在所有子线程执行完毕后再打印结果,否则主线程就阻塞等待
           // thread.join();
        }

        // 主线程休眠5s, 等待所有子线程执行完毕再打印
        TimeUnit.SECONDS.sleep(5);

        System.out.println("执行完毕,atomicInteger的值为: "+ atomicInteger.get());
        System.out.println("执行完毕,integer的值为 : "+ integer);
    }

    public static void  safeIncr() {
        atomicInteger.incrementAndGet();
    }
    public static void  unSafeIncr() {
        integer ++;
    }

    static class ThreadTask implements  Runnable{
        @Override
        public void run() {
            // 任务体,分别安全和非安全方式自增1000次
            for (int i = 0; i < 1000; i++) {
                safeIncr();
            }
            for (int i = 0; i < 1000; i++) {
                unSafeIncr();
            }
        }
    }
}

执行结果如下:

疑问:上文代码中注,我本想让主线程调用每个子线程 join方法,保证主线程在所有子线程执行完毕之后再执行打印结果,然而这样执行导致非安全的Integer自增结果也正确,猜想是在执行join方法,导致这10个子线程排队有序在执行了? 因此注释了该行代码 ,改为让主线程休眠几秒来保证在子线程执行后再打印。

AtomicInteger如何保证原子性,AtomicInteger持有Unsafe对象,其大部分方法是本地方法,底层实现可保证原子操作。

public class AtomicInteger extends Number implements java.io.Serializable {
    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();

来看一下 AtomicInteger 的自增方法 incrementAndGet(),先自增,再返回增加后的值。

代码如下:

  public final int incrementAndGet() {
       //调用unsafe的方法
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }

继续看unsafe如何实现

  public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
          //1.获取当前对象的内存中的值A
            var5 = this.getIntVolatile(var1, var2);
          //2. var1,var2联合获取内存中的值V,var5是期望中的值A, var5+var4 是将要更新为的新值
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
       //3. 更新成功,跳出while循环,返回更新成功时内存中的值(可能下一刻就被其他线程修改)
        return var5;
    }

执行流程图如下:

Unsafe 的compareAndSwapInt是本地方法,可原子地执行更新操作,更新成功返回true,否则false

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

3、CAS的ABA问题

什么是ABA问题?

例如 线程A获取变量atomicInteger =100, 想要将其修改为2019 (此时还未修改), 这时线程B抢先进来将atomicInteger先修改为101,再修改回atomicInteger =100,这时线程A开始去更新atomicInteger的值了,此时预期值和内存值相等,更新成功atomicInteger =2019;但是线程A 并不知道这个值其实已经被人修改过了。

代码演示如下:

/**
 * zdd
 * Description: cas的ABA问题
 */
public class CasTest1 {

   // static AtomicInteger atomicInteger = new AtomicInteger(100);
   /* 这里使用原子引用类,传入Integer类型,
    * 和AtomicInteger一样,AtomicReference使用更灵活,泛型可指定任何引用类型。
    * 也可用上面注释代码
    */
    static AtomicReference<Integer>  reference = new AtomicReference<>(100);

    public static void main(String[] args) {

      //1.开启线程A
        new Thread(()-> {
            Integer expect =  reference.get();
            try {
                //模拟执行任务,让线程B抢先修改
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println( "执行3s任务后, 修改值是否成功 "+ reference.compareAndSet(expect,2019)+ "  当前值为: "+ reference.get());
        },"A").start();
    //2.开启线程B
        new Thread(()-> {
            // expect1 =100
            Integer expect1 =  reference.get();
            //1,先修改为101,再修改回100,产生ABA问题
            reference.compareAndSet(expect1,101);
            //expect2 =101
            Integer expect2 =  reference.get();
            reference.compareAndSet(expect2, 100);
        },"B").start();

    }
}      

执行结果如下:可见线程A修改成功

A 执行3s任务后, 修改值是否成功:true  当前值为: 2019

4、ABA问题的解决方式

解决CAS的ABA问题,是参照数据库乐观锁,添加一个版本号,每更新一次,次数+1,就可解决ABA问题了。

AtomicStampedReference

/**
 * zdd
 * 2019/11/4 6:30 下午
 * Description:
 */
public class CasTest1 {
  //设置初始值和版本号
    static  AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);

    public static void main(String[] args) {
        //2,采用带有版本号的
        new Thread(()-> {
            Integer  expect = stampedReference.getReference();
            int     stamp = stampedReference.getStamp();
            try {
                //休眠3s,让线程B执行完ABA操作
                TimeUnit.SECONDS.sleep(3);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //此时 stamp=1,与实际版本号3不等,这里更新失败就是stamp没有获取到最新的
            System.out.println("是否修改成功: "+stampedReference.compareAndSet(expect, 101, stamp, stamp +1));
            System.out.println("当前 stamp 值: " + stampedReference.getStamp()+ "当前 reference: " +stampedReference.getReference());

        },"A").start();

        new Thread(()-> {
            Integer expect = stampedReference.getReference();
            int stamp = stampedReference.getStamp();
            try {
                //休眠1s,让线程A获取都旧的值和版本号
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 1,100 -> 101, 版本号 1-> 2
            stampedReference.compareAndSet(expect, 101 , stamp, stamp+1);
            //2, 101 ->100, 版本号 2->3
            Integer expect2 = stampedReference.getReference();
            stampedReference.compareAndSet(expect2, 100, stampedReference.getStamp(), stampedReference.getStamp() + 1);

        },"B").start();
    }
}

执行结果如下:

是否修改成功: false
当前 stamp 值: 3  当前 reference: 100

5、利用cas实现自旋锁

package cas;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReference;

/**
 * @author zdd
 * 2019/12/22 9:12 下午
 * Description: 利用cas手动实现自旋锁
 */
public class SpinLockTest {

    static   AtomicReference<Thread>  atomicReference = new AtomicReference<>();

    public static void main(String[] args) {
        SpinLockTest spinLockTest = new SpinLockTest();
        //测试使用自旋锁,达到同步锁一样的效果 ,开启2个子线程
        new Thread(()-> {
            spinLockTest.lock();
            System.out.println(Thread.currentThread().getName()+" 开始执行,startTime: "+System.currentTimeMillis());
            try {
                //休眠3s
                TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 结束执行,endTime: "+System.currentTimeMillis());
            spinLockTest.unLock();
        },"线程A").start();

        new Thread(()-> {
            spinLockTest.lock();
            System.out.println(Thread.currentThread().getName()+" 开始执行,startTime: "+System.currentTimeMillis());
            try {
                //休眠3s
                TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace();
            }
            System.out.println(Thread.currentThread().getName()+" 结束执行,endTime: "+System.currentTimeMillis());
            spinLockTest.unLock();
        },"线程B").start();
    }
    public static void lock() {
      Thread currentThread =  Thread.currentThread();
      for (;;) {
          boolean flag =atomicReference.compareAndSet(null,currentThread);
         //cas更新成功,则跳出循环,否则一直轮询
          if(flag) {
              break;
          }
      }
    }
    public static void unLock() {
        Thread currentThread = Thread.currentThread();
        Thread momeryThread  = atomicReference.get();
        //比较内存中线程对象与当前对象,不等抛出异常,防止未获取到锁的线程调用unlock
        if(currentThread != momeryThread) {
            throw new IllegalMonitorStateException();
        }
        //释放锁
        atomicReference.compareAndSet(currentThread,null);
    }
}

执行结果如下图:

6、总结

通过全文,我们可以知道cas的概念,它的优缺点;原子类的使用,内部借助Unsafe类循环cas更新操作实现无锁情况下保证原子更新操作,进一步我们能够自己利用循环cas实现自旋锁SpinLock,它与同步锁如ReentrantLock等区别在于自旋锁是在未获取到锁情况,一直在轮询,线程时非阻塞的,对cpu资源占用大,适合查询多修改少场景,并发性能高;同步锁是未获取到锁,阻塞等待,两者各有适用场景。

原文地址:https://www.cnblogs.com/flydashpig/p/12081566.html

时间: 2024-10-25 03:31:22

多线程之美6一CAS与自旋锁的相关文章

多线程的那点儿事(之自旋锁)

自旋锁是SMP中经常使用到的一个锁.所谓的smp,就是对称多处理器的意思.在工业用的pcb板上面,特别是服务器上面,一个pcb板有多个cpu是 很正常的事情.这些cpu相互之间是独立运行的,每一个cpu均有自己的调度队列.然而,这些cpu在内存空间上是共享的.举个例子说,假设有一个数据 value = 10,那么这个数据可以被所有的cpu访问.这就是共享内存的本质意义. 我们可以看一段Linux 下的的自旋锁代码(kernel 2.6.23,asm-i386/spinlock.h),就可有清晰的

CAS自旋锁

什么是自旋锁 自旋锁要从多线程下的锁机制说起,由于多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问.即每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁. 设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办? 通常有2种处理方式: 一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是本文的重点

多线程之美5一 AbstractQueuedSynchronizer源码分析&lt;一&gt;

AQS的源码分析 目录结构 1.什么是CAS ? 2.同步器类结构 3.CLH同步队列 4.AQS中静态内部类Node 5.方法分析 ? 5.1.acquire(int arg ) ? 5.2.release(int arg) 释放锁 6.总结 前言 在多线程环境下,我们一般会对临界区资源(共享资源)进行加锁,释放锁,保证同一时刻最多只有一个线程(独占模式),就如去公共厕所里,在使用一个小房间时会加锁避免自己在使用的时候,别人突然闯进来一样,引起不必要的麻烦,在使用完后,再打开锁,其他人才可使用

Java线程 - CAS自旋锁(spin-lock)

一.自旋锁提出的背景 由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问.即是每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁.设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?通常有2种处理方式.一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是自旋锁,他不用将县城阻塞起来(NON

Java的多线程机制:缓存一致性和CAS

Java的多线程机制:缓存一致性和CAS 一.总线锁定和缓存一致性 这是两个操作系统层面的概念.随着多核时代的到来,并发操作已经成了很正常的现象,操作系统必须要有一些机制和原语,以保证某些基本操作的原子性,比如处理器需要保证读一个字节或写一个字节是原子的,那么它是如何实现的呢?有两种机制:总线锁定和缓存一致性. 我们知道,CPU和物理内存之间的通信速度远慢于CPU的处理速度,所以CPU有自己的内部缓存,根据一些规则将内存中的数据读取到内部缓存中来,以加快频繁读取的速度.我们假设在一台PC上只有一

并发编程--CAS自旋锁

在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问.即是每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁.设计互斥算法时总是会面临一种情况,即

CAS机制与自旋锁

CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS. 它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子性. 具体体现于sun.misc.Unsafe类中的native方法,调用这些native方法,JVM会帮我们实现汇编指令,这些指令是CPU的原子指令,因此具有原子性. 1 public class CASDemo { 2 3 public static void main(String[] arg

乐观的并发策略——基于CAS的自旋

悲观者与乐观者的做事方式完全不一样,悲观者的人生观是一件事情我必须要百分之百完全控制才会去做,否则就认为这件事情一定会出问题:而乐观者的人生观则相反,凡事不管最终结果如何,他都会先尝试去做,大不了最后不成功.这就是悲观锁与乐观锁的区别,悲观锁会把整个对象加锁占为自有后才去做操作,乐观锁不获取锁直接做操作,然后通过一定检测手段决定是否更新数据.这一节将对乐观锁进行深入探讨. 上节讨论的Synchronized互斥锁属于悲观锁,它有一个明显的缺点,它不管数据存不存在竞争都加锁,随着并发量增加,且如果

SpinLock 自旋锁, CAS操作(Compare &amp; Set) ABA Problem

SpinLock 自旋锁 spinlock 用于CPU同步, 它的实现是基于CPU锁定数据总线的指令. 当某个CPU锁住数据总线后, 它读一个内存单元(spinlock_t)来判断这个spinlock 是否已经被别的CPU锁住. 如果否, 它写进一个特定值, 表示锁定成功, 然后返回. 如果是, 它会重复以上操作直到成功, 或者spin次数超过一个设定值. 锁定数据总线的指令只能保证一个机器指令内, CPU独占数据总线. 单CPU当然能用spinlock, 但实现上无需锁定数据总线. spinl