您的位置:首页 > 其它

理解多线程中的ThreadLocal?

2020-07-14 06:31 232 查看

1.ThreadLocal是什么以及作用?

概述:通过Thread的源码可以得知,ThreadLocal是Thread的一个局部变量,用来存储每个线程的变量副本,其中真正存储数据的是ThreadLocal中的一个静态内部类ThreadLocalMap中的一个Entry类型的数组,之所以会用到数组来存储,是由于我们每个线程可能需要存储多个不同类型的ThreadLocal变量副本。
作用:很好地解决了线程安全的问题,在多个线程访问同一变量时,通过线程隔离的方式,为每个线程创建一个属于自己的变量副本,这样线程之间操作的只是属于自己的那个副本,所以自然不会造成数据共享安全的问题。

2.源码分析?

变量副本的存储方式:如上图所示,ThreadLocal1表示存储的是int类型的变量,ThreadLocal2表示存储的是String类型的变量,左右两边则表示线程1和线程2,实际上,在我们通过ThreadLocal的set方法设置值的时候,值是以Entry条目的形式存储在ThreadLocal中的静态内部类ThreadLocalMap中,考虑到多种类型的变量是,以Entry数组的形式存储,Entry的key则是ThreadLocal类型的变量引用【注意,这里我们理解的线程的独有的变量副本其实是Thread的一个ThreadLocalMap类型的成员变量,而不是ThreadLocal】,Entry的value则为我们设置进去的Object类型的变量值。在对重复值的情况处理,在set方法中会调用一个nextHashCode()方法去解决重复值的问题。

3.相关api及其源码解析?

ThreadLocal类中提供了几个方法:

1.public T get() { }

2.public void set(T value) { }

3.public void remove() { }

4.protected T initialValue(){ }

3.1 get()方法是用来获取ThreadLocal在当前线程中保存的变量副本;在get方法的实现中,首先获取当前调用者线程,如果当前线程的threadLocals不为null,就直接返回当前线程绑定的本地变量值,否则执行setInitialValue方法初始化threadLocals变量。在setInitialValue方法中,类似于set方法的实现,都是判断当前线程的threadLocals变量是否为null,是则添加本地变量(这个时候由于是初始化,所以添加的值为null),否则创建threadLocals变量,同样添加的值为null。

在Thread类中定义:ThreadLocal.ThreadLocalMap threadLocals = null;

