关于ThreadLocal的那些事

项目中看到了个ThrealLocal,交互数据库都用到了它~ 虽然被封装起来了,但我还是看看它到底啥模样?此类优秀文章很多,自己写下总结方便日后温习。(基于jdk1.8)

一、什么是Threadlocal?

Threadlocal,顾名思义 本地线程啦。
官方说明:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable. ThreadLocal instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

基本用法

ThreadLocal<T> local = new ThreadLocal<T>();
local.set();
local.get()

此类提供线程局部变量。这些变量与正常变量不同,因为每个访问一个线程(通过其{@code get}或{@code set}方法)的线程都有其自己的,独立初始化的变量副本。 {@code ThreadLocal}实例通常是希望将状态与线程相关联的类中的私有静态字段(例如用户ID或交易ID)。

 

简而言之就是:
TreadLocal可以给我们提供一个线程内的局部变量,而且这个变量与一般的变量还不同,它是每个线程独有的,与其他线程互不干扰的通过get和set方法就可以得到当前线程对应的值。
实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置

思考: 1: 是否绝对安全 2:使用场景

二、Threadlocal的作用

ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。
在很多情况ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

三、Threadlocal的使用场景

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

四、Threadlocal的使用场景

在Java的多线程编程中,为保证多个线程对共享变量的安全访问,通常会使用synchronized来保证同一时刻只有一个线程对共享变量进行操作。这种情况下可以将类变量放到ThreadLocal类型的对象中,使变量在每个线程中都有独立拷贝,不会出现一个线程读取变量时而被另一个线程修改的现象。最常见的ThreadLocal使用场景为用来解决数据库连接、Session管理等。

五、Threadlocal的源码解析

ThreadLocal类中提供了几个方法:

1.public T get() { }

2.public void set(T value) { }

3.public void remove() { }

4.protected T initialValue(){ 

作为一个存储数据的类,关键点就在get和set方法。当然线程结束时防止产生 辣鸡 垃圾,需要及时remove掉存储的变量值。

 

1 . 看看ThreadLocalMap的set方法

//set 方法
public void set(T value) {
      //获取当前线程
      Thread t = Thread.currentThread();
      //实际存储的数据结构类型
      ThreadLocalMap map = getMap(t);
      //如果存在map就直接set,没有则创建map并set
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
  }
  
//getMap方法
ThreadLocalMap getMap(Thread t) {
      //thred中维护了一个ThreadLocalMap
      return t.threadLocals;
 }
 
//createMap
void createMap(Thread t, T firstValue) {
      //实例化一个新的ThreadLocalMap,并赋值给线程的成员变量threadLocals
      t.threadLocals = new ThreadLocalMap(this, firstValue);
}

可以看出每个线程持有一个ThreadLocalMap对象。每一个新的线程Thread都会实例化一个ThreadLocalMap(用于存取数据)并赋值给成员变量threadLocals,使用时若已经存在threadLocals则直接使用已经存在的对象。

既然来到了ThreadLocalMap,来看看它是什么妖魔鬼怪

在ThreadLocalMap中其实是维护了一张哈希表,这个表里面就是Entry对象,而每一个Entry对象简单来说就是存放了我们的key和value值。

/**
 * The table, resized as necessary.
 * table.length MUST always be a power of two.
 */
private Entry[] table;
/**
 * The initial capacity -- MUST be a power of two.
 */
//初始容量, 想想HashMap也是16
private static final int INITIAL_CAPACITY = 16; 

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
        table = new Entry[INITIAL_CAPACITY];
        //位运算,结果与取模相同,计算出需要存放的位置
        //threadLocalHashCode比较有趣
        int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
        table[i] = new Entry(firstKey, firstValue);
        size = 1;
        setThreshold(INITIAL_CAPACITY);
}

//Entry为ThreadLocalMap静态内部类,对ThreadLocal的若引用
//同时让ThreadLocal和储值形成key-value的关系
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

可以看出实例化ThreadLocalMap时创建了一个长度为16的Entry数组.通过hash Code与length位运算确定出一个索引值i,这个i就是被存储在table数组中的位置,通过操作table来读取。
显然table是set和get的焦点,在看具体的set和get方法前,先看下面这段代码。

//在某一线程声明了ABC三种类型的ThreadLocal
ThreadLocal<A> sThreadLocalA = new ThreadLocal<A>();
ThreadLocal<B> sThreadLocalB = new ThreadLocal<B>();
ThreadLocal<C> sThreadLocalC = new ThreadLocal<C>();

由前面我们知道对于一个Thread来说只有持有一个ThreadLocalMap,所以ABC对应同一个ThreadLocalMap对象。为了管理ABC,于是将他们存储在一个数组的不同位置,而这个数组就是上面提到的Entry型的数组table。 那么问题来了,ABC在table中的位置是如何确定的?为了能正常够正常的访问对应的值,肯定存在一种方法计算出确定的索引值i

//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;
	//获取索引值,这个地方是比较特别的地方 (扩容)
	int i = key.threadLocalHashCode & (len-1);

	//遍历tab如果已经存在则更新值
	for (Entry e = tab[i];
			 e != null;
			 e = tab[i = nextIndex(i, len)]) {
			ThreadLocal<?> k = e.get();

			if (k == key) {
					e.value = value;
					return;
			}

			if (k == null) {
					replaceStaleEntry(key, value, i);
					return;
			}
	}
	
	//如果上面没有遍历成功则创建新值
	tab[i] = new Entry(key, value);
	int sz = ++size;
	//满足条件数组扩容x2
	if (!cleanSomeSlots(i, sz) && sz >= threshold)
			rehash();
}

