ThreadLocal 的实现机制与踩坑

ThreadLocal 的实现机制与踩坑

ThreadLocal简介

ThreadLocal主要提供thread-local变量(线程本地变量),与共享变量不同,ThreadLocal 让每个线程都将目标数据复制一份作为线程私有,后续对于该数据的操作都是在各自私有的副本上进行,线程之间彼此相互隔离,也就不存在竞争问题。
访问变量的时候我们可以通过get/set方法访问。ThreadLocal变量一般是私有static类型,与线程状态紧密联系,比如绑定在线程的事务id或者用户数据。可以通过如下方式访问:

 import java.util.concurrent.atomic.AtomicInteger;
  
   public class ThreadId {
       // Atomic integer containing the next thread ID to be assigned
       private static final AtomicInteger nextId = new AtomicInteger(0);
  
       // Thread local variable containing each thread's ID
       private static final ThreadLocal<Integer> threadId =
           new ThreadLocal<Integer>() {
               @Override protected Integer initialValue() {
                   return nextId.getAndIncrement();
           }
       };
  
       // Returns the current thread's unique ID, assigning it if necessary
       public static int get() {
           return threadId.get();
       }
   }

类图

在这里插入图片描述

通过类图我们可以看到:

  1. ThreadLocal类目依赖ThreadLocalMap数据结构存储数据
  2. ThreadLocalMap类目是一个自定义的Entry对象
  3. ThreadLocalMap中key是ThreadLocal自身,value是ThreadLocal绑定的数据

ThreadLocal源码分析

set(T)方法

    public void set(T value) {
        //1. 获取当前线程
        Thread t = Thread.currentThread();
        //2. 获取当前线程绑定的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //3.1 设置当前线程和value到ThreadLocalMap
            map.set(this, value);
        else
            //3.2 如果没有ThreadLocalMap,则初始化
            createMap(t, value);
    }

getMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

createMap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

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

ThreadLocalMap初始化包含两个参数,当前线程和当前线程的绑定value
特别需要注意的是,对应的的数据是在数组链表中Entry中的下表i,而下标计算如下:

 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    private final int threadLocalHashCode = nextHashCode();
    
 	private static AtomicInteger nextHashCode = new AtomicInteger();
	//这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527
    private static final int HASH_INCREMENT = 0x61c88647;

    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

从下标计算可以看到,同一个线程重复生成ThreadLocal对象,对应的HashCode值是不一样的,进而确认唯一的ThreadLocal。

0x61c88647

在上一个被构造出的ThreadLocal的ID/threadLocalHashCode的基础上加上一个魔数0x61c88647的。

这个魔数的选取与斐波那契散列有关,0x61c88647对应的十进制为1640531527。

斐波那契散列的乘数可以用(long) ((1L << 31) * (Math.sqrt(5) - 1))可以得到2654435769,如果把这个值给转为带符号的int,则会得到-1640531527。换句话说 (1L << 32) - (long) ((1L << 31) * (Math.sqrt(5) - 1))得到的结果就是1640531527也就是0x61c88647 。
通过理论与实践,当我们用0x61c88647作为魔数累加为每个ThreadLocal分配各自的ID也就是threadLocalHashCode再与2的幂取模,得到的结果分布很均匀。
ThreadLocalMap使用的是线性探测法,均匀分布的好处在于很快就能探测到下一个临近的可用slot,从而保证效率。

map.set

如果已经对应的ThreadLocal已经存在数据,那我们需要将当前线程和value绑定存入ThreadLocalMap。

       private void set(ThreadLocal<?> key, Object value) {

            Entry[] tab = table;
            int len = tab.length;
           //1. 计算下标i
            int i = key.threadLocalHashCode & (len-1);
			//2. 获取i位置的Entry对象,如果发现hash冲突,则调用nextIndex
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //3. 获取Entry对象绑定的ThreadLocal
                ThreadLocal<?> k = e.get();
				//4. 如果绑定对象与设置对象相同,即是同一个线程设置,则直接替换value
                if (k == key) {
                    e.value = value;
                    return;
                }
				//5. 如果k为null,则替换当前k v
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			
            tab[i] = new Entry(key, value);
            int sz = ++size;
           //清理k为null的数据
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这里我们可以看到每次set方法之后都会做清理工具,所以jdk推荐ThreadLocal变量是private static的,这样子就可以一直持有ThreadLocal对象一直清理,避免内存泄露。
所以 出现内存泄露的前提必须是持有 value 的线程一直存活,这在使用线程池时是很正常的,在这种情况下 value 一直不会被 GC,因为线程对象与 value 之间维护的是强引用。此外就是 后续线程执行的业务一直没有调用 ThreadLocal 的 get 或 set 方法,导致不会主动去删除 key 为 null 的 value 对象,在满足这两个条件下 value 对象一直常驻内存,所以存在内存泄露的可能性。
比如我们的Web Server其实也可以看做一个大型线程池,这一点尤其要注意。

get()

    public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        /获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //根据当前线程 获取Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //记录初始值
        return setInitialValue();
    }

