1. Java中的原子操作
在java中,下列操作是原子操作:
- all assignments of primitive types except for long and double,除long和double的原始类型赋值
- all assignments of references,引用类型赋值
- all operations of java.concurrent.Atomic* classes,Atomic原子类
- all assignments to volatile longs and doubles,加了volatile的long和double类型的赋值
2. CAS(比较与交换,Compare and swap)
是一种有名的无锁算法。CAS 算法的过程是这样:它包含3个参数 CAS(V,E,N)。V(Value) 表示要更新的变量(内存值),E(Expect) 表示预期值(旧的),N 表示新值。当且仅当V值等于E值时,才会将 V 的值设为 N,如果 V 值和 E 值不同,则说明已经有其他线程做了更新,则当前线程什么都不做。最后,CAS返回当前V的真实值。
CAS是项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知这次竞争中失败,并可以再次尝试。
2.1 CAS的开销
前面说过了,CAS(比较并交换)是CPU指令级的操作,只有一步原子操作,所以非常快。而且CAS避免了请求操作系统来裁定锁的问题,不用麻烦操作系统,直接在CPU内部就搞定了。
3. sun.misc.Unsafe
CAS是一条CPU的原子指令(cmpxchg指令),不会造成所谓的数据不一致问题,Unsafe提供的CAS方法(如compareAndSwapXXX)底层实现即为CPU指令cmpxchg。Unsafe类中的CAS方法:
public final native boolean compareAndSwapObject(Object var1, long var2, Object var4, Object var5);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public final native boolean compareAndSwapLong(Object var1, long var2, long var4, long var6);
其他方法可以参考美团的此篇文章
3.1 AtomicInteger 源码解析
使用方式
//初始化,无参构造函数
AtomicInteger atomicCount = new AtomicInteger();
//或有参构造函数
AtomicInteger atomicCount = new AtomicInteger(1);
//调用具体方法
atomicCount.getAndIncrement();//返回当前值并+1
atomicCount.getAndAdd(2);//返回当前值并+2
atomicCount.incrementAndGet();//+1后并返回更新后的值
···省略
初始化
构造函数比较简单,没有多余的逻辑,直接看相关的成员变量和静态代码块:
private static final Unsafe unsafe = Unsafe.getUnsafe();
private static final long valueOffset;
//初始化的时候通过静态代码块给valueOffset赋值
static {
try {
// valueOffset是value这个成员变量在内存中的地址,便于后续通过内存地址直接进行操作。
// valueOffset 其实就是用来定位 value,后续 Unsafe 类可以通过内存地址直接对 value 进行操作。
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
// value用来存储实际值
private volatile int value;
了解了初始化逻辑,再来举个栗子看看getAndIncrement方法的实现:
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
// Unsafe类的getAndAddInt方法
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
// native方法,通过对象 var1 和成员变量相对于对象的内存偏移量 var2 来直接从内存地址中获取成员变量的值
var5 = this.getIntVolatile(var1, var2);
// native方法,CAS逻辑,通过对象 var1 和成员变量的内存偏移量 var2 来定位内存地址。
// 如果内存中的数值是var5,则返回true(跳出do...while),将当前地址的值更新为var5+var4;如果不是var5,则返回false,继续循环
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
可以清晰的看出getAndAddInt方法的逻辑为:
- 根据对象var1和内存偏移地址var2来定位内存地址,获取当前地址值
- 循环通过 CAS 操作更新当前地址值直到更新成功
- 返回旧值
3.2 AtomicInteger线程安全测试
public class AtomicIntegerTest {
private static final int THREAD_COUNTS = 20;
//测试Integer
private static Integer count = 0;
//此处若是去掉synchronized同步,由于count++不是原子操作,会得出错误的结果
private synchronized static void increment() {
count++;
}
//测试AtomicInteger
private static AtomicInteger atomicCount = new AtomicInteger(0);
private static void atomicIncrement() {
atomicCount.getAndIncrement();
}
public static void main(String[] args) throws InterruptedException {
Thread[] threads = new Thread[THREAD_COUNTS];
for (int i = 0; i < THREAD_COUNTS; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
increment();
atomicIncrement();
}
});
threads[i].start();
}
//注意:idea中run启动时,还有一个线程Thread[Monitor Ctrl-Break,5,main];所以要>2;debug启动时正常。
//返回活动线程的当前线程的线程组中的数量。
while (Thread.activeCount() > 2) {
Thread.yield();
}
Thread.currentThread().getThreadGroup().list();
//如果线程安全,则会输出20*1000的数值,即线程*循环次数
System.out.println("count---" + count);
System.out.println("atomicCount---" + atomicCount.get());
}
}
4. CAS的缺点
4.1 自旋消耗资源:循环时间长,开销很大。
从上面的源码可以看到在getAndAddInt方法执行时,如果CAS失败,会一直进行尝试。如果CAS长时间一直不成功,可能会给CPU带来很大的开销。
解决方法:破坏掉for死循环,当超过一定时间或者一定次数时,return退出。可参考JDK8新增的LongAddr,和ConcurrentHashMap采用的方法。
4.2 多变量共享一致性问题
CAS操作是针对一个变量的,如果对多个变量操作,1. 可以加锁来解决。2 .封装成对象类解决。
4.3 ABA问题
CAS检查的时候发现值没有改变,但是实质上它可能已经发生了改变 。可能会造成数据的缺失。
解决方法:同数据乐观锁的方式给它加一个版本号或者时间戳,如AtomicStampedReference。
具体的示例和验证,之后在写 :)