CAS,在硬件层面为并发安全保驾护航

Posted 毛奇志

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了CAS,在硬件层面为并发安全保驾护航相关的知识,希望对你有一定的参考价值。

一、前言

二、CAS操作

2.1 AtomInteger类保证原子性

synchronized和Lock都是先加锁再操作,都是悲观锁,这里学习一个先操作再加锁,这个就是CAS操作,是一种轻量级锁。CAS操作有一个ABA问题,如果共享变量是单向累加,就不存这个问题,如果是有加有减,需要通过Atomic类来处理。

volatile关键字是Java51个关键字中用的比较少的一个,它是一个与多线程并发的关键字,但是实际开发中,一般不会用到,使用synchronize+wait()+notify()/notifyAll()或 lock+await()+signal()/signalAll() 组合就好了,但是我们现在学习JVM的知识,所有有必要知道volatile关键字及其底层原理,这里先上示例:

主代码编译成字节码,如下:

我们这里查看Test.class文件,一步步看每一个指令码,getstatic iconst_1 iadd putstatic 分析了为什么最后结果小于20万的原因,看似是从根本上搞懂了这个问题。

实际上,对于Java程序,即使是打印出.class文件,一条一条分析字节码也是不严谨的(因为是编译出来只有一条字节码指令,也不能说这条指令就是一个原子操作,即一条字节码不一定是一个不可拆分的原子操作),一条字节码指令在解释执行时,解释器将要运行许多行代码才能实现它的语义,如果是编译执行,一条字节码指令也可能转化成若干条本地机器码指令,这里是值得注意的。

问题1:为什么小于20万?
回答1:因为 i++;是一个三步骤操作,不是原子操作,要在多线程中安全,要同时保证原子性、可见性和有序性,即使使用volidate关键字,也只是一个保证可见性和有序性(合理使用volidate禁止指令重排),没保证原子性,三步骤操作可以被打断,处理方式是下面的使用原子类AtomInteger完成。

问题2:为什么使用原子类AtomInteger可以完成?
回答2:synchronized == for + if(cas线程安全判断),在incrementAndGet()方法中使用 for循环 + if(cas判断)包裹,保证线程安全,所以最后等于20000,所以incrementAndGet()方法中的 for + if(cas线程安全判断) 保证了线程安全。

这里的AtomInteger类就是通过CAS操作,在保证原子性的同时,避免了锁的使用,总体性能比Synchronized高很多.

2.2 CAS三步操作

AtomicInteger类compareAndSet()方法通过原子操作实现了CAS操作,最底层基于汇编语言实现。

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

CAS是Compare And Set的一个简称,步骤如下:
① 已知当前内存里面的值current和预期要修改成的值new传入;
② 内存中AtomicInteger对象地址对应的真实值(因为有可能别修改)real与current对比;
③ 相等表示real未被修改过,是安全的,将new赋给real结束然后返回,不相等表示real已经被修改,结束并重新执行1直到修改成功。

如此,CAS相比Synchronized,在保证原子性的同时,避免了锁的使用,总体性能比Synchronized高很多.

在Java语言中,提供了两种锁策略保证线程安全

乐观锁(无锁操作):假设所有线程访问共享资源时不会出现冲突,使用CAS叫做比较交换来判断是否出现冲突,出现冲突就重试当前操作直到不冲突为止。其实Lock的底层也是CAS,后面讲Lock会讲到。

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

两种加锁方式比较如下:

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

问题1:cas的应用?
回答1:cas有两个应用,包括lock加锁保证原子性底层使用的是CAS,AtomicInteger类的incrementAndGet()方法保证 i++ 的原子性底层使用的也是CAS。

问题2:为什么lock比synchronized高效?
回答2:在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时Lock的性能要远远优于synchronized。所以说,在具体使用时要根据适当情况选择。

2.3 AtomicInteger类使用for循环+if(cas)来保证原子性

