一、原子类

java.util.concurrent.atomic下所有的类

二、没有用CAS之前

多线程下不使用原子类保证线程安全i++(基本数据类型)

package com.lori.juc2023.juc7;

public class casDemo1 {
    volatile int number = 0;
    //读取
    public int getNumber(){
        return number;
    }
    
    public synchronized  void setNumber(){
        number++;
    }
}

三、使用CAS之后

多线程环境,使用原子类保证线程安全i++(基本数据类型)

package com.lori.juc2023.juc7;

import java.util.concurrent.atomic.AtomicInteger;

public class casDemo1 {
//AtomicInteger 既能保证原子性,又比加锁轻便
   AtomicInteger atomicInteger = new AtomicInteger();
   
   public int getAtomicInteger(){
       return atomicInteger.get();
   }
    
   public void setAtomicInteger(){
       //相当于i++
       atomicInteger.getAndIncrement();
   }
}

四、CAS是什么

1、原理

  • compare and swap的缩写,中文翻译成比较并交换,实现并发算法时常用到的一种技术。
  • 它包含三个操作数:内存位置、预期原值及更新值。
  • 执行CAS操作的时候,将内存位置的值与预期原值比较:
    • 如果相匹配,那么处理器会自动将该位置值更新为新值,
    • 如果不匹配,处理器不做任何操作,多个线程同时执行CAS操作只有一个会成功。

举例说明: CAS有三个操作数,位置内存值V,旧的预期值A,要修改的更新值B。 当且仅当,旧的预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做,或者重来。 当它重来重试的这种行为称为---自旋 123.png

2、代码

package com.lori.juc2023.juc7;

import java.util.concurrent.atomic.AtomicInteger;

public class casDemo1 {


    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //看原始值是不是5,如果是5的话,将5修改为2023
        boolean compareAndSet = atomicInteger.compareAndSet(5, 2023);
        System.out.println(compareAndSet+"\t"+atomicInteger.get());
    }


}
  • 第一次执行结果: 124.png
  • 如果加多一次修改
package com.lori.juc2023.juc7;

import lombok.val;

import java.util.concurrent.atomic.AtomicInteger;

public class casDemo1 {


    public static void main(String[] args) {
        AtomicInteger atomicInteger = new AtomicInteger(5);
        //看原始值是不是5,如果是5的话,将5修改为2023
        boolean compareAndSet = atomicInteger.compareAndSet(5, 2023);
        System.out.println(compareAndSet+"\t"+atomicInteger.get());
        boolean andSet = atomicInteger.compareAndSet(5, 2023);
        System.out.println(compareAndSet+"\t"+atomicInteger.get());

    }
}

125.png

3、硬件级别的保证

  • CAS是JDK提供的非阻塞原子性操作,它通过硬件保证了此较-更新的原子性。它是非阻塞的且自身具有原子性,也就是说这玩意效率更高且通过硬件保证,说明这玩意更可靠。
  • CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的 CAS方法(如compareAndSwapxxx)底层实现即为CPU指令cmpxchg。
  • 执行cmpxchg指令的时候,会判断当前系统是否为多核系统,如果是就给总线加锁,只有一个线程会对总线加锁成功,加锁成功之后会执行CAS操作,也就是说CAS的原子性实际上是CPU实现独占的,比起用synchronized重量级锁,这里的排他时间要短很多,所以在多线程情况下性能会比较好。

126.png

  • var1:要操作的对象
  • var2:标识要操作对象中属性地址的偏移量
  • var4:表示要修改数据的期望值,也就是上一个版本拿到的值
  • var5/var6:表示要修改为的新值

4、谈谈对Unsafe类的理解

127.png

  • UnSafe类是CAS的核心类,由于Java犯法无法直接访问底层系统,需要通过本地方法native来访问,Unsafe相当于一个后门,基于该类可以直接操作特定的内存数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为Java中CAS操作的执行依赖于Unsafe类的方法。
  • 注意:UnSafe类中所有的方法都是native修饰的,也就是说UnSafe类中的方法都是直接调用操作系统底层资源执行相应的任务。
  • 变量valueOffset,表示该变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的 128.png
  • 变量value用volatile修饰,保证了多线程之间的内存可见性

