欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

程序员文章站 2022-07-10 19:28:02
文章目录问题:从AtomicInteger类compareAndSet()方法开始,这个方法到底要干什么?金手指:CAS原理三句(加黑的三句)1、从AtomicInteger类incrementAndGet()方法的源码出发,开启底层探索2、继续深入AtomicInteger类的compareAndSet方法3、继续深入native compareAndSwapInt()方法4、继续深入UNSAFE_ENTRY()方法5、继续深入的Atomic::cmpxchg()面试金手指(解释一下CAS,满分答案)小结...


文章目录


一、前言

二、CAS操作

2.1 CAS三步操作+CAS与阻塞同步的对比+三种锁

2.1.1 CAS三步操作

首先要说一下,AtomicInteger类compareAndSet通过原子操作实现了CAS操作,最底层基于汇编语言实现。

简单说一下原子操作的概念,“原子”代表最小的单位,所以原子操作可以看做最小的执行单位,该操作在执行完毕前不会被任何其他任务或事件打断。

CAS是Compare And Set的一个简称,如下理解:

1,已知当前内存里面的值current和预期要修改成的值new传入
2,内存中AtomicInteger对象地址对应的真实值(因为有可能别修改)real与current对比,
3,相等表示real未被修改过,是“安全”的,将new赋给real结束然后返回;不相等说明real已经被修改,结束并重新执行1直到修改成功

CAS相比Synchronized,避免了锁的使用,总体性能比Synchronized高很多.

2.1.2 CAS与内建锁比较

元老级的内建锁(synchronized) CAS操作
悲观锁,当存在线程竞争的情况下会出现线程阻塞以及唤醒带来的性能问题,对应互斥同步(阻塞同步),效率很低。 乐观锁,并不会直接挂起线程,会尝试若干次CAS操作,并非进行耗时的挂起与唤醒操作,因此非阻塞式同步。

金手指:cas的应用+为什么lock比synchronized高效?
cas的应用主要是lock里面还有AtomicInteger里面,所以在博客https://blog.csdn.net/qq_36963950/article/details/107740496
中说:在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

2.1.3 三种锁

CAS(无锁操作,乐观锁策略):使用CAS叫做比较交换来判断是否出现冲突,出现冲突就重试当前操作直到不冲突为止。

悲观锁(JDK1.6之前的内建锁):假设每一次执行同步代码块均会产生冲突,所以当线程获取锁成功,会阻塞其他尝试获取该锁的线程。

乐观锁(Lock机制):假设所有线程访问共享资源时不会出现冲突,既然不会出现冲突自然就不会阻塞其他线程。线程不会出现阻塞状态。

2.2 CAS的应用:AtomicInteger类中的compareAndSet()方法使用for+if(cas)保证线程安全

最近无意接触了AtomicInteger类compareAndSet(从JDK5开始),搜了搜相关资料,整理了一下

这里只要记住一点:
AtomicInteger类compareAndSet()方法使用for循环+if(cas)来保证线程安全就好了

2.2.1 从AtomicInteger类incrementAndGet()方法的源码出发,开启底层探索

compareAndSet典型使用为计数,如i++,++i,这里以i++为例:

 /**
     * Atomically increments by one the current value.
     *
     * @return the updated value
     */
    public final int incrementAndGet() { for (;;) { //获取当前值
            int current = get(); //设置期望值
            int next = current + 1; //调用Native方法compareAndSet,执行CAS操作 if (compareAndSet(current, next)) //成功后才会返回期望值,否则无线循环 return next; } } 

金手指:看到incrementAndGet()
一个for,一个if
这个for就是自旋,现在还没有成功自增就循环自旋;
这个if就是判断成功,current == real ,直接返回next(已经next=current+1,所以直接返回)
在这里,既看到了AtomicInteger类中的操作方法incrementAndGet()如何完成自旋代替阻塞同步,又看到了compareAndSet()方法如何使用CAS操作保证线程安全(就是一定要real==current才返回为true,从而保证安全)
所以,synchronzied == for自旋 + if(CAS判断当前线程安全)

2.2.2 继续深入AtomicInteger类的compareAndSet方法

compareAndSet方法实现:

JDK文档对该方法的说明如下:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

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

金手指:嵌入解释一下valueOffset变量

这里解释一下valueOffset变量,首先valueOffset的初始化在static静态代码块里面,代表相对起始内存地址的字节相对偏移量:

bash private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField(“value”));
} catch (Exception ex) { throw new Error(ex); }
}

