ThreadLocal 源码解析

本文将从如下几个方面介绍

前言html

栗子java

类图编程

ThreadLocal源码分析api

ThreadLocalMap 源码分析数组

ThreadLocal 可能会致使内存泄漏浏览器

前言

ThreadLocal 顾名思义就是在每一个线程内部都会存储只有当前线程才能访问的变量的一个副本,而后当前线程修改了该副本的值后而不会影响其余线程的值,各个变量之间相互不影响。安全

当咱们须要共享一个变量,而该变量又不是线程安全的时候,可使用 ThreadLocal 来复制该变量的一个副本;又好比,浏览器用户的登陆信息须要从当前请求request中获取,若是须要在不少地方会用到该用户的登陆信息, 一个解决办法是向这些全部用到的地方传递request参数,另一个办法就是利用ThreadLocal, 获取登陆信息后把它放到当前线程中的ThradLocal变量中,任何须要的时候从当前线程中取就能够了。oracle

栗子

首先看一个不使用 ThreadLocal 的简单不成熟栗子,每一个线程都要修改共享变量 i 的值:函数式编程

private int i = 0;

    private void createThread() throws InterruptedException {
        Thread thread = new Thread(() -> {
            i = 0;
            System.out.println(Thread.currentThread().getName() + " : " +  i);
            i+=10;
            System.out.println(Thread.currentThread().getName() + " : " +  i);
        });
        thread.start();
        thread.join();
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        for (int j = 0; j < 5; j++) {
            m.createThread();
        }

    }
输出:
Thread-0 : 0
Thread-0 : 10
Thread-1 : 0
Thread-1 : 10
Thread-2 : 0
Thread-2 : 10
Thread-3 : 0
Thread-3 : 10
Thread-4 : 0
Thread-4 : 10

在每一个线程修改该共享变量的值以前,都须要重置该变量的值,以后才会进行修改,这样结果才会符合咱们的预期。函数

接下来看下使用 ThreadLocal 是来实现的:

private int i = 0;
    // 为每一个线程建立变量 i 的副本
    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> i);

    private void createThread2() throws InterruptedException {
        Thread thread = new Thread(() -> {
            System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
            threadLocal.set(threadLocal.get() + 10);
            System.out.println(Thread.currentThread().getName() + " : " + threadLocal.get());
        });
        thread.start();
        thread.join();
    }

    public static void main(String[] args) throws InterruptedException {
        Main m = new Main();
        for (int j = 0; j < 5; j++) {
            m.createThread2();
        }
    }

输出:

Thread-0 : 0
Thread-0 : 10
Thread-1 : 0
Thread-1 : 10
Thread-2 : 0
Thread-2 : 10
Thread-3 : 0
Thread-3 : 10
Thread-4 : 0
Thread-4 : 10

能够看到,使用 ThreadLocal 一样实现上述的效果,可是不须要再每一个线程执行以前重置该共享变量了。

注:使用 join() 方法为了让线程顺序执行,线程1执行完了线程2再执行

源码分析

接下来看下 ThreadLocal 的一个实现

类图:先来看下它的一个类图

从该类图中,能够看到,ThreadLocal 并无实现任何的类,也没有实现任何的接口,它只有两个内部类,ThreadLocalMap SuppliedThreadLocalThreadLocalMap 类中还有一个 Entry 内部类,能够看到,类结构是很简单的。SuppliedThreadLocal 只是为了实现 Java 8 的函数式编程(Lambda表达式),能够忽略。关于 Java 8 的 Lambda 能够参考 Lambda表达式  Java 8 中的流--Stream

ThreadLoal 方法

T get() 返回当前线程本地变量的值
protected T initialValue() 初始化当前线程本地变量的值,默认为null,通常须要重写该方法
void remove() 删除再也不使用的 ThreadLocal
void set(T value) 设置当前线程本地变量的值
static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) 使用Lambda表达式设置初始值,和 initialValue() 做用是同样的

ThreadLocal 的方法使用都比较简单,接下来就看看它们是怎么实现的,

ThreadLocal

public class ThreadLocal<T> {
    // 哈希值
    private final int threadLocalHashCode = nextHashCode();
    
    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    //防止哈希冲突
    private static final int HASH_INCREMENT = 0x61c88647;

    // 当前线程的本地变量的初始值,默认为null,通常须要重写该方法
    protected T initialValue() {
        return null;
    }

    // Lambda 方式设置初始值
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

    // 构造方法
    public ThreadLocal() {
    }

