JUC并发编程16 | CAS自旋锁

CAS自旋锁

是什么,干什么,解决了什么痛点?如何解决,如何使用。

原子类:java.util.concurrent.atomic

在没有CAS之前,多线程环境不使用原子类保证线程安全i++等操作,会出现数据问题,如果直接加锁synchronized,资源的开销就比较大

在出现CAS之后,多线程环境,使用原子类保证线程安全i++,类似我们的乐观锁

CAS是什么

CAS是compare and swap的缩写,中文翻译为比较并交换,实现并发算法时常用的一种技术

CAS 包含三个操作数 —— 内存位置、预期原值及更新值

在执行CAS操作的时候,将内存位置的值与预期原值比较,

  • 如果相匹配,那么处理器会自动将该位置值更新为新值
  • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS只有一个会成功

CAS的原理

CAS 有三个操作数,位置内存值V,旧的预期值A,要修改的更新值为B

当且仅当就得预期值A与内存值V相同时,将内存值V修改位B,否则什么都不做,重来——即自旋

JUC并发编程16 | CAS自旋锁_第1张图片

这是通过硬件级别保证的

Unsafe 类

CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了比较-更新的原子性。

它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。

CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。

执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行cas操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

进入Unsafe方法查看源码

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

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

public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);

/**
上面三个方法都是类似的,主要对4个参数做一下说明。
var1:表示要操作的对象
var2:表示要操作对象中属性地址的偏移量
var4:表示需要修改数据的期望的值
var5/var6:表示需要修改为的新值
*/

1 Unsafe
是CAS的核心类,由于Java方法无法直接访问底层系统,需要通过本地〈native)方法来访问,Unsafe相当于一个后门,基于该类可以直接操作特定内存的数据。Unsafe类存在于sun.misc包中,共内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。

注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务

2 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的。

// AtomicInteger 类
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1);
}

// Unsafe 类
public final int getAndAddInt(Object var1, long var2, int var4) {
    int var5;
    do {
        // volatile 修饰,一旦var5被修改会被立即获知
        var5 = this.getIntVolatile(var1, var2);
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));

    return var5;
}

3 变量value使用volatile修饰,保证了多线程之间的内存可见性

假设线程A和线程B两个线程同时执行getAndAddInt操作(分别跑在不同CPU上)

  1. AtomicInteger里面的value原始值为3,即主内存中AtomicInteger的value为3,根据JMM模型,线程A和线程B各自持有一份值为3的value的副本分别到各自的工作内存。
  2. 线程A通过getIntVolatile(var1, var2)拿到value值3,这时线程A被挂起。
  3. 线程B也通过getlntVolatile(var1, var2)方法获取到value值3,此时刚好线程B没有被挂起并执行compareAndSwaplnt方法比较内存值也为3,成功修改内存值为4,线程B打完收工,一切OK。
  4. 这时线程A恢复,执行compareAndSwapInt方法比较,发现自己手里的值数字3和主内存的值数字4不一致,说明该值已经被其它线程抢先一步修改过了,那A线程本次修改失败,只能重新读取重新来一遍了。
  5. 线程A重新获取value值,因为变量value被volatile修饰,所以其它线程对它的修改,线程A总是能够看到,线程A继续执行compareAndSwapInt进行比较替换,直到成功。

原子引用AtomicReference

public class CASDemo {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference = new AtomicReference<>();
        User zhangsan = new User("zhangsan", 22);
        User lisi = new User("lisi", 24);
        atomicReference.set(zhangsan);
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(zhangsan,lisi)+"\t" + atomicReference.get().toString());
    }
}

CAS与自旋锁

通过cas操作完成自旋锁,A线程先进来,调用lock方法自己持有锁5秒;

B随后进来发现当前线程支持有所,进行自旋等待,直到A释放锁后B随后抢到

public class CASDemo {
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        Thread thread = Thread.currentThread();
        System.out.println("==============="+Thread.currentThread().getName()+" come in ==============");
        while (!atomicReference.compareAndSet(null,thread)) {}
    }
    public void unlock(){
        Thread thread =  Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println("==============="+Thread.currentThread().getName()+" task is over ==============");
    }
    public static void main(String[] args) throws InterruptedException {
        CASDemo casDemo = new CASDemo();
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(5);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t1").start();
        TimeUnit.SECONDS.sleep(1);
        new Thread(()->{
            casDemo.lock();
            try {
                TimeUnit.SECONDS.sleep(8);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }finally {
                casDemo.unlock();
            }
        },"t2").start();
    }
}

CAS的缺点

  • 循环时间长开销大
  • 具有ABA问题

循环时间长开销大

如果cas失败,会一直进行尝试。如果cas长时间一直不成功,可能会给cpu带来很大的开销

ABA问题

CAS会导致“ABA问题”。

CAS算法实现一个重要前提需要取出内存中某时刻的数据并在当下时刻比较并替换,那么在这个时间差类会导致数据的变化。

比如说一个线程1从内存位置V中取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作将值变成了B,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。

尽管线程1的CAS操作成功,但是不代表这个过程就是没有问题的。

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            // 初始条件是java
            System.out.println(stampedReference.getReference()+"\t初始条件是java:" + stampedReference.getStamp());
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t被改为mysql了" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t修改回Java了:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 不知道被就该过了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp());
            System.out.println(stampedReference.getReference()+"\t不知道被就该过了,此时还能改为mysql" + stampedReference.getStamp());

        },"t2").start();

    }
}
/**
Book(name=java, id=1)	初始条件是java:1
Book(name=mysql, id=2)	被改为mysql了1
Book(name=java, id=1)	修改回Java了:1
Book(name=mysql, id=2)	不知道被就该过了,此时还能改为mysql1
*/

解决:ABA

使用 AtomicStampedReference

内容 版本
A 1
B 2
A 3

解决代码

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {
    public static void main(String[] args) {
        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        boolean b;
        // 如果是java,且邮戳不变,那就换成mysql,同时邮戳+1
        b = stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
        // 把 java 换回来
        b = stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
        System.out.println(stampedReference.getReference()+"\t" + stampedReference.getStamp());
    }
}

上面演示了单线程的情况,下面演示多线程的cas情况

@Data
@AllArgsConstructor
@NoArgsConstructor
class Book{
    private String name;
    private int id;
}
public class ABADemo {

    public static void main(String[] args) {

        Book java = new Book("java",1);
        Book mysql = new Book("mysql",2);
        AtomicStampedReference<Book> stampedReference = new AtomicStampedReference<>(java, 1);
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 此时邮戳莫有启动,但是已经被改为mysql了
            stampedReference.compareAndSet(java, mysql, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号2:" + stampedReference.getStamp());

            // 修改回Java了
            stampedReference.compareAndSet(mysql, java, stampedReference.getStamp(), stampedReference.getStamp()+1);
            System.out.println(Thread.currentThread().getName()+"\t"+stampedReference.getReference()+"\t版本号3:" + stampedReference.getStamp());
        },"t1").start();
        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t首次版本号:" + stampedReference.getStamp());
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            // 被修改过了
            boolean b = stampedReference.compareAndSet(java, mysql, stamp, stampedReference.getStamp()+1);
            System.out.println(b+"\t"+stampedReference.getReference()+"\t" + stampedReference.getStamp());

        },"t2").start();
    }
}
/**

t1	首次版本号:1
t2	首次版本号:1
t1	Book(name=mysql, id=2)	版本号2:2
t1	Book(name=java, id=1)	版本号3:3
false	Book(name=java, id=1)	3
*/

你可能感兴趣的:(——,并发编程,——,java,jvm,算法)