Java原子类中CAS详解
作者:互联网
Java原子类中CAS详解
在Java中使用volatile关键字不保证操作的原子性从而在多线程环境下会出现问题,解决方法可以使用琐机制使用synchronized和lock进行加锁但是效率极低一般不使用这种方式解决原子性问题,在Java中的java.util.concurrent.atomic 包下有各种数据类型的原子类,使用原子类型来解决原子性问题最为高效
public class Atomicity {
private static int count = 0;
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
count++;
}
}).start();
}
//Java中至少有两个线程 main 线程和 gc 线程
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(count);
}
}
测试结果:
很显然没有使用原子类出现了原子性问题
使用原子类
public class Atomicity {
private static AtomicInteger count = new AtomicInteger(0);
public static void main(String[] args) {
for (int i = 0; i < 100; i++) {
new Thread(()->{
for (int j = 0; j < 1000; j++) {
count.incrementAndGet();
}
}).start();
}
//Java中至少有两个线程 main 线程和 gc 线程
while(Thread.activeCount()>2){
Thread.yield();
}
System.out.println(count);
}
}
测试结果:
没有出现原子性问题
原子类保证原子性是因为底层使用CAS机制,根本原因就是硬件底层执行了一条cpu指令仅凭该指令可以完成"比较并交换"一系列操作
compare and swap,解决多线程并行情况下使用锁造成性能损耗的一种机制,CAS操作包含三个操作数——内存位置(V)、预期原值(A)和新值(B)。如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值。否则,处理器不做任何操作。无论哪种情况,它都会在CAS指令之前返回该位置的值。CAS有效地说明了“我认为位置V应该包含值A;如果包含该值,则将B放到这个位置;否则,不要更改该位置,只告诉我这个位置现在的值即可。 ——《百度百科》
incrementAndGet()方法源码:
/**
* Atomically increments by one the current value.
*
* @return the updated value
*/
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
从源码可以看出,方法内又使用了unsafe对象的getAndAddInt方法问题来了这个unsafe对象又是什么?
sun.misc 下的Unsafe类 Java不能直接管理内存而Unsafe类可以帮助我们和C++那样来访问内存,而且Unsafe类中大多都是native方法
Unsafe类中getAndAddInt方法源码:
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
这就是原子类保证原子操作的核心代码了
首先do while循环很明显是自旋锁
然后compareAdnSwapInt是一个native方法,也就是它执行了一条cpu指令 该指令可以完成比较并且进行值交换操作,完成值的更新
unsafe.getAndAddInt(this, valueOffset, 1)
public final int getAndAddInt(Object var1, long var2, int var4)
var1代表当前的原子类对象->this对象
var2表示内存地址偏移量->valueOffset
var4则表示要增加的值 ->1
var5 = this.getIntVolatile(var1, var2)
根据传入的内存地址值 和偏移量得到原值
this.compareAndSwapInt(var1, var2, var5, var5 + var4)
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
本地方法通过c++调用底层硬件,该方法也就执行了一条指令该指令完成了"比较和交换一系列操作"
Object var1,就是传入的this对象 ->this
long var2就是内存地址偏移量->valueOffset
int var4就是内存原值->var5
int var5就是修改后的值->var5+1
ABA问题
CAS中会出现ABA问题,比如要修改A为B 但是中间进行了先将A修改为C然后又将C改回了A,最后进行A修改为B的操作的时候不知道进行了AC操作,这个未知的操作可能隐藏着问题。
解决ABA问题,使用到了乐观锁思想,每次修改就加上一个版本号做一个标记
在jdk 1.5之后为了解决ABA问题引入了AtomicStampedReferenc来解决ABA问题
示例演示了AtomicStampedReferenc可以解决ABA问题
public class ABA {
public static void main(String[] args) throws InterruptedException {
// 有参构造 传入初始值和初始的版本号
AtomicStampedReference<Integer> countStamped = new AtomicStampedReference<>(1, 1);
new Thread(()->{
int stamp = countStamped.getStamp();
System.out.println(Thread.currentThread().getName()+"------》期望的版本号"+stamp);
//获取到期望的版本号之后让正常线程休眠 让捣乱线程先去捣乱 修改版本号
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+"------》实际的版本号"+countStamped.getStamp());
//由于捣乱线程先执行 导致版本号增加 与期望的版本号不一致导致修改失败
System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(1,6,stamp,stamp+1));
},"正常线程").start();
//先让 正常线程获取到期望的版本号
TimeUnit.SECONDS.sleep(1);
new Thread(()->{
//捣乱线程 先将1->2 又将2->1
System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(1,2,countStamped.getStamp(),countStamped.getStamp()+1));
System.out.println(Thread.currentThread().getName()+"执行结果:"+countStamped.compareAndSet(2,1,countStamped.getStamp(),countStamped.getStamp()+1));
},"捣乱线程").start();
}
}
测试结果:
显而易见正常线程执行失败 尽管值是我们所期望的 但是由于版本号不是我们期望的也会导致修改失败 从而解决了ABA问题
标签:var5,Java,Thread,CAS,countStamped,版本号,int,线程,类中 来源: https://blog.csdn.net/weixin_43621226/article/details/113702317