    // 获取 ThreadLocalMap 
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    // 根据线程,和变量值建立 ThreadLocalMap
    // 每一个线程都在本身的 ThreadLocalMap
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

上述是 ThreadLocal 的一些辅助的方法,主要方法 set , get 方法主要是在 ThreadLocalMap 中实现,因此须要在下面结合 ThreadLocalMap 中来讲。

ThreadLocalMap

首先,ThreadLocalMap 是一个自定义的哈希映射,仅仅是用来维护线程本地变量的值,ThreadLocalMap 使用 WeakReferences 做为键,为了可以及时的GC,关于 WeakReferences ,能够参考 java虚拟机之初探

ThreadLocalMap 的定义

static class ThreadLocalMap {

        // 内部类,有两个属性:ThreadLocal 和 Object
        // ThreadLocal:做为key,当key==ull(即entry.get()== null)表示再也不引用该键,所以能够从表中删除
        // Object:本地变量的值
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        // Entry数组的初始容量,为16,必须为2的幂
        private static final int INITIAL_CAPACITY = 16;

        // Entry数组,可重置大小,数组的长度必须为2的幂
        private Entry[] table;

        // Entry数组中元素的个数
        private int size = 0;

        //Entry扩容的阈值,默认为0
        private int threshold; 

        //设置阈值,为 len 的三分之二
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        // Entry数组的下一个索引
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        // Entry数组的上一个索引
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

     ....方法.........
    }

从上述定义的属性和类能够看到,ThreadLocalMap 主要使用数组来实现的,数组的每一项是一个 Entry 对象,Entry 对象中会持有当前线程的引用和当前线程所绑定的变量值。结构以下所示:

接下来看下 ThreadLocalMap 方法的实现,在该部分中,须要结合 ThreadLocal 的方法一块儿来看,

get() 方法

// 返回当前线程所绑定的本地变量值,若是当前线程为null,则返回setInitialValue()方法中的值
public T get() {
    // 获取当前线程
	Thread t = Thread.currentThread();
    // 获取ThreadLocalMap 
	ThreadLocalMap map = getMap(t);
	if (map != null) {
        // 在 ThreadLocalMap 中获取当前线程对应的Entry,Entry 中存储了当前线程所绑定的本地变量的值
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
            // 获取当前线程所绑定的本地变量的值,并返回
			T result = (T)e.value;
			return result;
		}
	}
    // 若是当前线程没有的 ThreadLocalMap 中,则返回 setInitialValue 中的值
	return setInitialValue();
}

get() 方法不要有如下几步:

1.获取当前线程

2.获取线程内的 ThreadLocalMap,若是map已经存在,则以当前的ThreadLocal为键,获取Entry对象,并从从Entry中取出值

3.若是 map 不存在,则调用setInitialValue方法执行初始化

如今,来看下若是从 ThreadLocalMap中获取当前线程所对应的 Entry 对象:

getEntry() 方法以下

private Entry getEntry(ThreadLocal<?> key) {
    // 获取对应线程的hashcode
    // 计算 Entry数组的索引
	int i = key.threadLocalHashCode & (table.length - 1);
	Entry e = table[i];
    // 若是该索引处的Entry对象恰好等于key,则直接返回
	if (e != null && e.get() == key)
		return e;
	else
    // 若是上述条件不知足,则进入 getEntryAfterMiss 方法
		return getEntryAfterMiss(key, i, e);
}

getEntryAfterMiss()方法

该方法主要是,当在当前的索引中找不到对应的 Entry 对象时执行,在该方法内部,主要是在 Entry 数组中循环查找对应key,若是key为空,则进行清理操做

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    // 当前 Entry数组
	Entry[] tab = table;
	int len = tab.length;
    // 若是当前Entry数组 i 对应的位置的Entry对象不为空 
	while (e != null) {
		ThreadLocal<?> k = e.get();
        // 若是key等于Entry数组 i 对应的位置的Entry对象,则直接返回
		if (k == key)
			return e;
		if (k == null)
            // 若是 Entry 数组 i 对应的位置的 Entry 对象为空,则删除该 Entry 对象,resize Entry数组
			expungeStaleEntry(i);
		else
           // 不然,获取 Entry 数组的下一个索引位置,继续查找
			i = nextIndex(i, len);
		e = tab[i];
	}
	return null;
}

expungeStaleEntry()方法

当在 Entry 数组中对应的位置不存在任何引用的时候,进行 Entry 数组的清理操做,resize Entry 数组:

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

	// 把当前索引对应位置的对象设置为null
	tab[staleSlot].value = null;
	tab[staleSlot] = null;
	size--; // Entry数组大小减1

	// Rehash
	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;
				while (tab[h] != null)
					h = nextIndex(h, len);
				tab[h] = e;
			}
		}
	}
	return i;
}

在执行完上述方法后,get() 方法就会获得一个 Entry 对象,以后返回该对象的value,该value就是当前线程所绑定的本地变量的值。

