Atomic & Unsafe魔法类详解

原子操作

        原子(atom)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。在多处理器上实现原子操作就变得有点复杂。

CAS的基本思路就是,如果这个地址上的值和期望的值相等,则给其赋予新值,否则不做任何事儿,但是要返回原值是多少。自然CAS操作执行完成时,于是就有了循环CAS。很明显,循环CAS就是在一个循环里不断的做cas操作,直到成功为止。Java中的Atomic系列的原子操作类的实现则是利用了循环CAS来实现

CAS的底层原理

换句话说就是CAS为什么能保证原子性?

1, 靠的是底层的Unsafe类

2,Unsafe类是CAS的核心类,由于java无法直接访问底层系统,需要通过本地(native)方法访问,Unsafe相当于一个后门,该类可以直接操作特定的内存数据。Unsafe类存在于sun.misc包中,其内部方法操作可以像C的指针一样直接操作内存,因为java中的CAS依赖于Unsafe类中的啊方法

3,注意Unsafe类中的所有方法都是native修饰的,也就是说Unsafe类中的方法都直接调用操作系统底层资源执行相应任务,Unsafe类中的native方法是调用底层原语,原语是有原子性的。

CAS实现原子操作的三大问题

ABA问题

        因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。

        ABA问题的解决思路就是使用版本号。在变量前面追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。

怎么解决

AtomicReference原子引用

如果赋值操作不是线程安全的。若想不用锁来实现,可以用 AtomicReference这个类,实现对象引用的原子更新

两种解决方法:

1 AtomicStampedReference类:

        版本号原子引用,理解原子引用+新增一种机制,那就是修改版本号(类似时间戳)

        通过stamp这个标记(版本号)属性来记录CAS每次设置值的操作,而下一次再CAS操作时,由于期望的stamp与现有的stamp不一样,因此就会设置失败,从而杜绝ABA问题的复现

2 AtomicMarkableReference类

基本和AtomicStampedReference差不多,AtomicStampedReference主要关注版本号,即reference的值被修改了多少次,AtomicMarkableReference是使用boolean mark来标记reference是否被修改过,既然有了AtomicStampedReference为啥还需要再提供AtomicMarkableReference呢。在现实业务场景中,不关心引用变量被修改了几次,只是单纯的关心是否更改过

循环时间长开销大

        自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销

只能保证一个共享变量的原子操作

        当对一个共享变量执行操作时,我们可以使用循环CAS方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁

        还有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作,比如,有两个共享变量 i=2,j=a,合并一下 ij=2a,然后用CAS来操作ij。从java1.5开始,JDK提供了AtomicReference类来保证引用对象之间的原子性,就可以把多个变量放在一个对象里来进行CAS操作

AtomicInteger

        int addAndGet(int delta) : 以原子方式将输入的数值与实例中的值(AtomicInteger里的value)相加,并返回结果。

        boolean compareAndSet(int expect,int update):如果输入的数值等于预期值,则以原子方式将该值设置为输入的值

        int getAndIncrement():以原子方式将当前值加1,注意,这里返回的是自增前的值。

        int getAndSet(int newValue):以原子方式设置为newValue的值,并返回旧值

AtomicIntegerArray

        主要是提供原子的方式更新数组里的整型,其常用方法如下.

        int andAndGet(int i,int delta):以原子方式将输入值与数组中索引  i  的元素相加。

        boolean compareAndSet(int i, int expect, int update):如果当前值等于预期值,则以原子方式将数组位置  i   的元素设置成 update值

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

更新引用类型

        原子更新基本类型的AtomicInteger,只能更新一个变量,如果要原子更新多个变量,就需要使用这个原子更新引用类型提供的类,Atomic包提供了以下3个类

AtomicReference

        原子更新引用类型

AtomicStampedReference

        利用版本戳的形式记录了每次改变以后的版本号,这样的话就不会存在ABA问题了,这就是AtomicStampedReference的解决方案。AtomicMarkableReference跟AtomicStampedReference差不多,AtomicStampedReference是使用pair的int stamp 作为计数器使用,AtomicMarkableReference的pair使用的是boolean mark.

AtomicMarkableReference

        原子更新带有标记位的引用类型。可以原子更新一个布尔类型的标记位和引用类型。构造方法是AtomicMarkableReference(V initialRef,boolean initialMark)

Unsafe应用解析

        Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别,不安全操作的方法,如直接访问系统内存资源,自主管理内存资源等,这些方法在提升Java运行效率,增强Java语言底层资源操作能力方面起到了很大的作用,但由于Unsafe类使Java语言拥有了类似C语言一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度,不正确使用Unsafe类会使得程序出错的概率变大,使得java这种安全的语言变得不再安全,因此对Unsafe的使用一定要慎重。

        Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常

 如何获取Unsafe实例?

1,从getUnsafe方法的使用限制条件出发,通过Java命令行命令 -Xbootclasspath/a把调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例

java -Xbootclasspath/a:${path} //其中path为调用Unsafe相关方法的类所在jar包路径

2,通过反射获取单例对象theUnsafe

        

 Unsafe功能介绍

