JAVA基础:ThreadLocal

生活

世界上只有两句真理:1、人一定会死。2、程序一定有Bug。

什么是ThreadLocal

昨天学习了java中的线程Thread对象的生命周期,以及中断、线程礼让、线程加入以及三个过期方法的了解。
在Thread中有两个成员,

     ThreadLocal.ThreadLocalMap threadLocals = null;

    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

可以看到这两个成员都是ThreadLocal.ThreadLocalMap
见名知义:
可以猜到这是本地存储map,并且是与线程绑定的map,并且是线程私有,并不会被多个线程共享,因为每个Thread里都有独立的成员。

ThreadLocal的作用

class ConnectionManager {
     
    private static Connection connect = null;
     
    public static Connection openConnection() {
        if(connect == null){
            connect = DriverManager.getConnection();
        }
        return connect;
    }
     
    public static void closeConnection() {
        if(connect!=null)
            connect.close();
    }
}

如上是一个数据库连接管理类,由于没有加锁,在多线程环境下,可以出现多次初始化connection或者一个在操作,另一个线程却close 连接的情况。
这里需要好好想一想,这里真的需要共享连接吗,其实这里使connection私有就可以了,这里就可以用到ThreadLocal

下面来看下ThreadLocal的使用方法

  static ThreadLocal local  = new ThreadLocal<>();

    public static void main(String[] args) {
        local.set(1);
        System.out.println(local.get());
    }

注意main函数本身也是个线程,这个local的使用看起来好像就只是个存储变量的东西,先放置1进去,再取出来,得到1.
可是奇怪了,前面提到Thread中的是ThreadLocalMap,这里写的案例是ThreadLocal,第一次看到ThreadLocal我也挺疑惑的,下面来看看这到底是什么东西?

ThreadLocal图示

JAVA基础:ThreadLocal_第1张图片
Thread里维护了一个ThreadLocalMap threadLocals,
这是一个线程私有的map,底层是一个Entry数组,Entry的key是ThreadLocal对象,value是对应的值。注意这里的key ThreadLocal是一个弱引用。所以会存在一些问题。下面再说。

ThreadLocal源码解析

先来细看ThreadLocal的set源码:

 public void set(T value) {
 //取到当前线程
        Thread t = Thread.currentThread();
        //取到当前线程的threadLocals对象
        ThreadLocalMap map = getMap(t);
//如果不为空就设置覆盖原值
        if (map != null)
            map.set(this, value);
        else
        //否则创建map
            createMap(t, value);
    }

//createMap调用到ThreadLocalMap的构造器
 ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
 //创建entry数组,初始化长度16
            table = new Entry[INITIAL_CAPACITY];
            //找到对应的索引位置
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //把entry设置到对应位置
            table[i] = new Entry(firstKey, firstValue);
            //长度是1 
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

来看下具体的set方法

private void set(ThreadLocal key, Object value) {
	   //取到map的 entry数组
            Entry[] tab = table;
            //取到数组长度
            int len = tab.length;
            //算出存的key 索引
            int i = key.threadLocalHashCode & (len-1);
		
		//循环entry数组,直接找到一个空的entry
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal k = e.get();
		//如果找到了这个key,就直接设置并返回
                if (k == key) {
                    e.value = value;
                    return;
                }
		
		//如果发现key为空,说明被回收了,那就
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

//在空的位置 放置新的entry
            tab[i] = new Entry(key, value);
            //size+1
            int sz = ++size;
            //清理
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

下面来看下 替换entry replaceStaleEntry

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

            //在key为空的节点处往前找,一直到entry为空,找到最前面一个key空null 的entry
            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
	//一直往后面找,直到出现空的entry
           for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();

           //如果找到了一样的key,就改值,并把他与之前  根据这个key定位到的entry交换。
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;
                    
		//如果slotToExpunge跟staleSlot相等,说明在前面是没有key为null的entry,就设置它为现在的i,需要清理
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                        //清理
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

               
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

//如果后面没有key为null的entry,就直接设置在之前的entry位置
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

//如果两者不一样,说明找到了最前面的null,就清理
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
//如果一样,说明前后都是空的key,也就不需要清理什么了
        }

