ThreadLocal基本使用及原理分析

ThreadLocal基本使用及原理分析

学习ThreadLocal的基本使用以及了解其核心原理实现。jdk版本:1.8

ThreadLocal介绍

线程程序介绍

早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。

关于其变量

ThreadLocal很容易让人望文生义,想当然地认为是一个“本地线程”。其实,ThreadLocal并不是一个Thread,而是Thread的局部变量,也许把它命名为ThreadLocalVariable更容易让人理解一些。

所以,在Java中编写线程局部变量的代码相对来说要笨拙一些,因此造成线程局部变量没有在Java开发者中得到很好的普及。

– 摘要自百度百科

ThreadLocal使用

在ThreadLocal中,提供了三个核心方法,get、set和remove。通过get赋值、通过set获取值、通过remove删除,使用起来还是非常简单的。

public void contextLoads() throws IOException {
    ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
    threadLocal.set(1);
    System.out.println(threadLocal.get());
    threadLocal.remove();
    System.out.println(threadLocal.get());
    System.in.read();
}

接下来从源码的角度分析来了解ThreadLocal的核心原理。为什么他是线程的局部变量、怎么做到线程独占的、会存在什么问题。

set

public void set(T value) {
    //通过currentThread获取到当前执行线程
    Thread t = Thread.currentThread();
    //这里的ThreadLocalMap当成普通的hashMap来理解
    ThreadLocalMap map = getMap(t);
    if (map != null)//不为null直接set值,为null则初始化
        //key就是ThreadLocal对象本身
        map.set(this, value);
    else
        createMap(t, value);
}
//初始化就是直接new了
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

set方法本身非常简单,就是拿到当前线程的的ThreadLocalMap并赋值,因为key存的就是ThreadLocal对象本身,所以set方法不需要传key。接下来在看一下get方法。

get

public T get() {
    //通过currentThread获取到当前执行线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {//如果map是null的情况下做了一下初始化,否则从map中获取值,值本身存放在map.Entry中
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}
//初始化操作
private T setInitialValue() {
    	//初始化值就是一个null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

get方法同样简单,未初始化的情况下线初始化返回null值,已经初始化的情况下从map中获取值。这里的获取方式类似于1.8之前的hashMap,存放的是Entry数组,通过ThreadLocal的hashcode & entry数组的长度来拿到对应的下标并获取值,后面在来分析这段。

remove

public void remove() {
    //同样拿到threadLocalMap执行删除方法
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

三个方法分析完,可以得出,ThreadLocal就是拿到当前线程中的map来执行get、set和remove,key是ThreadLocal自身。可以发现贯穿流程的在于Thread的成员变量ThreadLocalMap,因此有必要了解一下关于ThreadLocalMap的实现。

ThreadLocalMap

定义
static class ThreadLocalMap {
    /**
     * 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继承了弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

        Entry(ThreadLocal<?> k, Object v) {
            //ThreadLocal key实际为entry的成员
            super(k);
            value = v;
        }
    }
    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;
    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    //数据存储在entry数组中
    private Entry[] table;
    /**
     * The number of entries in the table.
     */
    private int size = 0;
    /**
     * The next size value at which to resize.
     */
    private int threshold; // Default to 0
}

ThreadLocalMap并没有实现map的接口,自身是ThreadLocal的内部类,数据存储在ThreadLocalMap的内部类Entry中,自身维护一个Entry数组(类型1.8之前的hashMap)。在ThreadLocalMap中,是存在内存泄露的问题的,可以带着这个问题来阅读ThreadLocalMap的源码。

ThreadLocalMap.set
private void set(ThreadLocal<?> key, Object value) {

    // We don't use a fast path as with get() because it is at
    // least as common to use set() to create new entries as
    // it is to replace existing ones, in which case, a fast
    // path would fail more often than not.

    Entry[] tab = table;
    int len = tab.length;
    //类似hashMap获取下标的方式,但是这里是hashCode不是通过hashCode()方法获取的
    int i = key.threadLocalHashCode & (len-1);
	//这里是采用的开放寻址法来解决的hash碰撞的问题。
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {//遍历寻找到对应的entry并赋值
        ThreadLocal<?> k = e.get();

        if (k == key) {//key相同(ThreadLocal相同)则直接赋值
            e.value = value;
            return;
        }

        if (k == null) {//如果key为null
            //替换旧值
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	//如果tab[i]为null就直接创建一个entry并赋值了
    tab[i] = new Entry(key, value);
    int sz = ++size;
    //cleanSomeSlots是为了清除为null的key,解决内存泄露的问题。
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

set的代码逻辑参考如上注释,可以看到,这里的数据结构也是采用的散列表的结构,而关于ThreadLocal的hashcode,采用的Fibonacci Hashing,具体可以去了解一下斐波那契哈希的相关概念。采用了开放寻址法来解决hash碰撞的问题,因为这里的entry是弱引用的实现,因此为了优化可能出现的内存泄露问题,在set、replaceStaleEntry、cleanSomeSlots等方法处都会去清理这些stale key。找到对应的entry后,将value赋值给entry.value。

ThreadLocalMap.getEntry
private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else //table[i]没有找到或者不匹配的情况往后寻找
        return getEntryAfterMiss(key, i, e);
}

get的代码逻辑其实同set方法的寻找类似,看懂了set方法,get方法其实没有什么难度。

ThreadLocalMap.remove
/**
 * Remove the entry for key.
 */
private void remove(ThreadLocal<?> key) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        if (e.get() == key) {
            //remove方法调用的时候,如果匹配到key的时候,清除stale的entry。
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

remove会主动清理stale的entry,因为entry是弱引用,所以在使用ThreadLocal的时候要主动去调用remove,这样才能将对应的value移除,被GC回收。虽然在使用get、set方法时候也会cleanSomeSlots,但是需要触发场景。

内存泄露问题

分析完ThreadLocal和ThreadLocalMap的代码后,可以发现如果在使用ThreadLocal的时候,不主动调用remove方法时,可能会出现ThreadLocalMap中entry的value无法被GC回收的问题。虽然ThreadLocalMap是thread的成员变量,会随着thread的销毁而被回收,但是在日常开发中,我们往往会用到线程池,对于核心线程并不会被回收而是重复使用,导致thread一直存活且thread的成员变量ThreadLocalMap一直存活。因此在不用ThreadLocal后,一定记得调用remove销毁。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值