Java原子类

引言

Java的原子类涉及到了多线程,一般情况下遇到多线程考虑的第一个问题就是如何保证共享数据的安全性。java的synchronized还是锁以及其他的线程安全的数据结构,其实都是为了解决这个问题。原子类也是为了解决这个问题

java提供了无锁的基于高级机器指令的安全的类,以实现数据操作的原子性,这就是Java并发包中的原子类。(java.util.concurrent.atomic)

相比较于传统的互斥锁,无锁不存在加锁、解锁、线程切换的消耗,无锁的性能比较好,同时也能保证线程的安全性。

作为原子类,最重要的就是它的原子性。原子操作,原子操作是指不会被线程调度机制打断的操作,这种操作一旦开始,就一直运行到结束,中间不会有任何线程上下文切换。原子操作可以是一个步骤,也可以是多个操作步骤,但是其顺序不可以被打乱,也不可以被切割而只执行其中的一部分,将整个操作视作一个整体是原子性的核心特征。

无锁实现CAS

原子类之所以可以在无锁的情况下实现原子性,原因很简单,因为CAS(compare and swap 即比较并交换)。CAS是CPU提供的一条为了解决并发问题的指令。

这条指令有三个参数:共享变量的内存地址 A、用于比较的值 B 和共享变量的新值 C。只有当内存中地址A的值等于B,才能把地址A的值变成C,然后返回true,否则,内存中地址A的值不等于B,返回false.

也就是只有预期值B等于内存地址A中的值时,表明共享变量没有被其他线程修改过,才允许更新新值,这样就保证了原子性!

AtomicInteger atomic = new AtomicInteger(23);
int expect = 34;
int update = 45;
boolean flag = atomic.compareAndSet(expect,update);
System.out.println(flag);

我们来具体看一下compareAndSet方法


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

compareAndSet方法是通过调用Unsafe.compareAndSwapInt()方法实现,

Unsafe不是java并发包中类,而是java中的魔法类sun.misc.Unsafe

Unsafe为我们提供了访问底层的机制,这种机制仅供java核心类库使用,而不能被普通用户使用。

public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

这是一个native方法,底层是使用C/C++写的,主要是调用CPU的CAS指令来实现。这涉及到了java的底层机制,实际上java就是在不同的平台上调用不同的native方法实现对操作系统的访问的,这其实就体现了Java的与平台无关的特性。

现在再来看一下刚调用函数的四个实参,分别为:

(1)this:操作的对象;
(2)valueOffset:对象中字段的偏移量;
(3)expect:原来的值,即期望的值;
(4)update更新值;

重点看一下第二个参数valueOffset

static {
        try {
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
private volatile int value;

成员变量value是被volatile修饰的,value变量存储的就是这个对象具体的数值.volatile虽然不能保证原子性,但是可以保证一致性,即只要有线程对它修饰的数据进行了修改,其他线程读取这个数据的时候读取就是修改后的数据.

期望值有,更新值有,那么内存中的值,即value值怎么获取?根据当前的对象的引用定位具体的对象在内存中的位置,然后根据属性的偏移量定位到具体的属性值。不同的AtomicInteger类对象在内存中的位置虽然是不同的,但是"value"属性相对于对象地址的偏移量是一定的,所以就可以获取到value的值。

缺陷

ABA

CAS实现原子操作时有一个很严重的问题ABA,就是当你比较的时候,可能你的值被一个线程改了,然后另一个线程又改了回来,这样当你比较的时候发现和预期值一样,但其实是被改过的.

大部分情况下不需要关心这个,例如基本类型等,但是引用类型如果遇到ABA,前后的属性可能发生变化,就需要解决这个问题。

解决方法:在变量前加版本号,每次变量更新时将版本号加1,A -> B -> A,就变成 1A -> 2B -> 3A。

只能对单个共享变量操作

CAS只能对单个共享变量如是操作,对多个共享变量操作时则无法保证原子性,此时可以用锁。
将多个共享变量合成一个共享变量来操作。比如a=2,b=t,合并起来ab=2t,然后用CAS操作ab.JDK5提供AtomicReference保证引用对象间的原子性,它可将多个变量放在一个对象中来进行CAS操作。

原子类

java.util.concurrent.atomic包中有12个原子操作类,JDK1.8新增了4个类。
在这里插入图片描述

这12个类可以根据操作的数据类型可以分为4种类型:

原子更新基本类型

使用原子的方式更新基本类型

  • AtomicBoolean: 原子更新布尔类型。
  • AtomicInteger: 原子更新整型。
  • AtomicLong: 原子更新长整型。

原子更新数组类型

通过原子的方式更新数组里的某个元素

  • AtomicIntegerArray: 原子更新整型数组里的元素。
  • AtomicLongArray: 原子更新长整型数组里的元素。
  • AtomicReferenceArray: 原子更新引用类型数组里的元素。

原子更新引用类型

原子更新基本类型的AtomicInteger,只能更新一个值,如果更新多个值,比如更新一个对象里的值,那么就要用原子更新引用类型提供的类

  • AtomicReference: 原子更新引用类型。
  • AtomicStampedReference: 原子更新带有版本号的引用类型。
  • AtomicMarkableReferce: 原子更新带有标记位的引用类型。

原子更新字段类

如果需要原子的更新类里某个字段时,需要用到原子更新字段类。注意更新类的字段(属性)必须使用public volatile修饰符,这样才能保证可见性。

  • AtomicIntegerFieldUpdater: 原子更新整型的字段的更新器。
  • AtomicLongFieldUpdater: 原子更新长整型字段的更新器。
  • AtomicReferenceFieldUpdater: 原子更新引用类型的字段。

原子累加器

相对于原子更新基本类型它们的性能更好些,代价是消耗更多的内存空间。但是他们无法替代原子更新基本类型。

  • LongAccumulator
  • LongAdder
  • DoubleAccumulator
  • DoubleAdder

在并发程度较高时,AtomicLong使用的CAS操作失败频率也较高,且多了很多不必要的资源消耗,导致性能下降。考虑到AtomicLong中CAS竞争的资源单一,选择在有冲突时分散竞争资源,为每个线程分配一个Cell,让每个资源竞争对应的资源,大幅减少冲突。
LongAdder设置多个累加单元,在AtomicLong的基础上将单点的更新压力分散到各个节点,在高并发的时候通过分散提高了性能。

参考博文:
https://segmentfault.com/a/1190000019283779
https://blog.csdn.net/weixin_33958585/article/details/91405590
https://www.jianshu.com/p/b92f0b7da636

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值