在上面所说的 get() 方法中,若是 ThreadLocalMap 不存在,则执行 setInitialValue 进行初始化,下面看下setInitialValue:

setInitialValue()方法

private T setInitialValue() {
    // 调用 initialValue 方法,该方法默认返回null,通常须要重写该方法
	T value = initialValue();
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
    // 若是 ThreadLocalMap 已存在,则设置初值为 initialValue 方法的返回值
	if (map != null)
		map.set(this, value);
	else
        // 若是 ThreadLocalMap 不存在,则建立
		createMap(t, value);
	return value;
}

ThreadLocalMap.set()方法

ThreadLocalMap 的 set 方法,主要用来设置其对应的值:

private void set(ThreadLocal<?> key, Object value) {
	// 当前的Entry数组
	Entry[] tab = table;
	int len = tab.length;
    // 数组索引
	int i = key.threadLocalHashCode & (len-1);
    // 遍历 Entry 数组
	for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
		ThreadLocal<?> k = e.get();
        // 若是在 Entry 找到,则设置value
		if (k == key) {
			e.value = value;
			return;
		}
        // 若是当前的ThreadLocal为空,则调用replaceStaleEntry来更换这个key为空的Entry
		if (k == null) {
			replaceStaleEntry(key, value, i);
			return;
		}
	}
    // 若是在 Entry 数组中没有找到对应的key ,则建立,插入到数组中
	tab[i] = new Entry(key, value);
	int sz = ++size;
    // 清理 Entry 数组中为null的项,且若是数组大小大于等于咱们设置的阈值,则rehash数组
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
		rehash();
}

cleanSomeSlots 方法里面仍是会调用上面所说的 expungeStaleEntry 方法进行清理 Entry数组为null的项。

rehash()方法

若是当Entry数组的大小大于等于设置的阈值的话,Entry数组就须要进行扩容操做:

private void rehash() {
    // 清空Entry数组
	expungeStaleEntries();
    // 若是 数组大小大于等于 阈值的 3/4,则扩容
	if (size >= threshold - threshold / 4)
        // 扩容
		resize();
}

expungeStaleEntries()方法

private void expungeStaleEntries() {
	Entry[] tab = table;
	int len = tab.length;
	for (int j = 0; j < len; j++) {
		Entry e = tab[j];
		if (e != null && e.get() == null)
			expungeStaleEntry(j);
	}
}

resize()方法

把 Entry数组的容量扩大为原来的 2 倍:

private void resize() {
    // 旧的数组
	Entry[] oldTab = table;
    // 旧数组的长度
	int oldLen = oldTab.length;
    // 新的数组的长度为旧的的2倍
	int newLen = oldLen * 2;
	Entry[] newTab = new Entry[newLen];
	int count = 0;
    // 复制数据
	for (int j = 0; j < oldLen; ++j) {
		Entry e = oldTab[j];
		if (e != null) {
			ThreadLocal<?> k = e.get();
			if (k == null) {
				e.value = null; // Help the GC
			} else {
                // 从新计算数组的索引值
				int h = k.threadLocalHashCode & (newLen - 1);
				while (newTab[h] != null)
					h = nextIndex(h, newLen);
				newTab[h] = e;
				count++;
			}
		}
	}

	setThreshold(newLen);
	size = count;
	table = newTab;
}

ThreadLocal的 set() 方法

ThreadLocal 的 set 方法用来设置当前线程所绑定的变量的值,它的实现和setInitialValue差很少,:

public void set(T value) {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = getMap(t);
    // 若是 ThreadLocalMap  存在,则设置值
	if (map != null)
		map.set(this, value);
	else
        // 若是 ThreadLocalMap  不存在则建立
		createMap(t, value);
}

ThreadLocal 的remove() 方法

public void remove() {
	 ThreadLocalMap m = getMap(Thread.currentThread());
	 if (m != null)
         // 调用 ThreadLocalMap 的 remove 方法
		 m.remove(this);
 }
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) {
			e.clear();
			expungeStaleEntry(i);
			return;
		}
	}
}

以上就是 ThreadLocal 的一个实现过程。

ThreadLocal 可能会致使内存泄漏

 从上面的代码中能够看到,ThreadLocalMap 使用 ThreadLocal 的弱引用做为key,若是一个 ThreadLocal 没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal 就会被回收,这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些key为null的Entry的value,若是当前线程再迟迟不结束的话,这些key为null的Entry的value永远没法回收,形成内存泄漏。在 ThreadLocal 中 的 get, set 和remove 方法中,都对 Entry 的key进行的null的判断,若是为null,则会 expungeStaleEntry 进行清理操做;

因此,在线程中使用完 ThreadLocal 变量后,要记得及时remove掉。

相关文章
相关标签/搜索