remove()

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

ThreadLocal问题

基本上遇到过三类ThreadLocal问题,或者说我们比较容易踩坑:

  1. 使用先get后set模式导致绑定变量出现问题(一般在Web应用中,比较常见到)
  2. 没有调用remove方法导致的内存泄露问题
  3. ThreadLocal修饰共享变量,导致共享变量数据访问被共享

数据访问问题

这个数据我们自己的代码bug,尤其是web应用中我们通常使用ThreadLocal来方便的传递会话对象。
但是曾经有一次我们就发现线上的会话出现了问题,A用户访问到了B用户的数据,通过查看代码:

//获取ThreadLocal绑定对象 		
		HttpContext context = HttpContext.getContext();
        if (context != null && context.getUser() != null) {
            // 处理业务逻辑
           
        }else{
            context.setUser(new User());
        }

如上代码,每一次http请求,都是先判断是否存在,如果存在则做业务处理,不存在则设置,而WebServer的线程会共享,导致部分用户可能会获取之前用户已经设置的变量。


public class ThreadLocalWithThreadPool implements Callable<Boolean> {

    private static final int N_CPU = 4;


    private static ThreadLocal<String> threadLocal = ThreadLocal.withInitial(() -> {
        System.out.println("thread-" + Thread.currentThread().getId() + " init thread local");
        return "";
    });

    public static void main(String[] args) throws Exception {
        System.out.println("cpu core size : " + N_CPU);
        List<Callable<Boolean>> tasks = new ArrayList<>(N_CPU * 2);
        ThreadLocalWithThreadPool tl = new ThreadLocalWithThreadPool();
        for (int i = 0; i < N_CPU * 2; i++) {
            tasks.add(tl);
        }
        ExecutorService es = Executors.newFixedThreadPool(2);
        List<Future<Boolean>> futures = es.invokeAll(tasks);
        for (final Future<Boolean> future : futures) {
            future.get();
        }
        es.shutdown();
    }

    @Override
    public Boolean call() {
        String li = threadLocal.get();
        if (StringUtils.isNotEmpty(li)) {
            System.out.println(Thread.currentThread().getId() + "_get_" + li);
        } else {
            li = Thread.currentThread().getId() + "_" + RandomUtils.nextInt(10);
            System.out.println(Thread.currentThread().getId() + "_set_" + li);
            threadLocal.set(li);
        }
        return true;
    }
}

如上代码,我们会发现线程获取的变量一直是一样的。
这种就完全属于对ThreadLocal机制了解的问题了,需要提高个人编码水平

未调用remove方法导致的内存溢出问题


public class ThreadLocalWithMemoryLeak implements Callable<Boolean> {

    private static final int N_CPU = 4;

    private class My50MB {
        private byte[] buffer = new byte[50 * 1024 * 1024];

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            System.out.println("gc my 50 mb");
        }
    }

    private ThreadLocal<My50MB> threadLocal = new ThreadLocal<>();

    @Override
    public Boolean call() {
        System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
        threadLocal.set(new My50MB());
        return true;
    }

    public static void main(String[] args) throws Exception {

        TimeUnit.SECONDS.sleep(20);

        List<Callable<Boolean>> tasks = new ArrayList<>(N_CPU * 2);
        ThreadLocalWithMemoryLeak tl = new ThreadLocalWithMemoryLeak();
        for (int i = 0; i < N_CPU * 2; i++) {
            tasks.add(tl);
        }
        ExecutorService es = Executors.newFixedThreadPool(N_CPU * 2);
        List<Future<Boolean>> futures = es.invokeAll(tasks);
        for (final Future<Boolean> future : futures) {
            future.get();
            System.gc();
        }
        TimeUnit.SECONDS.sleep(30);

        es.shutdown();
        System.out.println("shutdown");
        TimeUnit.SECONDS.sleep(60);
    }
}

