并发系列--ThreadLocal

前言阅读

ThreadLocal以前在看源码的时候会使用到,并且面试时候也会问到,最近趁空余时间看了下源码,来此总结下。

ThreadLocal是线程内部的数据类,通过它可以在指定的线程中存储数据,对于其它线程是无法获取到数据,借此可以实现一些特殊功能。

使用场景

一、场景一 每个线程需要一个独享的对象

当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。最常见的使用场景就是Handler的Looper,它的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以实现。如果不采用ThreadLocal,那么系统就必须提供一个全局的哈希表供Handler查找指定线程的Looper,这样一来就必须提供一个类似于LooperManager的类了,但是系统并没有这么做而是选择了ThreadLocal,这就是ThreadLocal的好处。

又比如我们再使用一些非线程安全的API(如SimpleDataFormat),再多线程环境下一般都会考虑加锁,但是加锁可能会导致线程阻塞,性能会降低.这个时候可以考虑使用ThreadLocal提高程序执行效率,并且不需要加锁.

public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
    @Nullable
    @Override
    protected SimpleDateFormat initialValue() {
        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
    }
    
//    public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = ThreadLocal.withInitial(new Supplier<SimpleDateFormat>() {
//        @Override
//        public SimpleDateFormat get() {
//            return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//        }
//    });

};

public ExecutorService threadPool = Executors.newFixedThreadPool(10);

public String date(int seconds){
    Date date = new Date(1000*seconds);
    SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
    return dateFormat.format(date);
}

@Test
public void simpleDateFormatTest() throws IOException {

    for(int i = 0; i<1000; i++){
        final int finalI = i;
        threadPool.submit(new Runnable() {
            @Override
            public void run() {
                String date = date(finalI);
                System.out.println(date);
            }
        });
    }
    threadPool.shutdown();

    System.in.read();

}

二、场景二 每个线程内需要保存全局变量,复杂逻辑下的对象传递

还有一个场景是复杂逻辑下的对象传递,比如拦截器方法的传递,一个线程内任务过于复杂,可能表现为函数的调用栈比较深以及代码入口的多用性,这个时候可以采用ThreadLocal,采用ThreadLocal可以让线程内保存一个全局对象,在线程内只要通过get就可以获得到保存的全局变量。如果不采用ThreadLocal,那么有两种方法,第一种当函数的调用栈很深的时候,通过函数传参这样是比较麻烦的。第二种通过全局静态对象并发执行,但是如果线程过多,这样得保存多个静态全局变量,这样也是不可思议。这个时候也可以借用ThreadLocal,通过保存在自己的线程内部存储,就不会有方法2的这样问题。

模拟一个拦截器方法传递的Demo

//定义一个全局的ThreadLocal,保存一个线程内全局User对象
class UserContextHolder{
    public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User{

    String name;

    public User(String name) {
        this.name = name;
    }
}

//模拟一个任务一
class Service1{

    public void process(){
        User user = new User("User1");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }

}

//模拟一个任务二
class Service2{

    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("service2拿到用户名:"+user.name);
        user.name = "user2";
        UserContextHolder.holder.set(user);
        new Service3().process();
    }

}

//模拟一个任务三
class Service3{

    public void process(){
        User user = UserContextHolder.holder.get();
        System.out.println("service3拿到用户名:"+ user.name);
    }

}

@Test
public void Test2(){
    new Service1().process();
}

可以看到不需要通过参数传递,而是通过ThreadLocal保存中间需要传递的对象,避免了复杂任务函数调用栈深的麻烦。

三、优点

1.线程安全
2.不需要加锁,提高执行效率
3.更高效地利用内存,节省开销,比如,相比于每个任务新建SimpleDataFormat,显然用ThreadLocal可以节省内存和开销
4.避免传参麻烦,使得代码耦合度更低,更优雅

源码分析

使用场景讲完之后,就可以开始分析源码。JDK和SDK的ThreadLocal都有,这里分析的是Android 29的源码

一、ThreadLocal.ThreadLocalMap

在弄清存储过程之前先解决存放在哪里的问题。ThreadLocalMap就是用来存放的内部类。threadlocal每次获取这个map的时候都是在当前线程中获取。

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

这个ThreadLocalMap就是线程单例的,createMap时候就会保存到Thread中保存

  void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
  }

看下ThreadLocalMap的部分参数和构造方法

    static class ThreadLocalMap {

        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

 ...

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }
        ...
}
1.Entry

是一个key和value的对象,key为ThreadLocal,value为ThreadLocal对应的值,但是可以看到ThreadLocal作为key做了一些特殊处理,即弱引用对象,这样做的好处就是线程销毁时候,对应的实体就会被回收,不会出现内存泄漏。