1、i++线程不安全,通过atomicInteger.getAndIncrement()保证线程安全

  • CAS的全称是Compare-And-Swap,它是一条CPU并发原语
  • 它的功能是判断内存某个位置是否为预期值,如果是则修改为新值,这个过程是原子的
  • AtomicInteger类主要利用CAS+volatile和native方法来保证原子性,从而避免synchronized的高开销,执行效率大为提升。

129.png 130.png 131.png CAS并发原语体现在JAVA语言中就是sun.misc.Unsafe类中的各个方法。调用UnSafe类中的CAS方法,JVM会帮我们实现出CAS汇编指令。这是一种完全依赖于硬件的功能,通过它实现了原子操作。再次强调,由于CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

2、底层源码

132.png 133.png 134.png 135.png 136.png 137.png

3、原子引用

package com.lori.juc2023.juc7;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.util.concurrent.atomic.AtomicReference;

public class CASDemo2 {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference=new AtomicReference<>();
        User lori = new User("lori", 18);
        User yyqx = new User("yyqx", 19);
        atomicReference.set(lori);
        System.out.println(atomicReference.compareAndSet(lori, yyqx)+"\t"+atomicReference.get().toString());
    }
}

@Getter
@ToString
@AllArgsConstructor
class User{
    String name;
    int age;
}

138.png

  • 修改两次时,第二次无法修改,如下:
package com.lori.juc2023.juc7;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.ToString;

import java.util.concurrent.atomic.AtomicReference;

public class CASDemo2 {
    public static void main(String[] args) {
        AtomicReference<User> atomicReference=new AtomicReference<>();
        User lori = new User("lori", 18);
        User yyqx = new User("yyqx", 19);
        atomicReference.set(lori);
        System.out.println(atomicReference.compareAndSet(lori, yyqx)+"\t"+atomicReference.get().toString());
        System.out.println(atomicReference.compareAndSet(lori, yyqx)+"\t"+atomicReference.get().toString());
    }
}

@Getter
@ToString
@AllArgsConstructor
class User{
    String name;
    int age;
}

139.png

五、CAS与自旋锁,自旋锁借鉴CAS思想

1、概念

CAS是实现自旋锁的基础,CAS利用CPU指令保证了操作的原子性,已达到锁的效果,至于自旋呢?看字面意思也很明白,自己旋转。是指尝试获取锁的线程不会立即阻塞,而是采用循环的方式尝试获取锁,当线程发现锁被占用时,会不断循环判断锁的状态,直至获取。这样的好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

2、代码实现

package com.lori.juc2023.juc7;

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

/**
 * 实现一个自旋锁
 * 自旋锁的好处:循环比较获取没有类似wait的阻塞
 * 通过CAS操作完成自旋锁,A线程先进来调用MyLock方法,自己持有锁5s,B随后进来发现
 * 当有线程持有锁,只能通过自选等待,直到A释放锁后,B才能获取锁
 */
public class CASDemo3 {



    public static void main(String[] args) {
        SpinLockDemo lockDemo = new SpinLockDemo();
        new Thread(()->{
            lockDemo.lock();
            System.out.println("线程A获取到锁了");
             try {
                 TimeUnit.SECONDS.sleep(5);} catch (InterruptedException e) {throw new RuntimeException(e);}
             lockDemo.unlock();
        },"A").start();

         try {TimeUnit.MILLISECONDS.sleep(500);} catch (InterruptedException e) {throw new RuntimeException(e);}
        new Thread(()->{
            lockDemo.lock();
            System.out.println("线程B获取到锁了");
            lockDemo.unlock();
        },"B").start();

    }
}


class SpinLockDemo{
    AtomicReference<Thread> atomicReference = new AtomicReference<>();
    public void lock(){
        //获取当前线程
        Thread thread = Thread.currentThread();
        //如果是空的,就把线程放进去解锁,否则一直循环while自旋等待锁的释放
        System.out.println(Thread.currentThread().getName()+"come in**************************");
        while (!atomicReference.compareAndSet(null,thread)){
            //System.out.println("我在等锁!!!!!!!!!!!");
        }
    }