在生成一个AtomicInteger对象后,可以看做生成了一段内存,对象中各个字段按一定顺序放在这段内存中,字段可能不是连续放置的,

unsafe.objectFieldOffset(Field
f)这个方法准确地告诉我"value"字段相对于AtomicInteger对象的起始内存地址的字节相对偏移量。

private volatile int value;

/**
 * Creates a new AtomicInteger with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

/**
 * Creates a new AtomicInteger with initial value {@code 0}.
 */
public AtomicInteger() {
} 

在这段程序中,value是一个volatile变量,不同线程对这个变量进行操作时具有可见性,修改与写入操作都会存入主存中,并通知其他cpu中该变量缓存行无效,保证了每次读取都是最新的值

2.2.3 继续深入native compareAndSwapInt()方法

找到sun.misc.Unsafe.java:

/**
 * Atomically update Java variable to <tt>x</tt> if it is currently
 * holding <tt>expected</tt>.
 * @return <tt>true</tt> if successful
 */
public final native boolean compareAndSwapInt(Object o, long offset,int expected,int x); 

2.2.4 继续深入UNSAFE_ENTRY()方法

继续查找unsafe.cpp,(http://hg.openjdk.java.net/jdk7/jdk7/hotspot/file/9b0ca45cd756/src/share/vm/prims/unsafe.cpp):

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x)) UnsafeWrapper("Unsafe_CompareAndSwapInt"); oop p = JNIHandles::resolve(obj); jint* addr = (jint *) index_oop_from_field_offset_long(p, offset); return (jint)(Atomic::cmpxchg(x, addr, e)) == e; UNSAFE_END 

2.2.5 继续深入的Atomic::cmpxchg()

实现主要方法为Atomic::cmpxchg , 这个本地方法的最终实现在openjdk的如下位置:openjdk-7-fcs-src-b147-27jun2011\openjdk\hotspot\src\oscpu\windowsx86\vm\ atomicwindowsx86.inline.hpp(对应于windows操作系统,X86处理器)

// Adding a lock prefix to an instruction on MP machine
// VC++ doesn't like the lock prefix to be on a single line
// so we can't insert a label after the lock prefix.
// By emitting a lock prefix, we can define a label after it. #define LOCK_IF_MP(mp) __asm cmp mp, 0  \ __asm je L0      \
                       __asm _emit 0xF0 \
                       __asm L0:
 
inline jint     Atomic::cmpxchg (jint     exchange_value, volatile jint*     dest, jint     compare_value) { // alternative for InterlockedCompareExchange
  int mp = os::is_MP(); __asm { mov edx, dest
    mov ecx, exchange_value
    mov eax, compare_value
    LOCK_IF_MP(mp) cmpxchg dword ptr [edx], ecx } } 

如上面源代码所示,用嵌入的汇编实现的, CPU指令是 cmpxchg,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀(金手指:就是 LOCK_IF_MP(mp) 代码)。

金手指:对于LOCK_IF_MP(mp) 代码
如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock
cmpxchg).反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果).

lock前缀的作用说明:1禁止该指令与之前和之后的读和写指令重排序,2把写缓冲区中的所有数据刷新到内存中。

总的来说,Atomic(例如:AtomicInteger类)实现了高效无锁(金手指:底层还是用到排它锁,就是多处理器下的lock前缀,不过底层处理比java层处理要快很多)与线程安全(volatile变量特性),CAS一般适用于计数;