2.INITIAL_CAPACITY

初始容量table大小为16

3.threshold

当大于容量的2/3的时候会重新分配table大小(rehash)

二、set

数据存储当然分析了set和get就可以明白它的原理,首先从set开始分析

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //1
    if (map != null)  
        map.set(this, value); //3 
    else
        createMap(t, value); //2
}

注释1处从Thread中获取map,注释2是如果map没有创建,就会去创建map,注释3处,重点分析的地方,看下实现

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;
    //通过传入的key的hashCode计算出索引的位置,且运算,得到下标,这样子不容易重复
    int i = key.threadLocalHashCode & (len-1);

    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;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

首先通过传入的key的hashCode计算出索引的位置,且运算,得到下标,然后循环开始设置值:

1.如果当前指向的Entry是存储过的ThreadLocal,就直接将以前的数据覆盖掉,并结束。
2.如果当前这个Entry(有对象但是k==null),那就调用replaceStaleEntry将数据存储进去,并结束。这个操作里面的陈旧条目将会被删除。
3.如果找到最后为空,将退出循环,将值存在这里,size+1,并且执行了cleanSomeSlots,会清除部分陈旧Entry,如果清除不成功,并且大于等于阈值(容量的2/3)threshold就会rehash。至此数据就存储进去了。

Thread,ThreadLocal,ThreadLocalMap的之间的关系

在这里插入图片描述
在这里插入图片描述

面试题:ThreadLocal 如何保证Local属性?

当需要使用多线程时,有个变量恰巧不需要共享,此时就不必使用synchronized这么麻烦的关键字来锁住,每个线程都相当于在堆内存中开辟一个空间,线程中带有对共享变量的缓冲区,通过缓冲区将堆内存中的共享变量进行读取和操作,ThreadLocal相当于线程内的内存,一个局部变量。每次可以对线程自身的数据读取和操作,并不需要通过缓冲区与主内存中的变量进行交互。并不会像synchronized那样修改主内存的数据,再将主内存的数据复制到线程内的工作内存。ThreadLocal可以让线程独占资源,存储于线程内部,避免线程堵塞造成CPU吞吐下降。
  
在每个Thread中包含一个ThreadLocalMap,ThreadLocalMap的key是ThreadLocal的对象,value是独享数据。

三、rehash

这是LocalThread扩容的地方

private void rehash() {
    expungeStaleEntries();

    // Use lower threshold for doubling to avoid hysteresis(避免迟滞)
    if (size >= threshold - threshold / 4)
        resize();
}

首先会调用expungeStaleEntries()来去除陈旧无用的Entry(key==null),看下详细过程

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);
    }
}

这个会遍历table数组,挨个检测是不是陈旧的Entry,具体通过expungeStaleEntry()去除无用的Entry

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

    // expunge entry at staleSlot
    //将tab上stateSlot位置的对象清空
    tab[staleSlot].value = null; 
    tab[staleSlot] = null;
    size--;

    // Rehash until we encounter null
    Entry e;
    int i;
    //遍历staleSlot后面的元素
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        if (k == null) { //当前遍历的Entry的key为null,则将该位置的对象清空
            e.value = null;
            tab[i] = null;
            size--;
        } else { //当前遍历Entry的key不为空
            int h = k.threadLocalHashCode & (len - 1); // 重新计算该Entry的索引位置
            if (h != i) { //如果索引位置不为当前索引位置i
                tab[i] = null;  //则将i位置对象清空,寻找Entry的正确位置,清空的值保存在e中了

                // Unlike Knuth 6.4 Algorithm R, we must scan until
                // null because multiple entries could have been stale.
                //hash碰撞了,寻找下一个位置的元素,直到为null
                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

注意到会分两种情况,当前的ThreadLocal为null,则将当前位置清空,如果不为null,并且计算的hash索引值和当前位置不一样,就会重新寻找新的位置上的值。

值清除之后,就会检测,条件是size >= threshold - threshold / 4,即 size > len * 1/2,就通过resize进行扩容

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();
            if (k == null) {
                e.value = null; // Help the GC
            } else {
                int h = k.threadLocalHashCode & (newLen - 1);
                //hash检测
                while (newTab[h] != null)
                    h = nextIndex(h, newLen);
                newTab[h] = e;
                count++;
            }
        }
    }

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

这一部分首先会以两倍方式进行扩容,然后将数据拷贝到合适位置,然后将新的table数据的引用赋值给原来的table。

四、get

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

