ThreadLocal的原理

  ThreadLocal是 一种线程隔离机制,为我们提供了另一种解决线程并发访问的问题,利用副本机制解决了并发变量访问安全,采用了空间换时间的策略。

1、threadlocal的使用

如果希望变量在线程运行的时候不受其他线程更新的干扰,想让它成为一个线程级别的变量,则可以考虑使用threadlocal。
public class ThreadLocalTest {
    //值的初始化
    static final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        Thread thread[] = new Thread[3];
        for (int i = 0; i < 3; i++) {

            thread[i] = new Thread(new Runnable() {
                @Override
                public void run() {
                    int num = threadLocal.get();  //local变量值的获取;
                    threadLocal.set(num + 1);     //local变量值的设置;
                    System.out.println("threadLocal get: " + Thread.currentThread().getName() + " " + threadLocal.get());
                }
            }, "t" + i);
        }

        for (int i = 0; i < 3; i++) {
            thread[i].start();
        }
    }
}

运行结果

t1 threadLocal get: 1
t2 threadLocal get: 1
t0 threadLocal get: 1

从结果可以看到,线程之间threadlocal变量是互不影响的,互相隔离的,保证了访问的安全性。

2、ThreadLocal的数据结构

副本机制保证变量的独享性
  加锁保证了变量在多线程环境下的读写可见性,而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,也就是必须都是new出来的,直接把某个对象在各个线程中实例化一份,每个线程都有属于自己的对象,让变量与当前的线程绑定,类似copy on write技术的思想;空间换时间,多线程之间不能共享,从而隔离了多个线程对数据的访问冲突。比如context和session。
    ThreadLocal类源码分析 每个Thread内部维护着一个ThreadLocalMap,它是一个Map。这个映射表的Key是一个弱引用,其实就是ThreadLocal对象this本身,Value是真正存的线程变量Object。
public class ThreadLocal<T> {   

  private final int threadLocalHashCode = nextHashCode();
    private static final int HASH_INCREMENT = 0x61c88647; //斐波那契黄金分割魔数 - 线性探测-随机值的产生;

