ThreadLocal的那些杂事

我们都知道多个线程需要对一个共享变量进行修改操作的时候容易出现数据安全问题,如下如所示线程操作共享变量的图:

为了保证线程安全,一般使用者在访问共享变量的时候需要进行同步措施才能保证线程安全性,常见的方法是使用synchronized或者Lock锁机制。

有没有一种类似JMM中线程工作内存机制,也就是每个线程都有一份变量副本,然后每个线程对共享变量进行访问的时候访问的是自己线程的变量副本呢? 其实在Java中就有这样的东西,它就是ThreadLocal。

当创建一个ThreadLocal变量,每个线程都会复制一个变量到自己的本地内存,然后每个线程对共享资源进行访问的时候访问的都是线程自己的变量,这样就不会存在线程安全问题。

1、ThreadLocal的原理

ThreadLocal是Java中所提供的线程本地存储机制,可以利用该机制将数据缓存到某个线程内部,这样该线程可以在任意时刻、任意位置都可以获取缓存的数据,下图是ThreadLocal的实现原理图:

Thr ead Local 底层是通过 Thr ead LocalMap来实现的,每个Thread对象中都存在一个 Thr ead LocalMap,这个Map的可以是 Thr ead Local对象,value存储的是需要缓存的值。

在做set操作的时候,key是当前的线程,value是需要存储的值,如下是ThreadLocal的源代码:

public void set(T value) { 
        Thread t = Thread.currentThread(); 
        ThreadLocalMap map = getMap(t); 
        if (map != null) 
            map.set(this, value); 
        else 
            createMap(t, value); 
    }

在get数据的时候,其实是拿到当前的线程然后去ThreadLocalMap中去找这个key(也是当前的线程)对应的value值,get的源码如下所示:

public T get() { 
        Thread t = Thread.currentThread(); 
        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(); 
    }

关于ThreadLocal知识点的总结:

(a)ThreadLocalMap是ThreadLocal的内部类,它是真正存储数据的类,其内部还有一个Entry内部类,ThreadLocalMap就是使用一个Entry 数组来存储数据。

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

(b)ThreadLocalMap的默认初始容量为16;当ThreadLocalMap中的元素数量达到容量的2/3时会触发扩容机制,每次扩容ThreadLocalMap 的容量会是原来的2倍,这样的目的是减少哈希冲突。

#初始的容量 
private static final int INITIAL_CAPACITY = 16; 
#扩容 
 /** 
* The next size value at which to resize. 
*/ 
private int threshold; // Default to 0 
/**   
* Set the resize threshold to maintain at worst a 2/3 load factor. 
*/ 
private void setThreshold(int len) { 
   threshold = len * 2 / 3; 
}

2、ThreadLocal的内存泄漏问题

ThreadLocal存在内存泄漏的原因是因为ThreadLocalMap中包含一个Entry数组,Entry对象中的Key属于弱引用(Entry extends WeakReference<ThreadLocal<?>>),那么Entry对象中的Key可以被GC自动回收。

如果当Entry对象中的Key被GC自动回收后,对应的在堆上的ThreadLocal对象也会被GC回收掉了,经过一轮GC之后的结果如下图所示:

此时ThreadLocal中对应的value值依然被Entry引用,不能被GC自动回收,所以value是不能被GC自动回收的,这种情况下就会存在内存泄露的问题。

在正常情况下,ThreadLocal对象使用完之后会要把Entry对象进行回收,假设在线程池中使用ThreadLocal的时候,由于线程池中的核心线程不会被回收,这就导致了Entry上的value不会被回收,从而出现内存泄漏问题。

解决ThreadLocal内存泄漏的方案是在使用完ThreadLocal对象之后,手动调用ThreadLocal的remove()来清除Entry对象,remove方法的源码如下所示:

private void remove(ThreadLocal<?> key) { 
            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)]) { 
                if (e.get() == key) { 
                    e.clear(); 
                    expungeStaleEntry(i); 
                    return; 
                } 
            } 
        }

3、ThreadLocal应用场景

(1)存储用户的数据

在Web应用中,ThreadLocal可以存储用户数据(如用户的身份信息、权限)。每个线程可以通过ThreadLocal来隔离不同用户的数据。

(2)事务管理

数据库操作过程中,ThreadLocal可以用于管理事务。如在每个线程中维护数据库连接,使得在同一线程中的操作共享相同的事务上下文。

public class TransactionManager { 
  private static final ThreadLocal<Connection>connectionHolder = new ThreadLocal<>(); 

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

  public static void setConnection(Connection connection) { 
    connectionHolder.set(connection); 
  } 

  public static void clear() { 
    connectionHolder .remove(); 
  } 
}

(3)隔离线程,存储一些线程不安全的工具对象

在处理线程不安全的对象(如SimpleDateFormat)时,利用ThreadLocal 来存储DateForma实例,这样确保每个线程都拥有自己独立的 SimpleDateFormat实例,从而避免了线程间的竞争导致数据安全问题。

(4)跨层传递共享信息

假设A、B、C、D 这几个类需要互相传递共享信息,如果在每个方法都声明一个参数来接收共享信息就降低代码的可维护性,此时可以用ThreadLocal来存储共享变量的信息,我们在A类中存值,B、C、D便可以可以获取共享变量值,如下图所示:

4、InheritableThreadLocal

ThreadLocal类是不能提供子线程访问父线程的本地变量的,而InheritableThreadLocal类则可以做到这个功能,如下代码:

public class TestThreadLocal { 
  public static void main(string[]args)throws Exception { 
  InheritableThreadLocal<string> userId = new InheritableThreadLocal<>()userId.set("123456789"); 
  //主线程中创建子线程 
  Thread t1 = new Thread(()-> 
    System.out.println("子线程中获取用户id:" + userId.get()), 
     "龙虾编程"); 
     //启动子线程 
   t1.start(); 
  //主线程中获取用户id 
  System.out.println("主线程中获取用户id:"+ userId.get());

通过InheritableThreadLocal在主线程中子线程t1就可以获取到主线程中设置的用户信息,这样实现了子线程与主线程之间的通信

8