# Unsafe 操作基本数据类型与对象引发的问题分析
前阵子在微信群,有个昵称为 桃子 的朋友问了如下问题(为这么这么说?因为我记得也是他问了JVM的问题,然后我写文分析了之后,被其他人喷我报了 它 的课,听了写下的文章,为了防止再次被咬,毕竟吃一堑,长一智,所以我贴上截图。pass:桃子应该在他群里)。
问题描述
接下来我们来看代码,我简化一下。可以看到,当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
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
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
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
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
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
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