并发编程系列(六)—深入理解CAS和Unsafe类

并发编程系列(六)—深入理解CAS和Unsafe类

前言

大家好,牧码心今天给大家推荐一篇并发编程系列(六)—深入理解CAS和Unsafe类的文章,希望对你有所帮助。内容如下:

  • CAS概要
  • CAS核心思想
  • CAS缺点
  • CAS应用
  • unsafe类

CAS概要

在前面系列文章中分析过通过synchronized关键字可以保证多线程安全的访问共享资源,其原理是当前线程持有对象的内置锁,其他线程没有获取到该锁是无法访问,需要排队阻塞等待锁的释放。本篇我们将分析通过无锁化机制如何来保证线程安全,分析前我们先来看两个概念:

  • 两种锁机制

    • 悲观锁:所谓悲观锁就是假定会发生并发安全问题(每次取数据时都认为其他线程会修改),会进行加锁来屏蔽其他线程对数据进行修改。如使用synchronized和ReentrantLock等
    • 乐观锁:所谓乐观锁就是假定操作不会发生并发安全问题((不会有其他线程对数据进行修改)),因此不会加锁,但是在更新数据时会判断其他线程是否有没有对数据进行过修改。如使用版本号机制或CAS(compare and swap)算法。
  • 无锁化
    无锁是一种乐观策略,总是假设对共享资源的访问没有冲突,线程可以不停执行,无需加锁,无需等待,一旦发现冲突,无锁策略则采用一种称为CAS的技术来保证线程执行的安全性,这项CAS技术就是无锁策略实现的关键。

CAS 核心思想

  • 什么是CAS
    CAS全称 Compare And Swap(比较与交换),是一种无锁算法。在不使用锁(没有线程被阻塞)的情况下实现多线程之间的变量同步。java.util.concurrent包中的原子类就是通过CAS来实现了乐观锁。

  • 算法思想
    CAS采用乐观锁的机制,是一种无锁化技术。其执行函数为:CAS(V,E,N),函数包含3个操作数:

    • V:表示需要更新的值;
    • E:表示期望值;
    • N:表示拟写入的新值;
      函数思想是如果V相等于E,则将V的值写入N,若V不能于E,说明有其他线程做了更新,则当前线程什么也不做,但可以选择重新读取该变量再尝试再次修改该变量,也可以放弃操作。其原理图如下:
      CAS核心思想图
      说明:由于CAS采用乐观锁机制,它总认为自己可以成功完成操作,当多个线程同时使用CAS操作一个变量时,只有一个会胜出,并成功更新,其余均会失败,但失败的线程并不会被挂起,仅是被告知失败,并且允许再次尝试,当然也允许失败的线程放弃操作,基于这样的原理,CAS操作即使没有锁,同样知道其他线程对共享资源操作影响,并执行相应的处理措施。同时也不会出现死锁的情况。
  • CPU对CAS的支持
    或许我们可能会有这样的疑问,假设存在多个线程执行CAS操作并且CAS的步骤很多,有没有可能在判断V和E相同后,正要赋值时,切换了线程,更改了值。造成了数据不一致呢?答案是否定的,因为CAS是一种系统原语,原语属于操作系统用语范畴,是由若干条指令组成的,用于完成某个功能的一个过程,并且原语的执行必须是连续的,在执行过程中不允许被中断,也就是说CAS是一条CPU的原子指令,不会造成所谓的数据不一致问题。

CAS 缺点

CAS虽然能高效解决并发中的原子操作问题,但是CAS也存在几个问题:ABA问题,循环时间长开销大和只能保证一个共享变量的原子操作。

  • ABA问题
    因为CAS需要在操作值的时候检查下值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用CAS进行检查时会发现它的值没有发生变化,但是实际上却变化了。
    解决方案: CAS类似于乐观锁,即每次去拿数据的时候都认为别人不会修改,所以不会上锁,但是在更新的时候会判断一下在此期间别人有没有去更新这个数据。因此解决方案也可以跟乐观锁一样:使用版本号机制,如手动增加版本号字段,在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。
    JDK1.5开始Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。这个类的compareAndSet方法的作用是首先检查当前引用是否等于预期引用,并且检查当前的标志是否等于预期标志,如果全部相等,则以原子方式将该应用和该标志的值设置为给定的更新值。

  • 循环时间长开销大
    自旋CAS预测未来会获取到锁而不断循环等待。在经过若干次循环后,如果得到锁,就顺利进入临界区。如果还不能获得锁,那就会将线程在操作系统层面挂起,这种方式确实也是可以提升效率的。但问题是当线程越来越多竞争很激烈时,占用CPU的时间变长会导致性能急剧下降。
    解决方案: 设定自旋锁的循环次数来破坏掉死循环,当超过一定时间或者一定次数时退出循环。

    扫描二维码关注公众号,回复: 11105527 查看本文章
  • 只能保证一个共享变量的原子操作
    当对一个共享变量执行操作时,我们可以使用循环CAS的方式来保证原子操作,但是对多个共享变量操作时,循环CAS就无法保证操作的原子性。
    解决方案: 可以用锁,或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操作。比如有两个共享变量i=a,j=b,合并一下ij=ab,然后用CAS来操作ij。从JDK1.5开始提供了AtomicReference类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行CAS操作。

