ThreadLocal最全详解

649 阅读9分钟

ThreadLocal

笔者上个星期参加了微盟新零售的业务工程师面试,得到了几家offer,最终选择了一家上市国企,955不打卡,公积金拉满,下面是我面试遇到的一些问题,我觉得有必要深入探讨一下细节。

微盟_ABC看图.jpg

   面试官:看你项目用了ThreadLocal是什么,怎么实现(给我详细讲讲)?
   
  我(微微一笑):我先讲一下什么是ThreadLocal,首先,ThreadLocal
  不是用来解决共享对象的多线程访问问题的,一般情况下,通过
  ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,
  其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。

面试官:给我讲讲里面的ThreadLocal类中的变量

我:

  private final int threadLocalHashCode = nextHashCode();
  private static final int HASH_INCREMENT = 0x61c88647;
  private static AtomicInteger nextHashCode =     new AtomicInteger();

连续生成的散列码之间的差异-将隐式顺序线程本地ID转换为近似最优分布的乘法散列值,以获得两个大小表的幂。

看看官方文档的定义

    ThreadLocals rely on per-thread linear-probe hash maps attached to
     each thread (Thread.threadLocals and inheritableThreadLocals). The 
     ThreadLocal objects act as keys, searched via threadLocalHashCode. 
     This is a custom hash code (useful only within ThreadLocalMaps) that 
     eliminates collisions in the common case where consecutively 
     constructed ThreadLocals are used by the same threads, while remaining 
     well-behaved in less common cases  
   翻译官方文档的定义:
   这句话什么意思呢,ThreadLocals 依赖于附加到每个线程
   (Thread.threadLocals 和inheritable ThreadLocals)的每线程线性探针哈希映射。
   当把ThreadLocal 对象充当键,通过threadLocal ashCode 进行搜索。 
   这是一个自定义散列代码(仅在 ThreadLocalMaps 中有用),它在相同线程
   使用连续构造的 ThreadLocals 的常见情况下消除了冲突,同时在极端的情况下保持稳定的行为。
   
 面试官:小伙子可以呀,那你跟我说说他怎么实现每个线程区分开的呢?
   我: ThreadLocal类提供线程局部变量。这些变量不同于它们的普通对应变量,
   因为每个访问一个(通过其 getset 方法)的线程都有自己的、独立初始化的
   变量副本。 ThreadLocal 实例通常是希望将状态与线程相关联的类中的私有静态
   字段(例如,用户 ID 或事务 ID)。
   例如,下面的类生成每个线程本地的唯一标识符。线程的 id 在第一次调用 ThreadId.get() 时
   被分配,并且在后续调用中保持不变。
   导入 java.util.concurrent.atomic.AtomicInteger;
      // 包含每个线程 ID 的线程局部变量
   私有静态最终 ThreadLocal<Integer> threadId =
       新线程本地<整数>(){
           @Override 受保护的整数 initialValue() {
               返回 nextId.getAndIncrement();
       }
   };
  
   // 返回当前线程的唯一 ID,必要时分配它
   公共静态 int get() {
       返回 threadId.get();
   }
  

面试官:你再讲讲ThreadLocal与Synchronized的区别吧

我:ThreadLocal<T>其实是与线程绑定的一个变量。ThreadLocalSynchonized都用于
解决多线程并发访问。
但是ThreadLocal与synchronized有本质的区别:
1Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
2Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLo
cal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样
就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西
的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。

面试官:讲讲ThreadLocal的原理

要看原理那么就得从源码看起。

 ThreadLocal的set()方法:
public void set(T value) {
      //1、获取当前线程
      Thread t = Thread.currentThread();
      //2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,
      //则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          // 初始化thradLocalMap 并赋值
          createMap(t, value);
  }
       从上面的代码可以看出,ThreadLocal  set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。

