# Unsafe 操作基本数据类型与对象引发的问题分析

前阵子在微信群,有个昵称为 桃子 的朋友问了如下问题(为这么这么说?因为我记得也是他问了JVM的问题,然后我写文分析了之后,被其他人喷我报了 它 的课,听了写下的文章,为了防止再次被咬,毕竟吃一堑,长一智,所以我贴上截图。pass:桃子应该在他群里)。

img

问题描述

接下来我们来看代码,我简化一下。可以看到,当boolean 为基础数据类型时,当修改后将看到修改后的值,而修改为Boolean 对象类型后,修改后无效,读到的值仍为 true。本文接下来就分析 为何 会出现这种现象。在最原始的问题中:桃子同学认为是线程可见性问题,于是他加上getBooleanVolatile 但是结果然并卵,因为单线程执行根本不可能是可见性问题。

// 此时 boolean 为基础数据类型

public class Demo {

  boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

    demo.a = false;

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

 }

}

​

输出结果:

true

false// 此时 boolean 为对象类型

public class Demo {

  Boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

    demo.a = false;

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

 }

}

​

输出结果:

true

true
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
63
64
65

JVM 层面分析

我们来看如下代码。

// 将结果以byte输出,此时a变量为 Boolean 对象

public class Demo {

  Boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getByte(demo, a_offset)); // 注意此时为byte

    demo.a = false;

    System.out.println(MyUtils.getUnsafe().getByte(demo, a_offset));

 }

}

输出结果:

85

87// 将结果以byte输出,此时a变量为 boolean 基础数据类型

public class Demo {

  boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getByte(demo, a_offset));

    demo.a = false;

    System.out.println(MyUtils.getUnsafe().getByte(demo, a_offset));

 }

}

输出结果:

1

0
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

我们看到当为对象时输出了 85 和 87,而使用基础数据类型时,将输出 0 和 1,这是为何呢?我们来JVM对于boolean基础数据类型的定义。

typedef unsigned char jboolean; // 使用一个字节描述,所以1为true,0为false。pass:在C语言中,非零即真
1

那么接下来我们来看 unsafe.getByte 的源码。从源码中,我们看到当我们使用getBoolean时,将会把偏移量处的值直接获取用于判断真假。源码如下。

UNSAFE_ENTRY(jboolean, Unsafe_Get##Boolean(JNIEnv *env, jobject unsafe, jobject obj, jlong offset)) \

 UnsafeWrapper("Unsafe_Get"#Boolean); 

 GET_FIELD(obj, offset, jboolean, v); 

 return v; 

UNSAFE_END 

  

#define GET_FIELD(obj, offset, type_name, v) 

 oop p = JNIHandles::resolve(obj); // 获取demo对象地址

 type_name v = *(type_name*)index_oop_from_field_offset_long(p, offset) // 获取对象p地址向后偏移量为offset处的值,将其转为 jboolean 指针类型,也即char * 类型,然后 * 解引用,获取其中的值inline void* index_oop_from_field_offset_long(oop p, jlong field_offset) {

 jlong byte_offset = field_offset_to_byte_offset(field_offset);

 if (sizeof(char*) == sizeof(jint)) // 32 为 指针占用 4字节

  return (address)p + (jint) byte_offset; // 将指针值加上4字节偏移量,此时指针值指向 jboolean

 else

  return (address)p +    byte_offset;

}   
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

那么,这个时候,答案很明显了:当使用 Boolean 对象类型时,设置的值就是对象地址的值,而对象地址的值都是非0值,所以当使用 getBoolean 时,永远返回true,不管设置false还是true,而对于基础数据类型而言,0就是false,1 就是 true。不信我们来看如下代码。我直接把对象地址处放一个 0,这时 就是 false。

public class Demo {

  Boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

    MyUtils.getUnsafe().putByte(demo, a_offset, (byte)0);

    System.out.println(MyUtils.getUnsafe().getBoolean(demo, a_offset));

 }

}

输出结果:

true

false
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

那么,如何正确操作Boolean对象呢?答案:使用getObject。源码如下。

public class Demo {

  Boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getObject(demo, a_offset));

    demo.a=false;

    System.out.println(MyUtils.getUnsafe().getObject(demo, a_offset));

 }

}

输出结果:

true

false
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

但是请注意以下操作。为什么NPE呢?因为地址没了,被我设置为0了。

public class Demo {

  Boolean a = true;public static void main(String[] args) throws Exception {

    Demo demo = new Demo();

    long a_offset = MyUtils.getObjectFieldOffset("a", Demo.class);

    System.out.println(MyUtils.getUnsafe().getObject(demo, a_offset));

    MyUtils.getUnsafe().putByte(demo, a_offset, (byte)0);

    System.out.println(MyUtils.getUnsafe().getObject(demo, a_offset));

 }

}

输出结果:

true

Exception in thread "main" java.lang.NullPointerException

 at java.lang.String.valueOf(String.java:2994)

 at java.io.PrintStream.println(PrintStream.java:821)

 at org.com.hundun.Demo.main(Demo.java:12)
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