注释1先是通过Thread获取到map,注释2为map为null时候调用,看下实现

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;
}

initialValue()一般创建时候就会被重写,设置初始值,后面就是检测map是否为null,创建map设置,设置初始值的过程。

回到get方法注释3处,map不为null,就会通过map获取Entry

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);
}

它会通过hash值去获取索引值,如果这个索引值获取到的ThreadLocal就是传进来的key,直接返回。否则调用getEntryAfterMiss去从当前节点开始线性查找。

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;
}

从给定位置进行线性探测(循环),如果是就返回,只不过他在比较好的是判断了当前Entry 是否是陈旧无用的,如果是,就调用expungeStaleEntry(i) 去掉。

两个问题:

1.为什么循环的终止条件为什么是一旦找到一个空对象就停止返回null(表示没找到)呢?

答: 在进行放的时候,如果哈希碰撞了,就会进行线性探测再散列,现在挨着挨着找,如果当时是存放了数据的话,那么就会放到第一个是空的地方,然后第一个为空的地方不为空了,而现在取的时候都出现null的现象了,说明根本没有存过。

2.expungeStaleEntry(i) 中的重新放置不会放到当前i之前么?从而导致存了,却取不到数据现象。

答:不会,首先能保证的是从哈希函数算出的下标 H(key) 开始到当前的Entry 都是有效的,因为i开始就判断了 k == key 的,其次 expungeStaleEntry(staleSlot) 是从staleSlot开始,清除key为null的Entry,试想如果当前处理位置的下一位就是 目标Thread 的 ThreadLocalMap ,那么它将会被放在当前位置,因为,当前位置一定为空,从H(key)到当前位置一定都有其他Entry占着位置,这时候在 getEntryAfterMiss(ThreadLocal key, int i, Entry e) 中会再一次取当前位置的值,然后判断。

总结:
1.每一个线程都有变量 ThreadLocal.ThreadLocalMap threadLocals保存着自己的 ThreadLocalMap。
2.ThreadLocal 所操作的是当前线程的 ThreadLocalMap 对象中的 table 数组,并把操作的 ThreadLocal 作为键存储。

缺点分析

一、内存泄漏

Entry的key节点为弱引用,当GC时候,Entry中的key就会被回收,为null。

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收器回收。

当线程不终止,那么key对应的value就不能被回收,因为有如下调用链

Thread -> ThreadLocalMap -> Entry(key为null) -> value

因为value和Thread之间还存在这个强引用链路,所以无法会被回收,就可能会出现OOM。

JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会判断这个Entry节点中的key为null,把对应的value置为null,这个对象是可以被回收了。

但是一个ThreadLoca不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不终止,那么调用链就一直存在,那么就导致value的内存泄漏。

建议(阿里规约):

调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以在使用完ThreadLocal之后,应该调用remove方法。

刚才的场景二,使用完就可以通过remove删除。

二、空指针异常问题

@Test
public void Test3(){

    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

//  longThreadLocal.set(Thread.currentThread().getId());

    long value = longThreadLocal.get();

    System.out.println(value);

}

此时运行上面代码就会报空指针异常,问题就在返回值类型long了,如果用包装类型Long,就不会报错了。这个是装箱拆箱问题,所以这个需要注意。

自定义ThreadLocal

public class SimpleThreadLocal<T>{
    /**
     * Key为线程对象,Value为传入的值对象
     */
    private Map<Thread, T> valueMap = Collections.synchronizedMap(new HashMap<Thread, T>());

    /**
     * 设值
     * @param value Map键值对的value
     */
    public void set(T value) {
        valueMap.put(Thread.currentThread(), value);
    }

    /**
     * 取值
     * @return
     */
    public T get() {
        Thread currentThread = Thread.currentThread();
        //返回当前线程对应的变量
        T t = valueMap.get(currentThread);
        //如果当前线程在Map中不存在,则将当前线程存储到Map中
        if (t == null && !valueMap.containsKey(currentThread)) {
            t = initialValue();
            valueMap.put(currentThread, t);
        }
        return t;
    }

    public void remove() {
        valueMap.remove(Thread.currentThread());
    }

    public T initialValue() {
        return null;
    }

}

参考文章

1.ExecutorService 中 shutdown()、shutdownNow()、awaitTermination() 含义和区别

2.ThreadLocal 原理

3.Android的消息机制之ThreadLocal的工作原理

4.Android与Java中的ThreadLocal

5.对ThreadLocal实现原理的一点思考

6.Java并发:ThreadLocal详解

7.Java多线程编程-(8)-多图深入分析ThreadLocal原理

8.轻松使用线程 不共享有时是最好的

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值