‘短文’介绍cas的实现——Atomic类
前置知识:众所周知,在Java中,在不考虑分布式锁的情况下,实现同步的方式,主要分为悲观锁和乐观锁两种。在使用悲观锁时(如:Synchronized
,Lock
,ReentrantLock
),需要操作系统从用户态转换为内核态,而这个过程是比较消耗性能的;乐观锁(如:cas
)则不需要,它会倔强的一次一次地进行尝试,直到操作成功(当然你也可以让它试几次就放弃)。本篇文章就来给大家介绍一下cas
的实现——Atomic
类。
1、CAS
:
之所以说Atomic
类是cas
的实现,是因为Atomic类中的方法底层几乎都是调用了cas
——Compare And Swap
,其中涉及三个参数:
- 对象内存地址
- 预期旧值
- 新值
它通过内存地址找到该对象的位置,用预期旧值比较对象当前的值,如果相等,将新值赋给对象并返回True;如果不相等,则操作失败,不修改值,返回False。这些都是老生常谈的事情了,我就简要介绍,一笔带过。
噢对了,这个方法只是进行一次操作,大家常说的会多次修改,直到成功,那是搭配循环来使用的,如果成功,则退出循环。
2、AtomicInteger
:
了解了cas
之后,Atomic也就不难理解了,我主要介绍一下AtomicInteger
类,其他还有AtomicBoolean
、AtomicLong
等,原理都是一样的。
我们可以通过这样的方法来创建一个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的数字传进去。
- 当选择无参构造时,后续设置值需要调用
AtomicInteger
的set
方法:
再看一下AtomicInteger
类中对象的定义,使用了volatile
关键字,为的是保证对象的可见性和有序性
- 可见性:在多线程情况下,保证一个线程对于该对象进行了修改,其他线程是立即可知的
- 有序性:在多线程情况下,保证指令重排序不会影响程序的正确性
接下来让我介绍一下AtomicInteger
类中常用方法
-
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); }
-
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
为什么会有四个参数,下面再说。 -
getAndIncrement
()/** * Atomically increments by one the current value. * 简简单单加个一 * @return the previous value */ public final int getAndIncrement() { return unsafe.getAndAddInt(this, valueOffset, 1); }
相似的,还有
getAndDecrement
()方法,与之相反——简简单单减个一 -
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); }
-
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
是一个偏移量,指示AtomicInteger
的value
字段相对于对象头的位置。通过这个偏移量,可以在运行时直接访问AtomicInteger
的value
字段,而无需通过对象的引用进行间接访问,从而避免了锁定操作,提高了性能。参数三:预期旧值
参数四:新值
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
,直接修改,这些大家可以自己尝试一下,我就不再贴长串的令人烦躁的代码啦 ^_^