ThreadLocal源码解析以及初步使用

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

原文地址:https://www.cnblogs.com/frank9571/p/13532796.html