在2.1的代码中,volatile修饰的变量只能保证多线程情况下的可见性和有序性问题,如果需要保证 i++ 操作的原子性,最终是通过使用AtomicInteger类incrementAndGet()方法来完成最后等于20000的,那么,这个AtomicInteger类incrementAndGet()方法是如何将 i++ 这个三步操作原子化的呢?我们来看一下:

AtomicInteger类incrementAndGet()方法如下:

AtomicInteger类

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

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

从上面可以看到,incrementAndGet()方法由 “一个for + 一个if”组成,这个for就是自旋,现在还没有成功自增就循环自旋,这个if就是判断成功,current == real ,直接返回next(已经next=current+1,所以直接返回)。

incrementAndGet()方法中的if条件代码块中,调用了compareAndSet()方法,这个方法接收current和next两个参数,这个方法就是CAS操作,compareAndSet()方法实现如下:

AtomicInteger类

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

compareAndSet()方法含义:如果当前状态值等于预期值,则以原子方式将同步状态设置为给定的更新值。

这里嵌入解释一下compareAndSwapInt()方法中的这个实参valueOffset变量,首先valueOffset的初始化在static静态代码块里面,表示相对起始内存地址的字节相对偏移量:

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;
  
public AtomicInteger(int initialValue) {
    value = initialValue;
}
  
public AtomicInteger() {
} 

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

所以,现在我们知道,在AtomicInteger类的操作方法incrementAndGet()中,使用自旋代替阻塞(即 for + if(CAS判断当前线程安全) 代替 synchronzied)。进一步,在compareAndSet()方法中,使用CAS来实现这个自旋,就是一定要real==current才返回为true,从而保证安全。

让我们再次回到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);

继续查找unsafe.cpp,找到UNSAFE_ENTRY()方法,实现如下:

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

在UNSAFE_ENTRY()方法内部,调用方法为Atomic::cmpxchg,该方法源码如下:

// 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
  }
}

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

对于lock前缀指令,即对于LOCK_IF_MP(mp)代码,处理如下:

① 如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg);
② 如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果).

对于lock前缀指令,即对于LOCK_IF_MP(mp)代码,其作用在于:

① 禁止该指令与之前和之后的读和写指令重排序;
② 把写缓冲区中的所有数据刷新到内存中.

总的来说,Atomic(例如:AtomicInteger类)实现了高效无锁(底层还是用到排它锁,就是多处理器下的lock前缀,不过底层处理比java层处理要快很多)与线程安全(volatile变量特性),在保证原子性的同时,避免了锁的使用,总体性能比Synchronized高很多.

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

三、CAS的ABA问题

3.1 ABA问题定义

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();
}
}

输出结果:

true 修改后的值:2019

对于程序的解释:

CAS操作步骤一:初始值为100
CAS操作步骤二:线程t1将100改成101,然后又将101改回100
CAS操作步骤三:线程t2先睡眠1秒,等待t1操作完成,然后t2线程将值改成2019,可以看到,线程2修改成功

3.3 ABA问题解决

根本原因:ABA问题产生的原因是传统的CAS仅仅对业务数值的比较,current==real,就是认为没有修改过,这样的比较条件是不充分的。举例:对于 10 -> 11 ->10,仅仅比较数值,无法知道当前获取值是否已被修改过。

知道了ABA问题产生的原因,就知道解决这个问题的突破点:在获取到数值的时候,要找到一个办法知道当前获取到的数值是否已被修改过。

JDK的处理办法:提供两个类,不仅比较数值,还有比较当前的数值是否被修改过,比如:

(1) AtomicStampedReference使用int来记录版本号,表示当前数值是否被修改过(在current=real的条件下,版本号相同就是未被修改,版本号不同是已被修改);
(2) AtomicMarkableReference使用boolean来记录当前数值是否被修改过(在current=real的条件下,false就是未被修改,true是已被修改)。

