ThreadLocal 源码解析

目录

一. 前言

二. 基本使用

三. 源码解析

3.1. 成员变量

3.2. ThreadLocalMap

3.3. set() 方法

3.4. get() 方法

3.5. remove() 方法

3.6. expungeStaleEntry() 方法

3.7. rehash() 方法

四. 功能测试

五. 内存泄漏问题

六. 总结


一. 前言

    ThreadLocal的作用是提供线程内的局部变量,这种变量在多线程环境下访问时能够保证各个线程里变量的独立性。ThreadLocal无论在项目开发还是面试中都会经常碰到,本文就ThreadLocal的使用、主要方法源码详解、内存泄漏问题展开讨论。

    ThreadLocal是一个将在多线程中为每一个线程创建单独的变量副本的类;当使用ThreadLocal来维护变量时,ThreadLocal会为每个线程创建单独的变量副本,避免因多线程操作共享变量而导致的数据不一致的情况。

二. 基本使用

public class ThreadLocalDemo {
    static ThreadLocal threadLocal = new ThreadLocal<>();

    static ThreadLocal userThreadLocal = new ThreadLocal<>();

    public static void main(String[] args) {
        threadLocal.set(100); // 保存值
        System.out.println(threadLocal.get()); // 获取值

        User user = new User();
        user.setName("流华追梦");
        user.setAge(25);
        userThreadLocal.set(user); // 保存值
        System.out.println(userThreadLocal.get()); // 获取值
    }

    static class User {
        String name;

        Integer age;

        public String getName() {
            return name;
        }

        public void setName(String name) {
            this.name = name;
        }

        public Integer getAge() {
            return age;
        }

        public void setAge(Integer age) {
            this.age = age;
        }
    }
}

// 输出
100
User [name=流华追梦, age=25]

三. 源码解析

这个类提供了线程局部变量。这个类能使线程中的某个值与保存值的对象关联起来,例如:threadLocal.set(5):会将 threadLocal 和 5 作为键值对保存在该线程的threadLocals里。ThreadLocal提供了get与set等访问接口或方法,这些方法为每个使用该变量的线程都存有一份独立的副本(即每个线程的threadLocals属性),因此get总是返回由当前执行线程在调用set时设置的最新值。

只要线程处于活动状态并且ThreadLocal实例可以访问,每个线程就拥有对其线程局部变量副本的隐式引用;在一个线程消失之后,线程本地实例的所有副本都会被垃圾收集(除非存在对这些副本的其他引用)。

3.1. 成员变量

// 哈希Code
private final int threadLocalHashCode = nextHashCode();

/**
 * AtomicInteger类型,从0开始
 */
private static AtomicInteger nextHashCode =
	new AtomicInteger();

/**
 * 哈希Code每次增加的值为1640531527
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * 下一个哈希Code
 */
private static int nextHashCode() {
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}

从上面的定义可以知道,ThreadLocal的hashcode(threadLocalHashCode)是从0开始,每新建一个ThreadLocal,对应的hashcode就加0x61c88647。

3.2. ThreadLocalMap

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

		Entry(ThreadLocal k, Object v) {
			super(k);
			value = v;
		}
	}
	
	......
}

ThreadLocalMap是一个自定义哈希映射,仅用于维护线程本地变量值。ThreadLocalMap是ThreadLocal的内部类,主要有一个Entry数组,Entry的key为ThreadLocal,value为ThreadLocal对应的值。每个线程都有一个ThreadLocalMap类型的threadLocals变量。

3.3. set() 方法

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t); // 获取当前线程的ThreadLocalMap
	// 当前线程的ThreadLocalMap不为空则调用set()方法,this为调用该方法的ThreadLocal对象
	if (map != null)
		map.set(this, value);
	// map为空则调用createMap方法创建一个新的ThreadLocalMap,
	// 并将调用set()方法的ThreadLocal和传入的value作为Entry的key和value,
	// 新建一个Entry放入该ThreadLocalMap
	else
		createMap(t, value);
}

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

1、先拿到当前线程,再使用getMap方法拿到当前线程的threadLocals变量。
2、如果threadLocals不为空,则将当前ThreadLocal作为key,传入的值作为value,调用set方法(见下文代码块1详解)插入threadLocals。
3、如果threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap,调用set方法的ThreadLocal和传入的value作为该Entry的key和value。

注意此处的threadLocals变量是一个ThreadLocalMap,是Thread的一个局部变量,因此它只与当前线程绑定。

