Java并发指南2:ThreadLocal

ThreadLoacal是什么?

ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。那么ThreadLocal到底是什么呢?

API是这样介绍它的:
ThreadLocal是线程变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。
ThreadLocal实例通常是类中的private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

所以ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。

ThreadLocal提供了set和get访问器用来访问与当前线程相关联的线程局部变量。

ThreadLocal定义了四个方法:

  • get():返回此线程局部变量的当前线程副本中的值。
  • initialValue():返回此线程局部变量的当前线程的“初始值”。
  • remove():移除此线程局部变量当前线程的值。
  • set(T value):将此线程局部变量的当前线程副本中的值设置为指定值。

除了这四个方法,ThreadLocal内部还有一个静态内部类ThreadLocalMap
该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。
ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。

对于ThreadLocal需要注意的有两点:

  1. ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key。
  2. 是ThreadLocal包含在Thread中,而不是Thread包含在ThreadLocal中,有些小伙伴会弄错他们的关系。

Thread、ThreadLocal、ThreadLocalMap的关系

可以参考

每个线程中都有一个ThreadLocalMap数据结构,在线程中,可以通过ThreadLocal的get()/set()方法来访问变量。
(ThreadLocal只有一个,但threadLocalMap是每个线程自带的)
【ThreadLocal整体上给我的感觉就是,一个包装类。声明了这个类的对象之后,每个线程的数据其实还是在自己线程内部通过threadLocals引用到的自己的数据。只是通过ThreadLocal访问这个数据而已】
在这里插入图片描述

在get()方法中,获取数据时,先获取当前正在运行的线程t,然后通过getMap(t)会返回当前线程的threadLocalMap,在threadLocalMap中,键是ThreadLocal的引用,通过传递键获取map中对应的<key,value>键值对,如果获取成功,则返回value值。如果map为空,则初始化。

9.3总结:
①获取当前线程t(Thread) → ②获取当前线程t的ThreadLocalMap(通过getMap方法)→③通过传递键threadLocal获取map中的Entry<key,value>键值对 → ④获取成功,返回value值

get()源码

public T get() {
    Thread t = Thread.currentThread();//获取当前线程t
    ThreadLocalMap map = getMap(t);//获取线程t的threadLocalMap
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        //this是指当前threadLocal,传递键,获取到map中对应的entry
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;//返回value
            return result;
        }
    }
    return setInitialValue();
}

在这里插入图片描述

总结:get函数就是从当前线程的threadlocalmap中取出当前线程对应的变量的副本【注意,变量是保存在线程中的,而不是保存在ThreadLocal变量中】。
当前线程中,有一个变量引用名字是threadLocals,这个引用是在ThreadLocal类中createmap函数内初始化的。每个线程都有一个这样的threadLocals引用的ThreadLocalMap,以ThreadLocal和ThreadLocal对象声明的变量类型作为参数。这样,我们所使用的ThreadLocal变量的实际数据,通过get函数取值的时候,就是通过取出Thread中threadLocals引用的map,然后从这个map中根据当前threadLocal作为参数,取出数据。

getMap()源码:

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

threadLocals是Thread类的成员变量,初始化为null:

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

set函数的源码:
在这里插入图片描述
如果map不等于null的话就给map装<key,value>也就是threadLocal引用跟value
否则创建map,初始化该线程t的threadlocalmap,根据函数参数设置上初始值。也就是说,当前线程的threadlocalmap是在第一次调用set的时候创建map并且设置上相应的值的。

createMap()方法源码
在这里插入图片描述

解释如下:
1、在代码中声明的ThreadLocal对象,实际上只有一个。
2、在每个线程中,都维护了一个threadlocals(ThreadLocalMap)对象,在没有ThreadLocal变量的时候是null的。一旦在ThreadLocal的createMap函数中初始化之后,这个threadlocals(ThreadLocalMap)就初始化了。以后每次那个ThreadLocal对象想要访问变量的时候,比如set函数和get函数,都是先通过getMap(t)函数,先将线程的map取出,然后再从这个在线程(Thread)中维护的map中取出数据【以当前threadlocal作为参数】。

对于一个线程中有多个threadlocal的问题:
不同的线程局部变量,比如说声明了n个(n>=2)这样的线程局部变量threadlocal,那么在Thread中的threadlocals(ThreadLocalMap)中是怎么存储的呢?threadlocalmap中是怎么操作的?
答:在ThreadLocal的set函数中,可以看到,其中的map.set(this, value);把当前的threadlocal传入到map中作为键,也就是说,在不同的线程的threadlocals变量中,都会有一个以你所声明的那个线程局部变量threadlocal作为键的key-value。假设说声明了N个这样的线程局部变量变量,那么在线程的ThreadLocalMap中就会有n个分别以你的线程局部变量作为key的键值对。

从上面的分析中,可以看到,ThreadLocal的实现离不开ThreadLocalMap类,ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:

这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
在这里插入图片描述

关于内存泄漏

ThreadLocalMap源码分析
ThreadLocalMap内部通过Entry类来存储key和value,Entry类的定义为:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
 
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的,弱引用对象在Java虚拟机进行垃圾回收GC时,这个ThreadLocal会被回收。key变成null,但是value的值还在。也就是说,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无法访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。

该强引用链如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value

只要这个线程对象被gc回收,那些key为null对应的value也会被回收,这样也没什么问题,但在线程对象不被回收的情况下,比如使用线程池的时候,核心线程是一直在运行的,线程对象不会回收,若是在这样的线程中存在上述现象,就可能出现内存泄露的问题。

虚引用
一个对象是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。
为一个对象设置虚引用关联的唯一目的就是在这个对象被垃圾回收器回收时收到一个系统通知。

内存泄漏
内存泄漏(Memory Leak)是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。

那在ThreadLocalMap中是如何解决这个问题的呢?

在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。

(获取线程t,获取t.threadLocalMap,然后获取map.getEntry(当前threadLocal引用)的这个entry)
在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,该方法源码如下:

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    // i为存储key的Entry的索引位置
    Entry e = table[i];
    if (e != null && e.get() == key)//非空以及键为key
        return e;
    else
        return getEntryAfterMiss(key, i, e);
        //key是当前threadLocal,然后是索引位置,以及该位置的entry
}

通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源码如下:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
 //要判断e是不是为null,如果e不为null的话,因为可能存在哈希冲突,所以要进一个循环,进行线性查找出看key是不是为null了
    while (e != null) {//有一个循环
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
        //查找到了该key应该在的位置,但是这个地方key为null,说明弱连接被回收了
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}

要判断e是不是为null,如果e不为null的话,因为可能存在哈希冲突,所以要进一个循环,进行线性查找出看key是不是为null了。
ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,其源码如下:

private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
 
    // expunge entry at staleSlot
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;
 
    // Rehash until we encounter null
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
            (e = tab[i]) != null;
            i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;
 
                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

通过上述代码可以发现,若key为null,则该方法通过下述代码来清理与key对应的value以及Entry:

// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;

此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。

在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的ThreadLocal,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。

其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。

总结

  • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。
  • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。
  • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。

参考:
参考1

参考2

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值