从AtomicInteger谈谈CAS乐观锁

  因为最近自己在培训机构重造,课程进度略慢略浅,所以就想在平时上课的内容中做一些横向拓展,希望能够记忆深刻一些,所以就将一些个人觉得比较重要的内容通过博客的形式记录下来,主要还是起一个学习笔记的作用吧,所以有些地方可能比较混乱,还望路过的老哥见谅,如果发现有错的地方希望帮忙给小弟指正指正,感激不尽。

  言归正传,今天看博客的时候看到AtomicInteger,回顾了一下,自己脑子了除了知道他是一个原子操作相关的类就没有什么印象了,所以就准备再来啃啃这块略硬的骨头。

一、AtomicInteger存在的意义

  光从类名来看的话感觉AtomicInteger就是对Integer类的增强,AtomicInteger相关的API是在jdk1.5版本后添加的,我们也知道了这个类是与原子操作相关的,那么我们从头出发,先搞清楚什么是原子操作。

    1.1 原子操作

     如果这个操作所处的层(layer)的更高层不能发现其内部实现与结构,那么这个操作是一个原子(atomic)操作。
     原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分。
     将整个操作视作一个整体是原子性的核心特征。

      这是百度百科对于原子操作的定义,通俗来说原子操作就算一系列不可拆分的操作,要么全部执行要么全部不执行。这个概念其实很多地方都有用到,如数据库的的四大特性之中的原子性(为了防止忘了,顺便提一嘴数据库的四大特性:原子性、一致性、隔离性、持久性),东西越学越多过后会发现很多思想性的东西都是相通的,所以要多总结多思考,后面学习新东西时肯定也会容易些。

      知道了原子操作是什么,那么原子操作为什么要被添加到JAVA相关的API里面来呢?又为什么在1.5这个java已经发展了一段时间后的版本才来添加呢?一起再来看看吧。

    1.2 线程安全

      只要有多线程的地方就会出现线程安全问题,java必不例外。下面来看个例子,是我今天逛的大部分博客中都出现了例子,确实也是比较能说明问题。

package com.ddup.atomic.integer;

public class TestDemo {

    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {

        for (int i = 0; i < 100; i++) {
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        Thread.sleep(20);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                    for (int j = 0; j < 1000; j++) {
                        count++;
                    }
                }
            }).start();
        }

        while (Thread.activeCount() > 2) {
            Thread.sleep(1);
        }
        System.out.println(count);

    }
}

     上面的代码也很简单,开启100个线程对同一个变量count分别进行1000次自增操作,我们对程序的期望值是输出 100000,实际情况是无论运行多少次结果都是小于100000的。

     原因很简单,每个线程中的 ++  操作实际上可以分为三步: 获取当前变量值、给当前值+1、将新变量值写回处。正是因为这三步的存在,所以才导致了意外的结果。因为线程不可能保持速度完全的一致,所以每个线程进行到的具体步骤不同,就会导致结果低于2000;

     要解决问题很简单,我们都学过synchronized修饰符,知道用它修饰的代码块(或者方法)可以维持线程安全,事实也确实是如此的。使用但synchronized一直以来都有个令人诟病的缺点,就是synchronize实现是使用的阻塞式的线程同步方案,也就是说只能同时运行一个线程,其他线程在当前线程释放锁之前都是等待状态,对于性能的损耗是非常大的。于是AutomicInteger相关类的优势就展现出来了。

