• Welcome to the world's largest Chinese hacker forum

    Welcome to the world's largest Chinese hacker forum, our forum registration is open! You can now register for technical communication with us, this is a free and open to the world of the BBS, we founded the purpose for the study of network security, please don't release business of black/grey, or on the BBS posts, to seek help hacker if violations, we will permanently frozen your IP and account, thank you for your cooperation. Hacker attack and defense cracking or network Security

    business please click here: Creation Security  From CNHACKTEAM

Recommended Posts

前言

绑定直译为线程局部变量,或许将它命名为ThreadLocalVariable更为合适。其主要作用就是实现线程本地存储功能,通过线程本地资源隔离,解决多线程并发场景下线程安全问题。

ThreadLocal

接下来,通过绑定的使用案例、应用场景、源码分析来进行深层次的剖析,说明如何避免使用中出现问题以及解决方案。

使用案例

前面提到关于绑定的线程隔离性,通过下面一个简单的例子来演示绑定的隔离性。

包com。斯达瑞。测试。TL;

导入Java。util。ArrayList

导入Java。util。列表;

公共类ThreadLocalTest {

//声明一个绑定成员变量

private final ThreadLocalPerson TL=new thread local();

//声明一个目录作为参照对象

私有最终列表person list=new ArrayList();

公共静态void main(String[] args) {

新的ThreadLocalTest().test();

}

公共无效测试(){

//创建测试人对象

人员人员=新人();

person.setName('张三');

人。setage(24);

//创建线程一:再启动1s后,分别添加人对象到tl、列表对象中

新线程(()- {

尝试{

线程。睡眠(1000);

} catch (InterruptedException e) {

e。printstacktrace();

}

tl.set(人);

list.add(人);

系统。出去。println(线程。当前线程().getName()'[thread]get(): ' TL。get());

系统。出去。println(线程。当前线程().getName()'

  • get(): '列表。get(0));

},'线程-1 ')。start();

//创建线程二:在启动2s后,分别去tl、列表对象中取人对象

新线程(()- {

尝试{

线程。睡眠(2000年);

} catch (InterruptedException e) {

e。printstacktrace();

}

系统。出去。println(线程。当前线程().getName()'[thread]get(): ' TL。get());

系统。出去。println(线程。当前线程().getName()'

  • get(): '列表。get(0));

},'线程-2')。start();

}

//测试静态内部类人

静态类人员{

私有字符串名称;

私人年龄;

公共void集合名称(字符串名){

this.name=name

}

公共无效存储(整数){

this.age=年龄;

}

@覆盖

公共字符串toString() {

返回"人员{"

name='' name '\ ' '

", age=" + age + '}'; } } }

案例中使用两个线程同时对List和ThreadLocal对象进行操作,通过对照实验,从输出结果可以看到,ThreadLocal实现了线程间数据隔离,这也说明每一个Thread对象维护了自己的一份数据。

hread-1 [thread] get():Person{name='张三', age=24}
thread-1 [list] get():Person{name='张三', age=24}
thread-2 [thread] get():null
thread-2 [list] get():Person{name='张三', age=24}

应用场景

针对ThreadLocal而言,由于其适合隔离、线程本地存储等特性,因此天然的适合一些Web应用场景,比如下面所列举的例子:

  • 代替参数显式传递(很少使用)
  • 存储全局用户登录信息
  • 存储数据库连接,以及Session等信息
  • Spring事务处理方案

源码分析

通过使用案例的展示,接下来对ThreadLocal的实现原理进行简单分析。

WeakReference

在对ThreadLocal的源码展开描述之前,首先简单提一下Java中四种引用类型,强、软、若、虚之一的弱引用,这四种引用关系引用程度依次降低。Java中弱引用通过WeakReference表示,在JDK1.2引入。

public class WeakReference<T> extends Reference<T> {
    // 创建一个给定类型的对象弱引用
    public WeakReference(T referent) {
        super(referent);
    }
    // 创建一个给定类型的对象弱引用,并注册到队列
    public WeakReference(T referent, ReferenceQueue<? super T> q) {
        super(referent, q);
    }
}

弱引用用来描述非必须对象,被弱引用关联的对象只能生存到下一次垃圾收集发生为止。如果发生垃圾收集,无论内存空间是否满足,都会回收掉被弱引用关联的对象。
例如下面代码模拟,为了便于模拟出效果,指定虚拟机启动参数:-Xms4m -Xmx4m

public class WeakRefTest {
    @Override
    protected void finalize() {
        System.out.println("gc");
    }
    public static void main(String[] args) {
        for (int i = 0; i < 500; i++) {
            WeakRefTest weakRefTest = new WeakRefTest();
            new WeakReference<>(weakRefTest);
            if (i >= 450) {
                System.gc();
            }
        }
    }
}
  • finalize()是Object方法,当虚拟机在回收对象时,允许执行完该方法后再进行回收
  • System.gc()会通知虚拟机进行垃圾回收,并不会立即进行垃圾回收

执行结果:

gc
...

关于弱引用的特性,为什么ThreadLocal中要使用弱引用来维护一个对象,后面会继续进行描述。

ThreadLocalMap

ThreadLocalMap是ThreadLocal的一个静态内部类。每一个Thread对象实例中都维护了ThreadLocalMap对象,对象本质存储了一组以ThreadLocal为key(this对象实际使用的是唯一threadLocalHashCode值),以本地线程包含变量为value的K-V键值对。
在ThreadLocalMap内部还维护了一个Entry静态内部类,该类继承了WeakReference,并指定其所引用的泛型类为ThreadLocal类型。Entry是一个键值对结构,使用ThreadLocal类型对象作为引用的key。

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

查看Entry源码。Entry之所以使用数组结构,一个Thread在运行的过程中会存在多个ThreadLocal对象的场景,ThreadLocalMap作为ThreadLocal的静态内部类,需要维护多个ThreadLocal对象所存储的value值。

// 初始化默认容量为 16
private static final int INITIAL_CAPACITY = 16;
// 数据存储结构底层实现为Entry数组,其长度必须为2的倍数
private Entry[] table;
// table中Entry的实际数量,初始值为0
private int size = 0;
// 存储的阈值
private int threshold; // Default to 0
// resize扩容阈值加载因子为2/3
private void setThreshold(int len) {
    threshold = len * 2 / 3;
}

整个ThreadLocal类中核心内容都是对ThreadLocalMap进行操作,而ThreadLocalMap的核心内容都是围绕Entry组成的Map存储结构进行操作。关于ThreadLocal、ThreadLocalMap、Entry之间的关系如图所示:
ThreadLocal.png
ThreadLocal对象是当前线程的ThreadLocalMap的访问入口,Thread类中维护了两个关于ThreadLocalMap的成员变量。

// ThreadLocal变量
ThreadLocal.ThreadLocalMap threadLocals = null;
// InheritableThreadLocal变量,该类继承自ThreadLocal
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

threadLocals在Thread类中作为成员变量,初始化线程对象时并不会被赋予值,只有在使用ThreadLocal时进行赋值。查看ThreadLocal中的get方法

public T get() {
    // 获取当前操作线程
    Thread t = Thread.currentThread();
    // 调用getMap方法,返回当前线程的实例变量threadLocals值
    ThreadLocalMap map = getMap(t);
    // 如果返回map不为空,返回map中所存储的以当前ThreadLocal对象为key的值
    if (map != null) {
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    // 如果map为空进行map值的初始化
    return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
    // 返回传入线程(当前线程)中成员变量的threadLocals值
    return t.threadLocals;
}
private T setInitialValue() {
    // 调用initialValue()方法设置初始值,默认不设置任何值,可以在创建ThreadLocal
    // 对象时被重写进行初始化,只会进行一次初始化。
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}
void createMap(Thread t, T firstValue) {
    // 初始化当前线程对象实例变量threadLocals的值,Map所对应的key为当前ThreadLocal对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

接下来查看set方法

public void set(T value) {
    // 获取当前线程对象
    Thread t = Thread.currentThread();
    // 调用getMap方法,传入当前对象的值,获取当前线程的实例变量threadLocals值
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        // 如果map为空,创建ThreadLocalMap
        createMap(t, value);
}

而inheritableThreadLocals会在创建线程时,根据线程构造方法传参,确定是否进行初始化。

// 该init方法为Thread内部线程初始化方法,inheritThreadLocals是否继承父类变量,默认false
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals);
// 如果inheritThreadLocals为true并且parent(为当前线程,视为要被继承线程的父线程)
// 的ThreadLocal不为null,调用createInheritedMap方法进行继承初始化
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
// 为子线程创建一个新的ThreadLocalMap并初始化parentMap中的变量实现继承
static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
    return new ThreadLocalMap(parentMap);
}

注意事项

ThreadLocal提供了便利的同时当然也需要注意在使用过程中的一些细节问题。下面进行简单总结

异步调用

ThreadLocal默认情况下不会进行子线程对父线程变量的传递性,在开启异步线程的时候需要注意这一点,关于这一点可以通过Thread类构造方法提供的inheritThreadLocals参数进行封装,或者使用Spring根据装饰器模式进行封装的TaskDecorator类实现跨线程传递方法。

线程池问题

线程池中线程调用使用ThreadLocal 需要注意,由于线程池中对线程管理都是采用线程复用的方法,在线程池中线程非常难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测。另外重复使用可能导致ThreadLocal
对象未被清理,在ThreadLocalMap中进行值操作时被覆盖,或取到旧值。如下代码所示:

package com.starsray.test.thread;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;
public class ThreadLocalPoolTest {
    private final static ThreadLocal<AtomicInteger> tl = ThreadLocal.withInitial(() -> new AtomicInteger(0));
    static class Task implements Runnable{
        @Override
        public void run() {
            System.out.println(tl.get().incrementAndGet());
        }
    }
    
    public static void main(String[] args) {
        ExecutorService pool = Executors.newFixedThreadPool(2);
        for (int i = 0; i < 5; i++) {
            pool.execute(new Task());
        }
        pool.shutdown();
    }
}

期待的输出结果应该是1,实际输出结果,由于超出后线程被复用,输出结果也会取到旧值。

1
1
2
2
3
3

当然,如果必须要在线程池中使用ThreadLocal也不是不能使用,在线程池类ThreadPoolExecutor中定义了钩子函数,可以在初始化或者任务执行完做特殊处理,如初始化ThreadLocal或者记录日志。重写beforeExecute方法:

protected void beforeExecute(Thread t, Runnable r) { }

内存泄露

ThreadLocal对象不仅提供了get、set方法,还提供了remove方法。虽然get、set已经对空值进行清理,但在实际使用时,手动调用remove方法养成良好的编程习惯是非常有必要的。
ThreadLocal中主要的存储单元Entry类继承了WeakReference,该类的引用在虚拟机进行GC时会被进行清理,但是对于value如果是强引用类型,就需要进行手动remove,避免value的内存泄露。关于引用关系参考下图所示:
image

关于内存泄露这块的重点在于两部分:

  • ThreadLocal被一强(tl = new强引用)一弱(WeakReference<ThreadLocal<?>>)两部分引用,强引用可以通过编码解决(tl = null),而弱引用部分在GC时会自动清理掉key部分的引用。
  • 关于value部分的引用,如果是强引用类型的value通过remove方法可以清理,避免内存泄露。

具体细节查看remove部分的源码:

public void remove() {
    // 获取当前线程中threadLocals对应的ThreadLocalMap
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        // 如果ThreadLocalMap不为空,继续调用remove(this)方法
        m.remove(this);
}

查看remove(this)具体内容

private void remove(ThreadLocal<?> key) {
    // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    Entry[] tab = table;
    // tab的长度
    int len = tab.length;
    // 根据当前ThreadLocal对象的唯一threadLocalHashCode值并通过与操作
    // 获取当前ThreadLocal对象在table中value所在的下标值i
    int i = key.threadLocalHashCode & (len-1);
    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        // 对table进行遍历,如果Entry所对应的key为当前ThreadLocal对象,执行clear方法
        if (e.get() == key) {
            // 将引用置为null
            e.clear();
            // 清楚陈旧的键值对
            expungeStaleEntry(i);
            return;
        }
    }
}

查看clear()方法,Clear方法位于Reference类中,由于Entry类继承了WeakReference(继承WeakReference),此处的clear属于多态的应用。

public void clear() {
    this.referent = null;
}

接下来expungStaleEntry(i)方法则是整个remove的核心逻辑了,这里首先再明确以下两个变量的意义:

  • size:前面提到size是Entry的数量,即ThreadLocal中成员变量table的实际键值对数量
  • i:table中与当前ThreadLocal对象相匹配的Entry的key值的下标
private int expungeStaleEntry(int staleSlot) {
    // 创建新都tab数组,引用指向当前ThreadLocal对象中的table
    Entry[] tab = table;
    // tab的长度
    int len = tab.length;
    // 将tab中下标staleSlot(i)对应的value引用置为null
    tab[staleSlot].value = null;
    // 将tab中下标staleSlot的Entry置为null
    tab[staleSlot] = null;
    // Entry对应的长度减1
    size--;
    // Rehash until we encounter null 直到遇到null开始rehash
    Entry e;
    int i;
    // 从staleSlot后以索引开始遍历,直到遇到某个Entry不为空为止
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        // 获取Entry对应的ThreadLocal对象的引用key值
        ThreadLocal<?> k = e.get();
        if (k == null) {
            // 如果为空,将value和键值对同时置空,size减1
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            // 如果k不为null,说明弱引用未被GC回收,获取table中k对应的下标
            int h = k.threadLocalHashCode & (len - 1);
            // 判断传入下标,与当前k对象的下标是否一直
            if (h != i) {
                // 如果不一致,需要对tab中的值进行更新,直接清空
                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)
                    // 采用R算法的变种,从当前h开始寻找一个为null的值存储e
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    // 返回第一个entry为null的下标
    return i;
}

关于expungeStaleEntry中原作者对关键地方进行了英文注释,源码提及了Knuth 6.4 Algorithm R算法,R算法主要说明了如何从使用线性探测的散列表中删除一个元素。
与Knuth 6.4算法R不同,这里必须扫描到null,可能出现空的Entry,多个条目可能已经过时,由于不使用引用队列,因此只有在表开始空间不足时才能保证删除过时的条目。

总结

Thread对象中通过维护了一个ThreadLocal.ThreadLocalMap类型的threadLocals变量实现线程间变量隔离,并维护了一个ThreadLocal.ThreadLocalMap类型的inheritableThreadLocals变量实现线程间变量的继承,是否继承由线程初始化时inheritThreadLocals参数进行决定,默认不继承。
ThreadLocal中核心存储的类为ThreadLocalMap类,ThreadLocalMap类本身是一个定制化的Map,这个Map以当前ThreadLocal对象作为key值进行K-V存储。ThreadLocalMap的初始化容量为16,扩容因子为2/3。
ThreadLocalMap在进行存储时,会获取当前this对象的threadLocalHashCode值(这也是为什么使用ThreadLocal作为key的原因),该值是唯一的,只在ThreadLocalMap中有用,使用Unsafe提供的AtomicInt类操作获取。
ThreadLocalMap中进行存储的基本单位为Entry数组,数组下标通过threadLocalHashCode进行&运算并根据当前数组长度进行自动扩容。

说明:为什么ThreadLocal的key要使用当前ThreadLocal对象或者说是threadLocalHashCode的值,而不是使用当前线程对象?
一个ThreadLocal对象只会对应一个线程对象,但是一个Thread对象会存在多个ThreadLocal对象,之所以不使用Thread对象作为key,是为了避免多个ThreadLocal对象(或者说T、、hreadLocalMap)之间的互相影响。

Link to comment
Share on other sites