51工具盒子

依楼听风雨
笑看云卷云舒,淡观潮起潮落

关于ThreadLocal的九个知识点,看完别再说不懂了!

# (一)什么是ThreadLocal {#一-什么是threadlocal}

ThreadLocal顾名思义是保存在每个线程本地的数据,ThreadLocal提供了线程局部变量,即每个线程可以有属于自己的变量,其他线程无法访问。如果创建了一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本。每个线程可以通过set()和get()方法去访问ThreadLocal变量。

# (二)ThreadLocal如何使用 {#二-threadlocal如何使用}

ThreadLocal的使用很简单:

new ThreadLocal();   创建ThreadLocal对象
public void set(T value);   设置当前线程绑定的副本变量
public T get();   获取当前线程绑定的副本变量
public void remove();   移除当前线程绑定的副本变量

先通过一个例子来说明ThreadLocal的作用:下面这段代码用十个线程先给msg赋值,再获取值。

public class ThreadLocalTest {
    private String msg;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public static void main(String[] args) {
        ThreadLocalTest test=new ThreadLocalTest();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                //先给对象赋值,再取出这个值
                test.setMsg(Thread.currentThread().getName()+"的数据");
                System.out.println(Thread.currentThread().getName()+"---->"+test.getMsg());
            }).start();
        }
    }
}

由于msg是一个公共对象,因此一个线程中设置的值可能会被另外一个线程改掉。最后的结果就是当前线程赋的值和取出的值是不一样的。

这里就可以使用ThreadLocal解决这个问题,修改代码:setMsg和getMsg方法都通过threadLocal去设置,这样每个线程就有了属于自己线程私有的变量。

public class ThreadLocalTest {
    ThreadLocal<String> threadLocal=new ThreadLocal<>();
    private String msg;

    public String getMsg() {
        return threadLocal.get();
    }
    public void setMsg(String msg) {
        threadLocal.set(msg);
    }

    public static void main(String[] args) {
        ThreadLocalTest test=new ThreadLocalTest();
        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                test.setMsg(Thread.currentThread().getName()+"的数据");
                System.out.println(Thread.currentThread().getName()+"---->"+test.getMsg());
            }).start();
        }
    }
}

# (三)ThreadLocal和Synchronized的区别 {#三-threadlocal和synchronized的区别}

上面的这个例子,用synchronized也一样可以解决问题,我可以锁住这个test对象,使得同一个时刻只有一个线程可以去执行赋值和取值的代码:

public static void main(String[] args) {
    ThreadLocalTest test=new ThreadLocalTest();
    for (int i = 0; i < 100; i++) {
        new Thread(()->{
            synchronized (test){
                test.setMsg(Thread.currentThread().getName()+"的数据");
                System.out.println(Thread.currentThread().getName()+"---->"+test.getMsg());
            }
        }).start();
    }
}

但是synchronized和ThreadLocal实现的角度和思路是不同的。

synchronized是以时间换空间 ,让不同的线程排队访问。而ThreadLocal是以空间换时间,为每个线程都设置了一份变量。

# (四)ThreadLocalMap结构 {#四-threadlocalmap结构}

在每个Thread线程中,都维护了一个ThreadLocalMap,Key是ThreadLocal本身,value是真正存储的变量副本。

static class 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的结构(JDK8):

因为ThreadLocalMap是保存在每个Thread线程内部的,因此实现了线程隔离。

# (五)ThreadLocalMap是如何解决Hash冲突的 {#五-threadlocalmap是如何解决hash冲突的}

通过上面的结构我们可以观察到,ThreadLocalMap不像HashMap那样,采用数组加链表的方式,那么如果遇上Hash冲突后ThreadLocalMap是如何解决的呢?

从源码中看出了解决方式:

Entry[] tab = table;
int len = tab.length;
int i = key.threadLocalHashCode & (len-1);

首先每个ThreadLocal都有一个对应的threadLocalHashCode通过threadLocalHashCode & (len-1)可以算出应该放在Map的哪个位置。

ThreadLocal对应的key存在,就覆盖掉原来的值。

if (k == key) {
    e.value = value;
    return;
}

key为null,但是值不为null(stale陈旧的元素),就用新元素替换掉老的元素

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果ThreadLocal对应的key不存在且没有陈旧的元素,则在空的位置上创建新的entry,这里寻找空的位置用的是线性探测法,即如果计算出来的位置存在元素,就往后找,直到找到空的位置。

tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();

# (六)ThreadLocal的原理 {#六-threadlocal的原理}

ThreadLocal有三个主要的方法,set、get、remove,通过源码来分析:

# 4.1 set方法 {#_4-1-set方法}

