在上一篇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;}@Overridepublic 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