并发编程4-CAS和魔术类Unsafe

目录

CSA

什么是CAS

使用 CAS 方式也会有几个问题:

1.  ABA问题:

2. 循环时间长开销大:

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

ABA问题演示:

UnSafe类详解

基本介绍 Java魔法类

如何获取Unsafe实例?

1、从getUnsafe方法的使用限制条件出发,

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

Unsafe 类可用于操作:

1.CAS操作相关 :

 API方法介绍:

Unsafe底层如何实现CAS?

Unsafe的CAS 典型应用

2.线程调度 (重点)

 API方法介绍:

典型应用

3.内存屏障:

 API方法介绍:

典型应用

4.堆外内存操作

API方法介绍:

典型应用

 .........


CSA

什么是CAS

CAS的全称为Compare-And-Swap,直译就是对比交换。是一条CPU的原子指令,

其作用是让CPU先进行比较两个值是否相等,然后原子性地更新某个位置的值,

其实现方式是基于硬件平台的汇编指令,就是说CAS是靠硬件实现的,JVM只是封装了汇编调用,AtomicInteger类便是使用了这些封装后的接口。

CAS 操作包含三个操作数 —— 内存位置(V)、预期原值(A)和 新值(B)。

如果内存位置的值与预期原值相匹配,那么处理器会自动将该位置值更新为新值 。否则,处理器不做任何操作。

无论哪种情况,它都会在 CAS 指令之后返回该位置的值。

CAS操作是原子性的,所以多线程并发使用CAS更新数据时,可以不使用锁。

JDK中大量使用了CAS来更新数据而防止加锁(synchronized 重量级锁)来保持原子更新

通过CAS方式我们可以实现乐观锁

使用 CAS 方式也会有几个问题:

1.  ABA问题:

因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,

但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。此刻就会引发ABA 问题

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

从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。

这个类的compareAndSet方法作用是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

后文详解原子类 AtomicStampedReference

2. 循环时间长开销大:

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

如果JVM能支持处理器提供的 pause指令(暂停线程)那么效率会有一定的提升,pause指令有两个作用,

第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。

第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

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

当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,

但是对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作

比如有两个共享变量i=2,j=a,合并一下ij=2a,然后用CAS来操作ij。

从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

后文详解 原子类 AtomicReference

例如下面的代码分别用AtomicInteger和AtomicStampedReference来对初始值为100的原子整型变量进行更新,

AtomicInteger会成功执行CAS操作;而加上版本戳的AtomicStampedReference对于ABA问题会执行CAS失败:

ABA问题演示:

public class Test {
    private static AtomicInteger atomicInt = new AtomicInteger(100);
    private static AtomicStampedReference atomicStampedRef = new AtomicStampedReference(100, 0);

    public static void main(String[] args) throws InterruptedException {
        Thread intT1 = new Thread(new Runnable() {
            @Override
            public void run() {
                atomicInt.compareAndSet(100, 101);
                atomicInt.compareAndSet(101, 100);
            }
        });
        Thread intT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                }
                boolean c3 = atomicInt.compareAndSet(100, 101);
                System.out.println(c3); // true
            }
        });
        intT1.start();
        intT2.start();
        intT1.join(); // 阻断线程
        intT2.join(); // 阻断线程
        Thread refT1 = new Thread(new Runnable() {
                @Override
                public void run(){
                  try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {

                }
                  atomicStampedRef.compareAndSet(100, 101, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
                  atomicStampedRef.compareAndSet(101, 100, atomicStampedRef.getStamp(), atomicStampedRef.getStamp() + 1);
            }
        });

        Thread refT2 = new Thread(new Runnable() {
            @Override
            public void run() {
                int stamp = atomicStampedRef.getStamp();
                try {
                    TimeUnit.SECONDS.sleep(2);
                } catch (InterruptedException e) {
                }
                boolean c3 = atomicStampedRef.compareAndSet(100, 101, stamp, stamp + 1);
                System.out.println(c3); // false
            }
        });
       refT1.start();
       refT2.start();
}


UnSafe类详解

基本介绍 Java魔法类:Unsafe应用解析icon-default.png?t=M4ADhttps://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

