‘短文’介绍cas的实现——Atomic类

前置知识:众所周知,在Java中,在不考虑分布式锁的情况下,实现同步的方式,主要分为悲观锁和乐观锁两种。在使用悲观锁时(如:SynchronizedLockReentrantLock),需要操作系统从用户态转换为内核态,而这个过程是比较消耗性能的;乐观锁(如:cas)则不需要,它会倔强的一次一次地进行尝试,直到操作成功(当然你也可以让它试几次就放弃)。本篇文章就来给大家介绍一下cas的实现——Atomic类。

1、CAS

之所以说Atomic类是cas的实现,是因为Atomic类中的方法底层几乎都是调用了cas——Compare And Swap,其中涉及三个参数:

  • 对象内存地址
  • 预期旧值
  • 新值

它通过内存地址找到该对象的位置,用预期旧值比较对象当前的值,如果相等,将新值赋给对象并返回True;如果不相等,则操作失败,不修改值,返回False。这些都是老生常谈的事情了,我就简要介绍,一笔带过。

噢对了,这个方法只是进行一次操作,大家常说的会多次修改,直到成功,那是搭配循环来使用的,如果成功,则退出循环。

2、AtomicInteger

了解了cas之后,Atomic也就不难理解了,我主要介绍一下AtomicInteger类,其他还有AtomicBooleanAtomicLong等,原理都是一样的。

我们可以通过这样的方法来创建一个AtomicInteger对象:

AtomicInteger ai = new AtomicInteger();

此为AtomicInteger部分源码:

// 对象定义 
private volatile int value;

// 有参构造函数
public AtomicInteger(int initialValue) {
    value = initialValue;
}
// 无参构造函数
public AtomicInteger() {
}

public final void set(int newValue) {
    value = newValue;
}

从源码我们可以看出,我们可以选择有参构造或无参构造

  • 当选择有参构造时,需要将一个int的数字传进去。
  • 当选择无参构造时,后续设置值需要调用AtomicIntegerset方法:

再看一下AtomicInteger类中对象的定义,使用了volatile关键字,为的是保证对象的可见性和有序性

  • 可见性:在多线程情况下,保证一个线程对于该对象进行了修改,其他线程是立即可知的
  • 有序性:在多线程情况下,保证指令重排序不会影响程序的正确性

接下来让我介绍一下AtomicInteger类中常用方法

  1. getAndSet()

    /**
         * Atomically sets to the given value and returns the old value.
         * 设置新值,返回旧值
         * @param newValue the new value
         * @return the previous value
         */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }
    
  2. compareAndSet()

    /**
         * Atomically sets the value to the given updated value
         * if the current value {@code ==} the expected value.
         * 大名鼎鼎的cas,传入预期旧值和新值,返回是否修改成功
         * @param expect the expected value
         * @param update the new value
         * @return {@code true} if successful. False return indicates that
         * the actual value was not equal to the expected value.
         */
    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    别问我为什么是compareAndSet而不是compareAndSwap,这是Atomic对底层cas的封装,方法名设置为compareAndSet,我们在调用Atomic的方法时,不需要传对象的地址了,只需要传入传入预期旧值和新值就可以了。至于底层cas为什么会有四个参数,下面再说。

  3. getAndIncrement()

    /**
         * Atomically increments by one the current value.
         * 简简单单加个一
         * @return the previous value
         */
    public final int getAndIncrement() {
        return unsafe.getAndAddInt(this, valueOffset, 1);
    }
    

    相似的,还有getAndDecrement()方法,与之相反——简简单单减个一

  4. getAndAdd()

    /**
         * Atomically adds the given value to the current value.
         * 旧值+新值,得到旧值
         * @param delta the value to add
         * @return the updated value
         */
    public final int getAndAdd(int delta) {
        return unsafe.getAndAddInt(this, valueOffset, delta);
    }
    
  5. incrementAndGet()

    /**
         * Atomically increments by one the current value.
         * 旧值+1,返回新值
         * @return the updated value
         */
    public final int incrementAndGet() {
        return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
    }
    

同样的,也有decrementAndGet()方法——旧值-1,返回新值

上面的函数用法不难,我把原英文注释贴出来了,相信大家可以根据注释理解其用法,我主要给大家介绍一下unsafe类和它的方法所需要的参数:

  • unsafe