```js

那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。大家最后自己再idea上跟下源码,会有更深的认识。


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

      
  }
  可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,
  而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。

 这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
      t.threadLocals = new ThreadLocalMap(this, firstValue);
  }


  //ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
          table = new Entry[INITIAL_CAPACITY];
          int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
          table[i] = new Entry(firstKey, firstValue);
          size = 1;
          setThreshold(INITIAL_CAPACITY);
      }
    ThreadLocal的get方法
   
  public T get() {
      //1、获取当前线程
      Thread t = Thread.currentThread();
      //2、获取当前线程的ThreadLocalMap
      ThreadLocalMap map = getMap(t);
      //3、如果map数据为空,
      if (map != null) {
          //3.1、获取threalLocalMap中存储的值
          ThreadLocalMap.Entry e = map.getEntry(this);
          if (e != null) {
              @SuppressWarnings("unchecked")
              T result = (T)e.value;
              return result;
          }
      }
      //如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
      return setInitialValue();
  } 
private T setInitialValue() {
      T value = initialValue();
      Thread t = Thread.currentThread();
      ThreadLocalMap map = getMap(t);
      if (map != null)
          map.set(this, value);
      else
          createMap(t, value);
      return value;
  }
ThreadLocal的remove方法
public void remove() {
       ThreadLocalMap m = getMap(Thread.currentThread());
       if (m != null)
           m.remove(this);
   }

   remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。
   为什么要删除,这涉及到内存泄露的问题。实际上 ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,
   弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。所以如果 
   ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap中使用这个
   ThreadLocal 的 key 也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 
   value。ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除
   (remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池
   中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例子
   ,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作
   ,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。

面试官:ThreadLocal 常见使用场景

我:

1、每个线程需要有自己单独的实例
2、实例需要在多个方法中共享,但不希望被多线程共享
对于第一点,每个线程拥有自己实例,实现它的方式很多。例如可以在线程内部构建一个单独的实例。ThreadLocal
可以以非常方便的形式满足该需求。
对于第二点,可以在满足第一点(每个线程有自己的实例)的条件下,通过方法间引用传递的形式实现。
ThreadLocal 使得代码耦合度更低,且实现更优雅。

场景

1)存储用户Session

一个简单的用ThreadLocal来存储Session的例子:

private static final ThreadLocal threadSession = new ThreadLocal();

   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;
   }
场景二、数据库连接,处理数据库事务

场景三、数据跨层传递(controller,service, dao)

     每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法
     直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),
这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,
避免了将这个对象(如 user 对象)作为参数传递的麻烦。
比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用
service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。
这个例子和存储session有些像。

package com.Jason.threadlocal;


public class ThreadLocalDemo05 {
   public static void main(String[] args) {
       User user = new User("jack");
       new Service1().service1(user);
   }

}

class Service1 {
   public void service1(User user){
       //给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
       UserContextHolder.holder.set(user);
       new Service2().service2();
   }
}

class Service2 {
   public void service2(){
       User user = UserContextHolder.holder.get();
       System.out.println("service2拿到的用户:"+user.name);
       new Service3().service3();
   }
}

class Service3 {
   public void service3(){
       User user = UserContextHolder.holder.get();
       System.out.println("service3拿到的用户:"+user.name);
       //在整个流程执行完毕后,一定要执行remove
       UserContextHolder.holder.remove();
   }
}

class UserContextHolder {
   //创建ThreadLocal保存User对象
   public static ThreadLocal<User> holder = new ThreadLocal<>();
}

class User {
   String name;
   public User(String name){
       this.name = name;
   }
}



执行的结果:

service2拿到的用户:jack
service3拿到的用户:jack
场景四、Spring使用ThreadLocal解决线程安全问题 

我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,
在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean
(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)
中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,
因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。 
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放
功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程,如图所示。 

引用.jpg


这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有
对象所访问的同一ThreadLocal变量都是当前线程所绑定的。



下面的实例能够体现Spring对有状态Bean的改造思路:




代码清单9-5  TopicDao:非线程安全


public class TopicDao {
 //①一个非线程安全的变量
 private Connection conn; 
 public void addTopic(){
      //②引用非线程安全变量
     Statement stat = conn.createStatement();
     …
 }
由于①处的conn是成员变量,因为addTopic()方法是非线程安全的,必须在使用时创建一个新TopicDao实例
(非singleton)。下面使用ThreadLocal对conn这个非线程安全的“状态”进行改造: 

代码清单9-6  TopicDao:线程安全 


import java.sql.Connection;
import java.sql.Statement;
public class TopicDao {

//①使用ThreadLocal保存Connection变量
private static ThreadLocal<Connection> connThreadLocal = new ThreadLocal<Connection>();
public static Connection getConnection(){
       
      //②如果connThreadLocal没有本线程对应的Connection创建一个新的Connection,
      //并将其保存到线程本地变量中。
if (connThreadLocal.get() == null) {
  		Connection conn = ConnectionManager.getConnection();
  		connThreadLocal.set(conn);
            return conn;
  	}else{
            //③直接返回线程本地变量
  		return connThreadLocal.get();
  	}
  }
  public void addTopic() {

  	//④从ThreadLocal中获取线程对应的
       Statement stat = getConnection().createStatement();
  }


不同的线程在使用TopicDao时,先判断connThreadLocal.get()是否为null,如果为null,
则说明当前线程还没有对应的Connection对象,这时创建一个Connection对象并添加到本地线程变量中;
如果不为null,则说明当前的线程已经拥有了Connection对象,直接使用就可以了。这样,就保证了不同
的线程使用线程相关的Connection,而不会使用其他线程的Connection。因此,这个TopicDao就可以做到
singleton共享了。 
当然,这个例子本身很粗糙,将Connection的ThreadLocal直接放在Dao只能做到本Dao的多个方法共享
Connection时不发生线程安全问题,但无法和其他Dao共用同一个Connection,要做到同一事务多Dao
共享同一个Connection,必须在一个共同的外部类使用ThreadLocal保存Connection。但这个实例基本上
说明了Spring对有状态类线程安全化的解决思路。在本章后面的内容中,我们将详细说明Spring如何通过
ThreadLocal解决事务管理的问题。