在上一篇Java并发: 面临的挑战文章中说过CAS是解决原子性问题的方案之一。Unsafe提供了CAS的支持,支持实例化对象、访问私有属性、堆外内存访问、线程的启停等功能。
许多Java的并发类库都是基于Unsafe实现的,比如原子类AtomicInteger,并发数据结构ConcurrentHashMap,锁的基础组件LockSupport、AbstractQueuedSynchronizer等。Unsafe在JDK内部扮演着重要的角色。
这一篇我们学习Unsafe的使用,并用Unsafe实现一个自增ID生成器。
1. Unsafe的使用
1. 获取Unsafe
虽然Unsafe提供了getUnsafe()方法获取Unsafe对象,但是处于安全考虑,JDK限制必须是Root或者Platform Classloader加载的类才能使用getUnsafe()方法。好在我们能用反射获取Unsafe对象
private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
2. 实例化对象
通过Unsafe能够实例化一个Class的对象,特殊的是它仅分配内存,不会任何初始化,不调用构造函数。我们看个例子,假设我们有一个Data类,然后使用Unsafe实例化。
public static class Data {
private int value = 1;
public Data() {
value = 2;
}
public String toString() {
return "value:" + value;
}
}
// 实例化代码
Data data = (Data) unsafe.allocateInstance(Data.class);
System.out.println(data);
我们看一下输出,实例化后value依然是0,可以确定的是value=1、value=2都没有被执行。
3. 访问私有属性
通过Unsafe访问私有属性,可以通过3个维度
- 操作,读: get,写put
- 类型,可选值包括Byte、Short、Int、Long、Float、Double、Char、Boolean、Object
- 是否包含Volatile语义
假设我们要“读Byte类型的字段,不采用Volatile语义",选择方法getByte;如果要"写Int类型字段,用Volatile语义",选择方法putIntVolatile。下面是一个简单的示例
Field valueField = Data.class.getDeclaredField("value");
long valueOffset = unsafe.objectFieldOffset(valueField);
int value = unsafe.getInt(data, valueOffset);
unsafe.putIntVolatile(data, valueOffset, 3);
这里值得专门讲一下的是,使用Volatile语义的方法,比如putIntVolatile方法,和要读写的字段是否定义为volatile是没有关系的。即使字段定义是非volatile的,使用putIntVolatile修改字段,这个写依然符合volatile写的语义,即会将数据刷新到主内存中,但是因为字段是非volatile的,直接通过字段读不保证会主内从读,因此不保证可见性,改用getIntVolatile读,这时后是保证可见的。组合字段定义和调用的方法,支持volatile语义的规则如下图所示
4. 堆外内从访问
正常的对象都是在堆上分配的,会要频繁的经历GC,而且内从十分有限。Unsafe提供了机制直接在堆外申请一块内存,这些JVM和GC是不感知的,需要自己管理生命周期。对于某些需要常驻内存的场景,通过使用堆外内存,能大大的提高效率。我们来看个例子,假设我们有个OffHeapStudent类,保存学生的名字(String)和年龄(short),我们可以这样定义。Unsafe.allocateMemory能够分配一段内存,之后可以通过这个分配的内存的地址,使用类似访问私有属性的方法来读取和写入内存。
public static class OffHeapStudent {
private long address;
private Unsafe unsafe;
private int length;
public OffHeapStudent(Unsafe unsafe, String name, short age) {
this.unsafe = unsafe;
char[] cs = name.toCharArray();
length = cs.length * 2 + 2;
address = unsafe.allocateMemory(length);
for (int i = 0; i < cs.length; i++) {
unsafe.putChar(address + i * 2, cs[i]);
}
unsafe.putShort(address + cs.length * 2, age);
}
public String getName() {
char[] cs = new char[(length - 2) / 2];
for (int i = 0; i < cs.length; i++) {
cs[i] = unsafe.getChar(address + i * 2);
}
return new String(cs);
}
public short getAge() {
return unsafe.getShort(address + length - 2);
}
}
通过这段代码能测试我们的OffHeapStudent是否正常工作,看控制台输出,我们能正确的访问的name和age字段,说明我们的类正常工作了。
Unsafe unsafe = getUnsafe();
OffHeapStudent student = new OffHeapStudent(unsafe, "randy", (short) 20);
System.out.println(student.getName());
System.out.println(student.getAge());
5. CAS
Unsafe类里提供了大量的方法实现CAS,除了基础的compareAndSwapInt、compareAndSwapLong、compareAndSwapObject,还有大量的getAndAddXxx方法。我们拿之前Data类来做一个实例
public static class Data {
private int value = 1;
public Data() {
value = 2;
}
public String toString() {
return "value:" + value;
}
}
我们可以这样使用Unsafe,第二次操作是成功的。
public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
Unsafe unsafe = getUnsafe();
Field valueField = Data.class.getDeclaredField("value");
long valueOffset = unsafe.objectFieldOffset(valueField);
Data data = new Data();
boolean success = unsafe.compareAndSwapInt(data, valueOffset, 1, 3);
System.out.println("success: " + success + " ,value:" + data.value);
success = unsafe.compareAndSwapInt(data, valueOffset, 2, 3);
System.out.println("success: " + success + " ,value:" + data.value);
}
6. Park和UnPark
通过Unsafe的park和unpark方法,我们能将线程挂起和恢复运行。这两个方法是LockSupport实现的基础,而LockSupport是Java中很多锁和同步组件的实现基础。这里我们实现一个基本示例,有个Worker线程,运行时检测时间(timeNow)是否到10点了,如果没到10点,将当前线程挂起,等待通知后在重新检测。而在另外一个线程里,我们修改时间,并且在timeNow时间到了之后,通过unpark方法恢复Worker线程的执行。Worker线程代码如下
public static class Worker implements Runnable {
public void run() {
while (timeNow < 10) {
System.out.println("NotTimeYet, ParkThread");
getUnsafe().park(false, 0);
}
System.out.println("It's time to work.");
}
}
在main方法上,我们来启动线程,并unpark恢复线程运行
public static void main(String[] args) throws InterruptedException {
Thread t = new Thread(new Worker());
t.start();
TimeUnit.SECONDS.sleep(30);
timeNow = 10;
System.out.println("TimeIsUP, UnParkThread");
getUnsafe().unpark(t);
t.join();
}
通过控制台输出,我们能确定Unsafe.park()确实将线程挂起了,而在main线程执行unpark后恢复执行
NotTimeYet, ParkThread
TimeIsUP, UnParkThread
It's time to work.
2. 案例: 基于CAS的自增ID生成器
1. 定义接口
为了方便测试和对比,我们事先定义了ID接口,里边只有一个方法incrementAndGet: 自增并返回int值。
public interface ID {
public int incrementAndGet();
}
2. 对照实现
我们提供两种标准实现,CrashIntegerID: 不是线程安全的,导致中间会有ID重复;SyncIntegerID: 使用锁同步,用来作为性能比较基准。
public class CrashIntegerID implements ID{
private int id;
public CrashIntegerID(int start) {
this.id = start;
}
public int incrementAndGet() {
return id++;
}
}
public class SyncIntegerID implements ID{
private int id;
public SyncIntegerID(int start) {
this.id = start;
}
public synchronized int incrementAndGet() {
return id++;
}
}
3. 测试方法
提供了一个模板方法,接受ID接口的实现类,使用50个线程,通过覆写afterExecute打印执行耗时
private static void testInMultiThread(ID id) throws ExecutionException, InterruptedException {
long start = System.currentTimeMillis();
ExecutorService es = new ThreadPoolExecutor(50, 50, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<>()) {
protected void afterExecute(Runnable r, Throwable t) {
long now = System.currentTimeMillis();
System.out.println("time cost: " + (now - start));
}
};
for (int i = 0; i < 10_0000; i++) {
es.submit(() -> {
int v = id.incrementAndGet();
System.out.println(v);
});
}
es.shutdown();
}
4. 检查方法
控制台打印了所有生成的id,通过检查打印内容,我们能确定id是否有重复,比如用CrashIntegerID生成10w个id的时候,我们发现就已经有多个重复值。
randy@Randy:~$ cat num | egrep -v '^$' | sort -n | uniq -d
2643
5249
36473
53039
91835
5. CAS实现
使用Unsafe提供的getAndAdd方法对自增字段实现CAS的自增
public class CASIntegerID implements ID {
private int id;
private final Unsafe UNSAFE;
private final long idOffset;
{
try {
UNSAFE = getUnsafe();
idOffset = UNSAFE.objectFieldOffset(CASIntegerID.class.getDeclaredField("id"));
} catch (Exception e) {
throw new RuntimeException(e);
}
}
public CASIntegerID(int id) {
this.id = id;
}
@Override
public int incrementAndGet() {
return UNSAFE.getAndAddInt(this, idOffset, 1);
}
private static Unsafe getUnsafe() throws NoSuchFieldException, IllegalAccessException {
Field field = Unsafe.class.getDeclaredField("theUnsafe");
field.setAccessible(true);
return (Unsafe) field.get(null);
}
}
6. 性能对比
我们分别用SyncIntegerID、CASIntegerID生成100w个ID,看一下执行耗时的差异。因为afterExecute每个任务都打印System.out可能会耗时比例,实际上差异应该会更大。
SyncIntegerID | CASIntegerID | |
10w | 2932ms | 2698ms |
100w | 22993ms | 18429ms |
A. 参考资料
- Guide to Unsafe,Guide to sun.misc.Unsafe | Baeldung