线程进阶:多任务处理(17)——Java中的锁(Unsafe基础)
1. 概述
本专题在之前的文章中详细介绍了Java中最常使用的一种锁机制——同步锁。但是同步锁肯定是不适合在所有应用场景中使用的。所以从本文开始,笔者将试图通过两到三篇文章的篇幅向读者介绍Java中锁的分类、原理和底层实现。以便大家在实际工作中根据应用场景进行使用。本篇文章我们先介绍Java中关于锁的底层实现的基础类sun.misc.Unsafe。
2. Unsafe原子操作
在介绍Java中除同步锁以外的其它锁机制前,我们要介绍一个Java中基于操作系统级别的原子操作类sun.misc.Unsafe,它是Java中对大多数锁机制实现的最基础类。请注意,JDK 1.8和之前JDK版本的中sun.misc.Unsafe类可提供的方法有较大的变化,而本文的讲解主要是基于JDK 1.8进行的。sun.misc.Unsafe类提供的原子操作基于操作系统直接对CPU进行操作,而以下这些方法又是sun.misc.Unsafe类中经常被使用的:
2-1. 在你的代码中使用Unsafe
Java语言出于封装性和安全性的考虑,它不允许技术人员直接使用Unsafe类和其中的各种方法,特别是不能直接使用“new”的方式对sun.misc.Unsafe类进行实例化(否则会报告“java.lang.SecurityException: Unsafe”错误)。但是你可以通过Java提供的反射机制获取到Unsafe类的实例:
……
Field f = null;
sun.misc.Unsafe unsafe = null;
try {
f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
// 得到Unsafe类的实例
unsafe = (Unsafe) f.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
e.printStackTrace();
}
……
2-2. Unsafe典型方法
2-2-1. Unsafe.objectFieldOffset(Field) 和类似方法
Unsafe中除了objectFieldOffset(Field) 这个方法外,还有一个类似的方法staticFieldOffset(Field)。这两个方法用于返回类定义中某个属性在主存中设定的偏移量。请看以下代码:
……
// 注意本代码中的unsafe对象就是根据之前代码获取到的
// 开始使用unsafe对象,分别找到UserPojo对象中child属性和name属性的内存地址偏移量
// 首先是UserPojo类中的child属性,在内存中设定的偏移位置
Field field = UserPojo.class.getDeclaredField("child");
// 这就是一旦这个类实例化后,该属性在内存中的偏移位置
long offset = unsafe.objectFieldOffset(field);
System.out.println("child offset = " + offset);
// 然后是UserPojo类中的name属性,在内存中设定的偏移位置
Field fieldName = UserPojo.class.getDeclaredField("name");
long nameOffset = unsafe.objectFieldOffset(fieldName);
System.out.println("name offset = " + nameOffset);
……
这两个方法相比后面要介绍的各种Unsafe方法来说,显得更为重要。因为Unsafe对象中属性的操作方式都是直接通过内存偏移量的方式找到操作目标。
2-2-2. Unsafe.compareAndSwapObject(Object, long, Object, Object)和类似方法
Unsafe中除了compareAndSwapObject 这个方法外,还有两个类似的方法:unsafe.compareAndSwapInt和unsafe.compareAndSwapLong。这些方法的作用就是对属性进行比较并替换(俗称的CAS过程——Compare And Swap)。当给定的对象中,指定属性的值符合预期,则将这个值替换成一个新的值并且返回true;否则就忽略这个替换操作并且返回false。
请注意CAS过程是sun.misc.Unsafe类中除了获取内存偏移量以外,提供的最重要的功能了——因为Java中很多基于“无同步锁”方式的功能实现原理都是基于CAS过程。请看如下示例代码:
……
UserPojo user = new UserPojo();
user.setName("yinwenjie");
user.setSex(11);
user.setUserId("userid");
// 获得sex属性的内存地址偏移量
Field field = UserPojo.class.getDeclaredField("sex");
long sexOffset = unsafe.objectFieldOffset(field);
/*
* 比较并修改值
* 1、需要修改的对象
* 2、要更改的属性的内存偏移量
* 3、预期的值
* 4、设置的新值
* */
// 为什么是Object而不是int呢?因为sex属性的类型是Integer不是int嘛
if(unsafe.compareAndSwapObject(user, sexOffset, 11, 13)) {
System.out.println("更改成功!");
} else {
System.out.println("更改失败!");
}
……
首先创建一个UserPojo类的实例对象,这个实例对象有三个属性name、sex和userId。接着我们找到sex属性在主存中设定的偏移量sexOffset,并进行CAS操作。请注意compareAndSwapObject方法的四个值:第一个值表示要进行操作的对象user,第二个参数通过之前获取的主存偏移量sexOffset告诉方法将要比较的是user对象中的哪个属性,第三个参数为技术人员所预想的该属性的当前值,第四个参数为将要替换成的新值。
那么将方法套用到以上的compareAndSwapObject执行过程中:如果当前user对象中sex属性为11,则将这个sex属性的值替换为13,并返回true;否则不替换sex属性的值,并且返回false。
2-2-3. Unsafe.getAndAddInt(Object, long, int)和类似方法
类似的方法还有getAndAddLong(Object, long, long),它们的作用是利用Unsafe的原子操作性,向调用者返回某个属性当前的值,并且紧接着将这个属性增加一个新的值。在java.util.concurrent.atomic代码包中,有一个类AtomicInteger,这个类用于进行基于原子操作的线程安全的计数操作,且这个类在JDK1.8+的版本中进行了较大的修改。以下代码示例了该类的getAndIncrement()方法中的实现片段:
……
public class AtomicInteger extends Number implements java.io.Serializable {
……
private volatile int value;
……
private static final long valueOffset;
……
// 获取到value属性的内存偏移量valueOffset
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
……
/**
* 这是JDK1.8中的实现
* Atomically increments by one the current value.
* @return the previous value
*/
public final int getAndIncrement() {
return unsafe.getAndAddInt(this, valueOffset, 1);
}
……
}
通过以上代码的演示可以看到,AtomicInteger类中定义了一个value属性,并通过unsafe.objectFieldOffset方法获取到了这个属性在主存中设定的偏移量valueOffset。接着就可以在getAndIncrement方法中直接使用unsafe.getAndAddInt的方式,通过偏移量valueOffset将value属性的值加“1”。但是该方法的实现在JDK1.8之前的版本中,实现代码却是这样的:
// 获取偏移量valueOffset的代码类似,这里就不再展示了
……
public final int getAndIncrement() {
// 一直循环,直到
for (;;) {
int current = get();
int next = current + 1;
if (compareAndSet(current, next))
return current;
}
}
……
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
……
代码中采用的就是基于Unsafe的“乐观锁”进行实现的,后文中我们还会讲解java的自旋锁以及自旋锁的一种具体实现方式“乐观锁”,还会分析“乐观锁”适合使用的场景。在以上代码中,getAndIncrement方法内部会不停的循环,直到unsafe.compareAndSwapInt方法执行成功。但多数情况下,循环只会执行一次,因为多线程强占同一对象属性的情况并不是随时都会出现。
2-2-4. 其它典型方法
在JDK1.8中Unsafe还有一些其它实用的原子操作方法:
PutXXXXX(Object, long, short)
类似的方法包括:putInt(Object, long, int)、putBoolean(Object, long, boolean)、putShort(Object, long, short)、putChar(Object, long, char)、putDouble(Object, long, double)等,这些都是针对指定对象中在偏移量上的属性值,进行直接设定。这些操作发生在CPU一级缓存(L1) 或者二级缓存(L2)中,但是这些方法并不保证工作在其它内核上的线程“立即看到”最新的属性值。
putXXXXXVolatile(Object, long, byte)
类似的方法包括:putByteVolatile(Object, long, byte)、putShortVolatile(Object, long, short)、putFloatVolatile(Object, long, float)、putDoubleVolatile(Object, long, double)等等,这些方法的主要作用虽然也是直接针对偏移量改变指定对象中的属性值,但是这些方法保证工作在其它内核上的线程能“立即看到”最新的属性值——也就是说这些方法满足volatile语义(后续文章会详细介绍volatile的详细工作原理)
===============(接下文)