个子线程分别执行10000次计数器+1操作。
package tech.jarry.learning.netease;
import sun.misc.Unsafe;
import java.lang.reflect.Field;
/**
* @Description:
* @Author: jarry
*/
public class AtomicityWithCAS {
// 建立全局计数器,用于观察CAS原子性特点
volatile int k = 0;
// 定义Unsafe引用对象
private static Unsafe unsafe = null;
// 定义k的内存偏移量(可以理解为k在内存中地址,当然实际与C指针的内存地址是完全不同的)
private static long valueOffset;
static {
try {
// 利用反射获取Unsafe实例对象(正常途径是无法获取的)
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
// 由于unsafe是静态对象,所以传入null。想想也对,毕竟不同的实例对象的非静态对象当然是不同的,当然需要传入实例对象作为参数喽。
// 另外吐槽一句,我查看这段资料的时候,发现百度第一页的各个博客,几乎都是一样的示例代码。。。
unsafe = (Unsafe)field.get(null);
// 获取当前对象中全局计数器k的内存地址偏移
Field kField = AtomicityWithCAS.class.getDeclaredField("k");
kField.setAccessible(true);
valueOffset = unsafe.objectFieldOffset(kField);
} catch (NoSuchFieldException e) {
e.printStackTrace();
} catch (IllegalAccessException e) {
e.printStackTrace();
}
}
/**
* 执行全局计数器k+1的方法
*/
private void add(){
// 当CAS执行失败时,需要重新执行相关操作,直到执行成功。故CAS是一个自旋锁。
while(true) {
// 获取CAS操作所需的旧值
int current = unsafe.getIntVolatile(this,valueOffset);
// 进行CAS操作
if (unsafe.compareAndSwapInt(this,valueOffset,current,current+1)){
// 执行成功,就跳出循环
break;
}
}
}
/**
* 为了体现效果,这里开启了100个线程循环执行add()操作
* @throws InterruptedException
*/
public void run() throws InterruptedException {
for (int j = 0; j < 100; j++){
new Thread(new Runnable() {
@Override
public void run() {
// 每个线程执行10000次add()操作
for (int m = 0; m< 10000; m++){
add();
}
System.out.println(Thread.currentThread().getName()+" has run finished !");
}
}).start();
}
// 当前线程休眠2s,确保所有子线程执行完毕
Thread.sleep(2000);
System.out.println("k: "+k);
}
}
Main主函数
主线程调用同一包下的AtomicityWithNoDeal,AtomicityWithAtomic,AtomicityWithCAS三个类,观察运行效果。
package tech.jarry.learning.netease;
public class Main {
public static void main(String[] args) throws InterruptedException {
// (new AtomicityWithNoDeal()).run();
/**
* 运行结果:
* Thread-1 has run finished !
* 。。。。。。(略98个线程)
* Thread-83 has run finished !
* i: 440239
*/
// (new AtomicityWithAtomic()).run();
/**
* 运行结果:
* Thread-0 has run finished !
* 。。。。。。(略98个线程)
* Thread-69 has run finished !
* atomicInteger: 1000000
*/
// (new AtomicityWithCAS()).run();
/**
* 运行结果:
* Thread-1 has run finished !
* 。。。。。。(略98个线程)
* Thread-80 has run finished !
* k: 1000000
*/
}
}
注释
其实上述代码中,重要的地方,我都写上了相关的注释。如果还有什么不清楚的地方,可以@我。
CAS缺点
CAS当然是有缺点的,否则就没Synchronized什么事情了。
- 从概念及代码示例中可以看出,当CAS操作执行失败时,会继续进入下一个循环执行,直到CAS操作执行成功,这种行为称为自旋。自旋的实现让所有线程都处于高频运行,争抢CPU执行时间的状态。如果操作长时间不成功,会带来很大的CPU资源消耗(所以Java有锁的粗化/升级)。
- CAS仅能针对单个变量进行操作,不能用于多个变量来实现原子操作。
- ABA问题。
正如,我之前所提到的,看待技术问题要找到其特性的最初来源。如第二点中CAS之所以不能支持多个变量的原子操作,是因为CAS操作的原子性来源于硬件的支撑,而硬件只支持单个变量的原子操作,故CAS只能针对单个变量的原子操作进行操作。而有些文章或代码中提到通过CAS执行多个变量的原子操作,其实本质并不是针对多个变量,而是针对这些变量的集合或者总的对象的Reference操作的。这有点抽象,举个栗子。我将通过CAS操作转变了某个数组的引用变量的指向,看起来我实现了整个数组内多个元素转变的原子操作。但实际是我通过改变当前引用变量的指向实现的,CAS的原子操作针对的是这个指向Reference。具体代码可以参照Atomic包中的AtomicIntegerArray与AtomicReference等实现。
至于第一点,细究起来有非常多的内容,如锁的粗化,自旋是否可以优化等。其实CAS的自旋操作实际是确保了一定有CAS操作在执行,但这是通过牺牲CPU实现的。举个栗子,为了能够监听硬件串口返回的消息,我通过while(true)来不断获取串口发送过来的数据,直到我获得了一个完整数据包。
话头收回来,让我们谈谈第三点-ABA问题。
ABA概念
ABA问题,说