首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >使用ThreadLocal怕内存泄漏?那你应该来看看这篇文章

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

作者头像
Lvshen
发布2022-05-05 16:38:46
发布2022-05-05 16:38:46
5710
举报

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

使用示例

代码语言:javascript
复制
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();
        }
    }
}

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

运行结果如下:

代码语言:javascript
复制
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类的源码:

代码语言:javascript
复制
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代码我们发现:

代码语言:javascript
复制
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方法:

代码语言:javascript
复制
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,并存入值。

代码语言:javascript
复制
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」拿值。

代码语言:javascript
复制
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等都在不同程度上支持弱引用。 ❞

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

代码语言:javascript
复制
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());
 
 }
}

运行结果如下:

代码语言:javascript
复制
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被回收后,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法会被清除。从而避免内存泄漏。所以在用完ThreadLocal时,注意调用一下remove方法即可。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2020-08-06,如有侵权请联系 cloudcommunity@tencent.com 删除
目录
  • 使用示例
  • 底层原理
  • 弱引用
  • 解决内存泄漏问题
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档