前言最近在写一些业务代码的时候,遇到了一个需要生成随机数的场景。这时候自然而然的想到了jdk包中的Random类。但是出于对性能的极致追求,我考虑使用ThreadLocalRandom类进行优化。在查看ThreadLocalRandom的实现过程中,我跟踪了一些Unsafe的代码。整个过程下来,学到了很多东西,通过查找和提问解决了。我得到了很多疑惑,所以我在这篇文章中总结了一下。随机性能问题在使用Random类时,为了避免重复创建的开销,我们一般将实例化的Random对象设置为我们使用的服务对象的一个??属性或者静态属性,在线程竞争不激烈的情况下是没有问题的.但是在高并发的web服务中,使用同一个Random对象可能会造成线程阻塞。Random的随机原理是对一个“随机种子”进行固定的算术运算和位运算,得到一个随机结果,然后将这个结果作为下一个随机种子。在解决线程安全问题时,Random使用CAS来更新下一个随机种子。可以想象,如果多个线程同时使用这个对象,肯定会有一些线程连续执行CAS失败,导致线程阻塞。ThreadLocalRandomjdk的开发者自然是考虑到了这个问题,在concurrent包中加入了ThreadLocalRandom类。第一眼看到这个类名,还以为是通过ThreadLocal实现的,然后就想到了可怕的内存泄漏问题,但是点开源码的时候,并没有ThreadLocal的影子,反而有一大堆Unsafe——相关代码。我们看一下它的核心代码:UNSAFE.putLong(t=Thread.currentThread(),SEED,r=UNSAFE.getLong(t,SEED)+GAMMA);翻译成更直观的Java代码像:Threadt=Thread.currentThread();longr=UNSAFE.getLong(t,SEED)+GAMMA;UNSAFE.putLong(t,SEED,r);看起来很眼熟,就像我们平时得到的一样/setinMap,使用Thread.currentThread()获取当前对象中的key,以SEED随机种子为value。但是,使用对象作为键可能会导致内存泄漏。由于可能会创建大量的Thread对象,如果在回收时不清除Map中的值,Map会越来越大,最后内存溢出。Unsafefunction但是仔细看ThreadLocalRandom类的核心代码,发现并不是简单的Map操作。它的getLong()方法需要传入两个参数,而putLong()方法需要传入三个参数。查看源码发现都是nativemethod,看不到具体实现。这两个方法签名是:publicnativelonggetLong(Objectvar1,longvar2);publicnativevoidputLong(Objectvar1、longvar2、longvar4);虽然看不到具体的实现,但是可以查看它们的功能。下面是两个方法的功能介绍:putLong(object,offset,value)可以将对象内存地址offset后的最后四个字节设置为value。getLong(object,offset)从对象内存地址的offset偏移量读取四个字节,并返回为long。Unsafe作为Unsafe类中的一个方法,也透着一种“不安全”的感觉。具体表现是可以直接操作内存,无需任何安全检查。如果有问题,会在运行时抛出Fatal。报错,导致整个虚拟机退出。在我们的常识中,get方法是最容易抛出异常的地方,比如空指针、类型转换等,但是Unsafe.getLong()方法是一个非常安全的方法,它从某个内存位置读取四个字节,不管这四个字节是什么内容,总能成功转换成long类型。这个long类型的结果和业务是否匹配是另外一回事。set方法也比较安全。它把某个内存位置之后的四个字节改写成一个long值,而且几乎没有错误。那么这两种方式“不安全”的地方在哪里呢?它们的不安全之处不是这两个方法在执行过程中报错,而是在没有保护的情况下更改内存会导致其他方法在使用这段内存时报错。publicstaticvoidmain(String[]args)throwsNoSuchFieldException,IllegalAccessException{//Unsafe设置构造函数private,getUnsafe获取实例方法包private,包外只能通过反射获取Fieldfield=Unsafe.class.getDeclaredField("theUnsafe");场地。setAccessible(true);Unsafeunsafe=(Unsafe)field.get(null);//测试类是手写的测试类,只有一个String类型的测试类Testtest=newTest();test.ttt="12345";unsafe.putLong(test,12L,2333L);System.out.println(test.value);}运行上面的代码会报致命错误,报错信息为“AfatalerrorhasbeendetectedbytheJavaRuntimeEnvironment:...进程以退出代码134结束(被信号6中断:SIGABRT)”。从报错信息中可以看出虚拟机退出是因为这个fatalerrorabort。原因很简单。我使用unsafe将Test类的value属性的位置设置为一个long值2333,当我使用value属性时,虚拟机将这块内存解析成String对象,原来String对象对象的结构header被打乱,解析对象失败时抛出错误。比较严重的问题是报错信息中并没有类名、行号等信息,所以在复杂的项目中查这个问题就像大海捞针一样。但是Unsafe的其他方法不一定和这对方法一样。在使用的时候可能还需要注意其他的安全问题,我们以后再说。ThreadLocalRandom的实现那么ThreadLocalRandom安全吗?让我们回头看看它的实现。ThreadLocalRandom的实现需要Thread对象的配合。Thread对象中有一个属性threadLocalRandomSeed,它存储了线程专有的随机种子,这个属性在Thread对象中的偏移量是在加载ThreadLocalRandom类时确定的。具体方法是SEED=UNSAFE.objectFieldOffset(Thread.class.getDeclaredField("threadLocalRandomSeed"));我们知道一个对象占用的内存大小是在类加载后决定的,所以使用Unsafe.objectFieldOffset(class,fieldName)可以很安全的使用ThreadLocalRandom获取类中某个属性的偏移量,而当找到偏移量并可以确定数据类型。问题在寻找这些问题的过程中,我也有两个疑惑。第一个使用场景是ThreadLocalRandom。为什么非要用Unsafe来修改Thread对象中的随机种子呢?在Thread对象中添加get/set方法不是更方便吗?stackOverFlow上有人和我有同样的疑问,为什么threadlocalrandom实现的这么奇怪,接受的答案解释说对于jdk开发人员来说,Unsafe和get/set方法就像普通的工具一样,并没有具体的指导原则。这个答案并没有说服我,所以我又开了一个问题。我同意其中的评论,大意是ThreadLocalRandom和Thread不在同一个包中。如果添加了get/set方法,则get/set方法必须设置为public,违反了类的封闭性原则。还有一个关于内存布局的问题,我看到Unsafe.objectFieldOffset可以获取属性在对象内存中的偏移量后,用IDEA中的main方法尝试了上面提到的Test类,发现Test类的唯一属性是value相对于对象内存的偏移量是12,所以我对这12个字节的组成很迷惑。我们知道Java对象的对象头是放在Java对象内存的开头的,对象的MarkWord是对象头的开头。在32位系统中占4个字节,而在64位系统中占8个字节,而我用的是64位系统,无疑会占用8个字节的偏移量。MarkWord后面应该是Test类的类指针和数组对象的长度。数组的长度是4个字节,但是Test类不是数组,没有其他属性。可以排除数据长度,但指针也应该是64位系统。8个字节,为什么只用了4个字节?唯一的可能是虚拟机启用了指针压缩。指针压缩只能在64位系统中启用。启用后,指针类型只需要占用4个字节,但我没有指定使用指针压缩。查了一下,原来1.8之后默认开启了指针压缩。启用时使用-XX:-UseCompressedOops参数后,value的offset变为16。小结写代码的时候要多注意依赖库的具体实现,不然可能会踩到意想不到的坑,多看也无妨,细细研究才能学到更多。