    public T get();                
    public void set(T value);
    public void remove() ;
    protected T initialValue() ; //给变量初始化

}
  //threadlocal 的静态内部类
 static class ThreadLocalMap {
                      //key为弱引用
    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value; //强引用 value

        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    //初始大小 16
    private static final int INITIAL_CAPACITY = 16;
    //散列表table
    private Entry[] table;
    //初始元素个数
    private int size = 0;    
    //扩容的阈值
    private int threshold; // Default to 0
    //扩容的条件:长度达到总len * 2/3
    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

thredlocal的数据结构是什么?

内部实现: 使用了thradlocal 每个线程都有一个threadlocalMap对象,这是一个集合,这个集合里面会存储一个key-value的数据:
  • 每个线程都保存一个threadlocalmap对象;
  • Map的内容:Entry: key-value:
  • 散列表冲突解决:线形探测,里面用到了一个黄金分割数-- 魔数: 0x61c88647
threadLocal的key和value是什么?
  • key:this本身 ,弱引用,WeakReference<this>,这也是为什么可以直接get和set值,因为key已定:this对象;
  • value:我们设置的值,强引用;

3、ThreadLocal是否一定安全呢?

ThreadLocal使变量与线程作用域绑定,避免了线程共享,但是使用thradlocal就一定是安全的吗,这里需要注意如果设置的变量本身就是一个共享的对象,那还是会有安全问题,例如 当thradlocal是引用类型的数据的时候还是会存在数据安全问题。 java的参数的传递:
  • 传值; - 副本机制
  • 传引用 - 本质是共享
实例:
public class ThreadLocalRef {
    public static class NumIncrease {
        int num;
        public void incr() {
            num++;
        }
        public NumIncrease() {
            this.num = 0;
        }
    }
    static NumIncrease numIncrease = new NumIncrease();

    //此时传递的是地址,是一个引用类型的,当多个线程访问的时候还是存在线程安全问题;
    static ThreadLocal<NumIncrease> threadLocalRef = new ThreadLocal<NumIncrease>() {
        protected NumIncrease initialValue() {
            return numIncrease;  
        }
    };
    //此时传递的是一个副本值,是一个值类型的,当多个线程访问的时候就不存在线程安全问题;
    static ThreadLocal<NumIncrease> threadLocal = new ThreadLocal<NumIncrease>() {
        protected NumIncrease initialValue() {
            return new NumIncrease();  
        }
    };
    public static void main(String[] args) {
        for (int i = 0; i < 3; i++)
            new Thread(() -> {
                numIncrease.incr();
                System.out.println(Thread.currentThread().getName() + "threadLocalRef " + threadLocalRef.get().num);
                System.out.println(Thread.currentThread().getName() + " threadLocal " + threadLocal.get().num);
            }).start();
    }
}

运行结果:

Thread-0threadLocalRef 2
Thread-2threadLocalRef 3
Thread-2 threadLocal 0
Thread-1threadLocalRef 2
Thread-1 threadLocal 0
Thread-0 threadLocal 0

从结果可以看出,threadLocalRef是的对象地址,引用类型还是存在线程安全问题,值变成了不确定的2-2-3;而传递副本址的threadLocal就很好的做到了线程变量的隔离,这里需要注意。

4、Hash冲突的解决

Threadlocal对hash冲突的处理策略是: 线性探测法
Hash表是可以根据key进行直接访问的数据结构,也就是说我们可以通过 hash函数把key映射到hash表中的一个位置来访问记录,从而加快查找的速度,存放记录的数据就是hash表(散列表),当我们针对一个key通过hash函数计算产生的一个位置,在hash表中已经被另外一个键值对占用时,那么线性探测解决这个冲突就分两种情况:
  • 写入: 查找hash表中离冲突单元最近的空闲单元,把新的键值插入到这个空闲单元;
  • 查找: 根据hash函数计算的一个位置处开始往后查找,指导找到与key对应的value或者找到空的单元;
其他的hash冲突解决:
  • 再hash法;
  • 开放链地址法;
  • 建立一个 公共的溢出区;
冲突hash散列的黄金魔数
每个threadlocal对象都有一个hash值threadLocalHashCode,每初始化一个threadLocal对象,hash值就增加一个固定的大小:0x61c88647。
分布的比较均匀,避免了冲突,散列堆积,保证大概率索引的下一个坑是没有数据插入的。
hashcode中的黄金分割魔数: 0x61c88647 ,会让hashcode非常均匀;
private static final int HASH_INCREMENT = 0x61c88647;

public static void magicHash(int size) {
    int hashCode = 0;
    for (int i = 0; i < size; i++) {
        hashCode = i * HASH_INCREMENT + HASH_INCREMENT;
        System.out.println(hashCode & (size -1)); //todo 可以发现非常均匀的分布
    }
}

5、内存泄漏

源码中threadlocalmap中key和value的定义:
//key继承来弱引用,WeakReference,在gc的时候会被回收;
  static class Entry extends WeakReference<ThreadLocal<?>> {
  /** The value associated with this ThreadLocal. */
    Object value;  //value是强引用

    Entry(ThreadLocal<?> k, Object v) {
        super(k);     //key是弱引用,jvm在下一轮gc的时候就会回收;
        value = v;
    }
}

Entry类继承了WeakRefeeence<Threadlocal>,即每个Entry对象都有一个threadlocal弱引用。

  • key是this的 threadlocal弱引用;在下次gc的时候就会被回收;
  • value是强引用;
内存泄漏原因:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应被gc回收key的value就会导致内存泄漏。
分析: 因为如果弱引用key=ThreadLocal Ref被回收,key= threadLocal = null,但value此时还存在一条强引用链, Current Thread --> ThreadLocalMap-->Entry—>Value,这条强引用链会导致Entry不会回收,占着10m的内存,因为key被回收,此value永远也不会被访问到, 该回收的内存没有回收,造成了内存的泄漏,只有当线程结束,value才能被回收。也就是说,发生的内存泄漏的时间段是: key被回收到线程销毁的时间段内;但是如果线程一直存在,那么内存泄漏将一直存在。
结论:
只要这个线程对象能被及时回收,这个内存泄漏就能及时避免,只是在key被gc回收到线程结束期间value不会被回收,就发生了内存泄漏。
但重要的是:如果线程对象不被回收,泄漏就比较严重了,value会一直存在,尤其在高并发的场景:
  • * 静态变量:使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
  • * 线程池:分配使用了ThreadLocal又不再调用get()、set()、remove()方法,那么就会导致内存泄漏。
解决办法:
  • 使用完,调用remove()方法;
  • 为了减小内存泄漏的可能性,threadlocal本身也有清除策略;每次get和set的时候都会清除map里所有key为null的value;

6、local的继承-InheritableThreadLocal

  • 父子线程的ThreadLocal时可以传递继承的,InheritableThreadLocal
  • 子线程get父线程的值,且父子线程之间互不影响;
代码实例:
public class InheritableThreadLocal  {
    //可继承的threadlocal
    public static final ThreadLocal<String> local = new java.lang.InheritableThreadLocal<>();

    public static void main(String[] args){
        local.set("parent"); //父进程main

        System.out.println(Thread.currentThread().getName() + " " + local.get()); //parent
        new Thread(()->{
            ThreadLocal<String> localson = new ThreadLocal<>();
            localson.set("king-son");
            System.out.println(Thread.currentThread().getName() + local.get());   //parent
            local.set("son");
            System.out.println(Thread.currentThread().getName() + local.get());   //son
            System.out.println(Thread.currentThread().getName() + localson.get());//king-son

        }).start();

        System.out.println(Thread.currentThread().getName() + local.get());       //parent

    }
}

7、ThreadLocal源码分析

ThreadLocal的使用和方法非常简单,来看看具体做来那些事情。

7.1、set()

初始化: 前面分析了 set 方法第一次初始化 ThreadLocalMap,初始化一个大小为16的map 的过程
扩容条件: 当容量达到2/3时- 装载因子:0.66 ,扩容为2倍。主线程定义几个thradlocal变量,map中就有几个key;
长度为2^n,和hashmap一样,方便位运算,提高效率;
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

map不为空时set方法的执行逻辑:

  • 根据key(key是localhost的对象,value是值)的散列哈希计算Entry数组下标;
  • 通过线性探索探测从i开始往后一直遍历到数组的最后一个Entry
  • 如果map中的key和传入key相等,表示数据已经存在,直接覆盖;
  • 如果map中key为空,则用新的key/value覆盖,并清理key=null的数据;
  • 判断是否需要rehash扩容;
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); //每个线程都会存储一个ThreadLocalMap,获取map数据结构;
    if (map != null)
        map.set(this, value); //设置值
    else
        createMap(t, value);  //如果map为空,则创建一个map;
}

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    int i = key.threadLocalHashCode & (len-1); //获取value的索引值下标

    //这个for循环就是线形探测的过程
    for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();            //得到的是弱引用值

        if (k == key) {       //防止hash冲突,比较key的值;相等修改完成返回;
            e.value = value;
            return;
        }

        if (k == null) {                      //e != null,但是k == null ,这里说明什么?说明key被jvm回收了
            replaceStaleEntry(key, value, i); //清理策略,防治内存泄漏:替换脏数据,这里也是尽量避免内存泄漏问题;
            return;
        }
    }

    tab[i] = new Entry(key, value); //e == null,直接赋值就OK了;
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();                          //如果map中的值的个数大于阈值,大于总容量的2/3,直接rehash;
}

