PrefaceThreadLocal翻译成中文的意思是线程局部变量,也就是说它是一个线程中的私有变量,每个线程只能操作自己的私有变量,所以不会造成线程不安全。所谓线程不安全是指当多个线程同时写入同一个全局变量时(读操作不会涉及线程不安全),如果执行结果与我们预期的结果不一致,则称为线程不安全,否则,它被称为线程安全。Java语言中通常有两种方法来解决线程不安全的问题:使用锁(使用synchronized或Lock);使用线程本地。锁的实现是在多个线程写入全局变量时,通过排队的方式将全局变量一个一个写入,这样可以避免线程不安全的问题。比如我们在使用线程不安全的SimpleDateFormat格式化时间的时候,如果使用锁来解决线程不安全的问题,实现过程是这样的:从上图可以看出,虽然加锁的方法可以解决线程是不安全的,同时又带来了新的问题。使用锁时,线程需要排队执行,因此会带来一定的性能开销。但是如果使用ThreadLocal方法,则为每个线程创建一个SimpleDateFormat对象,这样就可以避免排队执行的问题。其实现过程如下图所示:PS:创建SimpleDateFormat也会消耗一定的时间和空间,如果线程复用SimpleDateFormat的频率比较高,使用ThreadLocal的优势比较大,否则可以考虑使用锁。但是在使用ThreadLocal的过程中,很容易出现内存溢出的问题,比如下面这个例子。什么是内存溢出?内存溢出(OutOfMemory,简称OOM)是指无用对象(不再使用的对象)继续占用内存,或者无用对象的内存不能及时释放,造成内存空间的浪费。称之为内存泄漏。内存溢出代码演示在开始演示ThreadLocal内存溢出问题之前,我们先使用“-Xmx50m”参数设置Idea,意思是设置程序运行的最大内存为50m,如果程序运行超出这个值,会出现内存溢出的问题,设置方法如下:设置后的最终效果是这样的:PS:因为我用的Idea是社区版,可能和你的界面不一样,你只需要点击“EditConfigurations...”找到“VMoptions”选项,设置“-Xmx50m”参数就可以了。配置完Idea,接下来我们来实现业务代码。代码中我们会创建一个大对象,里面会有一个10m的大数组,然后我们将这个大对象存储在ThreadLocal中,然后使用线程池进行5个以上的加法任务,因为最大运行内存为设置为50m,那么理想情况是5次加法运算后,会出现内存溢出问题。实现代码如下:importjava.util.concurrent.LinkedBlockingQueue;importjava.util.concurrent.ThreadPoolExecutor;importjava.util.concurrent.TimeUnit;publicclassThreadLocalOOMExample{/***定义一个10m的class*/staticclassMyTask{//创建一个10m的数组(单位换算为1M->1024KB->1024*1024B)privatebyte[]bytes=newbyte[10*1024*1024];}//DefineThreadLocalprivatestaticThreadLocaltaskThreadLocal=newThreadLocal<>();//主要测试代码publicstaticvoidmain(String[]args)throwsInterruptedException{//创建线程池ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(100));//执行10次调用for(inti=0;i<10;i++){//执行任务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/***线程池执行task*@paramthreadPoolExecutor线程池*/privatestaticvoidexecuteTask(ThreadPoolExecutorthreadPoolExecutor){//执行任务threadPoolExecutor.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println("Createobject");//创建对象(10M)MyTaskmyTask=newMyTask();//StoreThreadLocaltaskThreadLocal.set(myTask);//设置对象为null,表示不再使用这个对象myTask=null;}});}}上面程序的执行结果如下:从上图可以看出,当程序执行到第五次添加对象时,出现内存溢出问题。这是因为最大运行内存设置为50m,每次循环会占用10m内存,加上程序启动会占用一定内存,所以第五次添加任务时,会出现问题内存溢出原因分析内存溢出的问题和解决方法比较简单,重点是“原因分析”,我们要通过内存溢出的问题,找出ThreadLocal为什么会这样?什么导致内存溢出?要搞清楚这个问题(内存溢出问题),需要从ThreadLocal源码入手,所以我们先打开set方法的源码(例子中使用的是set方法),如下:publicvoidset(Tvalue){//获取当前线程Threadt=Thread.currentThread();//根据线程获取ThreadMap变量ThreadLocalMapmap=getMap(t);if(map!=null)map.set(this,value);//StorethecontentinthemapelsecreateMap(t,value);//创建map并将value存入map}从上面的代码可以看出Thread、ThreadLocalMap和set方法的关系:每个线程Thread有一个数据存储容器ThreadLocalMap,当执行ThreadLocal.set方法时,会将存储的值放到ThreadLocalMap容器??中,那么我们看一下ThreadLocalMap的源码:staticclassThreadLocalMap{//ArrayprivateEntry[]table实际存储的data;//存储数据的方法privatevoidset(ThreadLocal>key,Objectvalue){Entry[]tab=table;intlen=tab.length;inti=key.threadLocalHashCode&(len-1);for(Entrye=tab[i];e!=null;e=tab[i=nextIndex(i,len)]){ThreadLocal>k=e.get();//如果有对应的key,直接更新valueif(k==key){e.value=value;return;}//找空位插入valueif(k==null){replaceStaleEntry(key,value,i);return;}}//新建一个Entry插入到数组ta中b[i]=newEntry(key,value);intsz=++size;//判断是否需要扩容if(!cleanSomeSlots(i,sz)&&sz>=threshold)rehash();}//...忽略其他源码}从上面的源码我们可以看出ThreadMap中有一个Entry[]数组来存放所有的数据,而Entry是一个包含key和value的键值对,其中key是ThreadLocal本身,value是将ThreadLocal中存储的值根据以上内容,我们可以得到ThreadLocal相关对象的关系图,如下图:也就是说,它们之间的引用关系是这样的:Thread->ThreadLocalMap->Entry->Key,Value,所以当我们使用线程池存储对象时,由于线程池的生命周期很长,线程池会一直持有value值,那么垃圾回收器就无法回收该值,所以内存会一直存在被占用,导致内存溢出问题。解决方案ThreadLocal内存溢出的解决方案很简单。我们只需要在使用ThreadLocal之后执行remove方法就可以避免内存溢出问题,比如下面的代码:***定义一个10m的类*/staticclassMyTask{//创建一个10m的数组(单位换算为1M->1024KB->1024*1024B)privatebyte[]bytes=newbyte[10*1024*1024];}//定义ThreadLocalprivatestaticThreadLocaltaskThreadLocal=newThreadLocal<>();//测试代码publicstaticvoidmain(String[]args)throwsInterruptedException{//创建线程池ThreadPoolExecutorthreadPoolExecutor=newThreadPoolExecutor(5,5,60,TimeUnit.SECONDS,newLinkedBlockingQueue<>(100));//执行n个调用for(inti=0;i<10;i++){//执行任务executeTask(threadPoolExecutor);Thread.sleep(1000);}}/***线程池执行任务*@paramthreadPoolExecutor线程pool*/privatestaticvoidexecuteTask(ThreadPoolExecutorthreadPoolExecutor){//执行任务threadPoolExecutor.execute(newRunnable(){@Overridepublicvoidrun(){System.out.println("创建对象");尝试{//创建创建对象(10M)MyTaskmyTask=newMyTask();//storeThreadLocaltaskThreadLocal.set(myTask);//其他业务代码...}finally{//释放内存taskThreadLocal.remove();}}});}}同上程序的执行结果如下:从上面的结果可以看出,我们只需要在finally中执行ThreadLocal的remove方法即可,不会出现内存溢出问题。remove的秘密,为什么remove方法有这么大的魔力呢?我们打开remove的源码看看:可以看到,当remove方法之后,Thread中的ThreadLocalMap对象会被直接移除,这样Thread中就不再持有ThreadLocalMap对象了,所以即使Thread一直存活,也不会造成由Thread引起的内存溢出问题(ThreadLocalMap)内存占用。小结在本文中,我们通过代码来演示ThreadLocal内存溢出的问题。内存溢出严格来说不是ThreadLocal的问题,而是ThreadLocal使用不当导致的问题。为了避免ThreadLocal内存溢出的问题,只要在使用ThreadLocal之后调用remove方法即可。不过通过ThreadLocal内存溢出的问题,让我们弄清楚ThreadLocal的具体实现,以便我们以后更好的使用ThreadLocal,更好的应对面试。本文转载自微信公众号“Java中文社区”,可通过以下二维码关注。转载本文请联系Java中文社区公众号。