ThreadLocal的使用及内存泄漏问题分析

Java资料站

共 13536字,需浏览 28分钟

 ·

2021-04-22 13:31

点击上方蓝色字体,选择“标星公众号”

优质文章,第一时间送达

ThreadLocal原理概述

每一个Thread对象均含有一个ThreadLocalMap类型的成员变量threadLocals,它存储当前线程中所有ThreadLocal对象及其对应的值。部分源码如下

public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
 }

而ThreadLocalMap中的核心就是一个个Entry对象,以下是ThreadLocalMap的部分源码

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }

我们通过一张图来清楚地表示ThreadLocal的引用关系


ThreadLocal简单应用

业务需求:有10位顾客去餐厅分别点了1~10十款套餐,要求每个顾客都能准确拿到自己点的套餐


代码示例(不使用ThreadLocal的情况)

public class Demo {

    //餐厅菜单
    private String menu;

    private String getMenu() {
        return menu;
    }

    private void setMenu(String menu) {
        this.menu = menu;
    }


    public static void main(String[] args) {
        final Demo demo = new Demo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                 //顾客点餐
                    demo.setMenu(Thread.currentThread().getName() + "的套餐");
                    System.out.println("----------------------");
                    //顾客取餐
                    System.out.println(Thread.currentThread().getName() + "拿走了" + "["+demo.getMenu()+"]");
                }
            });
            thread.setName("顾客" + i);
            thread.start();
        }
    }
}

运行结果(尝试多运行几次,排除程序运行结果出现的偶然性)

D:\installPath\Java\jdk1.8.0_121\bin\java.exe "-javaagent:D:\installPath\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar=56135:D:\installPath\IntelliJ IDEA 2019.1.4\bin" -Dfile.encoding=UTF-8 -classpath D:\installPath\Java\jdk1.8.0_121\jre\lib\charsets.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\deploy.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\javaws.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jce.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfr.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jsse.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\management-agent.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\plugin.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\resources.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\Users\13209\IdeaProjects\spring-study\target\classes com.kang.controller.Demo
----------------------
----------------------
----------------------
顾客6拿走了[顾客5的套餐]
----------------------
----------------------
----------------------
顾客9拿走了[顾客9的套餐]
顾客3拿走了[顾客7的套餐]
----------------------
----------------------
顾客8拿走了[顾客10的套餐]
顾客4拿走了[顾客6的套餐]
----------------------
顾客1拿走了[顾客6的套餐]
顾客5拿走了[顾客10的套餐]
顾客7拿走了[顾客10的套餐]
----------------------
顾客2拿走了[顾客9的套餐]
顾客10拿走了[顾客10的套餐]

Process finished with exit code 0

我们开启了10个线程分别表示10个顾客,由于线程之间相互没有隔离,导致很多套餐被拿错,甚至有些套餐没人拿,这显然不符合我们的业务需求。那么我们在程序中引入ThreadLocal对象来看一下运行结果


代码示例(引入ThreadLocal对象)

public class Demo {
    //引入ThreadLocal对象
    ThreadLocal<String> threadLocal=new ThreadLocal();

    //餐厅菜单
    private String menu;

    private String getMenu() {
        //return menu;
        return threadLocal.get();
    }

    private void setMenu(String menu) {
        //this.menu = menu;
        //通过ThreadLocal对象将变量menu绑定到当前线程
        threadLocal.set(menu);
    }


    public static void main(String[] args) {
        final Demo demo = new Demo();
        for (int i = 1; i <= 10; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                 //顾客点餐
                    demo.setMenu(Thread.currentThread().getName() + "的套餐");
                    System.out.println("----------------------");
                    //顾客取餐
                    System.out.println(Thread.currentThread().getName() + "拿走了" + "["+demo.getMenu()+"]");
                }
            });
            thread.setName("顾客" + i);
            thread.start();
        }
    }
}

运行结果(尝试多运行几次,排除程序运行结果出现的偶然性)

