ThreadLocal基本使用及原理分析

ThreadLocal基本使用及原理分析

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

文章目录

  • ThreadLocal基本使用及原理分析
    • ThreadLocal介绍
      • 线程程序介绍
      • 关于其变量
    • ThreadLocal使用
      • set
      • get
      • remove
      • ThreadLocalMap
        • 定义
        • ThreadLocalMap.set
        • ThreadLocalMap.getEntry
        • ThreadLocalMap.remove
      • 内存泄露问题

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销毁。

你可能感兴趣的:(java,#,并发编程)