这是我参与11月更文挑战的第13天,活动详情查看:2021最后一次更文挑战
在Java中使用锁时,当一个线程没有获取到锁时会被阻塞挂起,这会导致线程上下文切换和重新调度开销。Java提供了非阻塞的volatile关键字解决共享变量可见性问题,这在一定程度上弥补了锁带来的开销问题,但是volatile关键字只能保证该共享变量的可见性,不能解决读-写-改等的原子性问题。CAS是Compare-and-swap(比较与替换)的简写,是一种有名的无锁算法,在Java中,我们主要分析Unsafe类,因为所有的CAS操作都是它来实现的,而在Unsafe类中这些方法也都是native方法。JDK中的Unsafe类提供了一系列的(Compare And Swap)CAS操作方法。我们一compareAndSwaoLong方法为例进行简单介绍。
-
boolean compareAndSwap(Object obj, long valueOffset, long expect, long update): compareAndSwap的意思为比较并替换。CAS需要四个操作数,分别为:对象内存位置、对象中的变量的偏移量、变量预期值和新的值。如果对象obj中内存偏移量为valueOffset的变量值为expect,则使用新的值update替换旧的值expect。
当然关于CAS操作还有个经典的ABA问题。为了避免这个问题,JDK中的AtomicStampedReference类给每个变量的状态值都配备了一个时间戳,从而避免了ABA问题。 JDK的rt.jar包中的Unsafe类提供了硬件级别的原子性操作,Unsafe类中的方法都是native方法,它们使用JNI的方式访问本地C++实现库。 下面介绍几个关键的Unsafe类方法:
-
long getAndSetLong(Object, long offset, long update)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,并设置变量volatile语义的值为update。
public final long getAndSetLong(Object, long offset, long update) { long l; do { l= getLongvolatile(obj, offset); } while(!compareAndSwapLong(pbj, offset, l, update)); return l; }由以上代码可知,首先在getLongvolatile获取当前变量的值,然后使用CAS原子操作设置新的值。这是使用while循环是考虑到在多个线程同时调用的情况下CAS失败时需要重试。
-
long getAndAddLong(Object, long offset, long addValue)方法:获取对象obj中偏移量为offset的变量volatile语义的当前值,为变量值为原始值+addValue。
public final long getAndSetLong(Object, long offset, long addValue) { long l; do { l= getLongvolatile(obj, offset); } while(!compareAndSwapLong(pbj, offset, l, l + addValue)); return l; }类似getAndSetLong的实现,只是这里进行CAS操作时,使用了原始值+传递的增量addValue。
Unsafe类使Java拥有了像C语言的指针一样操作内存空间的能力,同时也带来了指针的问题。过度的使用Unsafe类会使得出错的几率变大,因此Java官方并不建议使用的,官方文档也几乎没有。Oracle正在计划从Java 9中去掉Unsafe类,如果真是如此影响就太大了。通常我们最好也不要使用Unsafe类,除非有明确的目的,并且也要对它有深入的了解才行。 要想使用Unsafe类需要用一些比较tricky的办法。Unsafe类使用了单例模式,需要通过一个静态方法getUnsafe()来获取。但Unsafe类做了限制,如果是普通的调用的话,它会抛出一个SecurityException异常;只有由主类加载器加载的类才能调用这个方法。