多线程编程也适用,多个线程执行AtomicXXX类下面的方法,当某个线程执行的时候具有排他性,在执行方法中不会被打断,直至当前线程完成才会执行其他的线程(上面的AtomicInteger类中incrementAndGet()方法使用CompareAndSet()方法来完成保证加一操作的线程安全性,取代synchronized同步阻塞)。

三、CAS的ABA问题

3.1 什么是ABA问题(理论解释,一图就好了)

【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

3.2 代码重现ABA问题(代码,面试没用)

public class ABADemo { private static AtomicReference<Integer> atomicReference = new AtomicReference<Integer>(100); public static void main(String[] args) { new Thread(() -> { atomicReference.compareAndSet(100, 101); atomicReference.compareAndSet(101, 100); },"t1").start(); new Thread(() -> { try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(atomicReference.compareAndSet(100, 2019) + "\t修改后的值:" + atomicReference.get()); },"t2").start(); } } 
  • 初始值为100,线程t1将100改成101,然后又将101改回100
  • 线程t2先睡眠1秒,等待t1操作完成,然后t2线程将值改成2019
  • 可以看到,线程2修改成功

输出结果:
true 修改后的值:2019

3.3 ABA问题:原因、突破口、解决方式、源码支持

3.3.1 ABA问题的产生原因(背诵)

一句话小结:ABA问题产生的原因是传统的CAS仅仅对业务数值的比较,current==real,就是认为没有修改过,这样的比较条件是不充分的。

对于 10 -> 11 ->10,仅仅比较数值,无法知道当前获取值是否已被修改过。

3.3.2 ABA问题:从原因到突破口再到解决方式(背诵)

知道了ABA问题产生的原因,就知道解决这个问题的突破点:

核心原因是:对于 10 -> 11 ->10,仅仅比较数值,无法知道当前获取值是否已被修改过。

突破点:在获取到数值的时候,要找到一个办法知道当前获取到的数值是否已被修改过。

JDK的处理办法:两个类,不仅比较数值,还有比较当前的数值是否被修改过,其中,AtomicStampedReference使用int来记录版本号,表示当前数值是否被修改过(在current=real的条件下,版本号相同就是未被修改,版本号不同是已被修改);
AtomicMarkableReference使用boolean来记录当前数值是否被修改过(在current=real的条件下,false就是未被修改,true是已被修改)。

3.3.3 ABA问题两个类的源码:不仅比较数值,还有比较当前的数值是否被修改过

1、AtomicStampedReference类

这里使用的是AtomicStampedReference的compareAndSet函数,这里面有四个参数:

compareAndSet(V expectedReference, V newReference, int expectedStamp, int newStamp)。

(1)第一个参数expectedReference:表示预期值。

(2)第二个参数newReference:表示要更新的值。

(3)第三个参数expectedStamp:表示预期的时间戳。

(4)第四个参数newStamp:表示要更新的时间戳。

这个compareAndSet方法到底是如何实现的,我们深入到源码中看看。
【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

可以看到,最后的返回值,同时比较数值和版本号

Pair类

【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

casPair()方法
【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

2、AtomicMarkableReference类

【JavaSE 并发CAS】原理层面:CAS(AtomicInteger类compareAndSet()方法到底干了什么?)

3.4 代码解释ABA问题两个类处理

3.4.1 AtomicStampedReference

要解决ABA问题,可以增加一个版本号,当内存位置V的值每次被修改后,版本号都加1

AtomicStampedReference内部维护了对象值和版本号,在创建AtomicStampedReference对象时,需要传入初始值和初始版本号,
当AtomicStampedReference设置对象值时,对象值以及状态戳都必须满足期望值,写入才会成功

