JAVA并发(三)ThreadLocal与JAVA的四种引用

ThreadLocal用来在多线程环境中安全的保存某一个变量或对象,用以在当前线程的上下文传递。本文将介绍ThreadLoal的实现原理以及它的内存泄露问题,所以,首先会介绍JAVA中四种引用:强引用、软引用、弱引用、虚引用。

强引用、软引用、弱引用、虚引用

  • 强引用:正常new出来对象就是强引用,当内存不够的时候,JVM宁可抛出异常,也不会回收强引用对象。
  • 软引用(SoftReference):软引用生命周期比强引用低,在内存不够的时候,会进行回收软引用对象。软引用对象经常和引用队列ReferenceQueue一起使用,在软引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 弱引用(WeakReference):弱引用生命周期比软引用要短,在下一次GC的时候,扫描到它所管辖的区域存在这样的对象: 一个对象仅仅被weak reference指向, 而没有任何其他strong reference指向,,不管当前内存是否够,该对象都会被回收。弱引用和软引用一样,也会经常和引用队列ReferenceQuene一起使用,在弱引用所引用的对象被GC回收后,会把该引用加入到引用队列中。
  • 虚引用(PhantomReference):又叫幻象引用,与软引用,弱引用不同,虚引用指向的对象十分脆弱,我们不可以通过get方法来得到其指向的对象。它的唯一作用就是当其指向的对象变为不可达时,自己就被加入到引用队列,用作记录该引用指向的对象已被销毁。因此,无论对象是否覆盖了finalize方法,虚引用对象都没办法复活。

finallized方法: 当对象变成(GC Roots)不可达时(第一次回收),GC会判断该对象是否覆盖了finalize方法,若未覆盖,则直接将其回收。否则,若对象未执行过finalize方法,将其放入F-Queue队列,由一低优先级线程执行该队列中对象的finalize方法。因此,虚引用对象一旦变成不可达,便加入了引用队列,GC判断虚引用对象是否覆盖finalize方法时,发现虚引用已经加入了引用队列,自然就没办法将其放在F-Queue队列,而其他类型的对象一旦变成不可达,还可能再执行finalize方法,这就是为什么虚引用对象无法复活的原因
执行finalize方法完毕后,GC会再次判断该对象是否可达(第二次回收),若不可达,则进行回收,否则,对象“复活”。因此,对于重写了finallized方法的对象,会出现两个垃圾回收周期,这两个周期之间可能相隔了很久(取决于finalized方法执行是否及时),所以可能会出现大部分堆被标记为垃圾却还没有被回收,出现内存溢出的错误。

使用虚引用,上述情况将引刃而解,当一个虚引用加入到引用队列时,你绝对没有办法得到一个销毁了的对象。因为这时候,对象已经从内存中销毁了。因为虚引用不能被用作让其指向的对象重生,所以其对象会在垃圾回收的第一个周期就将被清理掉。

ThreadLocal

通常情况下,线程中对全局变量赋值后,可以被任何一个线程访问并修改的。

而创建全局变量ThreadLocal,通过ThreadLocal全局变量传递局部变量,该局部变量只能被当前线程访问,而且可以在线程的上下文传递,其他线程则无法访问和修改。

public class Test {
    private final ThreadLocal<String> mystr = new ThreadLocal<>();
    
    public void methodA() {
        mystr.set("test_str_1");
    }
}

实际上通过ThreadLocal设置的值是放入了当前线程的一个ThreadLocalMap实例中,所以只能在本线程中访问,其他线程无法访问。

实现原理

每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例,value是真正需要存储的Object。

从set()方法的实现,理解ThreadLocal实现

// jdk1.8 source code 
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
}

在调用set方法时

  • 首先获取当前线程 Thread.currentThread()
  • 利用当前线程获取一个ThreadLocalMap对象
  • 判断map是否为空,若为空,创建这个ThreadLocalMap对象并设置值,不为空,则设置值。

getMap()方法:

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

Thread类中,定义了两个属性,threadLocals的初始化是在调用ThreadLocal类中的getMap()方法时完成的,当线程退出时,会将threadLocalsinheritableThreadLocals置为null。

ThreadLocal.ThreadLocalMap threadLocals = null; // ThreadLocalMap对象
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null; // 子类可继承的ThreadLocalMap对象

// 线程退出后,将threadLocals和inheritableThreadLocals置为null
private void exit() {
   if (group != null) {
        group.threadTerminated(this);
        group = null;
   }
   /* Aggressively null out all reference fields: see bug 4006245 */
   target = null;
   /* Speed the release of some of these resources */
   threadLocals = null; 
   inheritableThreadLocals = null;
   inheritedAccessControlContext = null;
   blocker = null;
   uncaughtExceptionHandler = null;
}

现在,完成了前两步,获取当前线程的ThreadLocalMap对象。