Java魔法类:Unsafe应用解析icon-default.png?t=M4ADhttps://tech.meituan.com/2019/02/14/talk-about-java-magic-class-unsafe.html

Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,

这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。

但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。

在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎重

Unsafe类::通过单例模式实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常。所以尽管里面的方法都是 public 的,但是并没有办法使用它们,如下Unsafe源码所示:

public final class Unsafe {
  private static final Unsafe theUnsafe;
  private Unsafe() { }
  @CallerSensitive
  public static Unsafe getUnsafe() {
    Class classload = Reflection.getCallerClass();
    if(!VM.isSystemDomainLoader(classload.getClassLoader())) {     // 当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常
      throw new SecurityException("Unsafe");
    } else {
      return theUnsafe;
    }
  }
}

如何获取Unsafe实例?

1、从getUnsafe方法的使用限制条件出发,

通过Java命令行命令-Xbootclasspath/a把 调用Unsafe相关方法的类A所在jar包路径追加到默认的bootstrap路径中,

使得A被引导类加载器加载,从而通过Unsafe.getUnsafe方法安全的获取Unsafe实例。 java ­Xbootclasspath/a:${path} // 其中path为调用Unsafe相关方法的类所在jar包路径

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

public class UnsafeInstance { 
    public static Unsafe reflectGetUnsafe() { 
    	try {
     		Field field =  Unsafe.class.getDeclaredField("theUnsafe");
     		field.setAccessible(true); 
    		return (Unsafe) field.get(null); 
    	} catch (Exception e) { 
    		e.printStackTrace(); 
    	}
    return null; 
    } 
}

Unsafe 类可用于操作:

 API方法介绍:

1.CAS操作相关 :

完成原子操作,底层通过汇编指令 cmpxchg 实现

 API方法介绍:

参数列表:
@param o         包含要修改field的对象
@param offset    对象中某field的偏移量
@param expected  期望值 
@param update    更新值
@return          true | false
  
public final native boolean compareAndSwapObject(Object paramObject1, long paramLong, Object paramObject2, Object paramObject3);

public final native boolean compareAndSwapInt(Object paramObject, long paramLong, int paramInt1, int paramInt2);

public final native boolean compareAndSwapLong(Object paramObject, long paramLong1, long paramLong2, long paramLong3);
 
思考一下:以上三个方法  如果实现 boolean  Dubbo 等其他类型的CAS 操作呢?   通过进行类型转换。 那么又是如何实现类型转换的呢? 有趣哦  

Unsafe底层如何实现CAS?

不妨再看看Unsafe的compareAndSwap*方法来实现CAS操作,它是一个本地方法,实现位于unsafe.cpp中。

UNSAFE_ENTRY(jboolean, Unsafe_CompareAndSwapInt(JNIEnv *env, jobject unsafe, jobject obj, jlong offset, jint e, jint x))
  UnsafeWrapper("Unsafe_CompareAndSwapInt");
  oop p = JNIHandles::resolve(obj);
  jint* addr = (jint *) index_oop_from_field_offset_long(p, offset);
  return (jint)(Atomic::cmpxchg(x, addr, e)) == e;
UNSAFE_END

可以看到它通过汇编指令: Atomic::cmpxchg 来实现比较和替换操作。其中参数x是即将更新的值,参数e是原内存的值。 addr 为偏移量

Unsafe的CAS 典型应用

CAS在java.util.concurrent.atomic相关类、AQS、CurrentHashMap等实现上有非常广泛的应用。

如在AtomicInteger的实现中,静态字段valueOffset即为 字段value的内存偏移地址,

valueOffset的值在AtomicInteger初始化时,在静态代码块中通过Unsafe的objectFieldOffset方法获取。

在AtomicInteger中提供的线程安全方法中,通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址,从而可以根据CAS实现对value字段的原子操作。

public class AtomicInteger extends Number implements java.io.Serializable {
    private static final long serialVersionUID = 6214790243416807050L;

    // setup to use Unsafe.compareAndSwapInt for updates
    private static final Unsafe unsafe = Unsafe.getUnsafe();
    private static final long valueOffset; //字段value的内存偏移地址