public void set(T value) {
    //获取当前的线程
    Thread t = Thread.currentThread();
    //获取此线程中维护的ThreadLocalMap对象
    ThreadLocalMap map = getMap(t);
    //判断map是否为空
    if (map != null)
        //存在则设置当前ThreadLocal的entry
        map.set(this, value);
    else
        //如果不存在,先创建一个ThreadLocalMap对象,再将当前ThreadLocal和value作为第一个entry
        createMap(t, value);
}

总结起来一句话:获取当前线程的ThreadLocalMap,如果map不为空,将entry设置到Map中,否则创建一个Map,将entry设置到Map中

# 4.2 get方法 {#_4-2-get方法}

public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    //判断map是否为空
    if (map != null) {
        //获取当前ThreadLocal对应的entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        //如果entry不为空,返回对应的value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //否则创建Map返回一个初始值
    return setInitialValue();
}

总结为一句话:先获取当前线程的ThreadLocalMap,如果存在则返回对应的value值,否则创建Map并返回初始值。

# 4.3 remove {#_4-3-remove}

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

这个就不需要解释了,删除ThreadLocal为key的entry。

# (七)ThreadLocal的内存泄露问题 {#七-threadlocal的内存泄露问题}

首先理解一下什么是内存泄漏,内存泄漏是指程序中已经动态分配的堆内存由于各种原因未释放或者无法释放,导致内存的浪费。进而导致程序运行缓慢甚至到最后内存溢出。(所以经常听到这种说法:重启能解决百分之七十的问题)。

在了解ThreadLocal内存泄漏问题前,还需要知道一个知识点。

Java中的引用有四种:强软弱虚。

强引用:new一个对象就是强引用,垃圾回收器不会随意回收具有强引用的对象,除非他达到了可以回收的要求。

软引用:SoftReference,如果内存足够,不会回收这一类对象,如果内存不够,就会回收。

弱引用 :WeakReference,垃圾回收器扫描的时候,一旦发现对象只有弱引用,直接回收。

虚引用:PhantomReference,如果一个对象仅持有虚引用,那么它就和没有任何引用一样,虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。程序可以通过判断引用队列中是否已经加入了虚引用,来了解被引用的对象是否将要被垃圾回收。

为什么ThreadLocal会发生内存泄漏呢?

从前面ThreadLocalMap结构代码中可以发现,ThreadLocal是一个弱引用,我们可以画出Java虚拟机这个时候的引用图:

虚拟机栈中ThreadLocal引用指向堆内存ThreadLocal对象,同时当前线程的引用也会指向当前线程,线程到entry之间一直都是强引用。

ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

因此ThreadLocal发生内存泄漏有两个原因:

1、没有remove掉entry对象

2、Thread一直在运行(线程池中线程如果是复用的,这个线程就会一直运行)

之前有听人说ThreadLocal造成内存泄漏的原因是因为弱引用,其实不是,就算是强引用,只要线程还在运行的情况下,下面一条强引用链就不会消失,还是会导致内存泄漏。

ThreadLocal内存泄漏的根源是因为ThreadLocalMap拥有和Thread相同的生命周期,因此如果不手动remove掉entry对象,只要线程还在运行,就会发生内存泄漏。

# (八)既然强弱引用都会导致内存泄漏,为什么使用弱引用 {#八-既然强弱引用都会导致内存泄漏-为什么使用弱引用}

在介绍ThreadLocalMap解决Hash冲突时,有这样一段代码:

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果发现entry的key为null,就会用新的entry覆盖掉旧的entry。什么情况下key会等于null?就是ThreadLocal用完后没有remove,弱引用在下一次垃圾回收时会被自动清理,这个时候key就等于null了。然后通过上面这个代码,就能覆盖掉这些无用的垃圾对象,增加保障。

# (九)ThreadLocal中的对象可以共享吗? {#九-threadlocal中的对象可以共享吗}

答案是可以,使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值。

public static void main(String[] args) {
    ThreadLocal threadLocal=new InheritableThreadLocal();
    threadLocal.set("主线程中的数据");
    new Thread(()->{
        System.out.println(Thread.currentThread().getName()+"获取了"+threadLocal.get());
    }).start();
}

在主线程中创建ThreadLocal,并赋值,在子线程中同样获取到了。如果用ThreadLocal创建对象的话,子线程中获取到的就是null。

# (十)总结 {#十-总结}

关于ThreadLocal的知识点基本上已经全部讲清楚了,又任何错误欢迎在评论区指出,我是鱼仔,我们下期再见!

赞(1)
未经允许不得转载:工具盒子 » 关于ThreadLocal的九个知识点,看完别再说不懂了!