CAS自旋锁

什么是自旋锁

自旋锁要从多线程下的锁机制说起,由于多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问。即每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁。

设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?

通常有2种处理方式:

一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是本文的重点——自旋锁。他不用将线城阻塞起来(NON-BLOCKING)。

另一种是没有获得锁的进程就阻塞(BLOCKING)自己,继续执行线程上的其他任务,这就是 ——互斥锁(包括内置锁Synchronized还有ReentrantLock等等)。

CAS

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://www.cnblogs.com/lemonrel/p/11693255.html

时间: 2024-10-31 10:38:00

CAS自旋锁的相关文章

Java线程 - CAS自旋锁(spin-lock)

一.自旋锁提出的背景 由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问.即是每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁.设计互斥算法时总是会面临一种情况,即没有获得锁的进程怎么办?通常有2种处理方式.一种是没有获得锁的调用者就一直循环在那里看是否该自旋锁的保持者已经释放了锁,这就是自旋锁,他不用将县城阻塞起来(NON

并发编程--CAS自旋锁

在前两篇博客中我们介绍了并发编程--volatile应用与原理和并发编程--synchronized的实现原理(二),接下来我们介绍一下CAS自旋锁相关的知识. 一.自旋锁提出的背景 由于在多处理器系统环境中有些资源因为其有限性,有时需要互斥访问(mutual exclusion),这时会引入锁的机制,只有获取了锁的进程才能获取资源访问.即是每次只能有且只有一个进程能获取锁,才能进入自己的临界区,同一时间不能两个或两个以上进程进入临界区,当退出临界区时释放锁.设计互斥算法时总是会面临一种情况,即

SpinLock 自旋锁, CAS操作(Compare &amp; Set) ABA Problem

SpinLock 自旋锁 spinlock 用于CPU同步, 它的实现是基于CPU锁定数据总线的指令. 当某个CPU锁住数据总线后, 它读一个内存单元(spinlock_t)来判断这个spinlock 是否已经被别的CPU锁住. 如果否, 它写进一个特定值, 表示锁定成功, 然后返回. 如果是, 它会重复以上操作直到成功, 或者spin次数超过一个设定值. 锁定数据总线的指令只能保证一个机器指令内, CPU独占数据总线. 单CPU当然能用spinlock, 但实现上无需锁定数据总线. spinl

CAS机制与自旋锁

CAS(Compare-and-Swap),即比较并替换,java并发包中许多Atomic的类的底层原理都是CAS. 它的功能是判断内存中某个地址的值是否为预期值,如果是就改变成新值,整个过程具有原子性. 具体体现于sun.misc.Unsafe类中的native方法,调用这些native方法,JVM会帮我们实现汇编指令,这些指令是CPU的原子指令,因此具有原子性. 1 public class CASDemo { 2 3 public static void main(String[] arg

多线程之美6一CAS与自旋锁

1.什么是CAS CAS 即 compare and swap 比较并交换, 涉及到三个参数,内存值V, 预期值A, 要更新为的值B, 拿着预期值A与内存值V比较,相等则符合预期,将内存值V更新为B, 不相等,则不能更新V. 为什么预期值A与内存值V不一样了呢? 在多线程环境下,对于临界区的共享资源,所有线程都可以访问修改,这时为了保证数据不会发生错误,通常会对访问临界区资源加锁,同一时刻最多只能让一个线程访问(独占模式下),这样会让线程到临界区时串行执行,加锁操作可能会导致并发性能降低,而循环

可重入锁 &amp; 自旋锁 &amp; Java里的AtomicReference和CAS操作 &amp; Linux mutex不可重入

之前还是写过蛮多的关于锁的文章的: http://www.cnblogs.com/charlesblc/p/5994162.html <[转载]Java中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自> http://www.cnblogs.com/charlesblc/p/5935326.html <[Todo] 乐观悲观锁,自旋互斥锁等等> http://www.cnblogs.com/charlesblc/

虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁)

高效并发是JDK 1.6的一个重要主题,HotSpot虚拟机开发团队在这个版本上花费了大量的精力去实现各种锁优化技术,如适应性自旋(Adaptive Spinning).锁削除(Lock Elimination).锁膨胀(Lock Coarsening).轻量级锁(Lightweight Locking).偏向锁(Biased Locking)等,这些技术都是为了在线程之间更高效地共享数据,以及解决竞争问题,从而提高程序的执行效率. 13.3.1 自旋锁与自适应自旋 前面我们讨论互斥同步的时候,

Java并发包源码学习之AQS框架(二)CLH lock queue和自旋锁

上一篇文章提到AQS是基于CLH lock queue,那么什么是CLH lock queue,说复杂很复杂说简单也简单, 所谓大道至简: CLH lock queue其实就是一个FIFO的队列,队列中的每个结点(线程)只要等待其前继释放锁就可以了. AbstractQueuedSynchronizer是通过一个内部类Node来实现CLH lock queue的一个变种,但基本原理是类似的. 在介绍Node类之前,我们来介绍下Spin Lock,通常就是用CLH lock queue来实现自旋锁

谈论高并发(七)几个自旋锁的实现(二)

在谈论高并发(六)几个自旋锁的实现(一) 这篇中实现了两种主要的自旋锁:TASLock和TTASLock,它们的问题是会进行频繁的CAS操作.引发大量的缓存一致性流量,导致锁的性能不好. 对TTASLock的一种改进是BackoffLock,它会在锁高争用的情况下对线程进行回退,降低竞争,降低缓存一致性流量.可是BackoffLock有三个基本的问题: 1. 还是有大量的缓存一致性流量,由于全部线程在同一个共享变量上旋转,每一次成功的获取锁都会产生缓存一致性流量 2. 由于回退的存在,不能及时获