Java并发编程五原子类操作

10 篇文章 1 订阅


Java并发编程一:并发基础必知
Java并发编程二:Java中线程
Java并发编程三:volatile使用
Java并发编程四:synchronized和lock

原子类

当程序更新一个变量时,如果多线程更新会产生期望之外的值,比如在进行i++,虽然volatile可以保证可见性,但是无法保证原子性,即使使用synchronized可以保证多线程下不会出现误差,但是会阻塞线程、性能较低。1.5之后,jdk提供了Atomic包,这个包中的原子类提供了一种简单高效、线程安全的更新变量,那么synchronized是不是就不用了呢,其实不然,后续会说原子类的缺点。

原子更新基本类型

使用原子的方式更新基本类型,Atomic提供了三个:AtomicBoolean原子更新布尔类型,AtomicInteger原子更新整数类型,AtomicLong原子更新长整型。
常用方法如下:
int addAndGet(int delta):以原子方式将输入的数值与实例中的值(AtomicInteger里的
value)相加,并返回结果。
boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方
式将该值设置为输入的值。
void lazySet(int newValue):最终会设置成newValue,使用lazySet设置值后,可能导致其他
线程在之后的一小段时间内还是可以读到旧的值。
int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值。
int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。
int getAndDecrement( ):以原子方式将当前值减1,注意,这里返回的是自减前的值。
int incrementAndGet():以原子方式将当前值加1,返回的是自增后的值。
int decrementAndGet():以原子方式将当前值减1,返回的是自减后的值。

 public static void main(String[] args) {
        // 定义一个原子整数类初始值为0
        AtomicInteger atomicInteger = new AtomicInteger(0);
        // 开启十个线程 每个线程执行一百次
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                for (int j = 0; j <100 ; j++) {
                    atomicInteger.incrementAndGet();
                }
            }).start();
        }
        // 等待线程执行完
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        // 输出1000
        System.out.println(atomicInteger.get());
    }

原子更新数组类型

通过原子的方式更新数组里的某个元素,AtomicIntegerArray:原子更新整型数组里的元素。AtomicLongArray:原子更新长整型数组里的元素。AtomicReferenceArray:原子更新引用类型数组里的元素。

/ 定义一个原子整数数组 初始值为0
        AtomicIntegerArray atomicInteger = new AtomicIntegerArray(new int[]{0,0,0});
        // 开启十个线程 每个线程执行一百次
        for (int i = 0; i <10 ; i++) {
            new Thread(()->{
                for (int j = 0; j <100 ; j++) {
                    // 计算数组位置 自增1
                    atomicInteger.incrementAndGet(0);
                    atomicInteger.incrementAndGet(1);
                    atomicInteger.incrementAndGet(2);
                }
            }).start();
        }
        // 等待线程执行完
        while (Thread.activeCount()>2){
            Thread.yield();
        }
        // 输出 [1000,1000,1000]
        System.out.println(atomicInteger.toString());

注意的是,数组通过构造方法传递进去,然后AtomicIntegerArray会将当前数组复制一份,所以当AtomicIntegerArray对内部的数组元素进行修改时,不会影响传入的数组。

 int[] value = {1, 2};
        AtomicIntegerArray ai = new AtomicIntegerArray(value);
            ai.getAndSet(0, 3);
            System.out.println(ai.get(0));
            System.out.println(value[0]);
        }

运行结果:
3
1

原子更新引用类型

通过原子方式更新引用类型。AtomicReference:原子更新引用类型;AtomicReferenceFieldUpdater:原子更新引用类型里的字段;AtomicMarkableReference:原子更新带有标记位的引用类型。

public static void main(String[] args) {
        // 定义一个引用类型原子类
        AtomicReference<User> reference = new AtomicReference<>();
        // 定义个user 传入姓名和年龄
        User user = new User("zhangsan", 10);
        // 放入原子类中
        reference.set(user);
        // 定义个更新的user
        User updateUser = new User("lisi", 20);
        // 原子性更新user
        reference.compareAndSet(user,updateUser);
        // 输出 list
        System.out.println(reference.get().getName());
        // 输出 20
        System.out.println(reference.get().getAge());
    }

原子更新字段类

通过原子方式更新引用类型的字段类型。AtomicIntegerFieldUpdater:原子更新整型的字段的更新器;AtomicLongFieldUpdater:原子更新长整型字段的更新器;AtomicStampedReference:原子更新带有版本号的引用类型,该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题(下面有说)。

  // 定义一个引用字段类型原子类 更新user中的年龄
        AtomicIntegerFieldUpdater<User> atomicIntegerFieldUpdater= AtomicIntegerFieldUpdater.newUpdater(User.class,"age");
        // 定义个user 传入姓名和年龄
        User user = new User("zhangsan", 10);
        // 年龄自增
        int age = atomicIntegerFieldUpdater.incrementAndGet(user);
        // 输出 11
        System.out.println(age);

代码注释都很明确,不明白的可以自己动手敲一敲很简单。

原子类更新原理

就拿AtomicInteger为例子,每个类的方法都差不多,原理也基本一样,都是通过Unsafe来实现的,了解Unsafe之前得需要知道CAS。CAS全称是Compare And Swap 即比较并交换,使用乐观锁机制,包含三个操作数 —— 内存位置(V)、预期原值(A)和新值(B), 如果内存位置的值与预期原值相匹配,那么才会将该位置值更新为新值 。否则,处理器不做任何操作。Unsafe就是使用这种方法来操作原子性,我们不能直接使用Unsafe,它是由bootstrap启动类加载器加载的,获取unsafe,unsafe存放就是内存位置的值(V)valueOffset存放的是在AtomicInteger中偏移量也就是预期原值,value使用volatile修饰保证内存的可见性,也就是我们get方法返回的值。
在这里插入图片描述

就拿比如incrementAndGet方法,调用getAndInt方法来进行自增。在这里插入图片描述
实际上调用Unsafe中的方法,但是compareAndSwapInt方法已经被内置了,在1.7写的while(true)对内存值+1,而后使用cas修改变量的值,如果失败重试,直到成功。有兴趣的可以看下。
在这里插入图片描述
在这里插入图片描述

原子类的缺点

虽然原子在使用上简单,并且高效的解决了原子问题,但是还是有三个缺点。
1.循环时间长开销大。如果自旋CAS长时间不成功,会给CPU带来很大的开销。
2.只能保证一个共享变量的原子性操作。当对一个共享变量我们可以使用原子类,但是对于多个就无法保证了,这个时候就需要用锁来保证多个共享变量的原子性。
3.ABA问题。因为CAS在操作变量的时候,需要检查值有没有改变,如果没有发生改变则更新,但是如果一个值从A变成了B,又从B变成了A,那么使用CAS在进行检查的时候发现没有改变,但是实际上是变了的。再比如你的支付宝里有100(A),当你进行转账的时候转了50(B),由于网络延迟或者系统故障,没有及时到账,你再次转账这样相当于开了两个线程,这时候第一个线程成功了,OK转账成功,假如这时候有人给你转了50元,这时候你的余额又成了100(A),此时第二个线程就会发现余额还是100,它就会再次发生转账。解决方案也简单,在变量前追加版本号或者时间戳,每次变量更新都去更新版本号,比如A->B->A变成1A->2B->3A,JDK为我们提供了这种机制的更新AtomicStampedReference,还可以使用互斥锁来解决ABA问题。

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值