public class ABADemo { private static AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(100,1); public static void main(String[] args) { new Thread(() -> { System.out.println("t1拿到的初始版本号:" + atomicStampedReference.getStamp()); //睡眠1秒,是为了让t2线程也拿到同样的初始版本号
		try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicStampedReference.compareAndSet(100, 101,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); atomicStampedReference.compareAndSet(101, 100,atomicStampedReference.getStamp(),atomicStampedReference.getStamp()+1); },"t1").start(); new Thread(() -> { int stamp = atomicStampedReference.getStamp(); System.out.println("t2拿到的初始版本号:" + stamp); //睡眠3秒,是为了让t1线程完成ABA操作
		try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("最新版本号:" + atomicStampedReference.getStamp()); System.out.println(atomicStampedReference.compareAndSet(100, 2019,stamp,atomicStampedReference.getStamp() + 1) + "\t当前 值:" + atomicStampedReference.getReference()); },"t2").start(); } } 

1、初始值100,初始版本号1
2、线程t1和t2拿到一样的初始版本号
3、线程t1完成ABA操作,版本号递增到3
4、线程t2完成CAS操作,最新版本号已经变成3,跟线程t2之前拿到的版本号1不相等,操作失败

输出结果:
t1拿到的初始版本号:1
t2拿到的初始版本号:1
最新版本号:3
false 当前 值:100

金手指:
由此可知,这里返回为false,表示已经被修改过,因为(不仅比较数值,还有比较当前的数值是否被修改过,都满足才返回为true)

AtomicStampedReference可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D - > A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次
但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference

3.4.2 AtomicStampedReference

AtomicMarkableReference的唯一区别就是不再用int标识引用,而是使用boolean变量——表示引用变量是否被更改过

public class ABADemo { private static AtomicMarkableReference<Integer> atomicMarkableReference = new AtomicMarkableReference<Integer>(100,false); public static void main(String[] args) { new Thread(() -> { System.out.println("t1版本号是否被更改:" + atomicMarkableReference.isMarked()); //睡眠1秒,是为了让t2线程也拿到同样的初始版本号
		try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); } atomicMarkableReference.compareAndSet(100, 101,atomicMarkableReference.isMarked(),true); atomicMarkableReference.compareAndSet(101, 100,atomicMarkableReference.isMarked(),true); },"t1").start(); new Thread(() -> { boolean isMarked = atomicMarkableReference.isMarked(); System.out.println("t2版本号是否被更改:" + isMarked); //睡眠3秒,是为了让t1线程完成ABA操作
		try { TimeUnit.SECONDS.sleep(3); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("是否更改过:" + atomicMarkableReference.isMarked()); System.out.println(atomicMarkableReference.compareAndSet(100, 2019,isMarked,true) + "\t当前 值:" + atomicMarkableReference.getReference()); },"t2").start(); } } 

1、初始值100,初始版本号未被修改 false
2、线程t1和t2拿到一样的初始版本号都未被修改 false
3、线程t1完成ABA操作,版本号被修改 true
4、线程t2完成CAS操作,版本号已经变成true,跟线程t2之前拿到的版本号false不相等,操作失败

输出结果:
t1版本号是否被更改:false
t2版本号是否被更改:false
是否更改过:true
false 当前 值:100

金手指:
由此可知,这里返回为false,表示已经被修改过,因为(不仅比较数值,还有比较当前的数值是否被修改过,都满足才返回为true)

四、面试金手指(解释一下CAS,满分答案)

4.1 CAS三步操作+CAS与内建锁比较+三种锁

1,已知当前内存里面的值current和预期要修改成的值new传入
2,内存中AtomicInteger对象地址对应的真实值(因为有可能别修改)real与current对比,
3,相等表示real未被修改过,是“安全”的,将new赋给real结束然后返回;不相等说明real已经被修改,结束并重新执行1直到修改成功

4.1.1 CAS三步操作

首先要说一下,AtomicInteger类compareAndSet通过原子操作实现了CAS操作,最底层基于汇编语言实现。

简单说一下原子操作的概念,“原子”代表最小的单位,所以原子操作可以看做最小的执行单位,该操作在执行完毕前不会被任何其他任务或事件打断。

CAS是Compare And Set的一个简称,如下理解:

