竹笋

首页 » 问答 » 问答 » 快速掌握并发编程深入学习Thread
TUhjnbcbe - 2023/3/2 8:43:00

生活中的ThreadLocal

考试题只有一套,老师把考试题打印出多份,发给每位考生,然后考生各自写各自的试卷。考生之间不能相互交头接耳(会当做作弊)。各自写出来地答案不会影响他人的分数。

注意:考试题、考生、试卷。

用代码来实现:

publicclassThreadLocalDemo{//线程共享变量localVarpublicstaticThreadLocalStringlocalVar=newThreadLocal();staticvoidprint(Stringstr){//打印当前线程中本地内存中本地变量的值System.out.println(str+:+localVar.get());//清除本地内存中的本地变量localVar.remove();}publicstaticvoidmain(String[]args){Threadt1=newThread(newRunnable(){

Overridepublicvoidrun(){//设置线程1中本地变量的值localVar.set(全部写完);StringthreadName=Thread.currentThread().getName();//调用打印方法print(threadName);}},张三);Threadt2=newThread(newRunnable(){

Overridepublicvoidrun(){//设置线程2中本地变量的值localVar.set(写了一半);StringthreadName=Thread.currentThread().getName();//调用打印方法print(threadName);}},李四);Threadt3=newThread(newRunnable(){

Overridepublicvoidrun(){//设置线程2中本地变量的值localVar.set(完全没写);StringthreadName=Thread.currentThread().getName();//调用打印方法print(threadName);}},王二);t1.start();t2.start();t3.start();}}输出

李四:写了一半王二:完全没写张三:全部写完

背景

ThreadLocal:字面意思为线程本地或者本地线程。但是其实真正含义并非如此,真正的含义是线程本地变量(副本)。

java.lang.ThreadLocal是JDK1.2版本的时候引入的,本文是基于JDK1.8版本进行讲解的。

上面考试场景中的几个关键点我们这么可以这么理解:

考试题----共享变量,大家共享试卷-----考试题的副本考试----线程

ThreadLocal可以理解为每个线程想绑定自己的东西,相互不受干扰。比如上面的考试场景,考试题大家都是一样的。但是考试题进行复印出来后,每人一份,各自写写各自的,相互不受影响,这就正是ThreadLocal想要实现的功能。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。

可以想想生活中还有没有类似的例子。肯定非常多,只要我们用心去体会。

下面我们就来看看ThreadLocal到底是如何实现的。

ThreadLocal设计原理

ThreadLocal名字中第一个单词Thread表示线程,Local表示本地,我们就理解为线程本地变量了。想了解更多Thread,可看:快速掌握并发编程---Thread常用方法

先看看ThreadLocal的整体

最关心的三个公有方法:set、get、remove

构造方法

publicThreadLocal(){}构造方法里没有任何逻辑处理,就是简单的创建一个实例。

set方法

源码为

publicvoidset(Tvalue){//获取当前线程Threadt=Thread.currentThread();//这是什么*?ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);elsecreateMap(t,value);}先看看ThreadLocalMap是个什么东东

ThreadLocalMap是ThreadLocal的静态内部类。

set方法整体为

ThreadLocalMap构造方法

//这个属性是ThreadLocal的,就是获取hashcode(这列很有学问,但是我们的目的不是他)privatefinalintthreadLocalHashCode=nextHashCode();privateEntry[]table;privatestaticfinalintINITIAL_CAPACITY=16;//Entry是一个弱引用staticclassEntryextendsWeakReferenceThreadLocal?{Objectvalue;Entry(ThreadLocal?k,Objectv){super(k);value=v;}}ThreadLocalMap(ThreadLocal?firstKey,ObjectfirstValue){//数组默认大小为16table=newEntry[INITIAL_CAPACITY];//len为2的n次方,以ThreadLocal的计算的哈希值按照Entry[]取模(为了更好的散列)inti=firstKey.threadLocalHashCode(INITIAL_CAPACITY-1);table=newEntry(firstKey,firstValue);size=1;//设置阈值(扩容阈值)setThreshold(INITIAL_CAPACITY);}然后我们看看map.set()方法中是如何处理的

privatevoidset(ThreadLocal?key,Objectvalue){Entry[]tab=table;intlen=tab.length;//len为2的n次方,以ThreadLocal的计算的哈希值按照Entry[]取模inti=key.threadLocalHashCode(len-1);//找到ThreadLocal对应的存储的下标,如果当前槽内Entry不为空,//即当前线程已经有ThreadLocal已经使用过Entryfor(Entrye=tab;e!=null;e=tab[i=nextIndex(i,len)]){ThreadLocal?k=e.get();//当前占据该槽的就是当前的ThreadLocal,更新value结束if(k==key){e.value=value;return;}//当前卡槽的弱引用可能会回收了,key:nullvalue:xxxObject,//需清理Entry原来的value,便于垃圾回收value,且将新的value放在该槽里,结束if(k==null){replaceStaleEntry(key,value,i);return;}}//在这之前没有ThreadLocal使用Entry,并进行值存储tab=newEntry(key,value);//累计Entry所占的个数intsz=++size;//清理key为null的Entry,可能需要扩容,扩容长度为原来的2倍,并需要进行重新hashif(!cleanSomeSlots(i,sz)sz=threshold){rehash();}}从上面这个set方法,我们就大致可以把这三个进行一个关联了:

Thread、ThreadLocal、ThreadLocalMap。

get方法

remove方法

expungeStaleEntry方法代码里有点大,所以这里就贴了出来。

//删除陈旧entry的核心方法privateintexpungeStaleEntry(intstaleSlot){Entry[]tab=table;intlen=tab.length;tab[staleSlot].value=null;//删除valuetab[staleSlot]=null;//删除entrysize--;//map的size自减//遍历指定删除节点,所有后续节点Entrye;inti;for(i=nextIndex(staleSlot,len);(e=tab)!=null;i=nextIndex(i,len)){ThreadLocal?k=e.get();if(k==null){//key为null,执行删除操作e.value=null;tab=null;size--;}else{//key不为null,重新计算下标inth=k.threadLocalHashCode(len-1);if(h!=i){//如果不在同一个位置tab=null;//把老位置的entry置null(删除)//从h开始往后遍历,一直到找到空为止,插入while(tab[h]!=null){h=nextIndex(h,len);}tab[h]=e;}}}returni;}

对象引用

在Java里万事万物皆对象,这里有个对象,那么对象引用是什么呢?

Useruser=newUser(老田);关于上面这段代码的解释,很大部分人会说user是个对象。

一开始培训机构什么书籍里都说user是个对象,于是也就这么叫user是对象,这里的user指向了对象老田。这里的Useruser是定义了一个对象引用,可以指向任意的User对象,比如:

Useruser;user=newUser(张三);user=newUser(李四);一个队对象被user引用了,这里user把他叫做对象引用。

对象引用就好比男人,对象就是男人的老婆。根据目前我国法律规定,一个男人在任何时候最多只能有一个老婆,但是一辈子可以娶多个老婆。哈哈哈!!!

另外如果是下面

inta;a=1;a=;这里的a,我们通常称之为变量。所以上面的user我们也可以理解为变量。

在Java里对象的引用也是分几种类型的,分以下四种类型:

强引用软引用弱引用虚引用

强引用

强引用就是我们平时开发中用的最多的,比如说:

Personperson=newPerson(老田);这个person就是强引用。

当一个对象被强引用时候,JVM垃圾回收的时候是不会回收的,宁愿执行OOM(OutOfMemory)异常也绝不回收,因为JVM垃圾回收的时候会认为这个对象是被用户正在使用,若回收了很有可能造成无法想象的错误。

软引用

如果一个对象具有软引用,内存空间足够,JVM垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。

使用软引用能防止内存泄露,增强程序的健壮性。

java.lang.ref.SoftReference的特点是它的一个实例保存对一个Java对象的软引用,该软引用的存在不妨碍垃圾收集线程对该Java对象的回收。

也就是说,一旦SoftReference保存了对一个Java对象的软引用后,在垃圾线程对这个Java对象回收前,SoftReference类所提供的get()方法返回Java对象的强引用。

/***Returnsthisreferenceobjectsreferent.Ifthisreferenceobjecthas*beencleared,eitherbytheprogramorbythegarbagecollector,then*thismethodreturnscodenull/code.**

returnTheobjecttowhichthisreferencerefers,or*codenull/codeifthisreferenceobjecthasbeencleared*/publicTget(){To=super.get();if(o!=nullthis.timestamp!=clock)this.timestamp=clock;returno;}如果引用对象被清除或者被GC回收,这个get方法就返回null。

弱引用

弱引用也是用来描述非必需对象的,当JVM下一次进行垃圾回收时,无论内存是否充足,都会回收被弱引用关联的对象。在java中,用java.lang.ref.WeakReference类来表示。

与软引用不同的是,不管是否内存不足,弱引用都会被回收。

弱引用可以结合来使用,当由于系统触发gc,导致软引用的对象被回收了,JVM会把这个弱引用加入到与之相关联的ReferenceQueue中,不过由于垃圾收集器线程的优先级很低,所以弱引用不一定会被很快回收。

虚引用

虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期。在java中用java.lang.ref.PhantomReference类表示。如果一个对象与虚引用关联,则跟没有引用与之关联一样,在任何时候都可能被垃圾回收器回收。

注意:虚引用必须和引用队列关联使用,当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。如果程序发现某个虚引用已经被加入到引用队列,那么就可以在所引用的对象的内存被回收之前采取必要的行动。

好了上面就大概说了一下对象的四大引用,主要本文后面需要用到弱引用。

ThreadLocal内存泄漏

讲到内存泄漏,那我们还是把内存溢出和内存泄漏大致说一下。

内存溢出

在JVM如果发生内存溢出,说明内存不够实用,撑爆了,也就是我们说的OOM。大量内存得不到释放,又不断申请内存空间。

系统内存使用M,已经使用了M,可是你说你还想使用50M,于是系统就受不了。就像气球一样,原本已经到极限了,你还是使劲打气,很容易就导致气球爆炸了。就想你只能扛斤的东西,现在给你斤,肯定受不了。

内存泄漏

强引用所指向的对象不会被回收,可能导致内存泄漏,虚拟机宁愿抛出OOM也不会去回收他指向的对象。前面说到强引用的时候,如果对象一直被引用,JVM是不会回收他的,直到最后系统OOM。

看过《树先生》电影的人都知道,树先生家里的地被别人占用了,但是树先生不敢把人家怎么样。如果是很多人都去占用树先生家的地和财产,到最后树先生不就要饿死么。树先生这部电影确实好看,看完一遍基本上不知道在说什么,主要是树先生幻想得太多,很多人看了两遍也不是很懂。扯远了。。。

ThreadLocal内存泄漏

内存泄漏案例

模拟了一个线程数为THREAD_LOOP_SIZE的线程池,所有线程共享一个ThreadLocal变量,每一个线程执行的时候插入一个大的List集合,这里由于执行了次循环,也就是产生了个线程,每一个线程都会依附一个ThreadLocal变量:

publicclassThreadLocalOOMDemo{privatestaticfinalintTHREAD_LOOP_SIZE=;privatestaticfinalintMOCK_BIG_DATA_LOOP_SIZE=00;privatestaticThreadLocalListUserthreadLocal=newThreadLocal();publicstaticvoidmain(String[]args)throwsInterruptedException{ExecutorServiceexecutorService=Executors.newFixedThreadPool(THREAD_LOOP_SIZE);for(inti=0;iTHREAD_LOOP_SIZE;i++){executorService.execute(()-{threadLocal.set(newThreadLocalOOMDemo().addBigList());Threadt=Thread.currentThread();System.out.println(Thread.currentThread().getName());//threadLocal.remove();//不取消注释的话就可能出现OOM});try{Thread.sleep(0L);}catch(InterruptedExceptione){e.printStackTrace();}}//executorService.shutdown();}privateListUseraddBigList(){ListUserparams=newArrayList(MOCK_BIG_DATA_LOOP_SIZE);for(inti=0;iMOCK_BIG_DATA_LOOP_SIZE;i++){params.add(newUser(Java后端技术全栈,+i,man,i));}returnparams;}classUser{privateStringuserName;privateStringpassword;privateStringsex;privateintage;publicUser(StringuserName,Stringpassword,Stringsex,intage){this.userName=userName;this.password=password;this.sex=sex;this.age=age;}}}在设置IDEA或者eclipse中,设置JVM参数设置最大内存为-Xmx64m,以便模拟出OOM:

然后,运行上面的案例

从上面的案例中我们看到:线程池中的每一个线程使用完ThreadLocal对象之后再也不用,由于线程池中的线程不会退出,线程池中的线程的存在,同时ThreadLocal变量也会存在,占用内存!造成OOM溢出!

前面我们分析了Thread、ThreadLocal、ThreadLocalMap三者的关系

一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap中可以有多个ThreadLocal对象,其中一个ThreadLocal对象对应一个ThreadLocalMap中一个的Entry(也就是说:一个Thread可以依附有多个ThreadLocal对象)。

总结

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。

ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

值得注意的是图中的虚线,表示ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用的对象在GC时会被回收。

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统GC的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:

ThreadRef-Thread-ThreaLocalMap-Entry-value

永远无法回收,造成内存泄漏。

注意:其实在ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是如果上述代码中的这行代码

threadLocal.remove();把注释放开,这不会抛出OOM。

另外,网上很多文章都说这是由于弱引用导致的,个人认为不能把锅扔给弱引用,这和使用者有直接关系。如果使用得当是不会出现OOM的。

由于Thread中包含变量ThreadLocalMap,因此ThreadLocalMap与Thread的生命周期是一样长,如果都没有手动删除对应key,都会导致内存泄漏。但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除。因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

那为什么使用弱引用而不是强引用??

key使用强引用

当ThreadLocalMap的key为强引用回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

key使用弱引用

当ThreadLocalMap的key为弱引用回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。当key为null,在下一次ThreadLocalMap调用set(),get(),remove()方法的时候会被清除value值。

1