Java多线程(10)——ThreadLocal

ThreadLocal是Java框架中经常使用的工具。对于这个知识点,网上博文毛毛多,但有不少都存在一些错误。不过知识就是这样不断建立,发现问题,打破重建螺旋上升的过程。在此记录一下我的认识过程:

第一层级:初识

ThreadLocal顾名思义,线程局部变量。

因此ThreadLocal是线程独占而非处理多线程同步问题的。这在一些博文中有误解。

ThreadLocal就是在使用该对象的每一个线程中创建独立的副本,多个线程彼此之间是隔离的,实际上操作的是不同对象。

示例

public class ThreadLocalDemo{ 
    private static ThreadLocal<Integer> seqNum = new ThreadLocal<Integer>(){
        public Integer initialValue(){
            return 0;
        }
    };
    
    public int getNextNum(){
        seqNum.set(seqNum.get() + 1);
        return seqNum.get();
    }

    public static void main(String[] args){
        ThreadLocalDemo sn = new ThreadLocalDemo();
    
        TestClient t1 = new TestClient(sn);
        TestClient t2 = new TestClient(sn);
        TestClient t3 = new TestClient(sn);
        TestClient t4 = new TestClient(sn);
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }

    private static class TestClient extends Thread {
        private ThreadLocalDemo sn;
        
        public TestClient(ThreadLocalDemo sn){
            this.sn = sn;
        }

        public void run(){
            for(int i=0;i<3;i++){
                System.out.println("thread[" + Thread.currentThread().getName() + "] --> sn[" + sn.getNextNum() + "]");
            }
        }
        
    }
}

输出结果:

thread[Thread-0] --> sn[1]
thread[Thread-0] --> sn[2]
thread[Thread-2] --> sn[1]
thread[Thread-1] --> sn[1]
thread[Thread-1] --> sn[2]
thread[Thread-1] --> sn[3]
thread[Thread-3] --> sn[1]
thread[Thread-3] --> sn[2]
thread[Thread-2] --> sn[2]
thread[Thread-0] --> sn[3]
thread[Thread-2] --> sn[3]
thread[Thread-3] --> sn[3]

多提一句,可以发现代码没有引入任何其他类库,原来ThreadLocal和Thread都是java.lang包下的类,已经自动加载了。

源码分析

ThreadLocal的实现在java.lang.ThreadLocal类中;
ThreadLocal有一个内部类ThreadLocalMap;
Thread类中有一个threadLocals变量,类型是ThreadLoacl.ThreadLocalMap。

很多博文混淆不清就是因为不能理清楚ThreadLocal,ThreadLocalMap,Thread之间的关系。

##ThreadLocal

1,ThreadLocal是个泛型类:

public class ThreadLocal<T> {

2,ThreadLocal的域:

ThreadLocal只有三个域,

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
        new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

配合一个方法:

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

做的事情就是实现一个ThreadLocal的哈希值threadLocalHashCode,这个哈希值在ThreadLocalMap中用到。

3,ThreadLocal的方法:

Thread一共四个基本方法:

(1) void set(Object value)设置当前线程的线程局部变量的值。 
(2) public Object get()该方法返回当前线程所对应的线程局部变量。 
(3) public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。 
(4) protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次,ThreadLocal中的缺省实现直接返回一个null。

基本方法都是通过封装ThreadLocalMap的方法实现的,在看过了ThreadLocal源码后更好理解。

ThreadLocalMap

1, ThreadLocalMap没有继承任何Map,而是单独实现了一个Map功能。如果理解HashMap源码的话再看ThreadLocalMap会比较轻松。

2, ThreadLocalMap元素存储:

ThreadLocalMap定义了一个内部类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,值是一个Object。

有了Entry后,ThreadLocalMap持有一个Entry数组table来存储元素,和HashMap类似。

private Entry[] table;

3,ThreadLocalMap元素操作:

作为一个Map,自然要有get,set操作,在这里就用到了上面ThreadLocal中说到的threadLocalHashCode。以get为例:

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

可知ThreadLocalMap是使用threadLocalHashCode来做元素定位的。

Thread

Thread类和ThreadLocal相关的就是它的一个域:

    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

源码已经说明了,这个域的管理是ThreadLocal来做的,而Thread不操作它,唯一与之发生关系的就是线程退出方法exit():

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

把threadLocals置为空,这在下节的内存泄露问题还将提到。

再看ThreadLocal

之前我们看过Thread的大框知道了ThreadLocal的四个基本方法,也说明了基本方法是调用ThreadLocalMap实现的,那么接下来看如何实现的:

首先看获取和创建ThreadLocalMap的方法:

getMap

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

可知,是根据线程,获取线程的threadLocals域,但threadLocals域默认为空,所以有了创建方法createMap:

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

创建方法,发现在创建同时还放入了一个元素firstValue,由此可以推测,基本方法应该要利用getMap和createMap配合条件来实现的,让我们一探究竟:

get

    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();
    }
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

