https://www.cnblogs.com/gustavo

Gustavo's Blog

人类的赞歌是勇气的赞歌!

ThreadLocal介绍

介绍

ThreadLocal是一个线程变量工具类,提供了线程局部变量,就是为每一个使用该变量的线程都提供一个变量值的副本。我们可以利用ThreadLocal创建只能由同一线程读和写的变量。因此就算两个线程正在执行同一段代码,并且这段代码具有对ThreadLocal变量的引用,这两个线程也无法看到彼此的ThreadLocal变量。



常用方法

1 . public T get() 获取当前线程的副本变量值。

// 获取当前线程中的变量副本
public T get() {
    // 获取当前线程
    Thread t = Thread.currentThread();
    // 获取线程中的ThreadLocalMap对象
    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;
        }
    }
    // 若没有该变量副本,返回setInitialValue()
    return setInitialValue();
}

2 . public void set(T value) 保存当前线程的副本变量值。

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        // map不为空,直接将ThreadLocal对象作为key
        // 变量本身的值为value,存入map
        map.set(this, value);
    else
        // 否则,创建ThreadLocalMap
        createMap(t, value);
}

3 . public void remove() 移除当前前程的副本变量值。

public void remove() {
   ThreadLocalMap m = getMap(Thread.currentThread());
   if (m != null)
       m.remove(this);
}

4 . public static ThreadLocal withInitial(Supplier supplier) 初始化变量

//使用示例
public class ThreadLocalDemo {
    public static final ThreadLocal<String> THREAD_LOCAL = ThreadLocal.withInitial(() -> {
        System.out.println("invoke initial value");
        return "default value";
    });

    public static void main(String[] args) throws InterruptedException {
        new Thread(() ->{
            THREAD_LOCAL.set("first thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            THREAD_LOCAL.set("second thread");
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            THREAD_LOCAL.set("third thread");
            THREAD_LOCAL.remove();
            System.out.println(THREAD_LOCAL.get());
        }).start();

        new Thread(() ->{
            System.out.println(THREAD_LOCAL.get());
        }).start();

        SECONDS.sleep(1L);
    }
}

// 输出:
first thread
second thread
invoke initial value
default value
invoke initial value
default value

实现原理

ThreadLocalMap是ThreadLocal的核心,定义在ThreadLocal类里的内部类,他维护了一个Enrty数组。ThreadLocal存/取数据都是通过操作Enrty数组来实现的。
Enrty数组作为一个哈希表,将对象通过开放地址方法散列到这个数组中。作为对比,HashMap则是通过链表法将对象散列到数组中。
开放地址法就是元素散列到数组中的位置如果有冲突,再以某种规则在数组中找到下一个可以散列的位置,而在ThreadLocalMap中则是使用线性探测的方式向后依次查找可以散列的位置。
[✎ 这个挺好,也是一种哈希碰撞的解决思路]

ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。
✔ 每个Thread中都有一个ThreadLocal.ThreadLocalMap对象。

public class ThreadLocal<T> {

    // ThreadLocalMap是ThreadLocal的内部类
    static class ThreadLocalMap {

      // Entry类,内部key对应的是ThreadLocal的弱引用
      static class Entry extends WeakReference<ThreadLocal<?>> {
          // 变量的副本,强引用
          Object value;

          Entry(ThreadLocal<?> k, Object v) {
              super(k);
              value = v;
          }
      }
    }
}



扩容

哈希表一般都有扩容操作,那么它是如何触发扩容和如何扩容的呢? 在ThreadLocalMap中有一个阈值threshold=table长度 * 2/3。当size>=threshold时,遍历table并删除key为null的元素,如果删除后size>=threshold*3/4时,需要进行扩容操作。

// 扩容阈值(threshold = 底层哈希表table的长度 len * 2 / 3)
private void rehash() {            expungeStaleEntries();            if (size >= threshold - threshold / 4)                resize();        }

扩容操作比较简单,但是会先判断key是否为null,如果为null,将对应的value也设置为null,帮助gc。扩容时,新建一个大小为原来数组长度的两倍的数组,然后遍历旧数组中的entry并将其插入到新的hash数组中,在扩容的时候,会把key为null的Entry的value值设置为null. 以便内存回收,减少内存泄漏问题。

private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }



