“If you think you can, you can. And if you think you can’t, you’re right.”
1.Unsafe类了解
我们会发现在看源码的过程,有很多地方都用到了sun.misc.Unsafe这个类,为此,下面对这个类做一个详细的了解:
- java 生态圈。 几乎每个使用 java开发的工具、软件基础设施、高性能开发库都在底层使用了 sun.misc.Unsafe 。
- 这就是SUN未开源的sun.misc.Unsafe的类,该类功能很强大,涉及到类加载机制,其实例一般情况是获取不到的,源码中的设计是采用单例模式,不是系统加载初始化就会抛出SecurityException异常。
- 查阅一些资料后发现,Unsafe类官方并不对外开放,因为Unsafe这个类提供了一些绕开JVM的更底层功能,基于它的实现可以提高效率。
- 在jdk 1.9版本中对Unsafe提供了公开的API。
2.Unsafe的大部分方法
其主要分为以下几类:
- Info:主要返回某些低级别的内存信息。
1
2
public native int addressSize();
public native int pageSize();
- Objects: 主要提供Object和它的域操作办法:
1
2
public native Object allocateInstance(Class<?> var1) throws InstantiationException;
public native long objectFieldOffset(Field var1);
- Class: 主要提供Class和它的静态域操作方法:
1
2
3
4
public native long staticFieldOffset(Field var1);
public native Class<?> defineClass(String var1, byte[] var2, int var3, int var4, ClassLoader var5, ProtectionDomain var6);
public native Class<?> defineAnonymousClass(Class<?> var1, byte[] var2, Object[] var3);
public native void ensureClassInitialized(Class<?> var1);
- Arrays: 数组操作办法
1
2
public native int arrayBaseOffset(Class<?> var1);
public native int arrayIndexScale(Class<?> var1);
- Synchronization: 主要提供低级别同步原语
1
2
3
4
5
6
7
8
9
10
11
12
/** @deprecated */
@Deprecated
public native void monitorEnter(Object var1);
/** @deprecated */
@Deprecated
public native void monitorExit(Object var1);
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native void putOrderedInt(Object var1, long var2, int var4);
- Memory: 直接内存访问方法(绕过JVM直接操作本地内存)
1
2
3
4
public native long allocateMemory(long var1);
public native long reallocateMemory(long var1, long var3);
public native void setMemory(Object var1, long var2, long var4, byte var6);
public native void copyMemory(Object var1, long var2, Object var4, long var5, long var7);
3.Unsafe类的实例的获取
查看Unsafe的源码我们可以发现,它类设计只提供给JVM信任的启动类加载器所使用,是一个典型的单例模式类:
1
2
3
4
5
6
7
8
9
10
11
12
private Unsafe() {
}
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if(!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");
} else {
return theUnsafe;
}
}
虽然,它提供了一个getUnsafe()的静态方法,但是,如果直接调用这个方法会抛出一个SecurityException异常,这是因为Unsafe仅供java内部类使用,外部类不应该使用它。虽然这不可行,但是我们可以通过反射拿到它,如下所示:
1
2
3
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
4. Unsafe类的使用
4.1 创建实例
通过allocateInstance()方法,你可以创建一个类的实例,但是却不需要调用它的构造函数、初始化代码、各种JVM安全检查以及其它的一些底层的东西。即使构造函数是私有,我们也可以通过这个方法创建它的实例。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
public class UnsafePlayer {
public static void main(String[] args) throws Exception {
//通过反射实例化Unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
//实例化私有的构造函数
Player player = (Player) unsafe.allocateInstance(Player.class);
player.setName("jack");
System.out.println(player.getName());
}
}
public static class Player{
private int age;
private String name;
private Player() {
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
}
}
4.2 可以使用堆外内存
如果进程在运行过程中JVM上的内存不足了,会导致频繁的进行GC。理想情况下,我们可以考虑使用堆外内存,这是一块不受JVM管理的内存。
使用Unsafe的allocateMemory()我们可以直接在堆外分配内存,这可能非常有用,但我们要记住,这个内存不受JVM管理,因此我们要调用freeMemory()方法手动释放它。
假设我们要在堆外创建一个巨大的int数组,我们可以使用allocateMemory()方法来实现:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
public class OffHeapArray {
//一个int等于4个字节
private static final int INT = 4;
private long size;
private long address;
private static Unsafe unsafe;
static {
try{
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
}catch (NoSuchFieldException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}
}
//构造方法,分配内存
public OffHeapArray(long size){
this.size = size;
//参数字节数
address = unsafe.allocateMemory(size * INT);
}
//获取指定索引处的元素
public int get(long i){
return unsafe.getInt(address + i * INT);
}
//设置指定索引处的元素
public void set(long i, int value){
unsafe.putInt(address + i * INT,value);
}
//元素个数
public long size(){
return size;
}
//释放堆外内存
public void freeMemory(){
unsafe.freeMemory(address);
}
}
在构造方法中调用allocateMemory()分配内存,在使用完成后调用freeMemory()释放内存。使用方式如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) {
OffHeapArray offHeapArray = new OffHeapArray(4);
offHeapArray.set(0,1);
offHeapArray.set(1,2);
offHeapArray.set(2,3);
offHeapArray.set(3,4);
offHeapArray.set(2,5); //在索引2的位置重新放入元素
int sum = 0;
for (int i = 0; i < offHeapArray.size(); i++){
sum += offHeapArray.get(i);
}
//打印12
System.out.println(sum);
offHeapArray.freeMemory();
}
注意:最后,一定要记得调用freeMemory()将内存释放回操作系统。
4.3 通过内存偏移地址修改变量值
public native long objectFieldOffset(Field field);
返回指定静态field的内存地址偏移量,在这个类的其他方法中这个值只是被用作一个访问特定field的一个方式。这个值对于给定的field是唯一的,并且后续对该方法的调用都应该返回相同的值。
public native int arrayBaseOffset(Class arrayClass);
获取给定数组中第一个元素的偏移地址。为了存取数组中的元素,这个偏移地址与arrayIndexScale方法的非0返回值一起被使用。 public native int arrayIndexScale(Class arrayClass) 获取用户给定数组寻址的换算因子。如果不能返回一个合适的换算因子的时候就会返回0。这个返回值能够与arrayBaseOffset一起使用去存取这个数组class中的元素
public native boolean compareAndSwapInt(Object obj, long offset,int expect, int update);
在obj的offset位置比较integer field和期望的值,如果相同则更新。这个方法的操作应该是原子的,因此提供了一种不可中断的方式更新integer field。当然还有与Object、Long对应的compareAndSwapObject和compareAndSwapLong方法。
public native void putOrderedInt(Object obj, long offset, int value);
设置obj对象中offset偏移地址对应的整型field的值为指定值。这是一个有序或者有延迟的putIntVolatile方法,并且不保证值的改变被其他线程立即看到。只有在field被volatile修饰并且期望被意外修改的时候使用才有用。当然还有与Object、Long对应的putOrderedObject和putOrderedLong方法。
public native void putObjectVolatile(Object obj, long offset, Object value);
设置obj对象中offset偏移地址对应的object型field的值为指定值。支持volatile store语义。 与这个方法对应的get方法为:
public native Object getObjectVolatile(Object obj, long offset);
获取obj对象中offset偏移地址对应的object型field的值,支持volatile load语义 这两个方法还有与Int、Boolean、Byte、Short、Char、Long、Float、Double等类型对应的相关方法.
public native void putObject(Object obj, long offset, Object value);
设置obj对象中offset偏移地址对应的object型field的值为指定值。与putObject方法对应的是getObject方法。Int、Boolean、Byte、Short、Char、Long、Float、Double等类型都有getXXX和putXXX形式的方法。
下面通过一个组合示例来了解一下如何使用它们,详细如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
public class UnsafePlayerCAS {
public static void main(String[] args) throws Exception{
//通过反射实例化Unsafe
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
Unsafe unsafe = (Unsafe) f.get(null);
// 实例化Player
Player player = (Player) unsafe.allocateInstance(Player.class);
player.setAge(18);
player.setName("li lei");
for (Field field : Player.class.getDeclaredFields()) {
System.out.println(field.getName() + ":对应的内存偏移地址" + unsafe.objectFieldOffset(field));
}
System.out.println("-------------------");
// unsafe.compareAndSwapInt(arg0, arg1, arg2, arg3)
// arg0, arg1, arg2, arg3 分别是目标对象实例,目标对象属性偏移量,当前预期值,要设的值
int ageOffset = 12;
// 修改内存偏移地址为12的值(age),返回true,说明通过内存偏移地址修改age的值成功
System.out.println(unsafe.compareAndSwapInt(player, ageOffset, 18, 20));
System.out.println("age修改后的值:" + player.getAge());
System.out.println("-------------------");
// 修改内存偏移地址为12的值,但是修改后不保证立马能被其他的线程看到。
unsafe.putOrderedInt(player, 12, 33);
System.out.println("age修改后的值:" + player.getAge());
System.out.println("-------------------");
// 修改内存偏移地址为16的值,volatile修饰,修改能立马对其他线程可见
unsafe.putObjectVolatile(player, 16, "han mei");
System.out.println("name修改后的值:" + unsafe.getObjectVolatile(player, 16));
}
public static class Player{
@Getter @Setter private String name;
@Getter @Setter private int age;
private Player(){}
}
}
输出结果:
1
2
3
4
5
6
7
8
9
age:对应的内存偏移地址12
name:对应的内存偏移地址16
-------------------
true
age修改后的值:20
-------------------
age修改后的值:33
-------------------
name修改后的值:han mei
4.4 park/unpark
JVM在上下文切换的时候使用了Unsafe中的两个非常牛逼的方法park()和unpark()。
- 当一个线程正在等待某个操作时,JVM调用Unsafe的park()方法来阻塞此线程。
- 当阻塞中的线程需要再次运行时,JVM调用Unsafe的unpark()方法来唤醒此线程。
4.5 CompareAndSwap操作
JUC下面大量使用了CAS操作,它们的底层是调用的Unsafe的CompareAndSwapXXX()方法。这种方式广泛运用于无锁算法,与java中标准的悲观锁机制相比,它可以利用CAS处理器指令提供极大的加速。
比如,我们可以基于Unsafe的compareAndSwapInt()方法构建线程安全的计数器。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
public class Counter {
private volatile int count = 0;
private static long offset;
private static Unsafe unsafe;
static {
try{
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
offset = unsafe.objectFieldOffset(Counter.class.getDeclaredField("count"));
}catch (NoSuchFieldException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}
}
public void increment(){
int before = count;
while (!unsafe.compareAndSwapInt(this,offset,before,before+1)){
before = count;
}
}
public int getCount(){
return count;
}
}
我们定义了一个volatile的字段count,以便对它的修改所有线程都可见,并在类加载的时候获取count在类中的偏移地址。
在increment()方法中,我们通过调用Unsafe的compareAndSwapInt()方法来尝试更新之前获取到的count的值,如果它没有被其它线程更新过,则更新成功,否则不断重试直到成功为止。
我们可以通过使用多个线程来测试我们的代码:
1
2
3
4
5
6
7
8
9
10
11
12
public static void main(String[] args)throws Exception {
Counter counter = new Counter();
ExecutorService threadPool = Executors.newFixedThreadPool(100);
//起100个线程,每个线程自增10000次
IntStream.range(0,100)
.forEach(i ->threadPool.submit(()-> IntStream.range(0,10000).forEach(j->counter.increment())));
threadPool.shutdown();
Thread.sleep(2000);
// 打印1000000
System.out.println(counter.getCount());
}
4.6 抛出checked异常
我们知道如果代码抛出了checked异常,要不就使用try…catch捕获它,要不就在方法签名上定义这个异常,但是,通过Unsafe我们可以抛出一个checked异常,同时却不用捕获或在方法签名上定义它。
1
2
3
4
5
6
7
8
// 使用正常方式抛出IOException需要定义在方法签名上往外抛
public static void readFile() throws IOException {
throw new IOException();
}
// 使用Unsafe抛出异常不需要定义在方法签名上往外抛
public static void readFileUnsafe() {
unsafe.throwException(new IOException());
}
5.总结
使用Unsafe几乎可以操作一切:
- 实例化一个类
- 修改私有字段的值
- 抛出checked异常
- 使用堆外内存
- CAS操作;
- 阻塞/唤醒线程;
补充:实例化一个类的方式?
- 通过构造方法实例化与一个类
- 通过Class实例化一个类
- 通过反射实例化一个类(获取Constructor对象)
- 通过克隆实例化与一个类
- 通过反序列化实例化一个类
- 通过Unsafe实例化一个类
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class InstantialTest {
private static Unsafe unsafe;
static {
try{
Field f = Unsafe.class.getDeclaredField("theUnsafe");
f.setAccessible(true);
unsafe = (Unsafe) f.get(null);
}catch (NoSuchFieldException e){
e.printStackTrace();
}catch (IllegalAccessException e){
e.printStackTrace();
}
}
public static void main(String[] args)throws Exception {
//1. 构造方法
User user1 = new User();
//Class,里面也是反射
User user2 = User.class.newInstance();
//3. 反射
User user3 = User.class.getConstructor().newInstance();
//4. 克隆
User user4 = (User)user1.clone();
//5. 反序列化
User user5 = unserialize(user1);
//6. unsafe
User user6 = (User) unsafe.allocateInstance(User.class);
System.out.println(user1.age);
System.out.println(user2.age);
System.out.println(user3.age);
System.out.println(user4.age);
System.out.println(user5.age);
System.out.println(user6.age);
}
private static User unserialize(User user1)throws Exception{
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("D://object.txt"));
oos.writeObject(user1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("D://object.txt"));
User user5 = (User)ois.readObject();
ois.close();
return user5;
}
static class User implements Cloneable, Serializable{
private int age;
public User(){
this.age = 10;
}
@Override
protected Object clone()throws CloneNotSupportedException{
return super.clone();
}
}
}
输出结果如下:
1
2
3
4
5
6
10
10
10
10
10
0
通过Unsafe实例化的类,里面的age的值竟然是0,而不是10或者20。这是因为调用Unsafe的allocateInstance()方法只会给对象分配内存,并不会初始化对象中的属性,所以int类型的默认值就是0。