Unsafe 是一个提供了操作底层内存、执行不安全操作的类,它允许Java代码调用一些本地方法。它的作用是允许Java实现像C++那样直接操纵内存,而不用通过JVM的内存管理器。由于它具有访问Java虚拟机内部数据结构和内存的能力,因此使用 Unsafe 需要谨慎,如果使用不当,可能会导致内存泄漏或数据损坏。

在Java中,直接操作内存是被禁止的,因为这样容易造成安全问题。但是有些场景下需要直接操纵内存,例如一些高性能的并发框架,这时就需要使用到 Unsafe

常见的使用方式包括:实例化对象、修改对象的属性值、比较并交换对象属性值、分配内存、释放内存等。但需要注意的是,由于 Unsafe 涉及到底层操作,因此使用不当可能会引发一些安全问题,所以只有专业人士在特定场景下才应该使用 Unsafe。(嘿嘿,chatgpt真好用~)

​ 正是因为直接操作他的风险较大,所以类名叫unsafe,不过我们可以调用java给我们封装好的函数就行~

  • 方法参数

    下面是compareAndSet()函数,以此为例讲解一下其中参数

    public final boolean compareAndSet(int expect, int update) {
        return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
    }
    

    参数一:传入对象地址,this表示当前 AtomicInteger 对象实例

    参数二:偏移量

    AtomicInteger中,valueOffset是一个偏移量,指示AtomicIntegervalue字段相对于对象头的位置。通过这个偏移量,可以在运行时直接访问AtomicIntegervalue字段,而无需通过对象的引用进行间接访问,从而避免了锁定操作,提高了性能。

    参数三:预期旧值

    参数四:新值

3、AtomicInteger的使用

上面介绍的函数都是调用一次就运行一次,返回成功或失败,并不是大家所想的一直运行直到成功,这部分需求我们需要自己完成。比如使用while循环:

public class AtomicExample {
    public static void main(String[] args) {
        AtomicInteger ai = new AtomicInteger(3);
        // 计数用的
        long a = 0;
        while (true) {
            // 预期值4,修改成5
            boolean b = ai.compareAndSet(4, 5);
            System.out.println("cas修改了" + a++ + "次");
            if (b)
                break;
        }
        System.out.println("终于修改成功了!!修改后的结果为" + ai);
    }
}

这样的代码,他修改永远不会成功,一直进行循环,你会看着控制台a一直增加,顺便可以和朋友比一下谁的a加得快,比比谁的cpu更好 #_#

不能这样,要有其他线程来干预一下:

public class AtomicExample {
    public static void main(String[] args) {
        AtomicInteger ai = new AtomicInteger(3);
        System.out.println(ai.get());
        // 启动一个线程,睡200毫秒,等睡醒再运行代码,将ai修改成4
        new Thread(() -> {
            try {
                Thread.sleep(200);
                ai.set(4);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        
        // 计数用的
        int a = 0;
        while (true) {
            // 预期值4,修改成5
            boolean b = ai.compareAndSet(4, 5);
            System.out.println("cas修改了" + a++ + "次");
            if (b)
                break;
        }
        System.out.println("终于修改成功了!!修改后的结果为" + ai);
    }
}

跑一下这段代码,它会运行一会就停止,显示修改成功(200毫秒能执行五万多次,我这cpu也还行哈~)

cas修改了54016次 cas修改了54017次 cas修改了54018次 cas修改了54019次 cas修改了54020次 cas修改了54021次 cas修改了54022次 终于修改成功了!!修改后的结果为5

也可以不将循环条件设置为True,修改成尝试次数,如果尝试次数大于某个阈值,就退出循环,改为使用悲观锁Synchronized,直接修改,这些大家可以自己尝试一下,我就不再贴长串的令人烦躁的代码啦 ^_^

#我的求职思考##是时候巩固基础了#
全部评论
在typora里排版挺好的,发到这里以后,怎么感觉这么乱😡😡
1
送花
回复
分享
发布于 2023-04-26 23:41 北京
感谢楼主分享,学习了
点赞
送花
回复
分享
发布于 2023-04-26 21:51 四川
秋招专场
校招火热招聘中
官网直投
Atomic类中的方法底层几乎都是调用了CAS,这种实现方式可以提高并发性能
点赞
送花
回复
分享
发布于 2023-05-01 00:00 山西

相关推荐

2 1 评论
分享
牛客网
牛客企业服务