应用场景

1 线程资源一致性

我们每次对数据库操作,都会走JDBC getConnection,JDBC保证只要你是同一个线程过来的请求,不管是在哪个part,都返回的是同一个连接。这个就是使用ThreadLocal来做的。
当一个part过来的时候,JDBC会去看ThreadLocal里是不是已经有这个线程的连接了,如果有,就直接返回;如果没有,就从连接池请求分配一个连接,然后放进ThreadLocal里。
这样就可以保证一个事务的所有part都在一个连接里。TheadLocal可以帮助它维护这种一致性,降低「编程难度」。

2 分布式计算

3 比较常见的例子,应该是SimpleDateFormat了,这个对象在多线程下会出现一定的问题,一般在高并发的场景下,都会使用ThreadLocal给每个线程分配一个SimpleDateFormat对象。

private static ThreadLocal<SimpleDateFormat> sdf = ThreadLocal
            .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
  public static String format(Date date) {
        String msg = null;
        try {
            // 调用get方法 -> withInitial初始化 
            msg = sdf.get().format(date);
        } finally {
            // 用完 remove 
            sdf.remove();
        }
        return msg;
    }

4 全局变量
某些数据比如用户ID,很可能在整条业务线上多个方法中都需要用到,如果通过方法参数的形式一层一层的传递下去,整体代码显得凌乱不优雅,这时可以通过ThreadLocal的方式存储。通常可以通过AOP或者拦截器的方式进行赋值,执行完业务逻辑之后调用remove()方法。

private final static ThreadLocal<UserInfo> TL_USER = new ThreadLocal<>();

TL_USER.set(userInfo);

UserInfo userInfo = TL_USER.get();

TL_USER.remove();


5 mybatis
Mybatis使用SqlSessionManager保证了我们同一个线程取出来的连接总是同一个。它是如何做到的呢?其实很简单,就是内部使用了一个ThreadLocal。

private final ThreadLocal<SqlSession> localSqlSession = new ThreadLocal<>();


// 创建连接
public void startManagedSession() {
    this.localSqlSession.set(openSession());
}

// 取连接
@Override
public Connection getConnection() {
    final SqlSession sqlSession = localSqlSession.get();
    if (sqlSession == null) {
        throw new SqlSessionException("Error:  Cannot get connection.  No managed session is started.");
    }
    return sqlSession.getConnection();
}  


常见问题

原因


ThreadLocal threadlocal1 = new ThreadLocal();
强引用:threadlocal1对应图中【1】
弱引用:Key(WeakReference(threadlocal1))对应图中的【2】

但是当我们把threadlocal1 =null;也就是1处,断开强引用时,此时ThreadLocal对象只有一个弱引用,那么GC发生时,ThreadLocal对象被回收了,Entry变成了一个key为null的Entry。也叫脏Entry

[☛ 重点:ThreadLocalMap中的key是ThreadLocal对象,这个是一个弱引用,在gc时会被回收,此时这个节点就变成了一个没有key的节点,不会被回收,成为了脏数据,所以要显示的调用remove方法确保这种情况不发生]

特点是:
key为null,value不能被应用程序访问到,因为我们已经没有引用到他的引用了
Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value链存在,当前线程迟迟不结束(例如线程池),但不能被使用,成了脏数据,造成了内存泄漏。

过程如图:

内存泄漏问题

由于ThreadLocalMap的key是弱引用,而Value是强引用。这就导致了一个问题,ThreadLocal在没有外部对象强引用时,发生GC时弱引用Key会被回收,而Value不会回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。
如何避免泄漏
既然Key是弱引用,那么我们要做的事,就是在调用ThreadLocal的get()、set()方法时完成后再调用remove方法,将Entry节点和Map的引用关系移除,这样整个Entry对象在GC Roots分析后就变成不可达了,下次GC的时候就可以被回收。
如果使用ThreadLocal的set方法之后,没有显示的调用remove方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完ThreadLocal之后,记得调用remove方法。

posted @ 2022-08-20 09:54  BitBean  阅读(51)  评论(0编辑  收藏  举报