1 简介

  ThreadLocal提供了线程内存储变量的能力。

 

2 ThreadLocal的应用场景

如下图,方法1调用方法2,方法2调用方法3,方法3调用方法4

如果我们想要在方法4中使用方法1中的一个变量sa,可以怎么做?

1)通过参数传递

  在某些情况下可以,但是如果中间某个方法(如method2)是其它人提供的api,我们不能去更改这个方法的时候,就不能通过参数传递

 

2)把sa提出来定义为全局变量

  这样子虽然可以访问,但是会有线程安全问题

 

3)使用ThreadLocal

 

3 ThreadLocal的实际应用

  在使用spring的过程中,我们进行事务管理,通常会使用@Transactional注解。它就是应用了ThreadLocal

 

 同样的,如上图,方法一层层调用,要保证几个方法事务,那么它们几个方法必须使用同一个连接。不能方法1使用连接1,方法2使用连接2,这样子没有办法保证事务。那么各个方法怎么样获取到同一个连接呢,spring就使用了ThreadLocal

 

4 示例

4.1 代码

public class ThreadLocalTest {

    public static void main(String[] args) {
        ThreadLocal<PojoTest> tl = new ThreadLocal<>();

        new Thread(()->{

            tl.set(new PojoTest());
            System.out.println(tl.get());

        }).start();

        new Thread(()->{

            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(tl.get());

        }).start();

    }
}

 

4.2 执行结果

com.ruoyi.weixin.user.AopTest.PojoTest@75e3668f
null

 

4.3 执行说明

1)创建了一个ThreadLocal

2)在第一个线程中给这个ThreadLocal对象set一个PojoTest对象

3)在第一个线程中get去获取PojoTest对象,获取到了

4)但是在第二个线程中去获取的时候没有获取到

说明ThreadLocal里面存储的对象是线程隔离的

 

5 源码(set方法)

5.1 set方法

public void set(T value) {
    //1 获取当前线程(调用者线程)
    Thread t = Thread.currentThread();
    //2 获取到当前线程的一个属性threadLocals,它是一个map
    ThreadLocalMap map = getMap(t);
    
    if (map != null)
    //3 如果map不为null,直接把值存入map中,key就是当前的ThreadLocal实例对象
    map.set(this, value); 
  else
    // 4 如果map为空,创建map,再存值
    createMap(t, value); }

 

  

 

5.1.1 getMap()方法

  getMap方法很简单,直接返回了线程对象的一个变量threadLocals

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

 

5.1.2 再看一下threadLocals这个变量

发现它的类型是一个ThreadLocal的内部类ThreadLocalMap

ThreadLocal.ThreadLocalMap threadLocals = null;

也就是说getMap方法是获取到Thread的一个类型为ThreadLocalMap的变量

 

5.1.3 ThreadLocalMap的set方法

map.set(this, value);
private void set(ThreadLocal<?> key, Object value) {
            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)]) {
                ThreadLocal<?> k = e.get();

                if (k == key) {
                    e.value = value;
                    return;
                }

                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这个方法中,其它代码不用看,看最关键的一行代码,创建了一个Entry对象,它就是一个键值对对象

tab[i] = new Entry(key, value);
static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

Entry继承WeakReference(弱引用),WeakReference继承Reference,Reference中有一个成员变量:private T referent;

也就是说Entry有两个变量:T referentObject value

关于弱引用:https://www.cnblogs.com/jthr/p/15960725.html

 

我们看Entry的构造方法

1)super(k);

这行代码调用父类WeakReference的构造方法,使用ThreadLocal创建了一个WeakReference对象,并把它赋值给了referent

2)value = v;

把存储对象赋值给value

5.2 存储关系图

ThreadLocal.set(x)

实际上是把数据存储到了当前线程的成员变量ThreadLocalMap threadLocals

ThreadLocalMap中维护了一个Entry数组

Entry里面存储KEY-VALUE键值对,KEY是ThreadLocal的WeekReference对象,VALUE是要存储的对象

 

 6 为什么要使用弱引用

 

 我们创建了一个ThreadLocal对象,并调用了set方法

那么这个ThreadLocal对象就会有两个引用,一个是t指向它的一个强引用,一个是Entry的key(referent属性)指向它的一个弱引用。

当我们把t设置为null的时候,在垃圾回收的时候,这个ThreadLocal由于只有一个弱引用指向它,就会被回收

如果不使用弱引用,那么Entry的key(referent属性)指向它的就是一个强引用,即便我们把t设为null,它也不会被回收。只有在这个线程清空key(spring中线程使用完回到线程池会清空threadLocals)或者线程本身被销毁的时候,ThreadLocal才能被回收

 

7  示例及图解

public static void main(String[] args) {
        ThreadLocal<Integer> tc = new ThreadLocal();


        Thread t1 = new Thread(()->{
            try {
                tc.set(123);
                System.out.println("线程一" + tc.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t1");

        t1.start();


        Thread t2 = new Thread(()->{
            try {
                tc.set(456);
                System.out.println("线程二" + tc.get());
            } catch (Exception e) {
                e.printStackTrace();
            }
        },"t1");

        t2.start();
    }

 

 

  threallocal.set实际上把值存在了当前Thread里面的一个属性里面,这个属性是一个map。key就是threallocal对象,value就是值。key-value对象在数组中的位置通过key的hash值对数组长度取余获取,所以,一个ThreadLocal对象能够为线程提供一个key-value对象,同一个ThreadLocal对象对一个线程set的时候,多次set是会覆盖的。