    static {
        try {
            // 通过字段valueOffset的值可以定位到AtomicInteger对象中value的内存地址
            valueOffset = unsafe.objectFieldOffset
                (AtomicInteger.class.getDeclaredField("value"));
        } catch (Exception ex) { throw new Error(ex); }
    }
 	........
}

2.线程调度 (重点)

线程挂起 恢复 获取锁 释放锁

方法park、unpark即可实现线程的挂起与恢复,

将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;

unpark 可以终止一个挂起的线程,使其恢复正常。

 API方法介绍:

public native void park(boolean isAbsolute, long time); //阻塞线程 挂起
public native void unpark(Object thread); //取消阻塞线程 
public native void monitorEnter(Object o); //获得对象锁(可重入锁)
public native void monitorExit(Object o); //释放对象锁
public native boolean tryMonitorEnter(Object o);//尝试获取对象锁 

典型应用: AbstractQueuedSynchronizer 中很多实现都是基于Unsafe 操作线程底层;

典型应用

其一:Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用 LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,

而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现。

其二:通过Unsafe的monitorEnter、monitorExit方法可以实现跨方法 同步机制

3.内存屏障:

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

 API方法介绍:

//内存屏障,禁止load操作重排序。屏障前的load操作不能被重排序到屏障后,屏障后的load操作不能被重排序到屏障前
public native void loadFence();
//内存屏障,禁止store操作重排序。屏障前的store操作不能被重排序到屏障后,屏障后的store操作不能被重排序到屏障前
public native void storeFence();
//内存屏障,禁止load、store操作重排序
public native void fullFence();

典型应用

在Java 8中引入了一种锁的新机制——StampedLock,它可以看成是读写锁的一个改进版本。 (stamped 英[stæmpt] 美[stæmpt] adj. (信封或包裹)贴好邮票的; v. 在…上盖(字样或图案等); 把(字样或图案等)盖在…上; )

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内存 屏障,目的是避免上图用例中步骤②和StampedLock.validate中锁状态校验运算发生重排 序导致锁状态校验不准确的问题

4.堆外内存操作

这部分主要包含分配 扩充 释放 拷贝堆外内存 设置/获取给定地址中的值

API方法介绍:

public native long allocateMemory(long bytes); //分配内存, 相当于C++的malloc函数
public native long reallocateMemory(long address, long bytes);//扩充内存
public native void freeMemory(long address);//释放内存
public native void copyMemory(Object srcBase, long srcOffset, Object destBase, long destOffset, long bytes);//内存拷贝
public native void setMemory(Object o, long offset, long bytes, byte value);//在给定的内存块中设置值
// 获取给定地址值,忽略修饰限定符的访问限制。与此类似操作还有: 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);
/ /获取给定地址的byte类型的值(当且仅当该内存地址为allocateMemory分配时,此方法结果为确定的)
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来实现。

通过Unsafe.allocateMemory分配内存、Unsafe.setMemory进行内存初始化,而后构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,

以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放。 部分源码:

DirectByteBuffer(int cap) {                   // package-private
    super(-1, 0, cap, cap);
    boolean pa = VM.isDirectMemoryPageAligned();
    int ps = Bits.pageSize();
    long size = Math.max(1L, (long)cap + (pa ? ps : 0));
    Bits.reserveMemory(size, cap);
    long base = 0;
    try {
        base = unsafe.allocateMemory(size); // 获取内存
    } catch (OutOfMemoryError x) {
        Bits.unreserveMemory(size, cap);
        throw x;
    }
    unsafe.setMemory(base, size, (byte) 0); // 设置内存
    if (pa && (base % ps != 0)) {
        // Round up to page boundary
        address = base + ps - (base & (ps - 1));
    } else {
        address = base;
    }
    cleaner = Cleaner.create(this, new Deallocator(base, size, cap)); // 构建Cleaner对象用于跟踪DirectByteBuffer对象的垃圾回收,以实现当DirectByteBuffer被垃圾回收时,分配的堆外内存一起被释放
    att = null;

 .........

其他的功能点请自行查阅源码

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值