欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章

程序员文章站 2022-06-15 17:21:26
...

你是否还只是停留在增删改查的业务开发阶段,是否对多线程的东西很陌生,这篇文章我们来聊聊多线程里一个很重要的类ThreadLocalThreadLocal俗称本地线程,可以将变量存入Thread中,有着线程隔离的作用。

使用示例

public class ThreadLocalExample implements Runnable{

    private static final ThreadLocal<SimpleDateFormat> threadLocal = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));

    @Override
    public void run() {
        System.out.println("Thread Name= " + Thread.currentThread().getName() + " default Formatter = " + threadLocal.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadLocal.set(new SimpleDateFormat());

        System.out.println("Thread Name= " + Thread.currentThread().getName() + " Formatter = " + threadLocal.get().toPattern());
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample threadLocalExample = new ThreadLocalExample();
        for (int i = 0; i < 10; i++) {
            Thread thread = new Thread(threadLocalExample, "" + i);
            Thread.sleep(new Random().nextInt(1000));
            thread.start();
        }
    }
}

上面代码结果为:无论有多少线程,每一个线程都有两种结果。一种结果为自己设置的日期格式,一种为默认的日期格式。

运行结果如下:

Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 Formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 Formatter = yy-M-d ah:mm
Thread Name= 2 Formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 3 Formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 4 Formatter = yy-M-d ah:mm
Thread Name= 6 Formatter = yy-M-d ah:mm
Thread Name= 5 Formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 7 Formatter = yy-M-d ah:mm
Thread Name= 8 Formatter = yy-M-d ah:mm
Thread Name= 9 Formatter = yy-M-d ah:mm

使用场景:

如果你有一个变量想绑定到线程中,你可以使用ThreadLocal实现。

底层原理

上面说了变量保存到线程里,是如何保存的呢?我们来看看Thread类的源码:

public
class Thread implements Runnable {
   ...

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

发现有一个ThreadLocalMap,于是我猜想,变量是不是放在这个Mapvalue中呢?通过看ThreadLocalMap代码我们发现:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

ThreadLocalMap实际上是一个Entry,这个Entry「key」是一个ThreadLocal「value」是我们存进去的值。

细心的你可能发现了Entry继承了WeakReference<ThreadLocal<?>>。真是一脸懵逼,WeakReference又是个什么,发现自己在无知的道路上越走越远。其实源码上面有解释:

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.

大概意思是Entry里的「key」是一个弱引用。至于弱引用是什么我们后面介绍。

再来看看ThreadLocalset方法:

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

当执行set方法时,会从Thread中拿到ThreadLocalMap,如果ThreadLocalMap能拿到,把值存入ThreadLocalMap中,否则创建一个新的ThreadLocalMap,并存入值。

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

get方法就比较简单了,根据「key」拿值。

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

弱引用

上面说到ThreadLocalMap「key」是一个ThreadLocal的弱引用。那什么是弱引用呢?

我们来看看*的解释:

在计算机程序设计中,「弱引用」与强引用相对,是指不能确保其引用的对象不会被垃圾回收器回收的引用。一个对象若只被弱引用所引用,则被认为是不可访问(或弱可访问)的,并因此可能在任何时刻被回收。一些配有垃圾回收机制的语言,如Java、C#、Python、Perl、Lisp等都在不同程度上支持弱引用。

在进行垃圾回收时,回收器会回收掉这些弱引用。来看个弱引用的示例代码:

import java.lang.ref.WeakReference;
 
public class ReferenceTest {
 public static void main(String[] args) throws InterruptedException {
 
            WeakReference r = new WeakReference(new String("I'm here"));
            WeakReference sr = new WeakReference("I'm here");
            System.out.println("before gc: r=" + r.get() + ", static=" + sr.get());
            System.gc();
            Thread.sleep(100);
 
            //只有r.get()变为null
            System.out.println("after gc: r=" + r.get() + ", static=" + sr.get());
 
 }
}

运行结果如下:

before gc: r=I'm here, static=I'm here
after gc: r=null, static=I'm here

如果一个对象被弱引用着,那么经历一次「GC」,这个引用会被回收。

那么这个「Entry」为什么要使用弱引用呢?

如果「Entry」的key使用强引用,key的引用会一直指向ThreadLocal对象,如果线程Thread存在,Entry也一直存在,会有内存泄漏的危险。

但是即使使用弱引用还是会有内存泄漏的风险。ThreadLocal被回收,key的值变为null,会导致整个value再也无法被访问。虽然依然存在内存泄漏,但比强引用多了一层保障。

解决内存泄漏问题

那我们如何解决内存泄漏问题呢?

ThreadLocal结构图如下:

使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章

 

其实当对应的ThreadLocal被回收后,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法会被清除。从而避免内存泄漏。所以在用完ThreadLocal时,注意调用一下remove方法即可。

往期推荐

扫码二维码,获取更多精彩。或微信搜Lvshen_9,可后台回复获取资料

1.回复"java" 获取java电子书;

2.回复"python"获取python电子书;

3.回复"算法"获取算法电子书;

4.回复"大数据"获取大数据电子书;

5.回复"spring"获取SpringBoot的学习视频。

6.回复"面试"获取一线大厂面试资料

7.回复"进阶之路"获取Java进阶之路的思维导图

8.回复"手册"获取阿里巴巴Java开发手册(嵩山终极版)

9.回复"总结"获取Java后端面试经验总结PDF版

10.回复"Redis"获取Redis命令手册,和Redis专项面试习题(PDF)

11.回复"并发导图"获取Java并发编程思维导图(xmind终极版)

另:点击【我的福利】有更多惊喜哦。

使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章