D:\installPath\Java\jdk1.8.0_121\bin\java.exe "-javaagent:D:\installPath\IntelliJ IDEA 2019.1.4\lib\idea_rt.jar=56301:D:\installPath\IntelliJ IDEA 2019.1.4\bin" -Dfile.encoding=UTF-8 -classpath D:\installPath\Java\jdk1.8.0_121\jre\lib\charsets.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\deploy.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\access-bridge-64.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\cldrdata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\dnsns.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jaccess.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\jfxrt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\localedata.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\nashorn.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunec.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunjce_provider.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunmscapi.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\sunpkcs11.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\ext\zipfs.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\javaws.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jce.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfr.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jfxswt.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\jsse.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\management-agent.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\plugin.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\resources.jar;D:\installPath\Java\jdk1.8.0_121\jre\lib\rt.jar;D:\Users\13209\IdeaProjects\spring-study\target\classes com.kang.controller.Demo
----------------------
----------------------
----------------------
顾客2拿走了[顾客2的套餐]
顾客3拿走了[顾客3的套餐]
----------------------
----------------------
顾客1拿走了[顾客1的套餐]
----------------------
----------------------
----------------------
顾客5拿走了[顾客5的套餐]
----------------------
顾客7拿走了[顾客7的套餐]
----------------------
顾客6拿走了[顾客6的套餐]
顾客8拿走了[顾客8的套餐]
顾客9拿走了[顾客9的套餐]
顾客10拿走了[顾客10的套餐]
顾客4拿走了[顾客4的套餐]

Process finished with exit code 0


这样的设计便实现了业务需求,由此我们可以看出ThreadLocal类的作用是:提供线程内的局部变量,不同线程之间不会相互干扰,这种变量仅在线程的生命周期内起作用。


ThreadLocal的内存泄漏问题

首先我们先了解一下内存泄漏和内存溢出的概念

内存泄漏(Memory Leak):程序中已动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。我们都知道内存泄漏的堆积终将导致严重的内存溢出。

内存溢出(Out Of Memory):无法给申明的对象提供足够的内存空间,程序无法再正常运行,也就是我们常说的OOM。


说到这,我们顺便来回顾一下Java对象的四大引用

强引用(Strong Reference):最普遍使用的引用,垃圾回收器不会回收一个持有强引用的对象。即使内存空间不足时,Java虚拟机宁愿抛出Out Of Memory Error 错误,终止程序运行,也不会依靠回收具有强引用的对象来解决内存空间不足的问题。

软引用(Soft Reference):一个只持有软引用的对象,只要内存空间充足,垃圾回收器就不会回收它,一旦内存空间不足,就会回收这些对象的内存来保证程序的运行。

弱引用(Weak Reference):只持有弱引用的对象比只持有软引用的对象拥有更加短暂的生命周期。当垃圾回收器线程扫描到只具有弱引用的对象,不管当前内存空间是否充足,都会回收这些对象的内存。不过,垃圾回收器线程是一个优先级很低的线程,所以不一定会很快发现那些只具有弱引用的对象。

虚引用(Phantom Reference):就是字面意思,与其他引用都不同的是虚引用不会决定对象的生命周期,也就是说只持有虚引用的对象就和没有任何引用一样,任何时候都可能被垃圾回收器线程回收,所以虚引用必须和引用队列(Reference Queue)联合使用来发挥自己的作用。


内存泄漏的根本原因

所有Entry对象都被ThreadLocalMap类的实例化对象threadLocals持有,当ThreadLocal对象不再使用时,ThreadLocal对象在栈中的引用就会被回收,一旦没有任何引用指向ThreadLocal对象,Entry只持有弱引用的key就会自动在下一次YGC时被回收,而此时持有强引用的Entry对象并不会被回收。

简而言之:threadLocals对象中的entry对象不在使用后,没有及时remove该entry对象 ,然而程序自身也无法通过垃圾回收机制自动清除,从而导致内存泄漏。


解决方案:只要在使用完ThreadLocal对象后,调用其remove方法删除对应的Entry,即可从根本解决问题。

————————————————

版权声明:本文为CSDN博主「才哈哈」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

原文链接:

https://blog.csdn.net/weixin_45333509/article/details/115756410





粉丝福利:Java从入门到入土学习路线图

👇👇👇

👆长按上方微信二维码 2 秒


感谢点赞支持下哈 

浏览 22
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报