Java原子操作类四大数据类型 & 原子操作类CAS原理详解 (一)

java.util.concurrent.atomic(在JUC下的atomic包中)(jdk1.5开始提供)

这个包中的原子操作类提供了一种用法简单、性能高效、线程安全地更新一个变量的方式。

 

Atomic包下的这些类都是采用乐观锁策略CAS(又称为无锁操作)来更新数据的:假设所有线程访问共享资源的时候不会出现冲突,因此不会阻塞其他线程的操作。那么,如果出现冲突了怎么办:出现冲突就重试当前操作直到没有冲突为止。

对CAS最直观的理解:线程安全地更新变量,一般用synchronized,但是太重型、效率低,所以用CAS,它不通过synchronized同步的方式,而是使用一种乐观的策略,即先假设不会冲突直接更新,检测到冲突了再循环解决,直到没有冲突。

CAS的具体操作过程,以AtomicInteger类为例,是通过 Unsafe类下的native函数compareAndSwapInt(对应while()里的方法)自旋来保证原子性:???? 见本文“一、原子更新的基本类型”处代码解释

后面会讲一个 自旋锁:尝试获取锁的线程如果失败,不会阻塞,而是采用循环的方式去尝试获取锁,好处是减少线程上下文切换的消耗,缺点是循环会消耗CPU。

 

CAS的问题

1. 自旋时间过长。由compareAndSwapInt函数可知,自旋时间过长会对性能是很大的消耗。

2. ABA问题。因为CAS会检查旧值有没有变化,这里存在这样一个有意思的问题。比如一个旧值A变为了成B,然后再变成A,刚好在做CAS时检查发现旧值并没有变化依然为A,但是实际上的确发生了变化。解决方案可以添加一个版本号可以解决。原来的变化路径A->B->A就变成了1A->2B->3A,或使用AtomicStampedReference工具类。

AtomicInteger atomicInteger = new AtomicInteger(1);//默认为0
atomicInteger.compareAndSet(1,2);

AtomicStampedReference<Integer> integerAtomicStampedReference = new AtomicStampedReference<Integer>(1,1);
integerAtomicStampedReference.compareAndSet(1,2,1,2);

 

 

因为变量的类型有很多种,所以在Atomic包里也提供了很多类,大致可以属于4种类型的原子更新方式,它们基本都是使用Unsafe实现的包装类,具体类包括:

一、原子更新的基本类型

AtomicBoolean、AtomicInteger、AtomicLong (jdk1.8在atomic包中推出了LongAdder类,为了解决自旋导致的性能问题)(以原子操作的方式更新各种数据类型)

 

//AtomicInteger示例:
AtomicInteger atomicInteger = new AtomicInteger(); //参数什么都不写的话,默认是0
System.out.println(atomicInteger.getAndIncrement());

//那么getAndIncrement是如何实现原子操作的呢?让我们一起分析其实现原理,getAndIncrement的源码如下:
public final int getAndIncrement() {
    return unsafe.getAndAddInt(this, valueOffset, 1); //getAndIncrement()中调用了unsafe类已经封装好了的getAndAddInt(this, valueOffset, 1)方法,所以这里不重要(但是要注意这个方法unsafe.getAndAddInt(this, valueOffset, 1);不是native方法)
}

//该方法首先通过this.getIntVolatile(var1, var2); 获取var1指向的内存地址里var2的值,这里也就是获取旧的值;然后通过CAS的方式更新值为var5+var4,更新失败进入自循。
public final int getAndAddInt(Object var1, long var2, int var4) { 
    int var5;
    do {
        var5 = this.getIntVolatile(var1, var2); //先获取var1指向的内存地址里var2的值,这里也就是获取旧的值
    } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));  //再根据获取的旧值(参数3:var5),通过CAS的方式更新为新值(参数4:var5+var4),更新失败进入自循

    /**
        compareAndSwapInt:

    * 如果当前数值是expected,则原子的将Java变量更新成x
    * @return 如果更新成功则返回true
    这个方法是Unsafe类的方法,是native方法
    */
    //public final native boolean compareAndSwapInt(Object o , long offset , int expected, int x ); 

    return var5;
}

