本文目录:
CAS操作(又称为无锁操作)是一种乐观锁策略。它假设所有线程访问共享资源的时候不会出现冲突,因此不会阻塞其他线程的操作。那么,如果出现冲突了怎么办?无锁操作是使用CAS(compare and swap)来鉴别线程是否出现冲突,出现冲突就重试当前操作直到没有冲突为止。
举例说明:
Atomic包中的AtomicInteger类,是通过Unsafe类下的native函数compareAndSwapInt自旋来保证原子性,
其中incrementAndGet函数调用的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;
}
CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改为B,否则什么都不做。
可见只有自旋实现更新数据操作之后,while循环才能够结束。
compareAndSwapInt
函数可知,自旋时间过长会对性能是很大的消耗。Atomic包中原子更新基本类型的工具类:
AtomicBoolean:以原子更新的方式更新boolean;
AtomicInteger:以原子更新的方式更新Integer;
AtomicLong:以原子更新的方式更新Long;
这几个类的用法基本一致,这里以AtomicInteger为例总结常用的方法
原理不再赘述,参考上文compareAndSwapInt
函数。
AtomicInteger使用示例:
public class AtomicExample {
private static AtomicInteger atomicInteger = new AtomicInteger(2);
public static void main(String[] args) {
System.out.println(atomicInteger.getAndIncrement());
System.out.println(atomicInteger.incrementAndGet());
System.out.println(atomicInteger.get());
}
}
// 2 4 4
为了解决自旋导致的性能问题,JDK8在Atomic包中推出了LongAdder类。LongAdder采用的方法是,共享热点数据分离的计数:将一个数字的值拆分为一个数组。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多;要得到这个数字的话,就要把这个值加起来。相比AtomicLong,并发量大大提高。
优点:有很高性能的并发写的能力
缺点:读取的性能不是很高效,而且如果读取的时候出现并发写的话,结果可能不是正确的
Atomic包中提供能原子更新数组中元素的工具类:
AtomicIntegerArray:原子更新整型数组中的元素;
AtomicLongArray:原子更新长整型数组中的元素;
AtomicReferenceArray:原子更新引用类型数组中的元素
这几个类的用法一致,就以AtomicIntegerArray来总结下常用的方法:
AtomicIntegerArray与AtomicInteger的方法基本一致,只不过在前者的方法中会多一个指定数组索引位i。
AtomicIntegerArray使用示例:
public class AtomicExample {
private static int[] value = new int[]{1, 2, 3};
private static AtomicIntegerArray integerArray = new AtomicIntegerArray(value);
public static void main(String[] args) {
//对数组中索引为2的位置的元素加3
int result = integerArray.getAndAdd(2, 3);
System.out.println(integerArray.get(2));
System.out.println(result);
}
}
// 6 3
如果需要原子更新引用类型变量的话,为了保证线程安全,Atomic也提供了相关的类:
AtomicReference使用示例:
public class AtomicExample {
private static AtomicReference reference = new AtomicReference<>();
public static void main(String[] args) {
User user1 = new User("a", 1);
reference.set(user1);
User user2 = new User("b",2);
User user = reference.getAndSet(user2);
System.out.println(user);
System.out.println(reference.get());
}
static class User {
private String userName;
private int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
// User{userName='a', age=1}
// User{userName='b', age=2}
AtomicReferenceFieldUpdater使用示例:
public class AtomicExample {
public static void main(String[] args) {
AtomicReferenceFieldUpdater updater = AtomicReferenceFieldUpdater.newUpdater(Dog.class, String.class, "name");
Dog dog1 = new Dog();
updater.compareAndSet(dog1, dog1.name, "cat");
System.out.println(dog1.name);
}
}
class Dog {
volatile String name = "dog1";
}
如果需要更新对象的某个字段,Atomic同样也提供了相应的原子操作类:
要想使用原子更新字段需要两步操作:
原子更新字段类型类都是抽象类,只能通过静态方法newUpdater来创建一个更新器,并且需要设置想要更新的类和属性;
更新类的属性必须使用public volatile进行修饰;
AtomicIntegerFieldUpdater使用示例:
public class AtomicExample {
private static AtomicIntegerFieldUpdater updater = AtomicIntegerFieldUpdater.newUpdater(User.class, "age");
public static void main(String[] args) {
User user = new User("a", 1);
System.out.println(updater.getAndAdd(user, 5));
System.out.println(updater.addAndGet(user, 1));
System.out.println(updater.get(user));
}
static class User {
private String userName;
public volatile int age;
public User(String userName, int age) {
this.userName = userName;
this.age = age;
}
@Override
public String toString() {
return "User{" +
"userName='" + userName + '\'' +
", age=" + age +
'}';
}
}
}
AtomicStampedReference:原子更新引用类型,这种更新方式会带有版本号,从而解决CAS的ABA问题
AtomicStampedReference使用示例:
public class AtomicExample {
public static void main(String[] args) {
Integer init1 = 1110;
// Integer init2 = 126;
AtomicStampedReference reference = new AtomicStampedReference<>(init1, 1);
int curent1 = reference.getReference();
// Integer current2 = reference.getReference();
reference.compareAndSet(reference.getReference(), reference.getReference() + 1, reference.getStamp(), reference.getStamp() + 1);//正确写法
// reference.compareAndSet(current2, current2+1, reference.getStamp(), reference.getStamp() + 1);//正确写法
// reference.compareAndSet(1110, 1111, reference.getStamp(), reference.getStamp() + 1);//错误写法
// reference.compareAndSet(curent1, curent1+1, reference.getStamp(), reference.getStamp() + 1);//错误写法
// reference.compareAndSet(current2, current2 + 1, reference.getStamp(), reference.getStamp() + 1);
System.out.println("reference.getReference() = " + reference.getReference());
}
}
参考上面的代码,分享一个笔者遇到的一次坑。AtomicStampedReference的compareAndSet
函数中,前两个参数是使用包装类的。所以当参数超过128时,而且传入参数并不是reference.getReference()获取的话,会导致expectedReference == current.reference为false,则无法进行更新。
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)));
}