在并发编程中很容易出现并发安全问题, 最简单的例子就是多线程更新变量 i=1, 多个线程执行 i++ 操作, 就有可能获取不到正确的值, 而这个问题, 最常用的方法是通过 Synchronized 进行控制来达到线程安全的目的. 但是由于 synchronized 是采用的是悲观锁策略, 并不是特别高效的一种解决方案. 实际上, 在 J.U.C 下的 Atomic 包提供了一系列的操作简单, 性能高效, 并能保证线程安全的类去更新多种类型. Atomic 包下的这些类都是采用乐观锁策略 CAS 来更新数据.
CAS 原理与问题
CAS 操作 (又称为无锁操作) 是一种乐观锁策略. 它假设所有线程访问共享资源的时候不会出现冲突, 因此不会阻塞其他线程的操作. 那么, 如果出现冲突了怎么办? 无锁操作是使用 CAS(compare and swap)来鉴别线程是否出现冲突, 出现冲突就重试当前操作直到没有冲突为止.
CAS 的操作过程
举例说明:
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 循环才能够结束.
CAS 的问题
自旋时间过长. 由 compareAndSwapInt 函数可知, 自旋时间过长会对性能是很大的消耗.
ABA 问题. 因为 CAS 会检查旧值有没有变化, 这里存在这样一个有意思的问题. 比如一个旧值 A 变为了成 B, 然后再变成 A, 刚好在做 CAS 时检查发现旧值并没有变化依然为 A, 但是实际上的确发生了变化. 解决方案可以添加一个版本号可以解决. 原来的变化路径 A->B->A 就变成了 1A->2B->3C, 或使用 AtomicStampedReference 工具类.
Atomic 包的使用
原子更新基本类型
Atomic 包中原子更新基本类型的工具类:
AtomicBoolean: 以原子更新的方式更新 boolean;
AtomicInteger: 以原子更新的方式更新 Integer;
AtomicLong: 以原子更新的方式更新 Long;
这几个类的用法基本一致, 这里以 AtomicInteger 为例总结常用的方法
addAndGet(int delta): 以原子方式将输入的数值与实例中原本的值相加, 并返回最后的结果;
incrementAndGet() : 以原子的方式将实例中的原值进行加 1 操作, 并返回最终相加后的结果;
getAndSet(int newValue): 将实例中的值更新为新值, 并返回旧值;
getAndIncrement(): 以原子的方式将实例中的原值加 1, 返回的是自增前的旧值;
原理不再赘述, 参考上文 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
- LongAdder
为了解决自旋导致的性能问题, JDK8 在 Atomic 包中推出了 LongAdder 类. LongAdder 采用的方法是, 共享热点数据分离的计数: 将一个数字的值拆分为一个数组. 不同线程会命中到数组的不同槽中, 各个线程只对自己槽中的那个值进行 CAS 操作, 这样热点就被分散了, 冲突的概率就小很多; 要得到这个数字的话, 就要把这个值加起来. 相比 AtomicLong, 并发量大大提高.
优点: 有很高性能的并发写的能力
缺点: 读取的性能不是很高效, 而且如果读取的时候出现并发写的话, 结果可能不是正确的
原子更新数组类型
Atomic 包中提供能原子更新数组中元素的工具类:
AtomicIntegerArray: 原子更新整型数组中的元素;
AtomicLongArray: 原子更新长整型数组中的元素;
AtomicReferenceArray: 原子更新引用类型数组中的元素
这几个类的用法一致, 就以 AtomicIntegerArray 来总结下常用的方法:
addAndGet(int i, int delta): 以原子更新的方式将数组中索引为 i 的元素与输入值相加;
getAndIncrement(int i): 以原子更新的方式将数组中索引为 i 的元素自增加 1;
compareAndSet(int i, int expect, int update): 将数组中索引为 i 的位置的元素进行更新
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: 原子更新引用类型;
AtomicReferenceFieldUpdater: 原子更新引用类型里的字段;
AtomicMarkableReference: 原子更新带有标记位的引用类型;
AtomicReference 使用示例:
- public class AtomicExample {
- private static AtomicReference<User> 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 同样也提供了相应的原子操作类:
AtomicIntegeFieldUpdater: 原子更新整型字段类;
AtomicLongFieldUpdater: 原子更新长整型字段类;
要想使用原子更新字段需要两步操作:
原子更新字段类型类都是抽象类, 只能通过静态方法 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 +
- '}';
- }
- }
- }
解决 CAS 的 ABA 问题
AtomicStampedReference: 原子更新引用类型, 这种更新方式会带有版本号, 从而解决 CAS 的 ABA 问题
AtomicStampedReference 使用示例:
- public class AtomicExample {
- public static void main(String[] args) {
- Integer init1 = 1110;
- // Integer init2 = 126;
- AtomicStampedReference<Integer> 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 踩过的坑
参考上面的代码, 分享一个笔者遇到的一次坑. AtomicStampedReference 的 compareAndSet 函数中, 前两个参数是使用包装类的. 所以当参数超过 128 时, 而且传入参数并不是 reference.getReference()获取的话, 会导致 expectedReference == current.reference 为 false, 则无法进行更新.
- public boolean compareAndSet(V expectedReference,
- V newReference,
- int expectedStamp,
- int newStamp) {
- Pair<V> current = pair;
- return
- expectedReference == current.reference &&
- expectedStamp == current.stamp &&
- ((newReference == current.reference &&
- newStamp == current.stamp) ||
- casPair(current, Pair.of(newReference, newStamp)));
- }
最后, 限于笔者经验水平有限, 欢迎读者就文中的观点提出宝贵的建议和意见. 如果想获得更多的学习资源或者想和更多的是技术爱好者一起交流, 可以关注我的公众号『全菜工程师小辉』后台回复关键词领取学习资料, 进入前后端技术交流群和程序员副业群. 同时也可以加入程序员副业群 Q 群: 735764906 一起交流.
来源: https://www.cnblogs.com/mseddl/p/11541248.html