在ThreadLocalMap中的set方法与构造方法能看到以下代码片段。

  • int i = key.threadLocalHashCode & (len-1)
  • int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1)

简而言之就是将threadLocalHashCode进行 (初始容量减1之后)一个位运算(取模)得到索引i,threadLocalHashCode代码如下。

private final int threadLocalHashCode = nextHashCode();

/**
 * The next hash code to be given out. Updated atomically. Starts at
 * zero.
 */
private static AtomicInteger nextHashCode =
		new AtomicInteger();

/**
 * The difference between successively generated hash codes - turns
 * implicit sequential thread-local IDs into near-optimally spread
 * multiplicative hash values for power-of-two-sized tables.
 */
private static final int HASH_INCREMENT = 0x61c88647;

/**
 * Returns the next hash code.
 */
private static int nextHashCode() {
	//自增
	return nextHashCode.getAndAdd(HASH_INCREMENT);
}


因为static,在每次new ThreadLocal时因为threadLocalHashCode的初始化,会使threadLocalHashCode值自增一次,增量为0x61c88647; 0x61c88647是 斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突。
 

小结set:

  • 对于某一ThreadLocal来讲,他的索引值i是确定的,在不同线程之间访问时访问的是不同的table数组的同一位置即都为table[i],只不过这个不同线程之间的table是独立的。
  • 对于同一线程的不同ThreadLocal来讲,这些ThreadLocal实例共享一个table数组,然后每个ThreadLocal实例在table中的索引i是不同的。

 


想起一个数学家的笑话,

  • 中午食堂的汤咋样?
  • 你说他们的斐波那契汤啊!

2 . 接下来看看ThreadLocalMap的get方法

//ThreadLocal中get方法
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

/**
 * Variant of set() to establish initialValue. Used instead
 * of set() in case user has overridden the set() method.
 *
 * @return the initial value
 */
// 直接调用get,调用该方法初始化ThreadLocalMap ,代码一目了然,熟悉的createMap
private T setInitialValue() {
		T value = initialValue();
		Thread t = Thread.currentThread();
		ThreadLocalMap map = getMap(t);
		if (map != null)
				map.set(this, value);
		else
				createMap(t, value);
		return 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
            return getEntryAfterMiss(key, i, e);
   }

通过set方法, get方法也就清晰了,取数据,但是我们会发现这里的get并没像 java 其他数组一样传下标去取,hash而来的下表,无非是通过计算出索引直接从数组对应位置读取即可。

六、ThreadLocal特性

ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题,不同的点是

  • 1. Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突,并且相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值。

七 、ThreadLocal的内存泄露

先看张图

aHR0cHM6Ly9tbWJpei5xcGljLmNuL21tYml6X3BuZy9IN3FVTjZZNFlVbDM1V3FNVjJoYVpqdUp3cEJHRkJMa3JGbVB1ZkJrS2ppYjhnejQ5aWNpYk0yZGtXaWFPbDFMaWJvYTd3cDhJT2xoaWJkWU5WVUVJYWxKZkRhUS82NDA_d3hfZm10PXBuZw.jpg

如上图,每个线程找到自己维护的ThreadLocalMap,可以操作该数据结构,而ThreadLocalMap中维护的就是一个Entry数组,每个Entry对象就是我们存放的数据,它是个key-value的形式,key就是ThreadLocal实例的弱引用,value就是我们要存放的数据,也就是一个ThreadLocal的实例会对用一个数据,形成一个键值对。

1. 如何造成内存泄漏

Entry对象持有的是键就是ThreadLocal实例的弱引用,而弱引用会被(GC)回收掉,据上图,图中虚线就代表弱引用,如果这个ThreadLocal实例被回收掉,这个弱引用的链接也就断开了,如下图:

22321321BuZw.jpg

这样则Entry对象中的key就变成了null,所以这个Entry对象就没有被引用,因为key变成看null,就取不到这个value值了,再加上如果这个当前线程迟迟没有结束,ThreadLocalMap的生命周期就跟线程一样,这样就会存在一个强引用链,所以这个时候,key为null的这个Entry就造成了内存泄漏。
 

对于这种赖着不走的,要及时remove掉。

每次使用ThreadLocal就会随线程产生一个ThreadLocalMap,里面维护Entry对象,我们对Entry进行存取值,那么如果我们每次使用完ThreadLocal之后就把对应的Entry给删除掉

ThreadLocal中提供了一个remove方法:

private T referent;         /* Treated specially by GC */

/**
* 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) {
		e.clear();
		expungeStaleEntry(i);
		return;
	}
    }
}

public void clear() {
      this.referent = null;
}

可以看到remove方法,根据key删除掉对应的Entry,其中clear方法的注释

 

参考: https://blog.csdn.net/sinat_33921105/article/details/103295070

还有一部分参考的是简书的,emmm找不到原创了~ 总而言之, 参考 > 实践 > 总结 ,一套下来,学习一个知识点就很牢固了 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章