ThreadLocal学习

506 阅读4分钟

这是我参与新手入门的第1篇文章

初识ThreadLocal

我们先通过一段代码来认识一下 ThradLocal

主程序


package com.example.threadlocal_01.com.yang;
/**
 * static修饰 类加载的时候创建ThreadLocal对象 tl,并且所有对象共享
 * 线程一:通过set() 设置了一个张三的Person对象 打印输出
 * 线程二:通过get() 去获取tl对象 打印输出
 * 为了排除线程执行速度的影响,线程一睡眠1s,线程二睡眠2s
 */
public class ThreadLocalDemo01 {
    static ThreadLocal<Person> tl = new ThreadLocal<>();
    public static void main(String[] args) {
        // 线程一
        new Thread(() -> {
            SleepHelper.sleep(1);
            tl.set(new Person("张三"));
            System.out.println(Thread.currentThread().getName() +":"+ tl.get());
        }).start();
        // 线程二
        new Thread(() -> {
            SleepHelper.sleep(2);
            System.out.println(Thread.currentThread().getName() +":"+ tl.get());
        }).start();
    }
}

相关类

  • SleepHelper
public class SleepHelper {
    public static void sleep(int seconds) {
        try {
            Thread.sleep(1000L * seconds);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
  • Person
@Data
@AllArgsConstructor
public class Person {
    String name;
}

在执行代码之前,根据以往的经验,我们可以预测一下输出结果,带着答案去看结果,可以加深我们的印象。

  • 输出结果
Thread-0:Person(name=张三)
Thread-1:null

结果和我想的好像不太一样,在共享的 tl 对象里设置了值,第二个线程里确没有获取到。

总结

查阅资料,ThreadLocal可以理解为当前线程的一个全局变量,只作用于当前线程,所以线程一里的tl设置了值,线程二里获取不到,从定义上来说似乎可以解释的通,但是这tl必定是一个共享对象,这又和常理相悖,带着疑问,决定追看一下源码。

ThreadLocal源码探索

set()方法

public void set(T value) {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 传入当前线程对象,获取一个ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    // 如果map不为空,以this作为key,Person作为value,set进这个map,如果为空则创建一个
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

这里的this指的是调用set方法的tl对象

getMap(t)

ThreadLocalMap getMap(Thread t) {
        // 返回当前线程的 threadLocals   
        return t.threadLocals;
}

threadLocals

// Thread 对象的成员变量
ThreadLocal.ThreadLocalMap threadLocals = null;

get()

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();
}

getMap(t)

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

看到这我们知道, 任何一个Thread对象里带有一个Map类型的成员变量threadLocals,之前的Person对象存在了这里,所以其它线程肯定是获取不到此线程threadLocals里的值的.

使用ThreadLocal时注意项

先来了解两个概念:
内存泄漏:内存被占用,一直无法被释放
内存溢出:内存不断被消耗,最后内存不够用导致溢出
内存泄漏可能会导致内存溢出,但是内存泄漏不等于内存溢出

  • ThreadLocal使用可能会导致内存泄漏

当线程使用了ThreadLocal,如果线程一直存在,那么这个map占用的内存就永远不会被释放掉,就会导致内存泄漏,如果一直不做处理,久而久之最后就会造成严重的资源浪费,最终导致内存溢出。
所以在我们使用ThreadLocal的时候应该手动去清空map,让GC可以发现并回收内存。

remove()

public void remove() {
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }

线程池中使用ThreadLocal

  • 内存泄漏
  • 数据不一致

说到线程不死,很自然想到了线程池,当一个线程池中使用了ThreadLocal,如果使用完我们不主动去remove(),就存在内存泄漏的情况。
我们知道线程池的特性就是一次性创建多个线程,线程不销毁重复利用,那么就会造成,我上一次使用时map里设置的值没有清除,下一次其它方法使用的时候直接拿到的是上一次的值,就会出现数据混乱的情况。

扩展

ThreadLocalMap里的Entry对象

弱引用:GC发现就会被回收掉

Entry

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

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

我们发现这里的Entry继承自一个弱引用。这里发现了一个有趣的事,言语难以表达,看图

image.png

为什么这里使用弱引用,若果使用强引用,即使tl对象变成null了但是mapkey的引用依然指向ThreaLocal对象,所以依然会造成内存泄漏。就算ThreadLocal对象被回收了,key变成了null,导致了整个value对象再也无法被访问到,因此依然会有内存泄漏的情况,而使用弱引用则不会出现这种情况。

写在最后的话

最后,说一下写这篇文章的心路历程吧!
在决定写这个题目的时候,也看了很多大佬的文章,写的过程中也是很忐忑,怕犯一些低级错误,误导了看到这篇文章的朋友。所以在写的时候也格外仔细,写的过程中会发现,一个简单的知识点,理解很简单,但是要表达出来,让看的人懂,似乎不是件简单的事。在学习的道路上还需要砥砺前行。
如有纰漏,请各位大佬指正!