先看AtomicStampedReference类,我们这里使用AtomicStampedReference的compareAndSet函数,这里面有四个参数:

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

第一个参数expectedReference:表示预期值
第二个参数newReference:表示要更新的值
第三个参数expectedStamp:表示预期的时间戳
第四个参数newStamp:表示要更新的时间戳

这个compareAndSet方法到底是如何实现的,我们深入到源码中看看。

public boolean compareAndSet(V   expectedReference, V   newReference,
                             int expectedStamp, int newStamp) {
    Pair<V> current = pair;
    return expectedReference == current.reference && expectedStamp == current.stamp &&
        ((newReference == current.reference && newStamp == current.stamp) ||
         casPair(current, Pair.of(newReference, newStamp)));
}

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

Pair类

private static class Pair<T> {
    final T reference;
    final int stamp;
    private Pair(T reference, int stamp) {
        this.reference = reference;
        this.stamp = stamp;
    }
    static <T> Pair<T> of(T reference, int stamp) {
        return new Pair<T>(reference, stamp);
    }
}
casPair()方法

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

连在一起看,如下图:

再看另一个AtomicMarkableReference类,其使用boolean来记录当前数值是否被修改过,源码如下:

AtomicMarkableReference类

public boolean compareAndSet(V expectedReference,V newReference,
                             boolean expectedMark,boolean newMark) {
    Pair<V> current = pair;
    return
        expectedReference == current.reference && expectedMark == current.mark &&
        ((newReference == current.reference && newMark == current.mark) ||
         casPair(current, Pair.of(newReference, newMark)));
}
Pair类

private static class Pair<T> {
    final T reference;
    final boolean mark;
    private Pair(T reference, boolean mark) {
        this.reference = reference;
        this.mark = mark;
    }
    static <T> Pair<T> of(T reference, boolean mark) {
        return new Pair<T>(reference, mark);
    }
}
casPair()方法

private boolean casPair(Pair<V> cmp, Pair<V> val) {
    return UNSAFE.compareAndSwapObject(this, pairOffset, cmp, val);
}

连在一起看,如下图:

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

3.4.1 AtomicStampedReference类代码解决ABA问题

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

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

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.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();
    }
}

输出结果:

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

对于这个程序输出的解释:
输出1:初始值100,初始版本号1
输出2:线程t1和t2拿到一样的初始版本号
输出3:线程t1完成ABA操作,版本号递增到3
输出4:线程t2完成CAS操作,最新版本号已经变成3,跟线程t2之前拿到的版本号1不相等,操作失败

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

AtomicStampedReference可以给引用加上版本号,追踪引用的整个变化过程,如:A -> B -> C -> D - > A,通过AtomicStampedReference,我们可以知道,引用变量中途被更改了3次。

但是,有时候,我们并不关心引用变量更改了几次,只是单纯的关心是否更改过,所以就有了AtomicMarkableReference类,且看下文。

3.4.2 AtomicMarkableReference类代码解决ABA问题

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

import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicMarkableReference;

public class ABADemo2 {

    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();
    }
}

输出结果:

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

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

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

四、面试金手指

4.1 CAS操作

问题:CAS操作是什么?它和synchronized有什么不同?
回答:CAS是Compare And Set的一个简称,步骤如下:
① 已知当前内存里面的值current和预期要修改成的值new传入;
② 内存中AtomicInteger对象地址对应的真实值(因为有可能别修改)real与current对比;
③ 相等表示real未被修改过,是安全的,将new赋给real结束然后返回,不相等表示real已经被修改,结束并重新执行1直到修改成功。

如此,CAS相比Synchronized,在保证原子性的同时,避免了锁的使用,总体性能比Synchronized高很多,CAS与synchronized操作区别如下:

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

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

CAS的最大经典应用就是AtomicInteger类中的compareAndSet()方法使用for+if(cas)保证线程安全。