我们看到其中根据线程操作threadLocalMap,再根据ThreadLocal本身操作对象的过程。

总结:
一个ThreadLocal实例一方面本身代表着一个键,代表着一个指定类型<T>的变量,另一方面拥有着根据键,操作线程存储和获取这个变量的方法。

每个线程Thread都拥有一个ThreadLocalMap表,里面可以以键值对形式存不同类型的变量,其中键是ThreadLocal类型,值是ThreadLocal<T>的T类型。

概念之所以比较容易混淆,是因为在ThreadLocal中放了太多的东西,如果我来实现,将ThreadLocal和ThreadLocalMap分离,再将ThreadLocal的类型定义功能和线程操作功能分开,会更好理解。但写在一起可以做到更好的封装,应该会有利于安全性吧。

内存泄露问题

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreadLocalMap -> Entry -> value永远无法回收,造成内存泄漏。所以,如果这个线程对象被gc回收,这条引用链断裂,就不会出现内存泄露。但在threadLocal设为null和线程结束这两个时间点之间,value不会被回收掉,就发生了我们认为的内存泄露。比如使用线程池的时候,线程结束是不会销毁的,会再次使用的,就可能出现内存泄露。

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。

但是这些被动的预防措施并不能保证不会内存泄漏:

使用static的ThreadLocal,延长了ThreadLocal的生命周期,可能导致的内存泄漏。
分配使用了ThreadLocal又不再调用get(),set(),remove()方法,那么就会导致内存泄漏。

ThreadLocal 最佳实践

综合上面的分析,我们可以理解ThreadLocal内存泄漏的前因后果,那么怎么避免内存泄漏呢?

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
在使用线程池的情况下,没有及时清理ThreadLocal,不仅是内存泄漏的问题,更严重的是可能导致业务逻辑出现问题。所以,使用ThreadLocal就跟加锁完要解锁一样,用完就清理。

再深入

看似理解,但纸上得来终觉浅,遇到问题还是会发现模棱两可。对于遇到的问题,记录如下,适时更新。

1,ThreadLocalMap存储的时间键值对,那么键是什么,值是什么?

答:键是ThreadLocal实例,值是对应线程的变量副本。
详细说明:ThreadLocalMap把ThreadLocal实例作为键,而在实际方法中,是获取ThreadLocal的哈希值threadLocalHashCode。在复制一次源码:

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode =
        new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

仔细分析,threadLocalHashCode是final的,在ThreadLocal实例初始化是通过方法nextHashCode()计算得来,初始化后不可变。

而nextHashCode是static,是静态变量,再看nextHashCode()方法,也是static的,操作nextHashCode自增固定长度。所以每多一个ThreadLocal实例,nextHashCode就会增加,保证了每两个ThreadLocal之间的threadLocalHashCode都不样,这样把它作为键值对的键就可行了。

思考一下threadLocalHashCode有没有可能重复,答案是很难,当ThreadLocal实例很多,可能超过int值范围,这样可能会转回来,使两个哈希值相同。但这样的情况微乎其微,首先这需要有相当多的ThreadLocal实例,其次每次递增的步长HASH_INCREMENT可能也是有讲究的(我猜测,不确定)。所以此事不足为虑。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值