ThreadLocalMapThreadLocal的静态内部类,是基于Entry数组的map。EntrykeyThreadLocal弱引用,目的是当线程退出时把threadLocal实例置为null时,不再有强引用指向threadLocal实例,不影响threadLocal实例的垃圾回收。

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

threadlocal的生命周期中,存在这些引用. 看下图: 实线代表强引用,虚线代表弱引用.

在这里插入图片描述

与上面的分析一致,Entry的key为弱引用,它的引用链是ThreadLocalRef -> ThreadLocal ---> key,当栈中的ThreadLocalRef与堆中的ThreadLocal断开时,ThreadLocal实例就会被垃圾回收。

value为强引用,它的引用链是CurrentThreadRef -> CurrentThread -> ThreadLocalMap -> Entry -> value,只要当前线程没有关闭,CurrentThreadRef -> CurrentThread的引用就不会断开,value就不会被垃圾回收。只有当前thread结束以后, CurrentThread就不会存在栈中,强引用断开, CurrentThread, Map, value将全部被GC回收.

是否存在内存泄露?

上节提到当前线程没有退出,将会一直存在CurrentThread至value的引用链,即便将threadLocal手动设置为null也依然存在CurrentThread至value的引用链。这会给开发者产生一种内存泄露的错觉(错觉:value是通过threadLocal设置的,我明明将threadLocal设置为了null,为什么value还会占用内存?),尤其在使用线程池时更容易出现这样的错觉,因为线程池的线程结束后,会放回线程池中不销毁。

可以理解为:threadLocal没有内存泄露,泄露的是Entry。

JDK的优化

为了减缓这种错觉的产生,Java会在调用threadLocal实例的get、set方法且key为null时,清除Entry。以get方法为例:

// threadlocal.get()
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null) {
         ThreadLocalMap.Entry e = map.getEntry(this); //此处调用threadlocalMap.getEntry()
         if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
         }
    }
    return setInitialValue();
}

// threadlocalMap.getEntry()
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); // 没找到该key(threadlocal)时,调用该方法
}

// hash未命中时调用该方法
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) { // ThreadRef这条链还没断,thread未被销毁,entry不为Null
        ThreadLocal<?> k = e.get();
        if (k == key)
             return e;
        if (k == null)   // threadLocalRef这条链已断开,threadLocal实例为Null
             expungeStaleEntry(i); // 删除所有key为null的Entry
        else
             i = nextIndex(i, len);
             e = tab[i];
     }
    
     return null;
}

不仅在调用get方法,在调用set、remove方法时,threadLocal为null时,也会最终调用到expungeStaleEntry()方法 ,清除所有threadLocal为null时entry的强引用,这里不赘述了。

因此,正确的使用方式是,首先判断是否存在场景:threadLocal置为null?

如果存在,在调用完set、get后,记得调用remove方法显示的清除Entry的强引用。如果不存在,threadLocal一直在使用,没有被回收的必要,也不care脏读的情况,那更没必要去回收threadLocalMap中的Entry了。

脏读

示例如下,创建一个大小为8的线程池,向该线程池提交100次任务,因为使用的是线程池,线程不会被销毁,所以假设某一个线程写入了值,然后该线程处于空闲态,然后该线程再次读取时,读取到的是上次该线程运行时设置的值。

可能下面的例子很明显就看的出问题所在,但是当项目复杂时,在多处调用get,就比较容易出现这种问题。

不过这种情况也很容易避免,有两种方法:

  • set、get成对出现,set在前、get在后
  • 使用remove
public class Test {
    private final ThreadLocal<String> mystr = new ThreadLocal<>();
    
    public void methodA() {
    	ExecutorService executor = Executors.newFixedThreadPool(8);
        for (int i=0; i<100; i++) {
            executor.execute(new Runnable() {
                @Override
                public void run() {
                    if (i % 4 == 0) {
                        String s = mystr.get();
                    }
                	mystr.set("test"+i);
                }
            });
        }
        
    }
}

Hash碰撞

在某个线程中,每new一个ThreadLocal实例,该线程的ThreadLocalMap中就会新增的一个key,当ThreadLocal实例过多时,自然会出现hash碰撞。

HashMap的最大的不同在于,ThreadLocal.ThreadLocalMap结构非常简单,没有next引用,也就是说ThreadLocalMap中解决Hash冲突的方式并非链表/红黑树的方式,而是采用线性探测的方式,所谓线性探测,就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

线性探测的方式解决Hash冲突的效率很低,如果有大量不同的ThreadLocal对象放入map中时发送冲突,或者发生二次冲突,则效率很低。所以在开发的过程中,要避免这一点,提高运行效率。

与synchronized的区别

  • ThreadLocal用于处理线程内部上下文变量的传递,变量不会被其他线程访问,而synchronized修饰的变量,只要其他线程获取了锁,就能访问、修改
  • ThreadLocal没有锁的机制,没有锁的开销
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值