ThreadLocal作用、原理以及问题

ThreadLocal

1、ThreadLocal的作用

在多线程访问共享资源时会采取一定的线程同步方式(如:加锁)来解决带来的并发问题。(如图)

使用ThreadLocal对共享资源的访问也可以解决并发问题

作用:ThreadLocal提供了线程的本地变量,即当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量。

这里的本地内存并不是线程的工作内存,而是Thread类中的一个变量,而不是放在不是存放在ThreadLocal实例里面

这样做的好处

  • 线程安全,可以避免多线程访问同一个共享变量导致的并发问题。
  • 不需要加锁,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。

2、 ThreadLocal的原理

2.1 ThreadLocalMap

前面提到:每个线程的本地变量是放在调用线程Thread类中的一个变量threadLocals中,而不是放在不是存放在ThreadLocal实例里面

public class Thread implements Runnable {
 	 ....
         
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;  // 存放本地变量,默认为null
    
    ....
}

可以看到threadLocals真正的类型为ThreadLocalMapThreadLocalMap本质就是一个HashMap,当创建一个ThreadLocal对象时就会将此对象添加到map中。其中key就是当前ThreadLocal的实例对象引用(注意:这里的引用为弱引用),value则是用set方法设定的值。

2.1.1 为什么要用map?

这是因为在实际使用中可能会有多个ThreadLocal变量,因此需要将这些ThreadLocal添加到map中。

2.1.2 为什么ThreadLocalMap中把ThreadLocal对象存储为Key时使用的是弱引用
类型回收时间应用场景
强引用一直存活,除非GC Roots不可达所有程序的场景,基本对象,自定义对象等
软引用内存不足时会被回收一般用在对内存非常敏感的资源上,用作缓存的场景比较多,例如:网页缓存、图片缓存
弱引用只能存活到下一次GC前生命周期很短的对象,例如ThreadLocal中的Key。
虚引用随时会被回收, 创建了可能很快就会被回收可能被JVM团队内部用来跟踪JVM的垃圾回收活动
  • 一来说使用ThreadLocal时会有两个引用指向ThreadLocal对象,一个是通过new创建ThreadLocal对象的强引用,一个是ThreadLocalMap对ThreadLocal对象的弱引用。
  • 如果为强引用: 由于ThreadLocalMap是属于线程的,而我们创建多线程时一般是使用线程池进行创建,线程池中的部分线程在任务结束后是不会关闭的,那么这部分线程中的ThreadLocalMap将会一直持有对ThreadLocal对象的强引用,导致ThreadLocal对象无法被垃圾回收,从而造成内存泄漏
  • 因此设置为弱引用:在下一次垃圾回收时,无论内存空间是否足够,只有弱引用指向的对象都会被直接回收。所以将ThreadLocalMap对ThreadLocal对象的引用设置成弱引用,就能避免ThreadLocal对象无法回收导致内存泄漏的问题。

2.2 源码分析

我们从源码中了解ThreadLocal的原理,下面来看一下具体ThreadLocal是如何实现的。

2.2.1 set
public void set(T value) {
        // 获取当前线程
        Thread t = Thread.currentThread(); 
       // 获取当前线程中的threadLocals
        ThreadLocalMap map = getMap(t);  
        if (map != null) {
            // 调用map的set方法
            map.set(this, value);  
        } else {
            // 如果map为空则创建map
            createMap(t, value);  
        }
    }

其中getMap方法如下:

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

可以看出,set方法首先会获取当前的线程,然后会通过当前线程取出前面所说的类型为ThreadLocalMap的threadLocals变量,通过此变量将值存在当前的Thread中。由于threadLocals的默认值为空,所以当第一次调用set方法时会调用createMap方法。

createMap方法:其实就是new了一个ThreadLocalMap对象

 void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
2.2.2 get
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();  
    // 获取当前线程中的threadLocals
    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;
        }
    }
    // 如果threadLocals为空或者threadLocals对应的值为空,则初始化
    return setInitialValue();
}

可以看出,get方法主要就是获取当前线程中的threadLocals,如果不为空则取出对应的值,否则调用初始化方法setInitialValue

setInitialValue方法

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);
    }
    // ThreadLocal的又一个扩展。该ThreadLocal关联的值在线程结束前会被特殊处理,处理方式取决于回调方法threadTerminated(T value)
    if (this instanceof TerminatingThreadLocal) {
        TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
    }
    return value;
}

如果当前线程的threadLocals变量不为空,则设置当前线程的本地变量值为null,否则调用createMap方法

2.2.3 remove
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null) {
        // 从map中删除
        m.remove(this);
    }
}

每个线程的本地变量存放在线程自己的threadLocals中,如果当前线程一直不消亡,那么这些本地变量会一直存在,所以可能会造成内存溢出,因此使用完毕后记得调用remove方法删除对应线程的threadLocals重的本地变量。

3、ThreadLocal内存泄漏问题

在ThreadLocalMap中是使用Entry类型的数组实现的 ,Entry 如下

static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

从上面的代码可以看出,Entry继承了WeakReference,并且用ThreadLocal的弱引用作为key。

3.1 存在的问题

前面讲到,使用ThreadLocal的弱引用的主要原因是防止ThreadLocalMap一直持有对ThreadLocal对象的强引用,导致ThreadLocal对象无法被垃圾回收,从而造成内存泄漏。设置为弱引用,当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会在gc时被回收。但是对应的value的引用是强引用,因此还是会造成内存泄漏,这个时候ThreadLocalMap里面会存在key为null,但是value不为null的选项。所以ThreadLocal类定义了expungeStaleEntry方法用于清理key为null的value。expungeStaleEntry在remove中方法中调用。

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
    		// 计算当前ThreadLocal变量锁在的table数组位置,快速定位法
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
    		// 防止快速定位法失效,遍历table数组
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    // 调用WeakReference的clear方法清除弱引用
                    e.clear();
                    // 清除key为null的元素
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

expungeStaleEntry方法

        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为null的value引用
                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.
                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            return i;
        }

可以看出,在expungeStaleEntry方法中会从当前位置开始遍历table,并清理key为null的元素。

注意:虽然ThreadLocal有expungeStaleEntry方法清除key为null的元素。但是可以看出循环退出的条件为遇到null的元素,因此null之后的并且key为null的元素无法被清除。并且这种清除方式是不及时的。所以在一定情况下依然会发生内存泄漏,最好的办法就是每次调用完之后及时使用remove方法。

  • 1
    点赞
  • 15
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值