7.2、get()方法

从map中取值很简单,这里主要看一下thradlocal在get的时候,初始化map的过程;
public T get() {
    Thread t = Thread.currentThread(); //得到当前的线程对象
    ThreadLocalMap map = getMap(t);    //获取threadlocalmap对象
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this); //通过key-this 获取值:value
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    //如果map为空则初始化map
    return setInitialValue();
}

//从这里可以看出threadlocals是线程t的一个成员变量; 
ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals; //每个线程都有一个threadlocal变量
}

private T setInitialValue() {
    T value = initialValue();       //得到初始化的值;
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t); // 得到threadlocalmap对象
    if (map != null)
        map.set(this, value);       //map已创建,则直接set值;
    else
        createMap(t, value);        //创建thradlocalmap
    return value;
}
创建map对象
void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue); // 创建实例化map对象
}


private static final int INITIAL_CAPACITY = 16; //初始大小 16 
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    //设置table的初始大小;
    table = new Entry[INITIAL_CAPACITY]; 
    //得到value在hashtable中的下表索引位置
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    //将值key-value存入entry中;
    table[i] = new Entry(firstKey, firstValue);
    //记录map中值的个数
    size = 1;
    //设置threadlocal的扩容大小为 len*2/3
    setThreshold(INITIAL_CAPACITY);
}
//达到总容量的 2/3 就开始扩容
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

至此,threadlocalmap 创建工作完成。

7.3、remove()

所以最稳妥的方法就是在使用完ThreadLocal后及时调用 remove()方法。该方法会查找以当前Threadlocal对象为key的Entry,及时清理这个元素的key/value/entry本身,将其设置为null;并且也会向前向后查找清除key=null的数据value;
ps: 在使用 线程池的情况下,及时调用remove()清理ThreadLocalMap的脏值,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。
//清理不用的entry节点
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) {          // 找到要清除的key,直接clear()
            e.clear();                     // 清除key-value
            expungeStaleEntry(i); //执行清除策略,向前向后查找
            return;
        }
    }

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