public T get() {
//(1)获取当前线程
Thread t = Thread.currentThread();
//(2)获取当前线程的threadLocals变量
ThreadLocalMap map = getMap(t);
//(3)如果threadLocals变量不为null,就可以在map中查找到本地变量的值
if (map != null) {
//根据key获取对应的条目Entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//(4)执行到此处,threadLocals为null,调用该更改初始化当前线程的threadLocals变量
return setInitialValue();
}

private T setInitialValue() {
//protected T initialValue() {return null;}
T value = initialValue();
//获取当前线程
Thread t = Thread.currentThread();
//以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
return value;
}

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 getMap(Thread t) {
return t.threadLocals;
}

3.2 set()用来设置当前线程中变量的副本;如果调用getMap方法返回值不为null,就直接将value值设置到threadLocals中(key为当前线程引用,值为本地变量);如果getMap方法返回null说明是第一次调用set方法(前面说到过,threadLocals默认值为null,只有调用set方法的时候才会创建map),这个时候就需要调用createMap方法创建threadLocals。

public void set(T value) {
//(1)获取当前线程(调用者线程)
Thread t = Thread.currentThread();
//(2)以当前线程作为key值,去查找对应的线程变量,找到对应的map
ThreadLocalMap map = getMap(t);
//(3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
if (map != null)
map.set(this, value);
//(4)如果map为null,说明首次添加,需要首先创建出对应的map
else
createMap(t, value);
}

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

3.3 remove()用来移除当前线程中变量的副本,remove方法判断该当前线程对应的threadLocals变量是否为null,不为null就直接删除当前线程中指定的threadLocals变量;

1  public void remove() {
2     //获取当前线程绑定的threadLocals
3      ThreadLocalMap m = getMap(Thread.currentThread());
4      //如果map不为null,就移除当前线程中指定ThreadLocal实例的本地变量
5      if (m != null)
6          m.remove(this);
7  }

4.内存泄漏问题?

4.1 分析:由ThreadLocalMap源码可以得知ThreadLocal是一个弱引用,实质就是只要只要发生GC的操作,jvm就会回收掉该ThreadLocal对象。如果所示,key为ThreadLocal的一个实例对象,在发生GC时,由于其是弱引用,所以此时的key为null,自然,我们在ThreadLocalMap中无法通过key为null去获取对应的value;Entry 的 value 就会一直存在一条强 引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,而这块 value 永 远不会被访问到了,所以存在着内存泄露。
4.2 解决:一般来说,只有当线程结束时,Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value这条强引用链才会断开,current thread,ThreadLocalMap,value才会被GC回收,所以我们一般在不使用ThreadLocal变量之后都会手动调用remove()去强制清除Entry中的value值,避免长期占有内存,造成内存泄漏。
4.3 ThreadLocal分别使用强弱引用的情况:
key使用强引用:引用 ThreadLocal 的对象被回收了,但是 ThreadLocalMap还持有 ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 的对象实例不会 被回收,导致 Entry 内存泄漏。
key 使用弱引用:引用的 ThreadLocal 的对象被回收了,由于 ThreadLocalMap 持有 ThreadLocal 的弱引用,即使没有手动删除,ThreadLocal 的对象实例也会被 回收。value 在下一次 ThreadLocalMap 调用 set,get,remove 都有机会被回收。

比较两种情况,我们可以发现:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果都没有手动删除对应 key,都会导致内存泄漏,但是使用弱引用可 以多一层保障。
因此,ThreadLocal 内存泄漏的根源是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引 用。
总结
JVM 利用设置 ThreadLocalMap 的 Key 为弱引用,来避免内存泄露。
JVM 利用调用 remove、get、set 方法的时候,回收value。
当 ThreadLocal 存储很多 Key 为 null 的 Entry 的时候,而不再去调用 remove、
get、set 方法,那么将导致内存泄漏。
使用线程池+ ThreadLocal 时要小心,因为这种情况下,线程是一直在不断的
重复运行的,从而也就造成了 value 可能造成累积的情况。

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

5.线程不安全的场景?

我们知道每个线程独有的ThreadLocal,在ThreadLocalMap中保存的是ThreadLocal对象的引用,之所以不同,是因为线程保存的是不同的ThreadLocal对象引用,但是如果引用是同一个的话,则会造成线程无法隔离,会造成数据共享的问题。如下代码,如果Number对象加上static,则Number对象只会被实例化一次,也就是说不管多少个线程访问value这个threadlocal常量,都是用的同一个泛型为Number对象的ThreadLocal引用,所以该变量不能当成副本使用,此时会导致线程不安全。
解决办法:1.将如下的Number对象不用static修饰,这样每个线程每次访问的Number对象都是新创建的
2.在ThreadLocal初始化的时候,重写父类的initialValue()方法,给其赋一个初始值。

public static  Number number = new Number(0);
public static ThreadLocal<String> value = new ThreadLocal<String>(){
@Override
protected String initialValue() {
return "sunjiahao";
}

};

6.使用场景?

6.1 ThreadLocal的应用场景# 数据库连接

public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};

public static Connection getConnection() {
return connectionHolder.get();
}

6.2 ThreadLocal的应用场景# Session管理

public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}

6.3 ThreadLocal的应用场景# 多线程

* 描述 Java中的ThreadLocal类允许我们创建只能被同一个线程读写的变量。
* 因此,如果一段代码含有一个ThreadLocal变量的引用,即使两个线程同时执行这段代码,
* 它们也无法访问到对方的ThreadLocal变量。
*/
public class ThreadLocalExsample {
​
/**
* 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,
* 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,
* 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,
* 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
*/
public static class MyRunnable implements Runnable {
/**
* 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。
* 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的
* set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,
* 他们仍然无法访问到对方的值。
*/
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
//一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
//可以通过下面方法读取保存在ThreadLocal变量中的值
System.out.println("-------threadLocal value-------"+threadLocal.get());
}
}
​
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
​
运行结果
-------threadLocal value-------38
-------threadLocal value-------88

总结
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
1.在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: