深入讲解我们说的CAS自旋锁到底是什么
作者:风筝 发布时间:2022-05-21 14:43:08
什么是自旋锁
说道自旋锁就要从多线程下的锁机制说起,由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问。即每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁。
设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?
通常有2种处理方式:
一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是本文的重点——自旋锁。他不用将线城阻塞起来(NON-BLOCKING)。
另一种是没有获得锁的进程就阻塞(BLOCKING)自己,继续执行线程上的其他任务,这就是 ——互斥锁(包括内置锁Synchronized还有ReentrantLock等等)。
引言
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的自旋锁或乐观锁的核心操作。
它的实现很简单,就是用一个预期的值和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 true。否则,返回 false。
保证原子操作
任何技术的出现都是为了解决某些特定的问题, CAS 要解决的问题就是保证原子操作。原子操作是什么,原子就是最小不可拆分的,原子操作就是最小不可拆分的操作,也就是说操作一旦开始,就不能被打断,知道操作完成。在多线程环境下,原子操作是保证线程安全的重要手段。举个例子来说,假设有两个线程在工作,都想对某个值做修改,就拿自增操作来说吧,要对一个整数 i 进行自增操作,需要基本的三个步骤:
1、读取 i 的当前值;
2、对 i 值进行加 1 操作;
3、将 i 值写回内存;
假设两个进程都读取了 i 的当前值,假设是 0,这时候 A 线程对 i 加 1 了,B 线程也 加 1,最后 i 的是 1 ,而不是 2。这就是因为自增操作不是原子操作,分成的这三个步骤可以 * 扰。如下面这个例子,10个线程,每个线程都执行 10000 次 i++ 操作,我们期望的值是 100,000,但是很遗憾,结果总是小于 100,000 的。
static int i = 0;
public static void add(){
i++;
}
private static class Plus implements Runnable{
@Override
public void run(){
for(int k = 0;k<10000;k++){
add();
}
}
}
public static void main(String[] args) throws InterruptedException{
Thread[] threads = new Thread[10];
for(int i = 0;i<10;i++){
threads[i] = new Thread(new Plus());
threads[i].start();
}
for(int i = 0;i<10;i++){
threads[i].join();
}
System.out.println(i);
}
既然这样,那怎么办。没错,也许你已经想到了,可以加锁或者利用 synchronized 实现,例如,将 add() 方法修改为如下这样:
public synchronized static void add(){
i++;
}
或者,加锁操作,例如下面使用 ReentrantLock (可重入锁)实现。
private static Lock lock = new ReentrantLock();
public static void add(){
lock.lock();
i++;
lock.unlock();
}
CAS 实现自旋锁
既然用锁或 synchronized 关键字可以实现原子操作,那么为什么还要用 CAS 呢,因为加锁或使用 synchronized 关键字带来的性能损耗较大,而用 CAS 可以实现乐观锁,它实际上是直接利用了 CPU 层面的指令,所以性能很高。
上面也说了,CAS 是实现自旋锁的基础,CAS 利用 CPU 指令保证了操作的原子性,以达到锁的效果,至于自旋呢,看字面意思也很明白,自己旋转,翻译成人话就是循环,一般是用一个无限循环实现。这样一来,一个无限循环中,执行一个 CAS 操作,当操作成功,返回 true 时,循环结束;当返回 false 时,接着执行循环,继续尝试 CAS 操作,直到返回 true。
其实 JDK 中有好多地方用到了 CAS ,尤其是java.util.concurrent包下,比如 CountDownLatch、Semaphore、ReentrantLock 中,再比如 java.util.concurrent.atomic 包下,相信大家都用到过 Atomic* ,比如 AtomicBoolean、AtomicInteger 等。
这里拿 AtomicBoolean 来举个例子,因为它足够简单。
public class AtomicBoolean implements java.io.Serializable {
private static final long serialVersionUID = 4654671469794556979L;
// setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicBoolean.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
private volatile int value;
public final boolean get() {
return value != 0;
}
public final boolean compareAndSet(boolean expect, boolean update) {
int e = expect ? 1 : 0;
int u = update ? 1 : 0;
return unsafe.compareAndSwapInt(this, valueOffset, e, u);
}
}
这是 AtomicBoolean 的部分代码,我们看到这里面又几个关键方法和属性。
1、使用了 sun.misc.Unsafe 对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用;
2、value 表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下 volatile 可见性问题,欢迎关注
3、valueOffset 是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;
4、compareAndSet 方法,这就是实现 CAS 的核心方法了,在使用 AtomicBoolean 的这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。
使用场景
CAS 适合简单对象的操作,比如布尔值、整型值等;
CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;
比如 AtomicBoolean 可以用在这样一个场景下,系统需要根据一个布尔变量的状态属性来判断是否需要执行一些初始化操作,如果是多线程的环境下,避免多次重复执行,可以使用 AtomicBoolean 来实现,伪代码如下:
private final static AtomicBoolean flag = new AtomicBoolean();
if(flag.compareAndSet(false,true)){
init();
}
比如 AtomicInteger 可以用在计数器中,多线程环境中,保证计数准确。
ABA问题
CAS 存在一个问题,就是一个值从 A 变为 B ,又从 B 变回了 A,这种情况下,CAS 会认为值没有发生过变化,但实际上是有变化的。对此,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。
来源:https://mp.weixin.qq.com/s/VeHq-LFPTYbtO6DsHKwngw
猜你喜欢
- 太多的if-else不太直观,难以维护。 以下面代码为例,展示几种替代if else的方法。String input = &quo
- 目录1.@ 根据id更新2.@ 条件构造器作为参数进行更新3.@ lambda构造器mybatisplus update语句为null时没有
- 一 介绍本节给知识追寻者给大家带来的是springSecurity入门篇,主要是简述下springSecrurity的启动原理和简单的入门搭
- 问题提出:自己在做一个小网站充当练手,但是前端图片经过base64加密后传往后端在解码。但是一直都有问题,请大神赐教 publi
- 后端应用经常接收各种信息参数,例如评论,回复等文本内容。除了一些场景下面,可以特定接受的富文本标签和属性之外(如:b,ul,li,h1, h
- 前言本文简单介绍抽象类,接口以及它们的异同点,另附简单的代码举例。一、抽象类是什么?在 Java 语言中使用 abstract class
- MyBatis添加记录后获取主键ID,这是一个很常见的需求。这个需求有分为两种情况:(1)添加单条记录时获取主键值;(2)获取批量添加记录时
- 我们经常需要对我们的开发的软件做各种测试, 软件对系统资源的使用情况更是不可少, 目前有多个监控工具, 相比JProfiler对系统资源尤其
- package other;import java.security.MessageDigest;import java.security.
- 开门见山 项目运行的环境里面已经有该项目的所有代码依赖,所以项目的代码只要将自己的代码打入进去就能提交到环境中运行了。但是不好的地方就是项
- 本文章向大家讲解java中时间的获取和格式化, 一. 获取当前系统时间和日期并格式化输出:import java.util.Dat
- 一、特性无侵入:只做增强不做改变,引入它不会对现有工程产生影响,如丝般顺滑损耗小:启动即会自动注入基本 CURD,性能基本无损耗,直接面向对
- 本文实例总结了Android开发中Toast显示消息的方法。分享给大家供大家参考,具体如下:Android中提供一种简单的Toast消息提示
- java.math.BigDecimal及加减乘除计算BigDecimal简介BigDecimal用来对需要更大或更小的数进行任意精度的运算
- 绝大部分知识与实例来自O'REILLY的《Java网络编程》(Java Network Programming,Fourth Edi
- 本文实例为大家分享了java实现幸运抽奖功能的具体代码,供大家参考,具体内容如下本系统较为简单,未使用是什么多的算法,也未添加保存文件读取文
- 背景:Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化新Spring应用的初始搭建以及开发过程。该框架使用了
- 一、叙述当Spring的事件(Application Event)为Bean和Bean之间的消息同步提供了支持。当一个Bean处理完成一个任
- 采用继承Thead类实现多线程:优势:编写简单,如果需要访问当前线程,只需使用this即可,无需使用Thead.currentThread(
- 目录一、导入依赖二、前端实现三、后台逻辑三、页面效果四、可能会遇到的问题一、导入依赖这里还是用了Apache的POI插件,现在一般的spri