ThreadLocal原理探究

发布时间 2023-04-11 16:27:09作者: 小兵要进步
四大引用是什么,分别有什么特点:
1 强引用、软引用、弱引用、虚引用
强引用:发生gc的时候,只要对象还有引用,就不会被回收
软引用:发生gc的时候,内存够用就不会回收,内存不够时,就会回收。可以及时的避免oom。
Map<String,SoftReference<BitMap>> imageCache = new HashMap<String,SoftReference<BitMap>>();
弱引用:发生gc的时候,马上就会回收。
虚引用:虚引用并不会决定对象的生命周期。如果一个对象仅仅持有虚引用,那么它和没有任何引用一样,在任何时候都可能被垃圾收集器回收。它不能单独使用也不通过通过它访问对象。虚引用必须和引用队列(ReferenceQueue)联合使用。
虚引用的目的是跟踪对象被垃圾回收的状态。仅仅是提供了确保对象被finalize之后做某些事情的机制。PhantomReference的get方法总是返回null.
四种引用垃圾回收的场景:
 
思考:
1 ThreadLocal中的ThreadLocalMap的数据结构与关系?
Thread中有一个成员变量ThreadLocalMap。ThreadLocal中有一个静态内部类ThreadLocalMap,ThreadLocalMap中有一个静态内部类Entry.  Entry是对ThreadLocal的弱引用。
2 Entry的key是弱引用,为什么?
每个Thread对象维护着ThreadLocalMap的引用,ThreadLocalMap是ThreadLocal的内部类,用Entry进行存储。
调用ThreadLocal的set方法时,实际是往ThreadLocalMap设置值,key是ThreadLocal对象, value是传进来的对象
调用ThreadLocal的get方法时, 实际是从ThreadLocalMap中获取值
ThreadLocal本身并不存储值,它只是以自己作为key从线程的ThreadLocalMap获取value, 正因为如此,ThreadLocal实现了线程隔离,获取当前线程的局部变量值,不受其他线程影响。
 
ThreadLocal提供线程局部变量,每一个线程在访问ThreadLocal实例的时候(通过其get与set方法)都有自己的,独立的初始化变量副本。Threadlocal实例通常是类中的私有静态字段,使用它的目的是希望将状态与线程关联起来。
//ThreadLocal的方法
public void set(T value) {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null)
            map.set(this, value); // 使用this引用作为key,既做到了变量相关,又满足key不可变的要求。
       else
           createMap(t, value);
}
//ThreadLocalMap的方法
private void set(ThreadLocal<?> key, Object value) {
           // map中就是使用Entry[]保留所有的entry实例
           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)]) {
               ThreadLocal<?> k = e.get();
               //已经存在则替换旧值
               if (k == key) {
                    e.value = value;
                   return;
               }
               //在设置期间清理哈希表为空的内容,保持哈希表的性质
               if (k == null) {
                   replaceStaleEntry(key, value, i);
                   return;
               }
           }
           //不存在就新建一个entry
            tab[i] = new Entry(key, value);
           int sz = ++size;
           if (!cleanSomeSlots(i, sz) && sz >= threshold)
               rehash();
 }
//ThreadLocal的方法
public T get() {
       Thread t = Thread.currentThread();
       ThreadLocalMap map = getMap(t);
       if (map != null) {
           ThreadLocalMap.Entry e = map.getEntry(this);
           if (e != null) {
               @SuppressWarnings("unchecked")
               T result = (T)e.value;
               return result;
           }
       }
       return setInitialValue();
}
//ThreadLocalMap的方法
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);
}
 
public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null) {
        m.remove(this);
     }
}
 
想要存入的ThreadLocal中的数据实际上并没有存到ThreadLocal对象中去,而是以这个ThreadLocal实例作为key存到了当前线程中的一个Map中去了。
 
ThreadLocal内存泄露问题?
虚拟机栈栈中的ThreadLocal Ref 与堆上的ThreadLocal是强引用的关系,当虚拟机栈中的ThreadLocal Ref 用完之后,准备被回收时,如果entry与堆上的ThreadLocal不是弱引用的话,那么堆上的ThreadLocal对象就不会被回收。而如果是弱引用,当发生Gc时,ThreadLocal用完之后,不存在强引用,而唯一存在一个弱引用,此时ThreadLocal对象就可以被回收了。虽然弱引用可以让堆上的ThreadLocal对象被回收,但是Entry对象还因为线程的存在,有引用可达。ThreadLocal被回收之后,entry里面的key是null, 此时这个entry成为了陈旧项,value对象在线程存在时,仍然会存在,故而仍然有线程泄露的风险。因此就需要某种机制,来对Entry里面的key为null的value进行释放。ThreadLocal采用了线性探测来清除陈旧项(replaceStaleEntry与getEntryAfterMiss方法都干了这个活),从而防止了内存泄漏。当然,我们也可以在用完ThreadLocal之后,手动调用remove方法,去除线程中ThreadLocalMap中的这个entry.
4 ThreadLocal中为什么要加remove方法?
主要是为了及时地将线程中TreadLocalMap中的以这个ThreadLocal为key的entry中的value对象删除。这样就可以避免因为线程长期存在,而ThreadLocal实例用完之后导致的内存泄露问题。
总结:
ThreadLocal并不解决线程间共享数据的问题,而是为每个线程提供了一个独立的变量副本从而避免线程变量的线程安全问题。由于每个线程都有一个执属于自己的ThreadLocalMap,并维护了ThreadLocal与实例之间的映射关系因此就不存在线程安全以及锁的问题ThreadLocalMap的entry对ThreadLocal弱引用避免了ThreadLocal无法被回收的问题ThreadLocal自身的set、get、remove方法最终会调用expungeStaleEntry、cleanSomeSlots、replaceStaleEntry这三个方法回收键位null的entry对象的value值(实例)以及entry对象本身从而防止内存泄露