理解多线程中的ThreadLocal?

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

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值