7.4、ThreadLocal的内存清理策略

为了更好的使用内存,ThreadLocalMap本身对 已被回收key的value内存的清理也做了很多的工作,例如每次执行get()和set()方法的时候,如果get和set的时候获取的key=null,则附近大概率节点也会为null,所以就会 向前或者向后查找具体key=null的value数据进行清理。
执行垃圾清理策略的时机?
  • rehash() - 执行一次rehash()-扩容操作的时候会对key=null的值进行清理;
  • remove();直接清除key=null的值;
  • get()和set()方法的调用时会自动向前向后查找key=null的value进行清除;
// 替换清理过期的Entry节点
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;
    int slotToExpunge = staleSlot;
    //向前查找清理key被回收无用的value
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len))
        if (e.get() == null)
            slotToExpunge = i;

    //向后查找,清理key被回收无用的value
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

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

            tab[i] = tab[staleSlot];
            tab[staleSlot] = e;

            // Start expunge at preceding stale entry if it exists
            if (slotToExpunge == staleSlot)
                slotToExpunge = i;
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // If we didn't find stale entry on backward scan, the
        // first stale entry seen while scanning for key is the
        // first still present in the run.
        if (k == null && slotToExpunge == staleSlot)
            slotToExpunge = i;
    }

    // If key not found, put new entry in stale slot
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // If there are any other stale entries in run, expunge them
    if (slotToExpunge != staleSlot)
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}

8、threadlocal与同步机制的比较

   都是为了解决多线程中相同变量的访问冲突问题, ThreadLocal和线程同步机制相比有什么优势呢?
  在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。
  而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。
   对于多线程资源共享的问题, 同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式前者仅提供一份变量,让不同的线程排队访问;而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
 

9、问题思考

思考 一:为什么 key 不是 Thread ID?
    用线程id做key,无法区分多个value。
    如果是thread id,那一个thread就只能存储一个value值。从 controller -> service -> dao,这都是同一个线程,这样变量的设置就局限为一个了。为了满足同一个线程经历不同的阶段有不同的threadlocal,所以放弃了threadid,采用thread的this变量更加灵活。
 
思考二: key为什么要使用弱引用?
    弱引用帮我们更好的管理我们的内存资源,能及时的释放不再使用的线程级别的对象;主要是为了保证threadlocal对象内存的及时回收,弱引用的对象只能生存到下一次gc回收前;如果每个key都是强引用,那么这个threadlocal就会因为和entry存在强引用无法被回收,但其实大部分的线程级别的变量的生命周期都很短,除非线程结束,线程被回收了,map才会被回收,这样浪费了内存。
    强引用如果线程不回收,其实还是会引起内存的泄漏,因为线程也是gc-root,这样这个已经过期的本该回收的数据一直存在一个强引用,可达性分析还是可达,gc不会被回收,造成内存泄漏。
 
思考三 :为什么所有的 Thread不 共享一个全局的 ConcurrentHashMap ?
    既然每个Thread都有自己的局部变量存储空间,那map肯定是属于Thread类的成员变量,全局的map反而需要全局并发控制。
 
思考四: 为什么采取线性探测 处理冲突 ,不采用链表?
  • 第一:threadlocal使用数据量小,不像Hashmap一次存放的数据量比较大; 
  • 第二: 查询加速,利用线性探测的方式使得内存地址连续,查询效率极高,可以采用缓存加速;
但是线性探测也有缺点: 数据量多的情况下,冲突增加, 容易产生数据堆积;
 
 

10、小结

ThreadLocal虽然用法简单,但是还是有很多小细节值得注意和学习。
使用场景
  • 多线程访问共享变量的时候,每个线程都希望获得该变量的初始值的一个副本,线程级别控制的变量;
  • 多数据源的切换,数据库的访问对connection的控制;
  • 线程级别参数的优雅传递,避免冗余的型参传递;分布式锁的uuid传递;
  • 跟踪线程执行,traceId
使用建议
  • 一个线程只存放一个threadlocal,避免了冲突的发生,值少会降低冲突的概率;
  • 通过initialValue来初始化threadlocal变量的值;默认返回null
  • 使用完毕后,调用remove()方法避免内存泄漏;
  • 虽然是副本机制,但是也要考虑到引用地址的影响; 如果是引用对象,那么还是会存在数据安全问题;
 
    OK---莫等闲、白了少年头,空悲切。
 
 
 
水滴石穿,积少成多。学习笔记,内容简单,用于复习,梳理巩固。
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值