一、前言

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

tip:该程序由《深入理解Java虚拟机》提出,在idea中运行,必须使用debug模式,不能用run运行。

字节码:

我们这里查看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线程安全判断) 保证了线程安全。

二、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操作,并非进行耗时的挂起与唤醒操作,因此非阻塞式同步。

问题1:cas的两个应用?
回答1:cas的两个应用:lock里面 + AtomicInteger里面。

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

2.1.3 Java三种锁

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

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

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

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

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

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

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

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

incrementAndGet()解释:

  1. incrementAndGet()方法由 “一个for + 一个if”组成,
  2. 这个for就是自旋,现在还没有成功自增就循环自旋;
  3. 这个if就是判断成功,current == real ,直接返回next(已经next=current+1,所以直接返回)
  4. 小结:既看到了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的初始化在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中该变量缓存行无效,保证了每次读取都是最新的值

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前缀(tip:就是 LOCK_IF_MP(mp) 代码)。

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

lock前缀的作用说明:

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

总的来说,Atomic(例如:AtomicInteger类)实现了高效无锁(tip:底层还是用到排它锁,就是多处理器下的lock前缀,不过底层处理比java层处理要快很多)与线程安全(volatile变量特性),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

对于程序的解释:

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

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

3.3.1 ABA问题的产生原因(重点)

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

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

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

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

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

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

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

  1. AtomicStampedReference使用int来记录版本号,表示当前数值是否被修改过(在current=real的条件下,版本号相同就是未被修改,版本号不同是已被修改);
  2. 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方法到底是如何实现的,我们深入到源码中看看。

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

连在一起看,如下图:

2、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 AtomicStampedReference类代码解决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。

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

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

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操作,并非进行耗时的挂起与唤醒操作,因此非阻塞式同步。

问题1:cas的两个应用?
回答1:cas的两个应用:lock里面 + AtomicInteger里面。

问题2:为什么lock比synchronized高效?
回答2:在性能上来说,如果竞争资源不激烈,两者的性能是差不多的,而当竞争资源非常激烈时(即有大量线程同时竞争),此时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()方法,

    return i++;三步操作如何原子化,在之前三步就完成了,仅仅只在等待合适的时机返回。
public final int incrementAndGet() {
for (;;) {
int current = get(); // 第一步,读取
int next = current + 1; // 第二步,计算+1,;第三步,赋值给next
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一样,包裹的大了,性能就差了):

public final int incrementAndGet() {
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提上来,所以说 “阻塞同步和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;
}
}
// 小结: cas不参与任何实际业务逻辑,
// cas仅仅返回true|false作为线程安全的判断依据,不参与实际逻辑,和synchronized一样

所有有如下表:

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

