Java工程师知识树 / Java基础
无锁策略
对于并发控制而言,锁是一种悲观的策略。如果有多个线程同时需要访问临界区资源,就宁可牺牲性能让线程进行等待,,所以说锁会阻塞线程的执行。而无锁是一种乐观的策略,它会假设对资源的访问是没有冲突的,那自然也不需要等待,所以多个线程都是可以在不停顿的状态下持续执行的。
无锁的策略使用的是一种叫做比较交换的技术(CAS Compare and swap)来鉴别线程冲突的,一旦检测到冲突发生,就重试当前操作直到没有冲突为止。
CAS介绍
CAS(Compare and swap)即比较后(比较内存中的旧值与预期值)交换(将旧值替换成新值)。
CAS原理:在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作初始时读取的值)一样就更新,否则就不更新。
CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。
CAS操作过程:
如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。
上述过程图示为:
模拟CAS操作
package com.study.thead.cas;
public class CASTest {
public static void main(String[] args) {
CASCounter casCounter = new CASCounter();
for (int i = 0; i < 10000; i++) {
new Thread(() -> {
// System.out.println(casCounter.getAndUpdate()+ "----" + casCounter.getV());//在CAS指令之前返回该位置的值
System.out.println(casCounter.incrementAndGet()+ "----" + casCounter.getV());//incrementAndGet返回的是内存中的值(V)
}).start();
}
}
}
//实现一个CAS
class CASCounter {
private volatile long V;// 线程可见的数字 表示内存位置V
public long getV() { //
return V;
}
/**
* 定义一个 compare and swap 用于比较A与V 并赋值B
* @param A 预期原值(A)
* @param B 新值(B)
* @return
*/
private boolean compareAndSwap(long A, long B) {
synchronized (this) { //保证比较时的同步
if (V == A) {//比较A与V
V = B;
return true;
} else {
return false;
}
}
}
// 定义一个获取内存中的值并可以将新值加一的方法
public long getAndUpdate() {// jdk1.8自带方法
long prev; //
long next ;
// do...while语句:首先会执行一次do{}之内的语句,然后在while()内检查条件是否为真,如果条件为真的话,就会重复do...while这个循环,直至while()为假。
do {
prev = this.getV();//1. 获取内存中的值 prev代表预期原值(A)
next = prev + 1; //2. 初始化next值 next代表新值(B)
} while (!compareAndSwap(prev, next)); // 3. 比较内存中的值与预期原值(A) 不一样的话就将next+1
//在CAS指令之前返回该位置的值
return prev;//while内条件为假,即prev = this.getV() 单线程只执行一次,多线程可能会多次
}
// 定义一个获取内存中的值并可以将新值加一的方法
public final long incrementAndGet() {// jdk1.7前通俗易通版写法
long current,next;
while (true){// 一直执行到内存中的值(V)与预期原值(A)相等为止
current = this.getV();//获取内存中的值
next = current + 1;//新值(B)
if (compareAndSwap(current, next))//如果内存中的值(V)与预期原值(A)相等
return next;// 内存中的值(V)与预期原值(A)相等 跳出循环 返回新值(B)
}
}
}
// 执行结果 9999----10000
// 执行结果 10000----10000
CAS的ABA问题
CAS实现原子操作有一个假设:共享变量的当前值与当前线程提供的期望相同,就认为这个变量没有被其他线程修改过。
实际上这个假设不一定总是成立的。比如下图,线程2将V修改为X后再修改为V,表现的情况与假设情况一致。
CAS的ABA问题:共享变量经历了A->B->A的更新。
解决ABA问题的方案:为共享变量引入一个版本号。
JDK中就有个这样的实例:AtomicStampedReference
public class AtomicStampedReference {
private static class Pair { // AtomicStampedReference内部类用于对共享对象添加一个对应的版本号
final T reference;
final int stamp; // 对象对应的版本号
private Pair(T reference, int stamp) {
this.reference = reference;
this.stamp = stamp;
}
static Pair of(T reference, int stamp) {
return new Pair(reference, stamp);
}
}
...
//AtomicStampedReference 使用时需要传入一个版本号
//当带有时间戳的原子操作类AtomicStampedReference对应的数值被修改时,除了更新数据本身外,还必须要更新版本号initialStamp。
public AtomicStampedReference(V initialRef, int initialStamp) {
pair = Pair.of(initialRef, initialStamp);
}
...
//比较时将对象版本号一起传入
public boolean compareAndSet(V expectedReference,
V newReference,
int expectedStamp,
int newStamp) {
Pair current = pair;
return
expectedReference == current.reference &&
expectedStamp == current.stamp &&
((newReference == current.reference &&
newStamp == current.stamp) ||
casPair(current, Pair.of(newReference, newStamp)));
}
...
}
总结:AtomicStampedReference 通过一个键值对Pair存储数据和版本号,在更新时对数据和版本号进行比较,只有两者都符合预期才会调用Unsafe的compareAndSwapObject方法执行数值和版本号替换,也就避免了ABA的问题。
自旋问题
atomic类会多次尝试CAS操作直至成功或失败,这个过程叫做自旋。
通过自旋的过程我们可以看出自旋操作不会将线程挂起,从而避免了内核线程切换,但是自旋的过程也可以看做CPU死循环,会一直占用CPU资源。
这种情形在单CPU的机器上是不能容忍的,因此自旋一般都会有个次数限制,即超过这个次数后线程就会放弃时间片,等待下次机会。
因此自旋操作在资源竞争不激烈的情况下确实能提高效率,但是在资源竞争特别激烈的场景中,CAS操作会的失败率就会大大提高,这时使用中重量级锁的效率可能会更高。当前,也可以使用LongAdder类来替换,它则采用了分段锁的思想来解决并发竞争的问题。
所以,自旋问题的解决方案有两个:1.自旋设定一个次数限制;2.使用分段锁
CAS的底层实现原理
JDK1.8针对CAS操作的相关源码(以java.util.concurrent.atomic.AtomicInteger为例)
public class AtomicInteger extends Number implements java.io.Serializable {
private static final long serialVersionUID = 6214790243416807050L;
// 这个就是封装CAS操作的指针
private static final Unsafe unsafe = Unsafe.getUnsafe();
//原来内部的共享变量,就是这个value,并且使用volatile让其在多个线程之间可见
private volatile int value;
//初始化的构造函数
public AtomicInteger(int initialValue) {
value = initialValue;
}
//获取当前值
public final int get() {
return value;
}
//设置当前的共享变量的值
public final void set(int newValue) {
value = newValue;
}
//使用CAS操作设置新的值,并且返回旧的值
public final int getAndSet(int newValue) {
//使用指针unsafe类的三大原子操作方法之一
return unsafe.getAndSetInt(this, valueOffset, newValue);
}
//把expect与内部的value进行比较,如果相等,那么把value的值设置为update的值
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
//返回value,并把value + 1
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
//自增,并且返回自增后的值
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
}
....
public final int getAndAddInt(Object o, long offset, int delta) {
int v;
do {
v = getIntVolatile(o, offset);
} while (!compareAndSwapInt(o, offset, v, v + delta));
return v;
}
public native int getIntVolatile(Object o, long offset);
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
Java提供了一个Unsafe类,其内部方法操作可以像C的指针一样直接操作内存,方法都是native的。
CAS操作主要是通过Unsafe类调用JNI的代码实现的。(JNI:Java Native Interface为JAVA本地调用,允许java调用其他语言。)
public final native boolean compareAndSwapInt(Object o, long offset,
int expected,
int x);
对应的其他语言的相关代码有:
// 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
}
}
总结:CAS操作主要是通过Java提供的Unsafe类调用Native方法实现的。在把数据更新到主内存时,再次读取主内存变量的值,如果现在变量的值与期望的值(操作初始时读取的值)一样就更新,否则就不更新。变量的值与期望的值的比较操作是通过Native方法实现.