至于上面的代码里找到了相同的key以后为啥要交换,有一篇博客讲的很清楚,直接分享出来:
https://www.linuxidc.com/Linux/2018-01/149993.htm

下面来看下 expungeStaleEntry 和 cleanSomeSlots

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

//先把第一个要删的清空掉
            // expunge entry at staleSlot
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            // Rehash until we encounter null
            //再往后照
            Entry e;
            int i;
            for (i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal k = e.get();
                //如果key为空就清理
                if (k == null) {
                    e.value = null;
                    tab[i] = null;
                    size--;
                } else {
                //如果不为空就重新计算位置
                    int h = k.threadLocalHashCode & (len - 1);
                    //如果位置跟原来不一样就要交换
                    if (h != i) {
                        tab[i] = null;

                        // Unlike Knuth 6.4 Algorithm R, we must scan until
                        // null because multiple entries could have been stale.                //找到离h往后找最近的非空位置给他存进去
                         while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //返回的这个i 是开始为null entry的第一个位置
            return i;
        }


//继续清理一波
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];
                //如果出现非空且key为null的entry就清掉
                if (e != null && e.get() == null) {
                    n = len;
                    removed = true;
                    i = expungeStaleEntry(i);
                }
            } while ( (n >>>= 1) != 0);
            return removed;
        }

再看下rehash方法

private void rehash() {
//先清理一波
            expungeStaleEntries();

//当size大于等于容量四分之三就resize
            // Use lower threshold for doubling to avoid hysteresis
            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;
            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();
                    //k为空的话,就设置value为空,杜绝内存泄露
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                    //重算索引
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
			//把对应的entry放在离重新计算的索引 往后找最近的一个空位置上
                        newTab[h] = e;
                        count++;
                    }
                }
            }

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

上面是set方法 的详细执行方法:
简单来说,在set数据进来的时候,会先去对应的entry数组下找到hash得到的索引位置,如果这个位置有值,就一直往后找,直到找到一个空的位置塞进去,这是解决hash冲突的一种方法,线性探测法。在set的同时会去排查key为null的entry,把它清理掉。

至于get方法太简单了,不说了,自己体会

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null)
                return (T)e.value;
        }
        return setInitialValue();
    }

ThreadLocal有什么缺陷?

ThreadLocal这么好用,有什么缺陷呢?
由于ThreadLocal中的ThreadLocalMap
中entry保存的key对象是弱引用,即在GC过程中必然被回收的对象,所以可以出现entry不为空,key为空,value不为空的情况,这种时候这个value值永远不能拿到,造成内存泄露。
ThreadLocal在set方法的时候做了很多判断去清理这部分可能造成泄露的数据,但是可能还是会有泄露的情况。。

如何在子线程获取到父线程的数据?

注意这里的前提条件是不能传参 ,不能static
这是一道面试题,当时没有答出来。
我们知道ThreadLocal是线程本地存储变量,他是线程私有的,所以讲道理是不可能通过ThreadLocal在线程间传递参数。先来看下是不是这样呢?

static ThreadLocal local  = new ThreadLocal<>();

    public static void main(String[] args) {
        local.set(1);
        System.out.println("father :"+local.get());
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("child :"+ local.get());
            }
        }).start();
    }

结果:
father :1
child :null

可以看到,确实不行。

那么怎么实现呢?
回头看看Thread的成员,既然要获取父线程的数据,那Thread是不是有哪个成员可以保存父线程的数据呢?答案是inheritableThreadLocals。
这个对象的实例其实是InheritableThreadLocal.ThreadLocalMap

来试下,把上面代码中的ThreadLocal换成InheritableThreadLocal。
可以看到子线程确实获取了父线程的1.

如何实现的呢?
1、看InheritableThreadLocal的set方法

 public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    //第一次执行时
    //这时候createMap方法调用到
    //InheritableThreadLocal的createMap
    
void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }

//可以看到确实给Thread的inheritableThreadLocals赋值了,那这个inheritableThreadLocals如何传递给子线程呢

那就要看昨天的Thread的init方法了
if (parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
//当父线程inheritableThreadLocals不为空时,子线程根据这个map构造器自己的map,其实是copy数据,copy的是引用,也就是浅拷贝

你可能感兴趣的:(java)