CAS的应用

CAS广泛应用在JDK的并发包下的原子框架(java.util.concurrent.atomic)中,如AtomicXXX等类中都引用了CAS操作,而在java.util.concurrent中其它的大多数类在实现时都直接或间接的使用了这些原子变量类。下面看下AtomicInteger在使用CAS的地方:

// AtomicInteger 自增方法
public final int incrementAndGet() {
  return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
// Unsafe.class
public final int getAndAddInt(Object var1, long var2, int var4) {
  int var5;
  do {
      var5 = this.getIntVolatile(var1, var2);
  } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
  return var5;
}

我们查看AtomicInteger的自增函数incrementAndGet()的源码时,发现自增函数底层调用的是unsafe.getAndAddInt(),而getAndAddInt()循环获取给定对象o中的偏移量处的值v,然后判断内存值是否等于v。如果相等则将内存值设置为 v + delta,否则返回false,继续循环进行重试,直到设置成功才能退出循环,并且将旧值返回。整个“比较+更新”操作封装在compareAndSwapInt()中,在JNI里是借助于一个CPU指令完成的,属于原子操作,可以保证多个线程都能够看到同一个变量的修改值。

Unsafe类

上面我们分析了CAS的核心思想,缺点和应用场景,其中有些地方也会涉及到Unsafe类,那什么是Unsafe类,怎么获取Unsafe类以及有哪些功能呢?

  • 什么是Unsafe类
    Unsafe是位于sun.misc包下的一个类,主要提供一些用于执行低级别、不安全操作的方法,如直接访问系统内存资源、自主管理内存资源等,这些方法在提升Java运行效率、增强Java语言底层资源操作能力方面起到了很大的作用。但由于Unsafe类使Java语言拥有了类似C语言指针一样操作内存空间的能力,这无疑也增加了程序发生相关指针问题的风险。在程序中过度、不正确使用Unsafe类会使得程序出错的概率变大,使得Java这种安全的语言变得不再“安全”,因此对Unsafe的使用一定要慎用。
  • Unsafe类实现
    Unsafe类为一单例实现,提供静态方法getUnsafe获取Unsafe实例,当且仅当调用getUnsafe方法的类为引导类加载器所加载时才合法,否则抛出SecurityException异常,如下所示:
public class Unsafe {
	// 单例对象
	private static final Unsafe theUnsafe;
	private Unsafe() {}
	@CallerSensitive
	public static Unsafe getUnsafe() {
		Class var0 = Reflection.getCallerClass();
		// 仅在引导类加载器`BootstrapClassLoader`加载时才合法
		if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
		throw new SecurityException("Unsafe");
	} else {
		return theUnsafe;
		}
	}
}
  • Unsafe功能
    Unsafe提供的API的功能可分为内存操作、CAS、Class相关、对象操作、线程调度、系统信息获取、内存屏障、数组操作等几类,如图所示:
    Unsafe功能图
    下面将从内存操作,CAS操作来介绍,其他方面可自行查阅相关资料。
  • 内存操作
    内存操作主要包含堆外内存的分配、拷贝、释放、给定地址值操作等方法。引用源码:
//分配内存, 相当于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);

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

  1. 对垃圾回收停顿的改善。由于堆外内存是直接受操作系统管理而不是JVM,所以当我们使用堆外内存时,即可保持较小的堆内内存规模。从而在GC时减少回收停顿对于应用的影响。
  2. 提升程序I/O操作的性能。通常在I/O通信过程中,会存在堆内内存到堆外内存的数据拷贝操作,对于需要频繁进行内存间数据拷贝且生命周期较短的暂存数据,都建议存储到堆外内存。
    典型应用:比如DirectByteBuffer是Java用于实现堆外内存的一个重要类,通常用在通信过程中做缓冲池,如在Netty、MINA等NIO框架中应用广泛。DirectByteBuffer对于堆外内存的创建、使用、销毁等逻辑均由Unsafe提供的堆外内存API来实现。
  • 线程调度
    线程调度包括线程挂起、恢复、锁机制等方法。引用源码:
//取消阻塞线程
public native void unpark(Object thread);
//阻塞线程
public native void park(boolean isAbsolute, long time);
//获得对象锁(可重入锁)
@Deprecated
public native void monitorEnter(Object o);
//释放对象锁
@Deprecated
public native void monitorExit(Object o);
//尝试获取对象锁
@Deprecated
public native boolean tryMonitorEnter(Object o)

说明:方法park、unpark即可实现线程的挂起与恢复,将一个线程进行挂起是通过park方法实现的,调用park方法后,线程将一直阻塞直到超时或者中断等条件出现;unpark可以终止一个挂起的线程,使其恢复正常。
典型应用:Java锁和同步器框架的核心类AbstractQueuedSynchronizer,就是通过调用LockSupport.park()和LockSupport.unpark()实现线程的阻塞和唤醒的,而LockSupport的park、unpark方法实际是调用Unsafe的park、unpark方式来实现

发布了91 篇原创文章 · 获赞 27 · 访问量 9万+

猜你喜欢

转载自blog.csdn.net/xhwwc110/article/details/105720119
今日推荐