问题:CAS操作如何保证线程安全?
回答:AtomicInteger类中的compareAndSet()方法使用for+if(cas)保证线程安全。
解释:在Java的原子类,例如AtomicInteger类中,就有用到CAS操作,比如AtomicInteger类中的compareAndSet()方法,实现如下:

return i++;三步操作如何原子化,在之前三步就完成了,仅仅只在等待合适的时机返回。
public final int incrementAndGet() {
    for (;;) {
        int current = get();  // 第一步,读取
        int next = current + 1;   // 第二步,计算+1,;第三步,赋值给next
        if (compareAndSet(current, next))   // 三步都完成了,仅仅只在等待合适的时机返回。
            return next;
    }
}

问题:这个incrementAndGet()是如何在不使用synchronized的阻塞同步的前提下,完成多线程情况下,线程安全的自增操作?即 i++ 的硬件操作包括三步:从内存取出数据到寄存器,运算器中i++,从运算器中将数据放到内存中,既然不使用synchronized阻塞同步,那么这个AtomicInterger类中的incrementAndGet()方法如何在多线程情况下保证操作的原子性呢?

回答:synchronzied == for自旋 + if(CAS判断当前线程安全),使用for自旋,if中使用CAS判断当前线程安全,等到线程安全的情况下,返回next(已经next=current+1,所以直接返回),从而实现原子性。

synchronzied == for自旋 +if(CAS判断当前线程安全) 中,将if判断提上来,我们就可以知道很多东西(源码中不把if提上来是因为把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;
    }
}

问题:为什么将if提上来,可以保证原子性?
回答:保证原子性就是两点:保证加锁才进入临界区、保证走完临界区代码才解锁。
(1) 一定要线程安全才能进来: cas和synchronized都可以保证,cas一定要current==real才能进来,synchronized一定要获取到同步锁才能进来;
(2) 里面的不出去,外面的不能进来:cas和synchronized都可以保证,cas最后一句是是return next;,可以保证里面的 next = current + 1; 未执行完,就无法执行 return next; ,外面的就无法进来;synchronized也可以保证这一点,里面的不执行完不释放锁,外面的进不来。
因此,所有的cas包裹的都可以改写为synchronized包裹的,所有的synchronized包裹的都可以改写为cas包裹的。

注意: cas不参与任何实际业务逻辑,cas仅仅返回true|false,作为线程安全的判断依据,不参与实际逻辑,和synchronized一样。

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

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

对于lock前缀指令,即对于LOCK_IF_MP(mp)代码,处理如下:

① 如果程序是在多处理器上运行,就为cmpxchg指令加上lock前缀(lock cmpxchg);
② 如果程序是在单处理器上运行,就省略lock前缀(单处理器自身会维护单处理器内的顺序一致性,不需要lock前缀提供的内存屏障效果).

对于lock前缀指令,即对于LOCK_IF_MP(mp)代码,其作用在于:

① 禁止该指令与之前和之后的读和写指令重排序;
② 把写缓冲区中的所有数据刷新到内存中.

4.4 自旋、阻塞、自适应自旋

问题1:CAS是什么?
回答1:cas就是compare and swap,cas就是不断使用compare比较N和O, 一旦compare成功,就操作更改N,然后swap,N设置为V,不成功,返回N,不断compare。

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

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

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

4.5 CAS操作的公平性问题

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

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

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

五、尾声

CAS,在硬件层面为并发安全保驾护航,完成了。

天天打码,天天进步!!!

以上是关于CAS,在硬件层面为并发安全保驾护航的主要内容,如果未能解决你的问题,请参考以下文章

20款安全测试工具为程序安全保驾护航

9大方法为云安全保驾护航

腾讯蔡晨:十年沉淀,腾讯iOA为企业安全保驾护航

Office 365 安全和合规性管理平台为企业数据安全管理保驾护航

解锁黑科技!辅助驾驶系统为卡车安全行驶保驾护航

京麦开放平台:高性能API网关,为高并发商业网站保驾护航