Unsafe提供的API大致可分为内存操作,CAS,Class相关,对象操作,线程调度,系统信息获取,内存屏障,数组操作等几类,

 1,内存操作

这部分主要包含堆外内存的分配,拷贝,释放,给定地址值操作等方法

//分配内存,相当于C++的malloc函数

public native long allocateMemory(long bytes);

//扩充内存

public native long reallocateMemory(long address, long bytes);

//释放内存

public native void freeMemory(long address);

//在给定的内存块中设置值

public native void setMemory(Object o, long offset, long bytes, byte value);

//内存拷贝

public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);

//获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: getInt, getDouble,getLong,getChar等

public native Object getObject(Object o, long offset);

//为给定地址设置值,忽略修饰限定符的访问限制,与此类似操作还有: putInt,putDouble,putLong,putChar等

public native void putObject(Object o, long offset, Object x);

public native byte getByte(long address);

//为给定地址设置byte类型的值(当且仅当该内存地址为 allocateMemory分配 时,此方法结果才是确定的)

public native void putByte(long address, byte x);

        通常,我们在Java中创建的对象都处于堆内内存(heap)中,堆内内存是由JVM所管控的Java进程内存,并且它们遵循JVM的内存管理机制,JVM会采用垃圾回收机制统一管理堆内存。与之相对的是堆外内存,存在于JVM管控之外的内存区域,java中对堆外内存的操作,依赖于Unsafe提供的操作堆外内存的native方法。

使用堆外内存的原因

        对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。

        提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存

典型应用

       DirectByteBuffer是java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty,MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建,使用,销毁等逻辑均有Unsafe提供的堆外内存API实现

        下图为DirectByteBuffer构造函数,创建DirectByteBuffer的时候,通过Unsafe.allocateMemory分配内存,Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。

2,CAS相关

如下源代码释义所示,这部分主要为CAS相关操作的方法

 

 典型应用

        如下图所示,AtomicInteger的实现中,静态字段valueOffset即为字段value的内存偏移地址,valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

        下图为某个AtomicInteger对象自增操作前后的内存示意图,对象的基地址baseAddress="0x110000",通过baseAddress+valueOffset得到value的内存地址valueAddress= "0x11000c";然后通过CAS进行原子性的更新操作,成功则返回,否则继续重试,直到更新成功为止 

 3,线程调度

包括线程挂起,恢复,锁机制等方法

//取消阻塞线程

public native void unpark(Object thread);

//阻塞线程

pubic native void park(boolean isAbsolute, long time);

//获得对象锁(可重入锁)

public native void monitorEnter(Object o);

//释放对象锁

public native void monitorExit(Object o);

//尝试获取对象锁

public native boolean tryMonitorEnter(Object o);

方法park,unpark即可实现线程的挂起和恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现,unpark可以终止一个挂起的线程,使其恢复正常

典型应用

        Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park,unpark方法实际是调用Unsafe的park,unpark方法来实现的。

4,内存屏障

        在java 8中引入,用于定义内存屏障 (也称为内存栅栏,内存栅障,屏障指令等,是一类同步屏障指令,是CPU或编译器在对内存随机访问的操作中的一个同步点,使得此点之前的所有读写操作都执行后才可以开始执行次点之后的操作),避免代码重排序

// 内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前

public native void loadFence();

// 内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前

public native void storeFence();

// 内存屏障,禁止load,store操作重排序

public native void fullFence();

典型应用

        在java 8 中引入了一种锁的新机制--StampedLock, 它可以看成是读写锁的一个改进版本。StampedLock 提供了一种乐观读锁的实现,这种乐观读锁类似于无锁的操作,完全不会阻塞写线程获取写锁,从而缓解读多写少时写线程 “饥饿” 现象。由于StampedLock 提供的乐观读锁不阻塞写线程获取读锁,当线程共享变量从主内存load到线程工作内存时,会存在数据不一致问题,所以当使用StampedLock的乐观读锁时,需要遵从如下图用例中使用的模式来确保数据的一致性

 如上图用例所示计算坐标点Point对象,包含点移动方法move及计算此点到原点的距 离的方法distanceFromOrigin。在方法distanceFromOrigin中,首先,通过 tryOptimisticRead方法获取乐观读标记;然后从主内存中加载点的坐标值 (x,y);而后通过 StampedLock的validate方法校验锁状态,判断坐标点(x,y)从主内存加载到线程工作内存 过程中,主内存的值是否已被其他线程通过move方法修改,如果validate返回值为true, 证明(x, y)的值未被修改,可参与后续计算;否则,需加悲观读锁,再次从主内存加载(x,y) 的最新值,然后再进行距离计算。其中,校验锁状态这步操作至关重要,需要判断锁状态是 否发生改变,从而判断之前copy到线程工作内存中的值是否与主内存的值存在不一致

        下图为StampedLock.validate方法的源码实现,通过锁标记与相关常量进行位运算,比较来校验锁状态,在校验逻辑之前,会通过Unsafe的loadFence方法加入一个load内存屏障,目的是避免上图用例中步骤2 和StampedLock.valiate中锁状态校验运算发生重排序导致锁状态不准确的问题

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值