ThreadLocal那点事

本文围绕ThreadLocal展开,介绍其是线程变量,变量对其他线程隔离,有对象跨层传递、线程间数据隔离等使用场景。通过例子展示其用法,分析了源码,指出每个线程维护ThreadLocalMap引用等要点。还探讨了内存泄漏问题,因ThreadLocal是弱引用,ThreadLocalMap生命周期和Thread一样,使用完需执行remove操作避免内存溢出。

今天看自己项目中的代码遇到一个ThreadLocal,平时没遇到过,于是我决定自己好好去查查资料,这个是干嘛用的

1、ThreadLocal是什么

从名字我们就可以看到ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

从字面意思来看非常容易理解,但是从实际使用的角度来看,就没那么容易了,作为一个面试常问的点,使用场景那也是相当的丰富:

1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
2、线程间数据隔离
3、进行事务操作,用于存储线程事务信息。
4、数据库连接,Session会话管理。

现在相信你已经对ThreadLocal有一个大致的认识了,下面我们看看如何用?

2、ThreadLocal怎么用

既然ThreadLocal的作用是每一个线程创建一个副本,我们使用一个例子来验证一下:

public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();
        Random random = new Random();
        IntStream.range(0,5).forEach((a)->{
            new Thread(()->{
                String str = random.nextInt(10)+"";
                local.set(str);
                System.out.println("线程"+Thread.currentThread().getName()+"的值:   "+ local.get());
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }).start();
        });
    }

在这里插入图片描述

从结果我们可以看到,每一个线程都有各自的local值,我们设置了一个休眠时间,就是为了另外一个线程也能够及时的读取当前的local值。
如果说这个感觉还是不够再看下面的

private static ThreadLocal<String> local = new ThreadLocal<>();
    public static void main(String[] args) {
        int i1 = Runtime.getRuntime().availableProcessors();
        ExecutorService fixedThreadPool = Executors.newFixedThreadPool(i1);
        for (int i = 0; i < 8; i++) {
            int finalI = i;
            fixedThreadPool.execute(new Runnable() {
                public void run() {
                    try {

                        String s = local.get();

                        if(s==null || "".equals(s)){
                            local.set(finalI+"");
                        }
                        String s2 = local.get();
                        System.out.println("线程"+Thread.currentThread().getName()+ "值1:  " + s +"值2:   " + s2);

                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            });
        }
    }

在这里插入图片描述
从结果上看,前四个记录说明了,数据在线程之间是隔离的,后四条和前四条对比着看说明数据是每个线程只能读取自己的

3、ThreadLocal源码分析

设置值

public void set(T value) {
		//获取当前线程
        Thread t = Thread.currentThread();
        //这个就是获取当前线程的map,
        //ThreadLocalMap 目前就理解为普通map即可,key是ThreadLocal,value就是你设置的值
        ThreadLocalMap map = getMap(t);
        if (map != null)
        	//如果存在就进行修改里面的值
            map.set(this, value);
        else
        //如果不存在就创建map,并把当前值放入
            createMap(t, value);
    }
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

获取值

public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	//当前线程已经存在map的情况下自己取相应的值即可
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果当前线程不存在返回一个初始化的值
        return setInitialValue();
    }
 private T setInitialValue() {
 		//这个地方返回的是null,这也就明白线程第一次获取为啥是null了
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
protected T initialValue() {
        return null;
    }

从get和set方法对比可以,不管你调用的是get还是set一旦调用该线程就存在自己的map了,只是get存放的null,set存放的是你自己设置的值而已

删除:

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

其实内部源码很简单,现在我们总结一波

(1)每个Thread维护着一个ThreadLocalMap的引用

(2)ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

(3)ThreadLocal创建的副本是存储在自己的threadLocals中的,也就是自己的ThreadLocalMap。

(4)ThreadLocalMap的键值为ThreadLocal对象,而且可以有多个threadLocal变量,因此保存在map中

(5)在进行get之前,必须先set,否则会报空指针异常,当然也可以初始化一个,但是必须重写initialValue()方法。

(6)ThreadLocal本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。

在这里插入图片描述

4、ThreadLocal内存泄漏问题

为何存在内存泄漏的问题,这个地方要从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);
        }

ThreadLocalMap 就是ThreadLocal一个内部类,内部初始化的Entry这个类,但是Entry是弱引用
在这里插入图片描述

上面这张图详细的揭示了ThreadLocal和Thread以及ThreadLocalMap三者的关系。

1、Thread中有一个map,就是ThreadLocalMap

2、ThreadLocalMap的key是ThreadLocal,值是我们自己设定的。

3、ThreadLocal是一个弱引用,当为null时,会被当成垃圾回收

4、重点来了,突然我们ThreadLocal是null了,也就是要被垃圾回收器回收了,但是此时我们的ThreadLocalMap生命周期和Thread的一样,它不会回收,这时候就出现了一个现象。那就是ThreadLocalMap的key没了,但是value还在,这就造成了内存泄漏。
解决办法:使用完ThreadLocal后,执行remove操作,避免出现内存溢出情况。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值