原文链接:https:mp。weixin。qq。comsrYoAyu4pG2GHZYTuKvn8g前言 最近在写一些业务代码时遇到一个需要产生随机数的场景,这时自然想到jdk包里的Random类。但出于对性能的极致追求,就考虑使用ThreadLocalRandom类进行优化,在查看ThreadLocalRandom实现的过程中,又追了下来Unsafe有部分代码,整个流程下来,学到了不少东西,也通过搜索和提问解决了很多疑惑,于是总结成本文。Random的性能问题 使用Random类时,为了避免重复创建的开销,我们一般将实例化好的Random对象设置为我们所使用服务对象的属性或静态属性,这在线程竞争不激烈的情况下没有问题,但在一个高并发的web服务内,使用同一个Random对象可能会导致线程阻塞。 Random的随机原理是对一个随机种子进行固定的算术和定位运算,得到随机结果,再使用这个结果作为下一次随机的种子。在解决线程安全问题时,Random使用CAS更新下一次随机的种子,可以想到,如果多个线程同时使用这个对象,就肯定会有一些线程执行CAS连续失败,进而导致线程阻塞。ThreadLocalRandom jdk开发者自然考虑到了这个问题,在concurrent包内添加了ThreadLocalRandom类,第一次看到这个类名,我以为它是通过ThreadLocal实现的,进而想到恐怖的内存泄漏问题,但点进源码却没有ThreadLocal不是影子,而是存在着大量Unsafe相关的代码。 我们来看一下它的核心代码: UNSAFE。putLong(tThread。currentThread(),SEED,rUNSAFE。getLong(t,SEED)GAMMA); 翻译成更直观的Java代码就像:ThreadtThread。currentThread();longrUNSAFE。getLong(t,SEED)GAMMA;UNSAFE。putLong(t,SEED,r); 看上去非常眼熟,像我们平常说的Map里getset一样,以Thread。currentThread()获取到的当前对象里key,以SEED随机种子作为value。 但是以对象作为key是可能会造成内存泄漏的啊,由于Thread对象可能会大量创建,在回收时不removeMap里的value时会导致Map越来越大,最后内存溢出来。Unsafe功能 不过再仔细看ThreadLocalRandom类似的核心代码,发现并不是简单的Map操作,它的getLong()方法需要传入两个参数,而putLong()方法需要三个参数,查看源码发现它们都是native方法,我们看不到具体的实现。两个方法签名分别是:publicnativelonggetLong(Objectvar1,longvar2);publicnativevoidputLong(Objectvar1,longvar2,longvar4); 虽然看不到具体实现,但我们可以查得到它们的功能,下面是两个方法的功能介绍:putLong(object,offset,value)可以将object对象内存地址偏移offset后的位置的后四个字节设置为value。getLong(object,offset)会从object对象内存地址偏移offset后的位置读取四个字节作为long型返回。不安全性 作为Unsafe类内的方法,它也透露着一股Unsafe的气息,具体表现就是可以直接操作内存,而不做任何安全校验,如果有问题,则会在运行时抛出FatalError,导致整个虚拟机的退出。 在我们的常识里,get方法是最容易抛异常的地方,比如空指针、类型转换等,但Unsafe。getLong()方法是个非常安全的方法,它从某个内存位置开始读取四个字节,而不管这四个字节是什么内容,总能成功转成long型,至于这个long型结果是不是跟业务匹配就是另一回事了。而set方法也是比较安全的,它把某个内存位置之后的四个字节覆盖成一个long型的值,也几乎不会出错。 那么这两个方法不安全在哪呢? 它们的不安全并不是在这两个方法执行期间报错,而是未经保护地改变内存,会引起别的方法在使用这一段内存时报错。publicstaticvoidmain(String〔〕args)throwsNoSuchFieldException,IllegalAccessException{Unsafe设置了构造方法私有,getUnsafe获取实例方法包私有,在包外只能通过反射获取FieldfieldUnsafe。class。getDeclaredField(theUnsafe);field。setAccessible(true);Unsafeunsafe(Unsafe)field。get(null);Test类是一个随手写的测试类,只有一个String类型的测试类TesttestnewTest();test。ttt12345;unsafe。putLong(test,12L,2333L);System。out。println(test。value);} 运行上面的代码会得到一个fatalerror,报错信息为AfatalerrorhasbeendetectedbytheJavaRuntimeEnvironment:Processfinishedwithexitcode134(interruptedbysignal6:SIGABRT)。 可以从报错信息中看到虚拟机因为这个fatalerrorabort退出了,原因也很简单,我没有使用unsafe将Test类value属性的位置设置成了long型值2333,而当我使用value属性时,虚拟机会将这一块内存解析为String对象,原String对象对象头的结构被打乱了,解析对象失败抛出了错误,更严重的问题是报错信息中没有类名行号等信息,在复杂项目中排查这种问题真如同大海捞针。 不过Unsafe的其他方法可不一定像这一对方法一样,使用他们时可能需要注意另外的安全问题,之后有遇到再说。ThreadLocalRandom的实现 那么ThreadLocalRandom是不是安全的呢,再回过头来看一下它的实现。 ThreadLocalRandom的实现需要Thread对象的配合,在Thread对象内存在着一个属性threadLocalRandomSeed,它保存着这个线程专属的随机种子,而这个属性在Thread对象的offset,是在ThreadLocalRandom类加载时就确定了的,具体方法是SEEDUNSAFE。objectFieldOffset(Thread。class。getDeclaredField(threadLocalRandomSeed)); 我们知道一个对象所占用的内存大小在类被加载后就确定了的,所以使用Unsafe。objectFieldOffset(class,fieldName)可以获取到某个属性在类中偏移量,而在找对了偏移量,又能确定数据类型时,使用ThreadLocalRandom就是很安全的。疑问 在查找这些问题的过程中,我也产生了两个疑问点。使用场景 首先就是ThreadLocalRandom为什么非要使用Unsafe来修改Thread对象内的随机种子呢,在Thread对象内添加getset方法不是更方便吗? stackOverFlow上有人跟我同样的疑问,whyisthreadlocalrandomimplementedsobizarrely,被采纳的答案里解释说,对jdk开发者来说Unsafe和getset方法都像普通的工具,具体使用哪一个并没有一个准则。这个答案并没有说服我,于是我另开了一个问题,里面的一个评论我比较认同,大意是ThreadLocalRandom和Thread不在同一个包下,如果添加getset方法的话,getset方法必须设置为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的偏移量变成了16。小结 在写代码时还是要多注意查看依赖库的具体实现,不然可能踩到意想不到的坑,而且多看看并没有坏处,仔细研究一下还能学到更多。