1,已知当前内存里面的值current和预期要修改成的值new传入
2,内存中AtomicInteger对象地址对应的真实值(因为有可能别修改)real与current对比,
3,相等表示real未被修改过,是“安全”的,将new赋给real结束然后返回;不相等说明real已经被修改,结束并重新执行1直到修改成功

CAS相比Synchronized,避免了锁的使用,总体性能比Synchronized高很多.

4.1.2 CAS与内建锁比较

元老级的内建锁(synchronized) CAS操作
悲观锁,当存在线程竞争的情况下会出现线程阻塞以及唤醒带来的性能问题,对应互斥同步(阻塞同步),效率很低。 乐观锁,并不会直接挂起线程,会尝试若干次CAS操作,并非进行耗时的挂起与唤醒操作,因此非阻塞式同步。

金手指:cas的应用+为什么lock比synchronized高效?
cas的应用主要是lock里面还有AtomicInteger里面,所以在博客https://blog.csdn.net/qq_36963950/article/details/107740496
中说:在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

4.1.3 三种锁

CAS(无锁操作,乐观锁策略):使用CAS叫做比较交换来判断是否出现冲突,出现冲突就重试当前操作直到不冲突为止。

悲观锁(JDK1.6之前的内建锁):假设每一次执行同步代码块均会产生冲突,所以当线程获取锁成功,会阻塞其他尝试获取该锁的线程。

乐观锁(Lock机制):假设所有线程访问共享资源时不会出现冲突,既然不会出现冲突自然就不会阻塞其他线程。线程不会出现阻塞状态。

4.2 CAS的应用:AtomicInteger类中的compareAndSet()方法使用for+if(cas)保证线程安全

4.2.1 AtomicInteger类中的compareAndSet()方法使用for+if(cas)保证线程安全

在Java的原子类,例如AtomicInteger类中,就有用到CAS操作,比如AtomicInteger类中的compareAndSet()方法,

 public final int incrementAndGet() { for (;;) { int current = get(); int next = current + 1; if (compareAndSet(current, next)) return next; } } 

金手指:这个incrementAndGet()是在不使用synchronize的阻塞同步的前提下,完成多线程情况下,线程安全的自增操作?
那么问题来了,i++的硬件操作包括三步:从内存取出数据到寄存器,运算器中i++,从运算器中将数据放到内存中,既然不使用synchronized阻塞同步,那么这个AtomicInterger类中的incrementAndGet()方法如何在多线程情况下保证操作的原子性呢?
答案是:synchronzied == for自旋 + if(CAS判断当前线程安全)
使用for自旋,if中使用CAS判断当前线程安全,等到线程安全的情况下,返回next(已经next=current+1,所以直接返回)

将if提上来知道两点,进一步理解 synchronzied == for自旋 +if(CAS判断当前线程安全),将if判断提上来,我们就可以知道很多东西(源码中不把if提上来是因为把if提上来,包裹的大了,性能就变差了,和synchronized一样,包裹的大了,性能就差了)

 for (;;) { if (compareAndSet(current, next)){ // 源码中不把if提上来是因为把if提上来,包裹的大了,性能就变差了,和synchronized一样,包裹的大了,性能就差了
             //  将if判断提上来,就是一个synchronized,这就很好理解 
             //  所以说: synchronzied == for自旋 + if(CAS判断当前线程安全) //  将if提上来1,所以说:阻塞同步和cas同步独立相等,都是保证原子性:将if判断提上来,就是一个synchronized,这就很好理解
             //  将if提上来2,为什么都是保证原子性?
             // 1、一定要线程安全才能进来  cas和synchronized都可以保证,cas一定要current==real才能进来,synchronized一定要获取到同步锁才能进来
             // 2、里面的不出去,外面的不能进来,cas这里return next;之前都没有改变数据,所以里面的没出去,外面的不能进来;synchronized也可以保证这一点,里面的不执行完不释放锁,外面的进不来
                  int current = get(); int next = current + 1; return next; } } } ``` 

// 将if提上来1,所以说:阻塞同步和cas同步独立相等,都是保证原子性:将if判断提上来,就是一个synchronized,这就很好理解

public final int incrementAndGet() { for (;;) { if (compareAndSet(current, next)){ int current = get(); int next = current + 1; return next; } } } 等价
  public final int incrementAndGet() { synchronized(this){ int current = get(); int next = current + 1; return next; } } ``` 