getAndIncrement()是AtomicInteger的final方法,不可被重写;

getAndIncrement()内部调用了Unsafe类的getAndAddInt()方法,也是final的,也不可被重写;

getAndAddInt()内部的do中调用getIntVolatile,while中调用compareAndSwapInt,它俩都是Unsafe类中的native方法

 

LongAdder采用的方法是,共享热点数据分离的计数:将一个数字的值拆分为一个数组。不同线程会命中到数组的不同槽中,各个线程只对自己槽中的那个值进行CAS操作,这样热点就被分散了,冲突的概率就小很多;要得到这个数字的话,就要把这个值加起来。相比AtomicLong,并发量大大提高。

优点:有很高性能的并发写的能力

缺点:读取的性能不是很高效,而且如果读取的时候出现并发写的话,结果可能不是正确的

 

二、原子更新的数组类型

AtomicIntegerArray:原子更新整型数组中的元素;

AtomicLongArray:原子更新长整型数组中的元素;

AtomicReferenceArray:原子更新引用类型数组中的元素

 

//以AtomicIntegerArray为例
static int[] value = new int[] { 1, 2 };
static AtomicIntegerArray ai = new AtomicIntegerArray(value);
public static void main(String[] args) {
        ai.getAndSet(0,3); //0是下标,3是更新值
        System.out.println(ai.get(0));
        System.out.println(value[0]);
}

 输出接结果:

3
1

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

 

另外,关于Java数组初始化问题,再讲下:
1、数组必须指定大小,下面这种直接编译不通过

int[] a = new int[];//编译不通过

正答:

int[] a = new int[3];
or
int[] a = new int[]{1,2,3};
但
int[] a = new int[3]{1,2,3};//编译不通过
or
int[] a = {1,2,3}//长度也就固定为3了
or
int[] a = {}//长度固定为0

 

三、原子更新的引用类型

如果需要原子更新引用类型变量的话,为了保证线程安全,Atomic也提供了相关的类:

1. AtomicReference:原子更新引用类型;

2. AtomicReferenceFieldUpdater:原子更新引用类型里的字段;

3. AtomicMarkableReference:原子更新带有标记位的引用类型;

//以上几个类提供的方法几乎一样,所以此处我们仅以AtomicReference为例进行讲解,AtomicReference的使用示例代码如下:
public static AtomicReference<User> atomicUserRef = new AtomicReference<User>();

public static void main(String[] args) {
    User user = new User("kenan", 15);
    atomicUserRef.set(user);
    User updateUser = new User("qq", 17);

    //这个方法和atomicInteger.getAndIncrement()方法是同级别的
    atomicUserRef.compareAndSet(user, updateUser); //点进去,发现该方法实际依靠了unsafe.compareAndSwapObject方法
    System.out.println(atomicUserRef.get().getName());
    System.out.println(atomicUserRef.get().getOld());
}
static class User {
    private String name;
    private int old;
    public User(String name, int old) {
        this.name = name;
        this.old = old;
    }
    public String getName() {
        return name;
    }
    public int getOld() {
        return old;
    }
}

运行结果:

qq
17

其实现原理是依靠了unsafe.compareAndSwapObject方法:

public final boolean compareAndSet(V expect, V update) {
    return unsafe.compareAndSwapObject(this, valueOffset, expect, update);
}

 

四、原子更新字段类型

如果需要更新对象的某个字段,Atomic同样也提供了相应的原子操作类:

1. AtomicIntegeFieldUpdater:原子更新整型字段类;

2. AtomicLongFieldUpdater:原子更新长整型字段类;

3.AtomicStampedReference:原子更新带有版本号的引用类型。该类将整数值与引用关联起来,可用于原子的更新数据和数据的版本号,可以解决使用CAS进行原子更新时可能出现的ABA问题。()所以有四个参数:期望的旧值,新值,期望的旧版本,新版本)

即atomicStampedReference.compareAndSet(四个参数)

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值