继续浏览精彩内容
慕课网APP
程序员的梦工厂
打开
继续
感谢您的支持,我会继续努力的
赞赏金额会直接到老师账户
将二维码发送给自己后长按识别
微信支付
支付宝支付

Java并发编程之ThreadLocal详解

临摹微笑
关注TA
已关注
手记 286
粉丝 32
获赞 169

ThreadLocal是什么?


ThreadLocal是一个关于创建线程局部变量的类。

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。而使用ThreadLocal创建的变量只能被当前线程访问,其他线程则无法访问和修改。

ThreadLocal使用示例


示例1:ThreadLocal声明基本类型变量

https://img.mukewang.com/5d31c1700001ef9c06880501.jpg

示例1 代码段(1)


https://img1.mukewang.com/5d31c1720001534e07210264.jpg

示例1 代码段(2)


  执行程序,可以得到:


https://img4.mukewang.com/5d31c17500017a2d04740391.jpg

示例1 执行结果

从运行结果可以看出,对于基本类型变量,ThreadLocal确实是可以达到线程隔离作用的。

示例2:ThreadLocal声明自定义类型的对象

https://img4.mukewang.com/5d31c17b0001caf403750285.jpg

示例2 自定义类型


https://img2.mukewang.com/5d31c17d0001ae8a07150400.jpg

示例2 代码段(1)


https://img.mukewang.com/5d31c1800001af1306250352.jpg

示例2 代码段(2)


  执行程序,可以得到:


https://img1.mukewang.com/5d31c1870001d7be04610447.jpg

示例2 运行结果

从运行结果可以看出,对于自定义类型的对象,ThreadLocal也是可以达到线程隔离作用的。

示例3:ThreadLocal声明的变量都指向同一个对象

https://img1.mukewang.com/5d31c189000144e207010425.jpg

示例3 对程序代码稍作修改

对示例2的代码稍作修改,使得ThreadLocal声明的变量初始化时不再实例化一个新的对象,而是让它指向同一个对象,运行查看结果:


https://img4.mukewang.com/5d31c18c0001f1d505130435.jpg

示例3 运行结果

很显然,在这里,并没有通过ThreadLocal达到线程隔离的机制,可是ThreadLocal不是保证线程安全的么?这是什么鬼?  显然,虽说ThreadLocal让访问某个变量的线程都拥有自己的局部变量,但是如果这个局部变量都指向同一个对象的话,这个时候,ThreadLocal就失效了。

ThreadLocal源码剖析


ThreadLocal类的源码在java.lang包中。其中主要有四个方法:

1. get()

// 返回当前线程所对应的线程变量public T get() {    // 获取当前线程
    Thread t = Thread.currentThread();    // 获取当前线程的成员变量 threadLocal
    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();
}

从源码中可以看到,get()方法首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的键值对Entry,最后通过该Entry获取目标值result。

其中,getMap()方法可以获取当前线程所对应的ThreadLocalMap,其源代码如下:

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

2. set(T value)

// 设置当前线程的线程局部变量的值。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方法首先获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,其源代码如下:

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

3. initialValue()

// 返回该线程局部变量的初始值。protected T initialValue() {    return null;
}

该方法定义为protected级别且返回为null,很明显是要子类重写来实现它的,所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。

4. remove()

// 将当前线程局部变量的值删除public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());    if (m != null)
        m.remove(this);
}

该方法的目的是减少内存占用,避免出现因为线程迟迟未结束而导致内存泄漏的情况。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

ThreadLocalMap类


从ThreadLocal的源码中我们可以看到,ThreadLocal的实现比较简单,主要是依赖于ThreadLocalMap这个类,我们有必要好好理解一下后者。

根据命名就可以看出,ThreadLocalMap,它实际上是一个Map键值对。在其内部使用了Entry的方式来实现key-value的存储:

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

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

在上面的代码中,Entry内的Key就是ThreadLocal,而Value就是线程私有的那个变量。同时,Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用是一个弱引用。

下面来看一下ThreadLocalMap类中几个核心的方法:

1. set(ThreadLocal<?> key, Object value)

    private void set(ThreadLocal<?> key, Object value) {        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.

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

        tab[i] = new Entry(key, value);        int sz = ++size;        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

源码的意思简单明了,根据要保存的key到Entry数组中去匹配,如果key已经存在就更新值,否则创建新的entry写入。

值得注意的是,这里的set()操作和我们在集合Map了解的put()方式有点儿不一样,虽然他们都是key-value结构,不同点在于他们解决散列冲突的方式不同。 集合Map的put()采用的是拉链法,即在每个数组元素的位置,存入链表来解决冲突。而ThreadLocalMap的set()则是采用开放定址法来解决冲突的。

set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。在set()方法中还有一个变量很重要:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();

从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():

private static AtomicInteger nextHashCode = new AtomicInteger();private static final int HASH_INCREMENT = 0x61c88647;private static int nextHashCode() {    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。

2. getEntry(ThreadLocal<?> key)

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的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们要找的元素,则返回,否则调用getEntryAfterMiss()再寻找,源码如下:

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;    int len = tab.length;    while (e != null) {
        ThreadLocal<?> k = e.get();        if (k == key)            return e;        if (k == null)
            expungeStaleEntry(i);        else
            i = nextIndex(i, len);
        e = tab[i];
    }    return null;
}

这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

ThreadLocal与内存泄漏


(注:本节参考了博文  http://blog.xiaohansong.com/2016/08/06/ThreadLocal-memory-leak/)

前面提到过,每个Thread都有一个ThreadLocal.ThreadLocalMap,该map的key为ThreadLocal实例的一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收。

如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

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

            

作者:chenjieping1995
链接:https://www.jianshu.com/p/53579bd5e12f


打开App,阅读手记
0人推荐
发表评论
随时随地看视频慕课网APP