代码块1:

private void set(ThreadLocal key, Object value) {
	Entry[] tab = table;
	int len = tab.length;
	int i = key.threadLocalHashCode & (len-1); // 计算出索引的位置

	// 从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
	for (Entry e = tab[i];
		 e != null; // 当遍历到的Entry为空时结束遍历
		 e = tab[i = nextIndex(i, len)]) {
		ThreadLocal k = e.get(); // 拿到Entry的Key,也就是ThreadLocal

		// 该Entry的key和传入的key相等,则用传入的value替换掉原来的value
		if (k == key) {
			e.value = value;
			return;
		}

		// 该Entry的key为空,则代表该Entry需要被清空,调用replaceStaleEntry方法
		if (k == null) {
		    // 该方法会继续寻找传入key的安放位置,并清理掉key为空的Entry
			replaceStaleEntry(key, value, i);
			return;
		}
	}

	// 寻找到一个空位置,则放置在该位置上
	tab[i] = new Entry(key, value);
	int sz = ++size;
	// cleanSomeSlots是用来清理掉key为空的Entry,如果此方法返回true,则代表至少清理了1个元素,
	// 则此次set必然不需要扩容,如果此方法返回false则判断sz是否大于阈值
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash(); // 扩容
}

1、通过传入的key的hashCode计算出索引的位置
2、从索引位置开始遍历,由于不是链表结构,因此通过nextIndex方法来寻找下一个索引位置
3、如果找到某个Entry的key和传入的key相同,则用传入的value替换掉该Entry的value。
4、如果遍历到某个Entry的key为空,则调用replaceStaleEntry方法(见下文代码块2详解)
5、如果通过nextIndex寻找到一个空位置(代表没有找到key相同的),则将元素放在该位置上
6、调用cleanSomeSlots方法清理key为null的Entry,并判断是否需要扩容,如果需要则调用rehash方法进行扩容(见下文rehash方法详解)。

代码块2:replaceStaleEntry方法