    public void unlock(){
        //获取当前线程
        Thread thread = Thread.currentThread();
        atomicReference.compareAndSet(thread,null);
        System.out.println(Thread.currentThread().getName()+"锁释放**************************");
    }
}

140.png

3、CAS的缺点

  • 循环时间很长的话会导致CPU开销过大
  • ABA问题

ABA问题: CAS算法实现一个重要的前提需要取出内存中某时刻的数据并在当下时刻比较并且替换,那么在这个时间差会导致数据变化。 比如说一个线程1从内存位置V去取出A,这时候另一个线程2也从内存中取出A,并且线程2进行了一些操作,将A值修改为了B值,然后线程2又将V位置的数据变成A,这时候线程1进行CAS操作发现内存中仍然是A,预期OK,然后线程1操作成功。 尽管线程1的CAS操作成功,不代表这个过程是没有问题的。 解决ABA问题: 带版本号查询:

package com.lori.juc2023.juc7;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo4 {
    public static void main(String[] args) {
        Book book = new Book(1,"java");
        //传参1:引用,传参2:版本号
        AtomicStampedReference<Book> reference = new AtomicStampedReference<>(book,1);
        System.out.println(reference.getReference()+"\t"+reference.getStamp());  //Book(id=1, name=java)	1

        
        Book book1 = new Book(2,"mysql");
        boolean compareAndSet = reference.compareAndSet(book, book1, reference.getStamp(), reference.getStamp() + 1);
        System.out.println(compareAndSet+"\t"+reference.getReference()+"\t"+reference.getStamp());  //



        compareAndSet = reference.compareAndSet(book1, book, reference.getStamp(), reference.getStamp() + 1);
        System.out.println(compareAndSet+"\t"+reference.getReference()+"\t"+reference.getStamp());  //

    }
}


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

141.png

首先就使用原子类,不使用版本号:

package com.lori.juc2023.juc7;

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

public class CASDemo5 {
    static AtomicInteger atomicInteger = new AtomicInteger(100);
    public static void main(String[] args) {
        new Thread(()->{
           atomicInteger.compareAndSet(100,101);
           //暂停10毫秒
            try {
                TimeUnit.MILLISECONDS.sleep(10);} catch (InterruptedException e) {throw new RuntimeException(e);}
            atomicInteger.compareAndSet(101,100);
        },"t1").start();

        new Thread(()->{
            //暂停200毫秒,等线程1 执行完毕
             try {TimeUnit.MILLISECONDS.sleep(200);} catch (InterruptedException e) {throw new RuntimeException(e);}

            System.out.println("\t"+ atomicInteger.compareAndSet(100,2023)+"\t"+atomicInteger.get());
        },"t2").start();
    }
}

142.png

  • 使用版本号原子方法
package com.lori.juc2023.juc7;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.atomic.AtomicStampedReference;

public class CASDemo5 {
    //参数:初始值;版本号
  static AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference<>(100,1);
    public static void main(String[] args) {
        new Thread(()->{
            //拿到版本号
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"   首次版本号:::"+stamp);
            //暂停一段时间,确保t2拿到的版本号和t1一样
             try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {throw new RuntimeException(e);}
             //第一次修改值
            stampedReference.compareAndSet(100,101,stamp,stamp+1);
            int stamp2 = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t"+"线程1第一次修改后版本号:::"+stamp2+"\t"+stampedReference.getReference());
            //第二次修改值
            stampedReference.compareAndSet(101,100,stamp2,stamp2+1);
            int stamp3 = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"\t"+"线程1第2次修改后版本号:::"+stamp3+"\t"+stampedReference.getReference());

                },"t1").start();


        new Thread(()->{
            int stamp = stampedReference.getStamp();
            System.out.println(Thread.currentThread().getName()+"   首次版本号:::"+stamp);
            //暂停3s确保t1线程已执行完毕,值已经变回100
             try {TimeUnit.SECONDS.sleep(3);} catch (InterruptedException e) {throw new RuntimeException(e);}
            boolean b = stampedReference.compareAndSet(100, 2023, stamp, stamp + 1);
            System.out.println(Thread.currentThread().getName()+"\t"+"线程2第1次修改后版本号:::"+stamp+"  "+b+"\t"+stampedReference.getReference());
        },"t2").start();
    }

}

143.png