所有有如下表:

阻塞同步(又称互斥同步) 非阻塞同步(冲突检测同步)
悲观锁:1、先加锁,再操作(先加锁,再操作,操作完成后再释放锁,给所有线程公平竞争)2、悲观锁:抱着一种悲观的态度,害怕出现线程不安全问题,所有对于每一次线程操作之前都要加上锁,虽然降低了性能,但是提高了效率;3、阻塞同步:没有获取到锁的线程,即竞争锁失败的线程需要挂起,wait()或wait(时间参数),进入blocked阻塞状态,所以称为阻塞同步;4、实现方式:synchronized关键字和lock锁机制 乐观锁:1、先操作,若没有其他线程竞争,就操作成功了,若有其他线程竞争,产生冲突(冲突检测到冲突),再采取补救措施;2、乐观锁:抱着一种乐观的态度,也可说是一种侥幸的心理,不断重试,直至成功;3、非阻塞同步:操作过程中,不存在阻塞线程,所以称为非阻塞同步。4、实现方式:硬件的原子操作,下面介绍五条,重点CAS。.
代价 大,挂起线程和恢复线程的操作都需要转入内核态完成,代价大 不大,直接使用原子操作不需要阻塞线程

// 将if提上来2,为什么都是保证原子性?
// 1、一定要线程安全才能进来 cas和synchronized都可以保证,cas一定要current==real才能进来,synchronized一定要获取到同步锁才能进来
// 2、里面的不出去,外面的不能进来,cas这里return next;之前都没有改变数据,所以里面的没出去,外面的不能进来;synchronized也可以保证这一点,里面的不执行完不释放锁,外面的进不来

所以说,所有的cas包裹的都可以改写为synchronized包裹的,所有的synchronized包裹的都可以改写为cas包裹的。

4.2.2 CAS底层如何完成比较current和real的

底层使用用嵌入的汇编指令实现的, CPU指令是 cmpxchg,程序会根据当前处理器的类型来决定是否为cmpxchg指令添加lock前缀(金手指:就是 LOCK_IF_MP(mp) 代码)。

金手指:对于LOCK_IF_MP(mp) 代码
如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock
cmpxchg).反之,如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果).

lock前缀的作用说明:1禁止该指令与之前和之后的读和写指令重排序,2把写缓冲区中的所有数据刷新到内存中。

4.3 ABA问题:原因+突破口+解决方式+源码

4.3.1 ABA问题的产生原因(背诵)

一句话小结:ABA问题产生的原因是传统的CAS仅仅对业务数值的比较,current==real,就是认为没有修改过,这样的比较条件是不充分的。

对于 10 -> 11 ->10,仅仅比较数值,无法知道当前获取值是否已被修改过。

4.3.2 ABA问题:从原因到突破口再到解决方式(背诵)

知道了ABA问题产生的原因,就知道解决这个问题的突破点:

核心原因是:对于 10 -> 11 ->10,仅仅比较数值,无法知道当前获取值是否已被修改过。

突破点:在获取到数值的时候,要找到一个办法知道当前获取到的数值是否已被修改过。

JDK的处理办法:两个类,不仅比较数值,还有比较当前的数值是否被修改过,其中,AtomicStampedReference使用int来记录版本号,表示当前数值是否被修改过(在current=real的条件下,版本号相同就是未被修改,版本号不同是已被修改);
AtomicMarkableReference使用boolean来记录当前数值是否被修改过(在current=real的条件下,false就是未被修改,true是已被修改)。

4.3.3 源码解析:不仅比较数值,还有比较当前的数值是否被修改过,都满足才返回为true