private void replaceStaleEntry(ThreadLocal key, Object value,
                                       int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;
	Entry e;

	int slotToExpunge = staleSlot; // 清除元素的开始位置(记录索引位置最前面的)
	// 向前遍历,直到遇到Entry为空
	for (int i = prevIndex(staleSlot, len);
		 (e = tab[i]) != null;
		 i = prevIndex(i, len))
		if (e.get() == null)
			slotToExpunge = i; // 记录最后一个key为null的索引位置

	for (int i = nextIndex(staleSlot, len);
		 (e = tab[i]) != null;
		 i = nextIndex(i, len)) { // 向后遍历,直到遇到Entry为空
		ThreadLocal k = e.get();

		// 该Entry的key和传入的key相等,则将传入的value替换成该Entry的value
		if (k == key) {
			e.value = value;

			// 将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素)
			tab[i] = tab[staleSlot];
			tab[staleSlot] = e;

			// 如果相等,则代表上面的向前寻找key为null的遍历没有找到,
			// 即staleSlot位置前面的元素没有需要清除的,此时将slotToExpunge设置为i,
			// 因为原staleSlot的元素已经被放到i位置了,这时位置i前面的元素不需要清除
			if (slotToExpunge == staleSlot)
				slotToExpunge = i;
			// 从slotToExpunge位置开始清除key为空的Entry
			cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
			return;
		}

		// 如果第一次遍历到key为null的元素,并且上面的向前寻找key为null的遍历没有找到,
		// 则将slotToExpunge设置为当前的位置
		if (k == null && slotToExpunge == staleSlot)
			slotToExpunge = i;
	}

	// 如果key没有找到,则新建一个Entry,放在staleSlot位置
	tab[staleSlot].value = null;
	tab[staleSlot] = new Entry(key, value);

	// 如果slotToExpunge!=staleSlot,代表除了staleSlot位置还有其他位置的元素需要清除
	// 需要清除的定义:key为null的Entry,调用cleanSomeSlots方法清除key为null的Entry
	if (slotToExpunge != staleSlot)
		cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

1、slotToExpunge始终记录着需要清除的元素的最前面的位置(即slotToExpunge前面的元素是不需要清除的)。
2、从位置staleSlot向前遍历,直到遇到Entry为空,用staleSlot记录最后一个key为null的索引位置(也就是遍历过位置最前的key为null的位置)。
3、从位置staleSlot向后遍历,直到遇到Entry为空,如果遍历到key和入参key相同的,则将入参的value替换掉该Entry的value,并将i位置和staleSlot位置的元素对换(staleSlot位置较前,是要清除的元素),遍历的时候判断slotToExpunge的值是否需要调整,最后调用expungeStaleEntry方法(见下文expungeStaleEntry方法详解)和cleanSomeSlots方法(见下文代码块3详解)清除key为null的元素。
4、如果key没有找到,则使用入参的key和value新建一个Entry,放在staleSlot位置。
5、判断是否还有其他位置的元素key为null,如果有则调用expungeStaleEntry方法和cleanSomeSlots方法清除key为null的元素。

代码块3:cleanSomeSlots方法

private boolean cleanSomeSlots(int i, int n) {
	boolean removed = false;
	Entry[] tab = table;
	int len = tab.length;
	do {
		i = nextIndex(i, len); // 下一个索引位置
		Entry e = tab[i];
		if (e != null && e.get() == null) { // 遍历到key为null的元素
			n = len; // 重置n的值
			removed = true; // 标志有移除元素
			i = expungeStaleEntry(i); // 移除i位置及之后的key为null的元素
		}
	} while ( (n >>>= 1) != 0);
	return removed;
}

从 i 开始,清除key为空的Entry,遍历次数由当前的table长度决定,当遍历到一个key为null的元素时,调用expungeStaleEntry清除,并将遍历次数重置。至于为什么使用table长度来决定遍历次数,官方给出的解释是这个方法简单、快速,并且效果不错。

3.4. get() 方法

public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
	if (map != null) {
	    // 调用getEntry()方法,通过调用get()方法的ThreadLocal获取对应的Entry
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) { // Entry不为空则代表找到目标Entry,返回该Entry的value值
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue(); // 该线程的ThreadLocalMap为空则初始化一个
}

1、跟set方法差不多,先拿到当前的线程,再使用getMap方法拿到当前线程的threadLocals变量。
2、如果threadLocals不为空,则将调用get方法的ThreadLocal作为key,调用getEntry方法(见下文代码块5详解)找到对应的Entry。
3、如果threadLocals为空或者找不到目标Entry,则调用setInitialValue方法(见下文代码块4详解)进行初始化。

代码块4:setInitialValue方法

private T setInitialValue() {
	T value = initialValue(); // 默认null,需要用户自己重写该方法
	Thread t = Thread.currentThread(); // 当前线程
	ThreadLocalMap map = getMap(t); // 拿到当前线程的threadLocals
	// threadLocals不为空则将当前的ThreadLocal作为key,null作为value,插入到ThreadLocalMap
	if (map != null)
		map.set(this, value);
	// threadLocals为空则调用创建一个ThreadLocalMap,并新建一个Entry放入该ThreadLocalMap,
	// 调用set()方法的ThreadLocal和value作为该Entry的key和value
	else
		createMap(t, value);
	return value;
}

1、如果是threadLocals为空,创建一个新的ThreadLocalMap,并将当前的ThreadLocal作为key,null作为value,插入到新创建的ThreadLocalMap,并返回null。
2、如果threadLocals不为空,则将当前的ThreadLocal作为key,null作为value,插入到threadLocals。
3、注意上面的 initialValue() 方法为protected,如果希望线程局部变量具有非null的初始值,则必须对ThreadLocal进行子类化,并重写此方法。

代码块5:getEntry方法

private Entry getEntry(ThreadLocal key) {
    // 根据哈希Code计算出索引位置
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
	// 如果该Entry的key和传入的key相等,则为目标Entry,直接返回
	if (e != null && e.get() == key)
		return e;
	// 否则,e不是目标Entry,则从e之后继续寻找目标Entry
	else
		return getEntryAfterMiss(key, i, e);
}

1、根据hash code计算出索引位置。
2、如果该索引位置Entry的key和传入的key相等,则为目标Entry,直接返回。
3、否则,e不是目标Entry,调用getEntryAfterMiss方法(见下文代码块6详解)继续遍历。

代码块6:getEntryAfterMiss方法

private Entry getEntryAfterMiss(ThreadLocal key, int i, Entry e) {
	Entry[] tab = table;
	int len = tab.length;

	while (e != null) {
		ThreadLocal k = e.get();
		// 找到目标Entry,直接返回
		if (k == key)
			return e;
		// 调用expungeStaleEntry清除key为null的元素
		if (k == null)
			expungeStaleEntry(i);
		else
			i = nextIndex(i, len); // 下一个索引位置
		e = tab[i]; // 下一个遍历的Entry
	}
	return null; // 找不到,返回空
}

从元素e开始向后遍历,如果找到目标Entry元素直接返回;如果遇到key为null的元素,调用expungeStaleEntry方法(见下文expungeStaleEntry方法详解)进行清除;否则,遍历到Entry为null时,结束遍历,返回null。

3.5. remove() 方法

public void remove() {
	// 获取当前线程的ThreadLocalMap
	ThreadLocalMap m = getMap(Thread.currentThread());
	if (m != null)
		m.remove(this); // 调用此方法的ThreadLocal作为入参,调用remove方法
}

private void remove(ThreadLocal key) {
	Entry[] tab = table;
	int len = tab.length;
	// 根据hashCode计算出当前ThreadLocal的索引位置
	int i = key.threadLocalHashCode & (len-1);
	// 从位置i开始遍历,直到Entry为null
	for (Entry e = tab[i];
		e != null;
		e = tab[i = nextIndex(i, len)]) {
		if (e.get() == key) { // 如果找到key相同的
			e.clear(); // 则调用clear方法,该方法会把key的引用清空
			expungeStaleEntry(i); // 调用expungeStaleEntry方法清除key为null的Entry
			return;
		}
	}
}

方法很简单,拿到当前线程的threadLocals属性,如果不为空,则将key为当前ThreadLocal的键值对移除,并且会调用expungeStaleEntry方法清除key为null的Entry。

3.6. expungeStaleEntry() 方法

private int expungeStaleEntry(int staleSlot) {
	Entry[] tab = table;
	int len = tab.length;

	// expunge entry at staleSlot
	tab[staleSlot].value = null; // 将tab上staleSlot位置的对象清空
	tab[staleSlot] = null;
	size--;

	// Rehash until we encounter null
	Entry e;
	int i;
	for (i = nextIndex(staleSlot, len); // 遍历下一个元素,即(i+1)%len位置的元素
		 (e = tab[i]) != null; // 遍历到Entry为空时,跳出循环并返回索引位置
		 i = nextIndex(i, len)) {
		ThreadLocal k = e.get();
		if (k == null) { // 当前遍历Entry的key为空,则将该位置的对象清空
			e.value = null;
			tab[i] = null;
			size--;
		} else { // 当前遍历Entry的key不为空
			int h = k.threadLocalHashCode & (len - 1); // 重新计算该Entry的索引位置
			if (h != i) { // 如果索引位置不为当前索引位置i
				tab[i] = null; // 则将i位置对象清空,替当前Entry寻找正确的位置

				// 如果h位置不为null,则向后寻找当前Entry的位置
				while (tab[h] != null)
					h = nextIndex(h, len);
				tab[h] = e;
			}
		}
	}
	return i;
}

源码解读:从staleSlot开始,清除key为null的Entry,并将不为空的元素放到合适的位置,最后遍历到Entry为空的元素时,跳出循环返回当前索引位置。

PS:set、get、remove方法,在遍历的时候如果遇到key为null的情况,都会调用expungeStaleEntry方法来清除key为null的Entry。

3.7. rehash() 方法

private void rehash() {
	expungeStaleEntries(); // 调用expungeStaleEntries方法清理key为空的Entry

	// 如果清理后size超过阈值的3/4,则进行扩容
	if (size >= threshold - threshold / 4)
		resize(); // 扩容
}

/**
 * Double the capacity of the table.
 */
private void resize() {
	Entry[] oldTab = table;
	int oldLen = oldTab.length;
	int newLen = oldLen * 2; // 新表长度为老表的2倍
	Entry[] newTab = new Entry[newLen]; // 创建新表
	int count = 0;

	for (int j = 0; j < oldLen; ++j) { // 遍历所有元素
		Entry e = oldTab[j]; // 拿到对应位置的Entry
		if (e != null) {
			ThreadLocal k = e.get();
			if (k == null) { // 如果key为null,将value清空
				e.value = null; // Help the GC
			} else {
			    // 通过哈希Code计算新表的索引位置
				int h = k.threadLocalHashCode & (newLen - 1);
				// 如果新表的该位置已经有元素,则调用nextIndex()方法直到寻找到空位置
				while (newTab[h] != null)
					h = nextIndex(h, newLen);
				newTab[h] = e; // 将元素放在对应位置
				count++;
			}
		}
	}

	setThreshold(newLen); // 设置新表扩容的阈值
	size = count; // 更新size
	table = newTab; // table指向新表
}

1、调用expungeStaleEntries方法(该方法和expungeStaleEntry类似,只是把搜索范围扩大到整个表)清理key为空的Entry。
2、如果清理后size超过阈值的3/4,则进行扩容。
3、新表长度为老表2倍,创建新表。
4、遍历老表所有元素,如果key为null,将value清空;否则通过hash code计算新表的索引位置h,如果h已经有元素,则调用nextIndex方法直到寻找到空位置,将元素放在新表的对应位置。
5、设置新表扩容的阈值、更新size、table指向新表。

四. 功能测试

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

    public static void main(String[] args) throws InterruptedException {
        Thread thread1 = new ThreadOne(); // 线程1
        Thread thread2 = new ThreadTwo(); // 线程2

        thread2.start(); // 线程2开始执行
        TimeUnit.MILLISECONDS.sleep(100); // 睡眠,以等待线程2执行完毕

        thread1.start(); // 线程1开始执行
        TimeUnit.MILLISECONDS.sleep(100); // 睡眠,以等待线程1执行完毕

        // 此时线程1和线程2都已经设置过值,此处输出为空,
        // 说明子线程与父线程之间也是互不影响的
        System.out.println("main:" + threadLocal.get());
    }

    // 线程1
    private static class ThreadOne extends Thread {
        @Override
        public void run() {
            // 此时线程2已经调用过set(456),此处输出为空,说明线程之间是互不影响的
            System.out.println("ThreadOne:" + threadLocal.get());
            threadLocal.set(123);
            System.out.println("ThreadOne:" + threadLocal.get());
        }
    }

    // 线程2
    private static class ThreadTwo extends Thread {
        @Override
        public void run() {
            threadLocal.set(456); // 设置当前threadLocal的值为456
            System.out.println("ThreadTwo:" + threadLocal.get());
        }
    }
}
输出结果:

ThreadTo: 456
ThreadOne: null
ThreadOne: 123
main: null

从输出结果可以看出,线程1、线程2和主线程之间是彼此互不影响的。

五. 内存泄漏问题

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

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

从上面源码可以看出,ThreadLocalMap使用ThreadLocal的弱引用作为Entry的key,如果一个ThreadLocal没有外部强引用来引用它,下一次系统GC时,这个ThreadLocal必然会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value。

我们上面介绍的get、set、remove等方法中,都会对key为null的Entry进行清除(expungeStaleEntry方法,将Entry的value清空,等下一次垃圾回收时,这些Entry将会被彻底回收)。

但是如果当前线程一直在运行,并且一直不执行get、set、remove方法,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value,导致这些key为null的Entry的value永远无法回收,造成内存泄漏。

如何避免内存泄漏?

为了避免这种情况,我们可以在使用完ThreadLocal后,手动调用remove方法,以避免出现内存泄漏。

六. 总结

1、每个线程都有一个 ThreadLocalMap 类型的 threadLocals 属性。
2、ThreadLocalMap 类相当于一个Map,key 是 ThreadLocal 本身,value 就是我们的值。
3、当我们通过 threadLocal.set(new Integer(123)); ,我们就会在这个线程中的 threadLocals 属性中放入一个键值对,key 是这个threadLocal.set(new Integer(123)) 的 threadlocal,value就是值new Integer(123)。
4、当我们通过 threadlocal.get() 方法的时候,首先会根据这个线程得到这个线程的 threadLocals 属性,然后由于这个属性放的是键值对,我们就可以根据键 threadlocal 拿到值。 注意,这时候这个键 threadlocal 和我们 set 方法的时候的那个键 threadLocal 是一样的,所以我们能够拿到相同的值。
5、ThreadLocalMap 的get/set/remove方法跟HashMap的内部实现都基本一样,通过 "key.threadLocalHashCode & (table.length - 1)" 运算式计算得到我们想要找的索引位置,如果该索引位置的键值对不是我们要找的,则通过nextIndex方法计算下一个索引位置,直到找到目标键值对或者为空。
6、hash冲突:在HashMap中相同索引位置的元素以链表形式保存在同一个索引位置;而在ThreadLocalMap中,没有使用链表的数据结构,而是将(当前的索引位置+1)对length取模的结果作为相同索引元素的位置:源码中的nextIndex()方法,可以表达成如下公式:如果i为当前索引位置,则下一个索引位置 = (i + 1 < len) ? i + 1 : 0。

你可能感兴趣的:(#,Java线程,java,ThreadLocal,线程,多线程)