![image.png](https://img-blog.csdnimg.cn/img_convert/f2ff74c438b4487e4f85713e9886832a.png#align=left&display=inline&height=578&margin=[object Object]&name=image.png&originHeight=1156&originWidth=1734&size=147061&status=done&style=none&width=867在这里插入图片描述

我们发现,没有调用remove方法,在整个县城的生命周期中,内存都没有释放,那我们加上remove方法呢?

    @Override
    public Boolean call() {
        try {
            System.out.println("Thread-" + Thread.currentThread().getId() + " is running");
            threadLocal.set(new My50MB());
            return true;
        }finally {
            threadLocal.remove();
        }
    }

发现内存曲线也差不多,这里还需要继续理解,只能说gc还是可以发现这些弱引用的。

所以 出现内存泄露的前提必须是持有 value 的线程一直存活,这在使用线程池时是很正常的,在这种情况下 value 一直不会被 GC,因为线程对象与 value 之间维护的是强引用。此外就是 后续线程执行的业务一直没有调用 ThreadLocal 的 get 或 set 方法,导致不会主动去删除 key 为 null 的 value 对象,在满足这两个条件下 value 对象一直常驻内存,所以存在内存泄露的可能性。
那么我们应该怎么避免呢?前面我们分析过线程池情况下使用 ThreadLocal 存在小地雷,这里的内存泄露一般也都是发生在线程池的情况下,所以在使用 ThreadLocal 时,对于不再有效的 value 主动调用一下 remove 方法来进行清除,从而消除隐患,这也算是最佳实践吧。

ThreadLocal修饰共享变量

共享变量哪怕被ThreadLocal修饰,但是因为访问的还是共享变量,最终还是会出现访问问题。

InheritableThreadLocal

ThreadLocal无法满足父子线程之间的数据传递,于是就有了InheritableThreadLocal。


/**
 * InheritableThreadLocal 测试子线程与父线程之间传递数据
 */
public class InheritableThreadLocalTest {

    private static InheritableThreadLocal<Integer> requestIdThreadLocal = new InheritableThreadLocal<>();

    public void setRequestId(Integer requestId) {
        requestIdThreadLocal.set(requestId);
        doBusiness();
    }

    private void doBusiness() {
        System.out.println("首先打印requestId:" + requestIdThreadLocal.get());
        (new Thread(() -> {
            System.out.println("子线程启动");
            System.out.println("在子线程中访问requestId:" + requestIdThreadLocal.get());
        })).start();
    }

    public static void main(String[] args) {
        Integer reqId = 5;
        InheritableThreadLocalTest a = new InheritableThreadLocalTest();
        a.setRequestId(reqId);
    }
}

基于Thread对象,包含2个属性

   /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal values pertaining to this thread. This map is
     * maintained by the InheritableThreadLocal class.
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;

其中inheritableThreadLocals,用来实现父子线程传递问题,我们看InheritableThreadLocal源码也比较简单

public class InheritableThreadLocal<T> extends ThreadLocal<T> {

    protected T childValue(T parentValue) {
        return parentValue;
    }

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

    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}

唯一重写的就是getMap和createMap方法,其他继承自ThreadLocal
在Thread#init方法中,实现对inheritableThreadLocals的初始化

 Thread parent = currentThread();

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    static ThreadLocalMap createInheritedMap(ThreadLocalMap parentMap) {
        return new ThreadLocalMap(parentMap);
    }
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            Entry[] parentTable = parentMap.table;
            int len = parentTable.length;
            setThreshold(len);
            table = new Entry[len];

            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        int h = key.threadLocalHashCode & (len - 1);
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        size++;
                    }
                }
            }

如上代码,我们可以看到本质上就是一个拷贝父线程中 ThreadLocal 变量值的过程。

父线程和子线程在 ThreadLocal 变量的存储上仍然是隔离的,只是在初始化子线程时会拷贝父线程的 ThreadLocal 变量,之后在运行期间彼此互不干涉,也就是说在子线程启动起来之后,父线程和子线程各自对同一个 InheritableThreadLocal 实例的改动并不会被对方所看见。

FastThreadLocal

Netty重新设计了更快的FastThreadLocal,主要实现涉及FastThreadLocalThread、FastThreadLocal和InternalThreadLocalMap类,FastThreadLocalThread是Thread类的简单扩展,主要是为了扩展threadLocalMap属性。
类图如下
![image.png](https://img-blog.csdnimg.cn/img_convert/fe99e045e9aee1506dd679f3f391de57.png#align=left&display=inline&height=806&margin=[object Object]&name=image.png&originHeight=1612&originWidth=2620&size=2009826&status=done&style=none&width=1310在这里插入图片描述

public class FastThreadLocal<V> {

    private static final int variablesToRemoveIndex = InternalThreadLocalMap.nextVariableIndex();

    
    
    public static int nextVariableIndex() {
        int index = nextIndex.getAndIncrement();
        if (index < 0) {
            nextIndex.decrementAndGet();
            throw new IllegalStateException("too many thread-local indexed variables");
        }
        return index;
    }
    
       static final AtomicInteger nextIndex = new AtomicInteger();

nextIndex是InternalThreadLocalMap父类的一个全局静态的AtomicInteger类型的对象,这意味着所有的FastThreadLocal实例将共同依赖这个指针来生成唯一的索引,而且是线程安全的。

最终set方法,我们可以看到底层实现:

    public boolean setIndexedVariable(int index, Object value) {
        Object[] lookup = indexedVariables;
        if (index < lookup.length) {
            Object oldValue = lookup[index];
            lookup[index] = value;
            return oldValue == UNSET;
        } else {
            expandIndexedVariableTableAndSet(index, value);
            return true;
        }
    }

最终避免了hash冲突的计算等操作。

引用

https://my.oschina.net/wangzhenchao/blog/3212438
https://blog.csdn.net/nmgrd/article/details/88131869
https://my.oschina.net/andylucc/blog/614359

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值