用AtomicStampedReference解决ABA问题
悲观锁,乐观锁CAS、原子操作类的应用与浅析及Java8对其的优化
CAS(Compare and swap),即比较并交换,也是实现我们平时所说的【自旋锁或乐观锁】的核心操作。
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
public final boolean compareAndSet(boolean expect, boolean update)
它的实现很简单,【就是用 一个预期的值 和内存值进行比较,如果两个值相等,就用预期的值替换内存值,并返回 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); } }
这里面又几个关键方法和属性。
1、使用了 sun.misc.Unsafe 对象,这个类提供了一系列直接操作内存对象的方法,只是在 jdk 内部使用,不建议开发者使用;
2、value 表示实际值,可以看到 get 方法实际是根据 value 是否等于0来判断布尔值的,这里的 value 定义为 volatile,因为 volatile 可以保证内存可见性,也就是 value 值只要发生变化,其他线程是马上可以看到变化后的值的;下一篇会讲一下 volatile 可见性问题,欢迎关注
3、valueOffset 是 value 值的内存偏移量,用 unsafe.objectFieldOffset 方法获得,用作后面的 compareAndSet 方法;
4、compareAndSet 方法,这就是实现 CAS 的核心方法了,在使用这个方法时,只需要传递期望值和待更新的值即可,而它里面调用了 unsafe.compareAndSwapInt(this, valueOffset, e, u) 方法,它是个 native 方法,用 c++ 实现,具体的代码就不贴了,总之是利用了 CPU 的 cmpxchg 指令完成比较并替换,当然根据具体的系统版本不同,实现起来也有所区别,感兴趣的可以自行搜一下相关文章。
使用场景
CAS 适合简单对象的操作,比如布尔值、整型值等;
CAS 适合冲突较少的情况,如果太多线程在同时自旋,那么长时间循环会导致 CPU 开销很大;
ABA问题:
线程1准备用CAS将变量的值由A替换为B,在此之前,线程2将变量的值由A替换为C,又由C替换为A,然后线程1执行CAS时发现变量的值仍然为A,所以CAS成功。但实际上这时的现场已经和最初不同了,尽管CAS成功,但可能存在潜藏的问题。
例如下面的例子:
现有一个用单向链表实现的堆栈,栈顶为A,这时线程T1已经知道A.next为B,然后希望用CAS将栈顶替换为B:
head.compareAndSet(A,B);
在T1执行上面这条指令之前,线程T2介入,将A、B出栈,再pushD、C、A,此时堆栈结构如下图,而对象B此时处于游离状态:
此时轮到线程T1执行CAS操作,检测发现栈顶仍为A,所以CAS成功,栈顶变为B,但实际上B.next为null,所以此时的情况变为:
其中堆栈中只有B一个元素,C和D组成的链表不再存在于堆栈中,平白无故就把C、D丢掉了。
以上就是由于ABA问题带来的隐患,各种乐观锁的实现中通常都会用版本戳version来对记录或对象标记,避免并发操作带来的问题,并发包下倒是有 AtomicStampedReference 提供了根据版本号判断的实现,可以解决一部分问题。它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:
import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import java.util.concurrent.atomic.AtomicStampedReference; public class ABA { private static AtomicInteger atomicInt = new AtomicInteger(100); private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0); public static void main(String[] args) throws InterruptedException { Thread intT1 = new Thread(new Runnable() { @Override public void run() { atomicInt.compareAndSet(100, 101); atomicInt.compareAndSet(101, 100); } }); Thread intT2 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } boolean c3 = atomicInt.compareAndSet(100, 101); System.out.println(c3); // true } }); intT1.start(); intT2.start(); intT1.join(); intT2.join(); Thread refT1 = new Thread(new Runnable() { @Override public void run() { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { } atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1); atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1); } }); Thread refT2 = new Thread(new Runnable() { @Override public void run() { int stamp = atomicStampedRef.getStamp(); try { TimeUnit.SECONDS.sleep(2); } catch (InterruptedException e) { } boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1); System.out.println(c3); // false } }); refT1.start(); refT2.start(); } }
。。
相关推荐
深入讲解CAS自旋锁的实现机理和原理 CAS(Compare and Swap)是实现自旋锁或乐观锁的核心操作,它的出现是为了解决原子操作的问题。在多线程环境下,原子操作是保证线程安全的重要手段。CAS操作的实现很简单,就是...
本文将围绕"无锁编程之自旋锁的C++实现"这一主题,详细介绍无锁编程和自旋锁的概念,并分析提供的C++代码实现。 首先,无锁编程(Lock-Free Programming)强调的是在没有锁的情况下实现并发操作。这种方法的关键...
本文主要讨论了四种锁类型:乐观锁、悲观锁、自旋锁以及Java中的synchronized同步锁,并深入解析了synchronized锁的内部机制,包括其核心组件、实现方式以及锁的状态。 1. **乐观锁**:乐观锁假设在多线程环境下,...
自旋锁是多线程编程中的一个重要概念,用于在共享资源上实现轻量级的同步。自旋锁的原理是当一个线程试图获取已被其他线程持有的锁时,它不会立即阻塞,而是会“自旋”等待,直到锁被释放为止。这样做的好处是可以...
首先,原子自旋锁的核心在于原子操作,如CAS(Compare and Swap)指令,这是一个无锁编程中的基础操作,可以确保在多线程环境下对共享变量的修改是原子性的,不会被其他线程中断。 当线程试图获取自旋锁时,它会...
在Go语言中,自旋锁是一种用于并发控制的机制,它允许线程在等待锁释放时持续循环检查,而不是进入阻塞状态。自旋锁的核心思想是,如果锁已经被其他线程持有,当前线程不会立即休眠,而是会“自旋”等待,直到锁变为...
自旋锁的一个经典实现是基于无锁编程中的Compare And Swap (CAS) 操作。如以下代码所示: ```java public class SimpleSpinningLock { private AtomicReference<Thread> ref = new AtomicReference(); public ...
- **原子操作**:自旋锁通常依赖于硬件提供的原子操作,如`compare-and-swap (CAS)`或`fetch-and-add (FAA)`,以确保锁的获取和释放是无冲突的。 - **无锁编程**:自旋锁的实现可以基于无锁编程技术,通过原子操作...
自旋锁SpinLock唉….刚写完了!别白嫖啊,点赞关注,给你们福利啊~~转载请标注! 好兄弟们,不会真有人看不懂CAS吧?反正我是没看懂… 一. CAS是什么? import java.util.concurrent.atomic.AtomicInteger; /** * 1. CAS...
文件名"cas"可能是指Cas自旋锁(Compare and Swap Spin Lock),这是一种基于CAS操作实现的非阻塞锁。 学习和理解这些概念对于提升多线程编程的能力至关重要,它们可以帮助开发者编写出更加高效、并发性能更强的...
一、自旋锁 自旋锁是一种基础的同步原语,用于保障对共享数据的互斥访问。与互斥锁的相比,在获取锁失败的时候不会使得线程阻塞而是一直自旋尝试获取锁。当线程等待自旋锁的时候,CPU不能做其他事情,而是一直处于...
自旋锁的实现是基于CAS(Compare And Swap)算法的,它可以在不使用锁的情况下实现多线程之间的变量同步。 自旋锁的定义是指当一个线程在获取锁的时候,如果锁已经被其他线程获取,那么该线程将循环等待,然后不断...
为了效率,不使用C++语言提供的Mutex互斥量,而使用不使用线程被阻塞的方式,即所谓的自旋锁,这是自旋锁的一种实现方式,使用C++11的原子变量,不用锁机制,实现的一种无锁的自旋锁
锁机制可以分为偏向锁、轻量级锁、重量级锁、自旋锁等。锁机制可以保证在多线程环境下,数据的安全和一致性。 五、Disruptor 框架 Disruptor 框架是一个高性能的并发框架,使用原子操作和锁机制来实现高效的并发...
本压缩包"MutexSpinlockAndCAS.tar.gz"包含了关于互斥锁(Mutex)、自旋锁(Spinlock)以及比较器和交换(Compare-And-Swap, CAS)操作的源码示例。这些同步原语在并发环境中用于防止数据竞争,确保共享资源的正确...
本文主要探讨了Java中的两种广义锁概念——乐观锁和悲观锁,以及自旋锁和适应性自旋锁的区别和应用场景。 1. 乐观锁与悲观锁: 乐观锁认为在读取数据时不会有其他线程修改,因此在读取时不加锁,而在更新数据时检查...
- **CAS自旋锁**:在并发环境中,通过CAS操作确保多个线程安全地访问共享资源,避免了锁带来的阻塞和上下文切换开销。 - **MemoryStore**:这是一个内存存储模块,负责数据的持久化和快速检索,可能采用了类似B+树...
在本项目“interprocess-spin-lock-trial”中,重点探讨了自旋锁的实现,特别是Paterson算法和基于原子操作的比较和交换(CAS,Compare-and-Swap)方法。 自旋锁的基本概念是,当一个线程试图获取已被其他线程持有...
在Java中,自旋锁主要通过CAS操作体现,例如在ConcurrentHashMap等并发集合中就有应用。 3. 可重入锁 可重入锁也称为递归锁,它允许同一个线程多次进入被该锁保护的代码块。实现可重入锁的关键在于记录当前持有锁的...