大家好,今天来为大家分享文件上传网站源码分享错误的一些知识点,和文件上传网站源码分享错误怎么解决的问题解析,大家要是都明白,那么可以忽略,如果不太清楚的话可以看看本篇文章,相信很大概率可以解决您的问题,接下来我们就一起来看看吧!
欢迎大家关注我的微信公众号【老周聊架构】,Java后端主流技术栈的原理、源码分析、架构以及各种互联网高并发、高性能、高可用的解决方案。
一、前言
写这篇文章的目的是因为现在网上很多关于ThreadLocal的文章,很大一部分都不太准确。
比如说:
ThreadLocal内部有个map,键为线程对象;ThreadLocal的数据结构是个数组;还有说ThreadLocal存在内存泄露,但里面的get、set以及remove方法能防止ThreadLocal内存泄露问题。
都是不准确的哈,太误导人了。这里老周先来点开胃小菜,先说一下第一个问题。
1.1ThreadLocal的内部ThreadLocalMap,键为ThreadLocal。
那些说键为Thread对象的,它们不看源码的吗?
大家产生错误的认知,我猜测是ThreadLocal的set方法引起的。下面判断map为null的话,不是把当前线程t当作key设置进去吗?
事实真的是这样吗?源码也要看仔细点吧,继续跟createMap方法,你会发现:
t.threadLocals其实是Thread内部的ThreadLocalMap,这里正在给Thread的ThreadLocalMap赋值呢,而且ThreadLocalMap的key是this,也就是当前ThreadLocal,而不是Thread。
好了第一个错误我们搞清楚了,我们继续来搞清楚第二个错误。
1.2ThreadLocal的数据结构是个环形数组
说ThreadLocal的数据结构是个数组也是没有仔细看源码的,好多文章画的图说ThreadLocal的数据结构是数组,太误导人了。
这里老周给出正确的ThreadLocal的数据结构
1.2.1ThreadLocal的数据结构
ThreadLocalMap维护了Entry环形数组,数组中元素Entry的逻辑上的key为某个ThreadLocal对象(实际上是指向该ThreadLocal对象的弱引用),value为代码中该线程往该ThreadLoacl变量实际塞入的值。
1.2.2ThreadLocal类结构
1.2.3Thread、ThreadLocal、ThreadLocalMap的关系
1.3ThreadLocal里的get、set以及remove方法能保证不内存泄露吗?
这个问题使我们本文重点分析的问题,这里老周先说下结论。
get,set两个方法都不能完全防止内存泄漏,还是每次用完ThreadLocal都勤奋的remove一下靠谱。
再详细分析这个问题之前,我们下面会来看下所需的前置知识。
二、内存泄露
2.1什么是内存泄露?
首先你得知道什么叫内存泄露吧,不然后面分析会很吃力。
内存泄漏(MemoryLeak)是指程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
站在Java的角度来说,就是JVM创建的对象永远都无法访问到,但是GC又不能回收对象所占用的内存。少量的内存泄漏并不会出现什么严重问题,无非是浪费了一些内存资源罢了,但是随着时间的积累,内存泄漏的越来越多就会导致内存溢出,程序崩溃。
2.2Java四中引用类型
在JDK1.2之前,“引用”的概念过于狭隘,如果Reference类型的数据存储的是另外一块内存的起始地址,就称该Reference数据是某块地址、对象的引用,对象只有两种状态:被引用、未被引用。
这样的描述未免过于僵硬,对于这一类对象则无法描述:内存足够时暂不回收,内存吃紧时进行回收。例如:缓存数据。
在JDK1.2之后,Java对引用的概念做了一些扩充,将引用分为四种,由强到弱依次为:
强引用在Java中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。软引用软引用需要用SoftReference类来实现,对于只有软引用的对象来说,当系统内存足够时它不会被回收,当系统内存空间不足时它会被回收。软引用通常用在对内存敏感的程序中。弱引用弱引用需要用WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,总会回收该对象占用的内存。虚引用虚引用需要PhantomReference类来实现,它不能单独使用,必须和引用队列联合使用。虚引用的主要作用是跟踪对象被垃圾回收的状态。
三、为什么ThreadLocalMap采用开放地址法来解决哈希冲突
JDK中大多数的类都是采用了链地址法来解决hash冲突,为什么ThreadLocalMap采用开放地址法来解决哈希冲突呢?首先我们来看看这两种不同的方式:
3.1链地址法
这种方法的基本思想是将所有哈希地址为i的元素构成一个称为同义词链的单链表,并将单链表的头指针存在哈希表的第i个单元中,因而查找、插入和删除主要在同义词链中进行。列如对于关键字集合{28,93,90,3,21,11,19,31,18},我们假如数组的长度为8,那我们用8为除数,进行除留余数法:
3.2开放地址法
这种方法的基本思想是一旦发生了冲突,就去寻找下一个空的散列地址(这非常重要,源码都是根据这个特性,必须理解这里才能往下走),只要散列表足够大,空的散列地址总能找到,并将记录存入。
我们还是以上面的关键字集合{28,93,90,3,21,11,19,31,18}来演示,我们用散列函数f(key)=keymod16。当计算前S个数{28,93,90,3,21,11}时,都是没有冲突的散列地址,直接存入(蓝色下标代表已存入了数据,空白的下标可以存放数据):
在这里插入图片描述
当计算到集合中的19的时候,发现f(19)=3,此时就与3所在的位置冲突。
于是我们应用上面的公式f(19)=(f(19)+1)mod10=4。于是将19存入下标为4的位置。这其实就是房子被人买了于是买下一间的作法,以此类推。
3.3链地址法和开放地址法的优缺点
3.3.1链地址
优点:
处理冲突简单,且无堆积现象,平均查找长度短;链表中的结点是动态申请的,适合构造表不能确定长度的情况;相对而言,拉链法的指针域可以忽略不计,因此较开放地址法更加节省空间;插入结点应该在链首,删除结点比较方便,只需调整指针而不需要对其他冲突元素作调整。
缺点:
指针占用较大空间时,会造成空间浪费。
3.3.2开放地址法
优点:
当节点规模较少,或者装载因子较少的时候,使用开发寻址较为节省空间,如果将链式表的指针用于扩大散列表的规模时,可使得装载因子变小从而减少了开放寻址中的冲突,从而提高平均查找效率。
缺点:
容易产生堆积问题;不适于大规模的数据存储;结点规模很大时会浪费很多空间;散列函数的设计对冲突会有很大的影响;插入时可能会出现多次冲突的现象,删除的元素是多个冲突元素中的一个,需要对后面的元素作处理,实现较复杂。
3.4ThreadLocalMap采用开放地址法原因??
ThreadLocal中看到一个属性HASH_INCREMENT=0x61c88647,0x61c88647是一个神奇的数字,让哈希码能均匀的分布在2的N次方的数组里,即Entry[]table,关于这个神奇的数字网上有很多解析,这里就不多说了。ThreadLocal往往存放的数据量不会特别大(而且key是弱引用又会被垃圾回收,及时让数据量更小),这个时候开放地址法简单的结构会显得更省空间,同时数组的查询效率也是非常高,加上第一点的保障,冲突概率也低。
四、ThreadLocal内存泄露
上面我们已经说了内存泄露的概念,这里我还是再说下ThreadLocal的内存泄露是怎么回事。
根据ThreadLocal的源码,可以画出一些对象之间的引用关系图,实线表示强引用,虚线表示弱引用:
如上图,ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用它,那么系统gc的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:
ThreadRef->Thread->ThreaLocalMap->Entry->value
永远无法回收,造成内存泄露。
那Java源码团队就没有啥解决方案吗?当然有,前面已经说过,由于key是弱引用,因此ThreadLocal可以通过key.get()==null来判断key是否已经被回收,如果key被回收,就说明当前Entry是一个废弃的过期节点,ThreadLocal会自发的将其清理掉。
ThreadLocal会在以下过程中清理过期节点:
调用set()方法时,采样清理、全量清理,扩容时还会继续检查。调用get()方法,没有直接命中,向后环形查找。调用remove()时,除了清理当前Entry,还会向后继续清理。
那么正好回到我们前言的第三个问题:
还有说ThreadLocal存在内存泄露,但里面的get、set以及remove方法能防止ThreadLocal内存泄露问题。
那么老周的问题是:get、set以及remove方法真的能防止ThreadLocal内存泄露吗?
这里你自己可以翻看源码思考下再来看我接下写的,这样有思考收获才更大。
这里分界线假装你思考完了哈,那我们就来讲本文的最重要的一部分了。
事先约定:
4.1remove方法能否防止内存泄露?
一开始都是有效的entry,并且每个entry的key通过散列算法后算出的位置都是自己所在的位置(都在自己的位置上的话之后的线性清扫中不会造成搬移,因为ThreadLocalMap的散列表用的是开放地址法,所以entry可能因为hash冲突而不在自己位置上)
要达成下面的效果,就要一直没有失效的entry出现,并且一直实现插入,也就是一直执行set方法。
假设entry循环数组有16个槽位
如果执行一次remove,把图中的某个entry无效化。??
然后我们来看下ThreadLocalset方法
4.2.1代码块①
遇到key和我们当前调用set的ThreadLocal相等的entry,则只用直接把entry的value设置一下就好了,和HashMap的put(key,A);put(key,B);中A被替换成B同理。
4.2.2代码块②
遇到无效entry,是我们重点关注的地方。
4.2.3代码块③
遇到空槽,直接插入,并且尝试指数清扫,如果指数清扫不成功并且当前entry的使用槽数到达阈值则重散列。
我们重点来关注代码块②
这里方便演示,我假设下标9为有效entry,也正好是set方法的位置。
我们接着上面的代码块②分析,要调用到replaceStaleEntry方法。
下标9为有效entry的话,我们在remove方法中说过,set不是在自己原本的位置上,而是被hash冲突到其它位置上了,则把它们搬去离原本位置最近的后边空槽上。即下标12为hash冲突的有效entry,我们标为绿色下标12。
第一个for循环中,开始向前找,找到最靠前的无效entry,直到遇到空槽为止,当然可能会绕循环数组一圈绕回来,但是因为使用的槽数如果到达阈值,就会rehash,不可能所有槽都用完,所以会遇到空槽的。
如下图:
因为没有找到,所以slotToExpunge=staleSlot,也就是上图红色下标10位置的entry。
接着往下看:
我们重点关注k==key的情况,也就是i遍历到图中绿色下标12槽位的情况。这种情况下会执行一次线性清扫,然后执行对数清扫,最后返回。
如下图:
从slotToExpunge位置开始,先进行一轮线性清扫:
和之前一样,一上来先把待清扫槽位置空(红色下标12无效entry的位置),之后遇到红色下标12后面那个空位(也就是黑色下标14的空位),所以停下来。线性清扫返回空位的下标做为参数传给对数清扫。
对数清扫:清扫次数=log2(N),N是循环数组大小,本例中是16,所以要清扫4次,每次清扫是通过调用线性清扫实现的。
这里你可能会问了,为啥这里是对数清扫了?而且清扫次数为啥是log2(N)啊?
别着急,源码是个好东西,源码写的很清楚了。
老周你不要骗我,这里明明是循环数组的长度啊。
哈哈,没错,确实是循环数组的长度,作者想表达的是循环数组的长度是扫描控制的参数,具体的清扫参数是log2(N)。等等,那清扫参数log2(N)怎么来的呢?不着急,源码往下看,看到while循环了吧,while((n>>>=1)!=0),没错就是这个得到清扫参数。我们例子的循环数组长度为16,代入到里面去,无符号向右移动一位,直到等于0跳出循环。16->8->4->2->1,共循环4次,并且只有遇到无效entry时才执行线性清扫。
注:老周这里为了严谨,还是要再提一嘴。这里的n是循环数组的长度,只是replaceStaleEntry方法调用时,但当从插入调用时,这个参数n是元素的数量。
显然,4次扫描中都没有无效entry。
这里removed返回false,接着cleanSomeSlots返回,一直返回到replaceStaleEntry,并且继续返回,最后从set方法返回。
结果显而易见,红色下标5位置的无效entry未被清除,导致内存泄露。
结论:
set方法的清扫程度不够深,set方法并不能防止内存泄漏。
4.3get方法能否防止内存泄露?
直接跟进getEntry方法:
get方法相对来说简单点,在原本属于当前key的位置上找不到当前key的entry的话,就会根据开放地址法线性遍历找到key对应的entry。
k==null的话,执行线性清扫expungeStaleEntry方法,顺便把路上无效的entry清除掉。
还是看我们上面的例子:
根据前面说的遇到的entry是有效的,但是不是在自己原本的位置上,而是被hash冲突到其它位置上了,则把它们搬去离原本位置最近的后边空槽上。这样在get的时候,会最快找到这个entry,减少开放地址法遍历数组的时间。所以蓝色下标12的位置会搬移到黑色下标10号位置。
搬移后的图:
因为是直接取线性清扫开始的位置,所以k=key是true,所以返回绿色10号位置的entry,查找成功。
结果显而易见,红色下标5位置的无效entry未被清除,导致内存泄露。
结论:
get方法的清扫程度不够深,get方法并不能防止内存泄漏。
五、总结
本文主要以市面上关于ThreadLocal都不太准确的文章进行了一番论证并给出正确的结论。特别重点介绍了ThreadLocal中ThreadLocalMap的大致实现原理以及ThreadLocal内存泄露的问题。
ThreadLocalMap的Entry的key是弱引用,如果外部没有强引用指向key,key就会被回收,而value由于Entry强引用指向了它,导致无法被回收,但是value又无法被访问,因此发生内存泄漏。
关于内存泄漏,我们重点从源码层面分析了get、set、remove方法,并图文并茂的演示了get、set方法不能防止内存泄漏,而remove方法能防止内存泄漏的结论。
所以,开发者应该尽量在使用完毕后及时调用remove删除节点,这里老周建议用Spring的AOP思想对remove方法进行切面,省的使用完毕后忘记调用remove方法清除。
END,本文到此结束,如果可以帮助到大家,还望关注本站哦!