金手指:
由此可知,这里返回为false,表示已经被修改过,因为(不仅比较数值,还有比较当前的数值是否被修改过,都满足才返回为true)

4.4 CAS中的自旋会浪费大量的处理器资源(CPU) 简单来说就是太耗费时间

cas就是compare and swap
cas就是不断使用compare比较N和O
一旦compare成功,就操作更改N,然后swap,N设置为V,
不成功,返回N,不断compare

自旋是什么?使用对比的方式认识自旋和阻塞

自旋与阻塞:举个栗子来说,当你开车到了一个十字路口时,这时发现亮的是红灯,那么这时的你就有两种选择,要么将车子直接熄火等待,要么踩住刹车等待;而这时的熄火和刹车就相当于阻塞和自旋,那么我们该如何去选择使用哪种处理方法呢?当又回到刚才的栗子,当你到达十字路口时发现,额的神呀,今天的红灯的等待时间竟然有半个小时,这时你二话不说将车子熄火,自己蒙头大睡来等待红灯;但是当你发现红灯只有10秒钟时,你就会选择踩住刹车来等待红灯;你这里的处理机制就是在不同的情况下,哪种方法使得车子耗油最少就选择哪个方法。又回到主题上,所以不能说自旋就一定会比阻塞的性能好。

小结:自旋和阻塞
自旋是线程失败后没有停止下来,还是在不停的尝试获取同步锁;
阻塞是线程获取锁失败后就停止下来,需要等时间过后或其他线程唤醒(计时阻塞+阻塞)。

自旋问题的处理,jdk使用自适应自旋

为了解决这个问题,CPU就采取了一种处理机制:自适应自旋(根据以往自旋等待时能否获取到锁,来动态调整自旋的时间(循环尝试数量))

自适应自旋和出现其实也是与现实生活相关的,再次回到上个栗子中,如果之前不熄火等到了绿灯,那么这次不熄火的时间就长一点,多等会也没事;如果之前不熄火没等待绿灯, 那么这次不熄火的时间就短一点。自适应自旋也是如此,如果在上一次自旋时获取到锁,则此次自旋时间稍微变长一点;如果在上一次自旋结束还没有获取到锁,此次自旋时间稍微短一点。

面试官问题:解释一下自适应自旋?
如果在上一次自旋时获取到锁,多给一点机会,则此次自旋时间稍微变长一点;
如果在上一次自旋结束还没有获取到锁,少给一点机会,此次自旋时间稍微短一点。

4.5 CAS操作的公平性问题

公平性就可以这样理解:

公平模式 比如一个锁被很多线程等待是时,锁会选择等待时间最长的线程访问它的临界资源,可以和队列类比一下理解为先到先得原则(lock锁)它就是公平的。
非公平模式 而当一个锁是可以被后来的线程抢占时,它就是非公平性的,比如内建锁(饥饿问题:由于访问权限总是分配给了其他线程,而造成一个或多个线程被饿死的现象)。

自旋也是一种不公平的模式:处于阻塞状态的线程无法立刻竞争被释放的锁;而处于自旋状态的线程很有可能先获取到锁。

面试官:谈一谈你对公平锁的理解?cas的自旋操作是公平的吗?
第一,公平锁是没有意义的,强制实现公平锁只会降低效率,非公平锁可以得到更好的效率;这也就是为什么synchronized重量锁是非公平的,cas的自旋操作也是非公平的,lock默认是非公平的,只是可以实现公平锁,所以说,从jdk源码,公平锁只是个可选项,并不是一个默认推荐项。
第二,公平锁的含义:每次执行等待时间最长的那个线程,底层必须使用队列来实现
第三,公平锁的实现:synchronized重量锁是非公平的,cas的自旋操作也是非公平的,lock默认是非公平的,只是可以实现公平锁。
第四,lock是如何使用队列来实现公平锁的:详见另外一篇博客。

五、小结

原理层面:CAS 完成了。

本文地址:https://blog.csdn.net/qq_36963950/article/details/108025879