六、原子操作CAS
一、什么是原子操作?如何实现原子操作?
CAS:Compare And Swap,比较并且交换。隶属于乐观锁机制。
什么是原子操作?
假设现在有A,B两个操作,如果某个线程执行A操作,当另外一个线程执行B操作的时候,要么这个B全部执行完,要么这个B完全不执行,那么对于A、B来讲,他们彼此就是原子的。
在数据库层面,这种操作就是事务操作,严格意义上来说事务操作也是属于原子操作的一种。
如何实现原子操作
可以利用synchronize关键字,但是会引发一系列问题:
- 1.synchronize是阻塞式的,一个线程拥有锁后,其他的线程必须等待
- 2.等待中的线程优先级很高,但是迟迟拿不到锁怎么办?
- 3.等待中的线程竞争很激烈,但是拿到锁的线程迟迟不释放锁怎么办?
解决办法CAS
CAS可以完美地解决上述的问题,进而更完美地实现原子操作,它利用了现代处理器都支持的CAS指令,这个指令是CPU级别的指令。
CAS包含的要素
1.内存地址v:修改的对象或者变量的内存地址
2.期望值A:
3.新值B
当我去改这个内存地址上所对应的对象或者变量的时候,我期望在我改的时候,这个值是多少,如果是A,我就把他改成B,如果不是A,那我就不能改。将B值替换为A值。
即比较---->交换
。
用java语言来讲,这个操作需要两个语句,一个是比较,一个是交换。
而在CPU层面,只要你执行了这个指令,我可以保证别的指令都被阻塞,只有这一个CAS指令操作完了才允许别的指令进行操作。
在JDK层面来讲,用到了循环(自旋、死循环),直到成功为止,原理如下:
这种思想就是乐观锁。
用一句话来概括CAS如何实现线程安全?
CAS在语言层面不作处理,我们把它交给了CPU和内存,利用CPU的能力实现硬件层面阻塞,进而实现CAS的线程安全。
二、CAS引起的问题
1.ABA问题
下面的两种情况下会出现ABA问题。
1.A最开始的内存地址是X,然后失效了,又分配了B,恰好内存地址是X,这时候通过CAS操作,却设置成功了
这种情况在带有GC的语言中,这种情况是不可能发生的,为什么呢?拿JAVA举例,在执行CAS操作时,A,B对象肯定生命周期内,GC不可能将其释放,那么A指向的内存是不会被释放的,B也就不可能分配到与A相同的内存地址,CAS失败。若在无GC的,A对象已经被释放了,那么B被分配了A的内存,CAS成功。
2.线程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来对记录或对象标记,避免并发操作带来的问题,在Java中,AtomicStampedReference<E>也实现了这个作用,它通过包装[E,Integer]的元组来对对象标记版本戳stamp,从而避免ABA问题,例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,AtomicInteger会成功执行CAS操作,而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败。
package concur.lock;
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<Integer> atomicStampedRef =
new AtomicStampedReference<Integer>(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) {
e.printStackTrace();
}
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) {
e.printStackTrace();
}
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();
System.out.println("before sleep : stamp = " + stamp); // stamp = 0
try {
TimeUnit.SECONDS.sleep(2);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("after sleep : stamp = " + atomicStampedRef.getStamp());//stamp = 1
boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp+1);
System.out.println(c3); //false
}
});
refT1.start();
refT2.start();
}
}
如何解决?
增加版本号,也就是说在每个变量前面都要加一个版本号,每次修改的时候都对其版本+1。其实在大多数开发过程中,我们是不关心ABA问题的。但是ABA问题在一线互联网公司的面试中是经常问到的。
- 1.ABA问题的解决思路是使用版本号,每次变量更新的时候版本号加1,那么A->B->A就会变成1A->2B->3A
- 2.从jdk1.5开始,jdk的Atomic包里就提供了两个类来解决ABA问题,一个是
AtomicStampedReference
,另一个是AtomicMarkableReference
,AtomicStampedReference这个类中的compareAndSet方法的作用就是首先检查当前引用是否等于预期引用,并且检查当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值更新为指定的新值。
AtomicStampedReference
和AtomicMarkableReference
的区别
AtomicStampedReference带了版本号,关心被修改过几次,AtomicMarkableReference只关心有没有人修改过。
2.开销问题
自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果jvm能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:
第一,它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
第二,它可以避免在退出循环的时候因内存顺序冲突(Memory Order Violation)而引起CPU流水线被清空(CPU Pipeline Flush),从而提高CPU的执行效率。
3.只能保证一个变量的原子操作
当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。还有一个方法,就是把多个共享变量合并成一个共享变量来操作。比如,有两个共享变量i=2,j=a合并一下ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference
类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作。
三、原子操作类的使用
jdk中相关原子操作类的使用
- 更新基本类型类:AtomicBoolean,AtomicInteger,AtomicLong
- 更新数组类:AtomicIntegerArray,AtomicLongArray,AtomicReferenceArray
- 更新引用类:AtomicReference,AtomicMarkableReference,AtomicStampeReference
- 原子更新字段类:AtomicReferenceFiledUpdater,AtomicIntegerFiledUpdater,AtomicLongFiledUpdater
举例:
import java.util.concurrent.atomic.AtomicInteger;
/**
*类说明:演示基本类型的原子操作类
*/
public class UseAtomicInt {
static AtomicInteger ai = new AtomicInteger(10);
public static void main(String[] args) {
//返回的是我自增以前的值
int i = ai.getAndIncrement(); // i++
//返回自增以后的值
int b = ai.incrementAndGet();// ++i
System.out.println(i +"------"+ b);
//ai.compareAndSet();
int fianl = ai.addAndGet(24);
System.out.println("加了24之后的值为:"+fianl);
}
}
运行结果:
原子操作类的使用
import java.util.concurrent.atomic.AtomicIntegerArray;
/**
*类说明: 演示原子操作数组
*/
public class AtomicArray {
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
ai.getAndSet(0, 3);
System.out.println(ai.get(0));
System.out.println(value[0]);//原数组不会变化
}
}
运行结果:
原子操作数组
注意:
原子操作只会操作原子类的值,不会操作原数组,原子操作类的值再怎么变也不会影响原数组的值
运用原子操作类修改两个变量的值
import java.util.concurrent.atomic.AtomicReference;
/**
*类说明:演示引用类型的原子操作类
*/
public class UseAtomicReference {
static AtomicReference<UserInfo> atomicUserRef;
public static void main(String[] args) {
UserInfo user = new UserInfo("Mark", 15);//要修改的实体的实例
atomicUserRef = new AtomicReference(user);
UserInfo updateUser = new UserInfo("Bill",17);
atomicUserRef.compareAndSet(user,updateUser);
System.out.println(atomicUserRef.get());
System.out.println(user);
}
//定义一个实体类
static class UserInfo {
private volatile String name;
private int age;
public UserInfo(String name, int age) {
this.name = name;
this.age = age;
}
public String getName() {
return name;
}
public int getAge() {
return age;
}
@Override
public String toString() {
return "UserInfo{" +
"name='" + name + '\'' +
", age=" + age +
'}';
}
}
}
运行结果:
AtomicReference
这是运用AtomicReference修改两个变量的值,本质上是包装成一个变量,对这一个变量进行修改。