原子类与自旋锁原理初探

Posted 若曦`

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了原子类与自旋锁原理初探相关的知识,希望对你有一定的参考价值。

1. 原子性

原子性是指在一个操作中就是cpu不可以在中途暂停然后再调度,既不被中断操作,要不执行完成,要不就不执行

也就是一个线程在某个代码块的执行过程中,不能被其他线程抢用cpu资源而导致中断

2. 原子变量

此篇博客为介绍原子类的部分原理为主

有关原子类的详细介绍可以看这位大佬的博客 Java16个原子类介绍-基于JDK8

原子变量属于原子类,本质是对一些基本类型包装类的升级

原子类中的原子变量,采用了以下两点保证可见性和原子性

  • volatile 修饰内部的值保证可见性
  • 使用CAS算法保证数据的原子性

(1) 内部的CAS

以AtomicInteger原子变量的getAndAdd(int dalta)方法为例

下面写错了,不是c++的代码,就是java的,因为一直没用过do while,都忘了java也有do while的循环语法…

CAS算法是对于并发操作共享数据的支持

CAS算法的3个操作数

  1. 内存值 (V)
  2. 预估值 (A)
  3. 更新值 (B)
    仅当V==A的时候,V=B,否则就不进行任何操作
  • 比较和替换是一步原子性操作,不能被其他线程抢用

(2) Unsafe类

原子类中的CAS算法本质是调用Unsafe类中的native本地方法去对数据进行操作(直接调用c++对内存进行操作,所以效率也会比较快)

(3) CAS的缺点

CAS算法虽然能实现自旋锁,但是有以下3个缺点

  • 循环会耗时
  • 一次性只能保证一个共享变量的原子性
  • ABA问题

(4) 乐观锁

乐观锁有两种实现:自旋锁和版本比对

自旋锁

自旋锁也就是使用普通的CAS算法,在后序章节会有详解

自旋锁容易出现上述的ABA问题 也就是数据是期望的,但是是被其他线程动过的数据,比如将2020->2021->2020,虽然最后还是2020,但已经被动过了

版本比对

比如一个线程执行任务,就会比对数据的版本号,如果版本号没有被其他线程动过,那么才对数据进行操作

3. 原子引用

原子引用指的也就是AtomicReference,作用是对"对象"进行原子操作。

它提供了一种读和写都是原子性的对象引用变量,同样采用CAS,不过内存值和预估值是比较对象的地址

(1) AtomicReference和AtomicInteger的差异

  • AtomicInteger是对整数的封装,底层采用的是compareAndSwapInt实现CAS,比较的是数值是否相等

  • AtomicReference则对应普通的对象引用,底层使用的是compareAndSwapObject实现CAS,比较的是两个对象的地址是否相等。

综上所述,原子引用能够保证你在修改对象引用时的线程安全性。

(2) 解决ABA问题

解决ABA问题,可以采用自旋锁+版本号来解决

自旋锁+版本(时间戳)比对

源码分析


使用示例

  public class TestAtomic 
    public static void main(String[] args) 
        // 自旋锁+版本号的原子类 构造函数的Stamp 1 代表版本号(或者说是时间戳)
        // 注意参数传入值类型的时候会自动装箱 int->Integer 如果Ref的值(第一个参数)设置过大 超出Integer指定范围(-128~127之间) Integer会在堆上new一块新的内存地址,导致比对不正确
        AtomicStampedReference<Integer> stampedReference = new AtomicStampedReference(100,1);

        new Thread(()->
            //记录当前的num值
            int num=100;
            //比较并交换值
            stampedReference.compareAndSet(num,++num,1,2);
            //获取当前版本号:getStamp()
            int stamp = stampedReference.getStamp();
            if(num==101) 
                System.out.println("a=>将值更新为了101,"+"当前版本号为:"+stamp);
            
            try 
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            //比较并交换值
            stampedReference.compareAndSet(num,++num,2,3);
            //获取当前版本号:getStamp()
            stamp = stampedReference.getStamp();
            if(num==102) 
                System.out.println("b=>将值更新为了102,"+"当前版本号为:"+stamp);
            
            try 
                Thread.sleep(100);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            //比较并交换值
            stampedReference.compareAndSet(num,++num,3,4);
            //获取当前版本号:getStamp()
            stamp = stampedReference.getStamp();
            if(num==103) 
                System.out.println("c=>将值更新为了103,"+"当前版本号为:"+stamp);
            
        ).start();
    



注意点,也就是当预期值或者说版本号是int类型,且AtomicStampedReference泛型指定为Integer的时候,要注意值的大小

4. 自旋锁

自旋锁本身是靠CAS算法实现的,与Lock和synchronized无关

CAS算法是对于并发操作共享数据的支持,在目录2中有更具体的说明

CAS算法的3个操作数

  1. 内存值 (V)
  2. 预估值 (A)
  3. 更新值 (B)
  • 也就是仅当V==A的时候,V=B,否则就不进行任何操作
  • 比较和替换是一步原子性操作,不能被其他线程抢用

通过以上两点特性实现的自旋锁

自己实现一个自旋锁

/**
 * 自己实现一个自旋锁 (原子引用+CAS)
 * @author ruoxi
 */
public class My_CAS_Lock 
    /**
     * 使用原子引用,指定泛型类型为Thread
     */
    static AtomicReference<Thread> atomicReference = new AtomicReference<>();
    /**
     * CAS算法实现自旋锁的加锁
     */
    public void myLock() 
        //获取当前线程
        Thread thread = Thread.currentThread();
        //CAS算法实现 自旋锁
        //预期值:thread=null  读取内存中的值:当前线程的引用 thread = this.thread
        //如果当前线程不是空,则不能获取锁(说明有线程拿到锁了),开始一直自旋
        while(!atomicReference.compareAndSet(null,thread))
            try 
                //使线程每次自旋间隔为1毫秒
                TimeUnit.MILLISECONDS.sleep(1);
             catch (InterruptedException e) 
                e.printStackTrace();
            
            System.out.println(thread.getName()+"=>在等待解锁..自旋中..");
        
        //加锁 (其实只是利用CAS算法将其他线程隔离了)
        System.out.println(thread.getName()+"拿到了锁");
    
    /**
     * CAS算法实现自旋锁的解锁
     */
    public void unlock()
        Thread thread = Thread.currentThread();
        System.out.println(thread.getName()+"释放了锁");
        //将当前线程置换为空,也就是释放了锁
        atomicReference.compareAndSet(thread,null);
    
    /**
    * 测试
    */
    public static void main(String[] args) 
        //实例化一个自己实现的自旋锁
        My_CAS_Lock lock = new My_CAS_Lock();
        //开启两个线程,都需要获取自旋锁才能执行
        for (int i = 0; i < 2; i++) 
            new Thread(()->
                //上锁
                lock.myLock();
                try 
                    //让线程沉睡10毫秒再解锁
                    TimeUnit.MILLISECONDS.sleep(10);
                 catch (InterruptedException e) 
                    e.printStackTrace();
                finally 
                    //解锁
                    lock.unlock();
                
            ,"线程"+(i+1)).start();
        
    

以上是关于原子类与自旋锁原理初探的主要内容,如果未能解决你的问题,请参考以下文章

互斥锁,自旋锁,原子操作原理和实现

CAS 自旋锁

CAS机制与自旋锁

原子变量和自旋锁

synchronized实现原理及其优化-(自旋锁,偏向锁,轻量锁,重量锁)

Java——聊聊JUC中的CAS原理