问题:为什么将if提上来,可以保证原子性?
回答:保证原子性就是两点:保证加锁才进入临界区、保证走完临界区代码才解锁。

  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前缀(tip:就是 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,仅仅比较数值,无法知道当前获取值是否已被修改过。

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

问题:如何实现比较当前的数值是否被修改过?
回答:第一,记录版本号;第二,用boolean记录当前数值是否被修改过。
具体:JDK的中提供两个类,不仅比较数值,还有比较当前的数值是否被修改过,其中,

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

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

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

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

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

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

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

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

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

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

4.5 CAS操作的公平性问题

问题1:公平模式和非公平模式?
回答1:

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

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

问题2:谈一谈你对公平锁的理解?cas的自旋操作是公平的吗?
回答2:

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

五、小结

原理层面:CAS操作全解析,完成了。

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

【Java并发011】原理层面:CAS操作全解析的更多相关文章

  1. Java并发编程原理与实战五:创建线程的多种方式

    一.继承Thread类 public class Demo1 extends Thread { public Demo1(String name) { super(name); } @Override ...

  2. [Java并发] AQS抽象队列同步器源码解析--锁获取过程

    要深入了解java并发知识,AbstractQueuedSynchronizer(AQS)是必须要拿出来深入学习的,AQS可以说是贯穿了整个JUC并发包,例如ReentrantLock,CountDo ...

  3. [Java并发] AQS抽象队列同步器源码解析--独占锁释放过程

    [Java并发] AQS抽象队列同步器源码解析--独占锁获取过程 上一篇已经讲解了AQS独占锁的获取过程,接下来就是对AQS独占锁的释放过程进行详细的分析说明,废话不多说,直接进入正文... 锁释放入 ...

  4. Java并发编程原理与实战四十三:CAS ---- ABA问题

    CAS(Compare And Swap)导致的ABA问题 问题描述 多线程情况下,每个线程使用CAS操作欲将数据A修改成B,当然我们只希望只有一个线程能够正确的修改数据,并且只修改一次.当并发的时候 ...

  5. Java并发编程原理与实战十九:AQS 剖析

    一.引言在JDK1.5之前,一般是靠synchronized关键字来实现线程对共享变量的互斥访问.synchronized是在字节码上加指令,依赖于底层操作系统的Mutex Lock实现.而从JDK1 ...

  6. Java并发编程原理与实战三十五:并发容器ConcurrentLinkedQueue原理与使用

    一.简介 一个基于链接节点的无界线程安全队列.此队列按照 FIFO(先进先出)原则对元素进行排序.队列的头部 是队列中时间最长的元素.队列的尾部 是队列中时间最短的元素.新的元素插入到队列的尾部,队列 ...

  7. Java并发编程原理与实战四十二:锁与volatile的内存语义

    锁与volatile的内存语义 1.锁的内存语义 2.volatile内存语义 3.synchronized内存语义 4.Lock与synchronized的区别 5.ReentrantLock源码实 ...

  8. Java并发编程原理与实战四十:JDK8新增LongAdder详解

    传统的原子锁AtomicLong/AtomicInt虽然也可以处理大量并发情况下的计数器,但是由于使用了自旋等待,当存在大量竞争时,会存在大量自旋等待,而导致CPU浪费,而有效计算很少,降低了计算效率 ...

  9. Java并发编程原理与实战三十一:Future&FutureTask 浅析

    一.Futrue模式有什么用?------>正所谓技术来源与生活,这里举个栗子.在家里,我们都有煮菜的经验.(如果没有的话,你们还怎样来泡女朋友呢?你懂得).现在女票要你煮四菜一汤,这汤是鸡汤, ...

  10. Java并发编程原理与实战二十:线程安全性问题简单总结

    一.出现线程安全性问题的条件 •在多线程的环境下 •必须有共享资源 •对共享资源进行非原子性操作   二.解决线程安全性问题的途径 •synchronized (偏向锁,轻量级锁,重量级锁) •vol ...

随机推荐

  1. .NET使用StackTrace获取方法调用者信息

    前言 在日常工作中,偶尔需要调查一些诡异的问题,而业务代码经过长时间的演化,很可能已经变得错综复杂,流程.分支众多,如果能在关键方法的日志里添加上调用者的信息,将对定位问题非常有帮助. 介绍 Stac ...

  2. 第六章 部署node运算节点服务

    一.部署Kubelet 1.1 集群规划 主机名 角色 IP hdss7-21 kubelet 10.4.7.21 hdss7-22 kubelet 10.4.7.22 注意:部署以10.4.7.21 ...

  3. JavaScript 之 原型对象、对象原型 —— { }

    JavaScript -- 构造函数 // 构造函数 function Player(name, age) { this.name = name; this.age = age; } JavaScri ...

  4. 使用 Spring Boot Admin 监控应用状态

    程序员优雅哥 SpringBoot 2.7 实战基础 - 11 - 使用 Spring Boot Admin 监控应用状态 1 Spring Boot Actuator Spring Boot Act ...

  5. STL再回顾(非常见知识点)

    目录 为人熟知的pair类型 再谈STL 迭代器的使用 常用的STL容器 顺序容器 vector(向量) 构造方式 拥有的常用的成员函数(java人称方法) string 构造方式 成员函数 dequ ...

  6. Skype for Business server 数据库安装

    之前安装了SFB 2015标准版,但是没有安装归档据库,现在打算重新安装.环境中安装的是默认自带的SQL EXPRESS. 继续安装向导,安装SQL数据库.但是在最后的时候遇到了问题. 安装向导报错 ...

  7. 利用Kafka的Assign模式实现超大群组(10万+)消息推送

    引言 IM即时通信场景下,最重要的一个能力就是推送:在线的直接通过长连接网关服务转发,离线的通过APNS或者极光等系统进行推送.   本文主要是针对在线用户推送场景来进行总结和探讨:如何利用Kafka ...

  8. Django 之模版层

    一.模板简介 将前端页面和Python 的代码分离是一种的开发模式. 为此 Django专门提供了模板系统 (Template System,即模板层)来实现这种模式. Django 的模板 = HT ...

  9. Prometheus 监控 Kubernetes Job 资源误报的坑

    转载自:https://www.qikqiak.com/post/prometheus-monitor-k8s-job-trap/ 昨天在 Prometheus 课程辅导群里面有同学提到一个问题,是关 ...

  10. kubeoperator 使用外部mysql

    1.导出 kubeoperator 的数据库 sql 文件,然后导入到外部mysql 2.正常关闭 kubeoperator 3.关闭 kubeoperator 不会影响已经部署的 k8s 集群 4. ...