ThradLocal
主要为了实现线程之间的数据隔离的。当线程之间需要维护同样一个属性的时候,但是当前属性不是一个线程安全的属性的时候就需要创建一个ThreadLocal来实现这个属性的原子性。典型的就是dfs属性。
1.ThreadLocal的set原理
先走ThreadLocal的set方法
1 /** 2 * Sets the current thread's copy of this thread-local variable 3 * to the specified value. Most subclasses will have no need to 4 * override this method, relying solely on the {@link #initialValue} 5 * method to set the values of thread-locals. 6 * 7 * @param value the value to be stored in the current thread's copy of 8 * this thread-local. 9 */ 10 public void set(T value) { 11 Thread t = Thread.currentThread(); 12 ThreadLocalMap map = getMap(t); 13 if (map != null) 14 map.set(this, value);//将当前ThreadLocal实例对象和value传入 15 else 16 createMap(t, value); 17 }
首先得到线程,然后调用getMap方法得到一个Thread LocalMap,看一下getMap()
1 /** 2 * Get the map associated with a ThreadLocal. Overridden in 3 * InheritableThreadLocal. 4 * 5 * @param t the current thread 6 * @return the map 7 */ 8 ThreadLocalMap getMap(Thread t) { 9 return t.threadLocals;//线程对象的threadLocals属性 10 }
1 /* ThreadLocal values pertaining to this thread. This map is maintained 2 * by the ThreadLocal class. */ 3 ThreadLocal.ThreadLocalMap threadLocals = null;//线程私有的 4 5 /* 6 * InheritableThreadLocal values pertaining to this thread. This map is 7 * maintained by the InheritableThreadLocal class. 8 */ 9 ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;//用来线程之间传递的
既然定义为Map自然而然的联想到了HashMap,然后联想到了hash函数 + Entry Table + 链表
但是根据源码来看,他和HashMap并不相同。他的map只是使用的Entry Table来存储元素,通过hash来定位。
实际上一个线程可以有多个不同类型的ThreadLocal变量,但是他们都存储在当前线程的ThreadlocalMap里面,所以经过hash之后自然而然就有可能发生hash碰撞。
static class Entry extends WeakReference<ThreadLocal<?>> {//继承了弱引用*****拿小本本记好,后面有大用 /** The value associated with this ThreadLocal. */
//发现并没有前驱节点和后继节点,说明不存在链表和红黑树的结构
Object value; Entry(ThreadLocal<?> k, Object v) {//同时节点存储的是key是ThreadLocal k, 和一个Object super(k); value = v; } }
取完ThreadLocalMap之后进行set方法,将当前线程和Value传入。
注意上面的英文注解:不会使用一个快速的方式例如get方法因为通常使用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);//先hash,计算出来节点所在位置 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; } } //当前几点是null tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); }
2.ThreadLocal的get原理
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t);//获取当前线程私有的ThreadLocalMap if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this);//getEntry() if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue();//返回默认值 }
getEntry()
private Entry getEntry(ThreadLocal<?> key) { int i = key.threadLocalHashCode & (table.length - 1);//hash Entry e = table[i]; if (e != null && e.get() == key) return e; else return getEntryAfterMiss(key, i, e);//get方法并没有命中,这时候就是发生hash碰撞,也就是当前几点的key并不相同 }
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) { Entry[] tab = table; int len = tab.length; while (e != null) { ThreadLocal<?> k = e.get(); if (k == key) return e; if (k == null)//处理脏节点 expungeStaleEntry(i); else i = nextIndex(i, len); e = tab[i]; } return null; }
3.因为是线程私有的所以一定只存储在jvm栈中么?
其实不是的,因为ThreadLocal实例实际上也是被其创建的类持有(更顶端应该是被线程持有),而ThreadLocal的值其实也是被线程实例持有,它们都是位于堆上,只是通过一些技巧将可见性修改成了线程可见。
(上面这句话来自 敖丙 的博客,这里感谢 敖丙 大神)
4.使用ThreadLocal来为每个线程做一个副本,也就是基础使用
package thread.threadLocal; /** * @ Author :fqg * @ Date :Created in 17:37 2020/8/19 */ public class ThreadLocalTest { private static ThreadLocal<Integer> sqNum = new ThreadLocal<Integer>(){ public Integer initialValue(){//这个方法是重写的,第一次以为是自己定义的,报了空指针 return 0; } }; public int getNextNum(){ sqNum.set(sqNum.get() + 1); return sqNum.get(); } static class Test extends Thread{ private ThreadLocalTest sn; public Test(ThreadLocalTest sn){ this.sn = sn; } public void run(){ for(int i = 0; i < 3; i ++){ System.out.println("thread [" + currentThread().getName() + "]---->sn[" + sn.getNextNum() + "]"); } } } public static void main(String[] args) { ThreadLocalTest sn = new ThreadLocalTest(); Test t1 = new Test(sn); Test t2 = new Test(sn); Test t3 = new Test(sn); t1.start(); t2.start(); t3.start(); } }
结果
实际使用
/**sdf有全局变量线程不安全,用ThreadLocal提供线程安全的sdf*/ public static final ThreadLocal<DateFormat> SDF = new ThreadLocal<DateFormat>() { @Override protected DateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd-HH:mm:ss"); } };
(以上来自Csdn 淘气的高老板)
5.ThreadLocal的弱引用和内存泄漏问题
弱引用:如果使用弱引用,每次GC会将只存在弱引用的内存空间进行回收。
前面两个处理脏节点的注解,为什么一个节点的Key会为null呢?当你存储的时候你是将自己作为key存储进去的啊,怎么会变成null了呢
ThreadLocal 在没有外部强引用之后,被GC了。所以Entry‘里面的key自然而然就变成了null。然后线程却复用了,也就是说线程却没有消亡value被继续持有,就发生了内存泄漏。
解决方法:使用remov方法,在每次使用之后调用remove方法来清除其value。
/** * Remove the entry for key. */ private void remove(ThreadLocal<?> key) { Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1);//hash for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { if (e.get() == key) {//命中 e.clear(); expungeStaleEntry(i); return; } } }
总结
使用不多,但是用明白了可以装B