二、AtomicInteger解析

    前面已经介绍过原子操作了,顾名思义AtomicInteger就是原子操作的Integer,将上面例子中的代码中的共享变量替换为AutomicInteger,将count++替换为 count.getAndIncrement() 或者 count.incrementAndGet(),那就能达到我们的预期结果了。这两个方法本质上是没什么区别的,只是一个方法返回的是更新后的值,一个返回的是更新前的值。

    要想知道它是如何通过不阻塞的方式来实现线程安全的的,那就必须深入到源码来找原因了。

    多看源码身体好!

    那先从简单的入手,先看看AtomicInteger的成员变量、构造方法以及初始化用的静态代码块。

    

    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset;

    static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
    private volatile int value;
  
    public AtomicInteger(int initialValue) {
        value = initialValue;
    }

    public AtomicInteger() {
    }

 

   value、构造方法我们肯定是能理解的(value使用的volatile修饰符也很意思,我打算下次重新写一篇来单独介绍),static是在对valueOffset进行初始化也能够看出来,但是这个valueOffset和这个Unsafe是什么呢?生啃一会儿没有进展的情况下,通过翻阅博客了解到了valueOffset名为内存偏移量,名字看起来很不好理解对吧,其实就是一个指向value变量在内存中的地址值,用处会在后面讲到,现在关键我们需要搞明白这个Unsafe是什么,才能理解为什么AtomicInteger能够实现线程同步。Unsafe这个类名字非常有意思,从名字上就直接告诉了我们它非常不安全,点进这个类的源码我们很快就知道了原因,里面的大部分类都使用native修饰,我们都知道一般和操作系统或者硬件相关的操作才会使用native方法,这个类中大篇幅的native方法,当然非常不安全了,所以这个类只能被jdk内部代码调用,我们是碰不到的。大概了解了Offset后,我们也就i知道了静态代码块中的代码就是通过unsafe中的native方法获取到value的内存偏移量并赋给了valueOffset。

   接下来再看我们用到的getAndIncrement()方法,

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

 

   再继续到unsafe里看看有没有什么猫腻

   

    public final int getAndAddInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);①
        } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4) ②);

        return var5;
    }

 

   真相渐渐的浮出水面了,我们直接看Unsafe中的getAndAddInt方法,它接收了当前对象的引用、内存偏移量、和一个增长量。①处的代码是一个获取volatileInt,关于volatile先不过多赘述,我们只要知道是要获取value内存地址处的最新值就行。最关键的代码就是②这一句了,这个方法的参数从左到右分别是当前对象引用、value内存偏移量、从内存中取到的value值、以及当前需要更新的值,有了这些值,就已经具备了达到AtomicInteger关键机制CAS的条件了。

三、乐观锁之CAS

   我们都知道为实现线程安全最常见的解决方案就是sychronized,synchronized是典型的悲观锁,默认认为每一次操作都会发生冲突,所以在同一时间段仅允许一个线程进行操作(阻塞),这样最明显直观的感受就是效率低下(在目前的版本中,为提升效率synchronized在转换为重量锁之前也是使用的CAS机制来实现了)。有悲观锁必然就有乐观锁,乐观锁采用更宽松的加锁机制,它认为每次线程操作都不会发生冲突,接下来讲讲乐观锁的实现。乐观锁的关键如下,

  预期值:修改操作前对当前数据的预期值,如果实际值与预期值不符,则放弃当前操作,获取最新的预期值并再次尝试,直到实际值与预期值相符。

  目标值:对当前数据修改的目标值,若预期值与实际值相同时,就修改实际值。

 

  看到这里我们也就明白了AtomicInteger的实现原理了,将将var5与内存中var2位置的值进行比较,如果相同则将var5 + var4修改到var2位置,如果不相同则获取最新的var2处值赋值给var5再次尝试。成功替换后返回true退出循环,返回修改前var2处的值。

四、CAS的弊端

  1、ABA问题

     之前在一篇博客中看到过一个关于ABA问题非常形象的例子,看完你一定能理解ABA问题。

     小明的银行卡中有100元,有一天他要取五十块钱出来,但是取款机出了问题,取钱的操作被提交了两次,理想的情况下是一次操作成功另一次操作失败。取钱的两次操作分别为线程1和线程2,线程1和线程2的预期值都是100,目标值都是50。线程1先顺利操作成功,将实际值修改为50,但线程2因为其他未知原因阻塞了,暂时未进行操作。这时小明妈妈正好给小明汇款,开启汇款线程3,预期值50目标值100,顺利更新成功,实际值又被更新为100。突然小明取钱的线程2又恢复了正常,看到当前银行卡中的实际值正好符合预期值,便执行更新操作,将实际值更新为了50。小明哭了。

     不知道我描述的够不够清楚,如果觉得没太懂的同学可以移步到大佬博客,大佬讲的要生动清楚些,我写出来也主要是一个加强记忆的作用。---->什么是CAS机制?

     大佬也明确的给了解决方案,就是通过添加版本号,每一次修改都版本号都会+1,这样就有效的避免了ABA的问题。

  2、性能开销大

     这个也很好理解,一个线程如果每次预期值与实际值都不匹配,那它一直重复同样的操作,不断的获取和比较,如果同时有多个线程进行循环操作,对于CPU的开销还是非常大的。

 

转载于:https://www.cnblogs.com/dc5e/p/11140435.html

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值