ThreadLocal 面试看这一篇就够了

注明:本文源码基于JDK1.8版本

文章目录

      • 什么是 ThreadLocal
      • ThreadLocal 数据结构
      • Java 的四种引用类型
      • ThreadLocalMap 中的key为什么要用弱引用?
      • ThreadLocal set 方法详解
      • 神奇的 0x61c88647
      • ThreadLocalMap 扩容机制
      • ThreadLocal get 方法详解
      • InheritableThreadLocal
        • InheritableThreadLocal 简单介绍
        • InheritableThreadLocal 原理
        • InheritableThreadLocal 注意点
      • ThreadLocal 应用案例
        • 管理数据库连接。
        • MDC日志链路追踪。
      • ThreadLocal 使用注意事项

什么是 ThreadLocal

  ThreadLocal 称为线程本地变量,当使用ThreadLocal维护变量时,每个Thread拥有一份自己的副本变量,多个线程互不干扰,从而实现线程间的数据隔离。
  ThreadLocal维护的变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

我们先来个简单的示例:

public class ThreadLocalTest01 {

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();

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

        // 子线程t1调用threadLocal的set方法赋值
        Thread t1 =  new Thread(() ->{
            threadLocal.set("abc");
            System.out.println("t1 赋值完成");

            System.out.println("t1线程中get:"+threadLocal.get());
        });

        // 子线程t2调用threadLocal的get方法取值
        Thread t2 = new Thread(() ->
            System.out.println("t2线程中get:"+threadLocal.get())
        );

        t1.start();
        t1.join();
        t2.start();

        // 主线程调用threadLocal的get方法取值
        System.out.println("主线程中get:"+threadLocal.get());

    }

}

输出结果:

t1 赋值完成
t1线程中get:abc
主线程中get:null
t2线程中get:null

通过程序可以看出,threadLocal 在t1线程中通过set方法保存的数据,其它线程是访问不到的。

ThreadLocal 数据结构

ThreadLocal 面试看这一篇就够了_第1张图片

  • 每个线程对应一个Thread对象,Thread对象中,有一个ThreadLocal.ThreadLocalMap成员变量。
  • ThreadLocalMap 类似于HashMap,维护的都是key-value键值对,不同的是,HashMap数据结构是数组+链表/红黑树,而ThreadLocalMap数据结构为数组。
  • ThreadLocalMap 数组中存放的是静态内部类对象Entry(ThreadLocal k, Object v),可以简单的认为,ThreadLocal 对象为key,set的内容为value。( 实际上key为弱引用WeakReference> )

Java 的四种引用类型

前边介绍ThreadLocal数据结构时,提到ThreadLocalMap 的key是弱引用。弱引用有什么用?这里为什么要用弱引用?为了弄清楚这些问题,我们先来了解一下Java的四种引用类型。

  • 强引用:我们平常用的最多的就是强引用类型,如Object obj = new Object(),当我们创建一个Object对象时,栈内存中会有一个obj变量,指向堆内存中分配的Object对象,这种引用就是强引用。只要有强引用存在,JVM即使内存不足抛出OOM异常,垃圾回收器也不会回收它。
  • 软引用:软引用使用SoftReference修饰,如SoftReference sr = new SoftReference<>(new Object()),栈内存中有一个sr,通过强引用关联着堆内存中分配的SoftReference对象,而SoftReference对象中会有一个软引用指向分配的Object对象。 关于软引用指向的Object对象,当JVM堆内存不足时,就会被垃圾回收器回收。软引用用来描述一些有用但并不是必需的对象,如图片缓存、网页缓存的实现。
  • 弱引用:弱引用使用WeakReference修饰,如WeakReference wr = new WeakReference<>(new Object())。如果一个对象只被弱引用关联着,那么只要发生垃圾回收,这个对象就会被回收掉。弱引用的一个典型应用场景就是ThreadLocalMap。
  • 虚引用:虚引用使用PhantomReference修饰,如PhantomReference pr = new PhantomReference<>(new Object(), QUEUE), 虚引用和前面的软引用、弱引用不同,它并不影响对象的生命周期,虚引用唯一的作用就是用队列接收对象即将死亡的通知,通过这种方式来管理堆外内存。Netty中零拷贝(Zero Copy)就是虚引用的典型应用。

    ThreadLocalMap 中的key为什么要用弱引用?

    探究这个问题之前,我们先来看下弱引用在ThreadLocalMap中的表现是什么样子的。

    /**
     * 测试没有强引用关联ThreadLocal对象时,Entry中的虚引用key是否被回收
     */
    public class ThreadLocalTest02_GC {
    
        public static void main(String[] args) throws Exception {
    
            // 有强引用指向ThreadLocal对象
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            threadLocal.set("abc");
    
            // 没有强引用指向ThreadLocal对象
            new ThreadLocal<>().set("def");
    
            // Thread中成员变量threadLocals是默认访问类型,只允许同一个包里类访问,我们可以通过反射方式拿到。
            Thread t = Thread.currentThread();
            Class<? extends Thread> clz = t.getClass();
            Field field = clz.getDeclaredField("threadLocals");
            field.setAccessible(true);
            Object threadLocalMap = field.get(t);
            Class<?> tlmClass = threadLocalMap.getClass();
            Field tableField = tlmClass.getDeclaredField("table");
            tableField.setAccessible(true);
            Object[] arr = (Object[]) tableField.get(threadLocalMap);
            for (Object o : arr) {
                if (o != null) {
                    Class<?> entryClass = o.getClass();
                    Field valueField = entryClass.getDeclaredField("value");
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true);
                    referenceField.setAccessible(true);
                    System.out.println(String.format("key:%s,值:%s", referenceField.get(o), valueField.get(o)));
                }
            }
    
        }
    }
    

    输出结果如下:

    key:java.lang.ThreadLocal@4d7e1886,值:abc
    key:java.lang.ThreadLocal@3cd1a2f1,值:[Ljava.lang.Object;@2f0e140b
    key:java.lang.ThreadLocal@7440e464,值:java.lang.ref.SoftReference@49476842
    key:null,值:def
    key:java.lang.ThreadLocal@78308db1,值:java.lang.ref.SoftReference@27c170f0

    从输出结果中我们可以看到,值为"abc"的记录key是存在的,而值为"def"的记录,对应的key为null。这说明,没有强引用存在时,弱引用指向的对象会被垃圾回收器回收。

    ThreadLocalMap的key定义成弱引用,那么当localThread对象没有强引用指向时,就会被gc回收,避免造成内存泄漏。不过,这里key虽然被回收了,但是value依然会出现内存泄漏问题。只有当线程生命周期结束,或者触发清理算法时,value才能被gc回收。

    注:这里除了我们set的两条数据,还有其它三条数据,如StringCoding编解码使用的数据,我们可以忽略

    ThreadLocal set 方法详解

    
    	ThreadLocal<String> threadLocal = new ThreadLocal<>();
        threadLocal.set("abc");
    	---------------------------------------------------------
    	public void set(T value) {
    		// 获取当前线程对象
            Thread t = Thread.currentThread();
            // 从线程对象t中获取ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    	---------------------------------------------------------
    	// 从线程对象t中获取ThreadLocalMap对象
    	ThreadLocalMap getMap(Thread t) {
            return t.threadLocals;
        }
        ---------------------------------------------------------
        // 创建线程对象t的成员变量ThreadLocalMap对象,
        // 初始化一条数据:this(指的是threadLocal对象)为key,firstValue为value
        void createMap(Thread t, T firstValue) {
            t.threadLocals = new ThreadLocalMap(this, firstValue);
        }
    

    如上代码所示,当我们调用set方法保存信息“abc”时,先通过Thread.currentThread()获取当前线程对象t,再获取线程对象t中的ThreadLocalMap类型变量map,如果map不为null,则直接插入一条key/value键值对数据(threadLocal为key,设置的值“abc”为value),如果map为null,则创建一个ThreadLocalMap,并让map指向这个新创建的对象,并初始化一条key/value键值对数据(threadLocal为key,设置的值“abc”为value)。

    我们先来看看map为null时,new ThreadLocalMap(this, firstValue)都做了些什么操作。

    	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    		// 创建一个Entry数组,数组初始容量为INITIAL_CAPACITY(16)
    		table = new Entry[INITIAL_CAPACITY];
    		// 计算下标位置
    		int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    		table[i] = new Entry(firstKey, firstValue);
    		size = 1;
    		// 设置阈值
    		setThreshold(INITIAL_CAPACITY);
    	}
    	---------------------------------------------------------
    	private final int threadLocalHashCode = nextHashCode();
    	
    	private static AtomicInteger nextHashCode =
            new AtomicInteger();
            
        private static final int HASH_INCREMENT = 0x61c88647;
    
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
        ---------------------------------------------------------
        // 设置阈值大小,当数组中的元素大于等于阈值时,会触发rehash方法进行扩容
        private void setThreshold(int len) {
    		threshold = len * 2 / 3;
    	}
    

    再来看看map不为null时

    	private void set(ThreadLocal<?> key, Object value) {
    	
    		Entry[] tab = table;
    		int len = tab.length;
    		int i = key.threadLocalHashCode & (len-1);
    
    		// 开放定址法查找可用的槽位(用于解决HASH冲突)
    		for (Entry e = tab[i];
    			 e != null;
    			 e = tab[i = nextIndex(i, len)]) {
    			ThreadLocal<?> k = e.get();
    
    			// 如果槽位上已经有值,并且key相同,则替换value值
    			if (k == key) {
    				e.value = value;
    				return;
    			}
    
    			// 如果槽位上有值,并且key已经被GC回收了,触发探测式清理,清理掉过时的条目
    			if (k == null) {
    				replaceStaleEntry(key, value, i);
    				return;
    			}
    		}
    
    		// 找到空的槽位,将key和value插入此槽位
    		tab[i] = new Entry(key, value);
    		int sz = ++size;
    		// 触发清理,并判断如果清理后的size达到了阈值,则进行rehash进行扩容
    		if (!cleanSomeSlots(i, sz) && sz >= threshold)
    			rehash();
    	}
    
    	---------------------------------------------------------
    	// 定向寻址,寻找下一个位置,如果到了最后,则再从0下标开始
    	private static int nextIndex(int i, int len) {
    		return ((i + 1 < len) ? i + 1 : 0);
    	}
    

    在结构上是不是跟HashMap非常相似,通过Hash运算找到在数组下标中的位置并插入。不同的是,HashMap解决Hash冲突的方式是链表/红黑树,而ThreadLocalMap中用的是开放定址法。(清理key被回收的条目的具体算法逻辑这里就不介绍了,有兴趣的同学可以去看下源码。)

    神奇的 0x61c88647

    我们可以看到一个值0x61c88647,每当创建一个ThreadLocal对象时,hashCode增量都是这个数值,这是一个很特殊的数值,它是Integer有符号整数的0.618倍,既黄金比例,斐波拉契数列。hash增量用这个数字,带来的好处就是 hash 分布非常均匀。

    用代码来演示一下:

    	public static void main(String[] args) throws IOException {
    
            threadLocalHashTest(16);
            System.out.println("-------------------------------------------");
            threadLocalHashTest(32);
    
        }
    
        public static void threadLocalHashTest(int n){
            int HASH_INCREMENT = 0x61c88647;
            int nextHashCode = HASH_INCREMENT;
            for(int i=0; i<n; i++ ){
                System.out.print((nextHashCode & (n-1)));
                System.out.print(" ");
                nextHashCode += HASH_INCREMENT;
            }
    
        }
    

    输出结果为:

    7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 0
    7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0

    ThreadLocalMap 扩容机制

    在ThreadLocalMap.set() 方法的最后,如果执行完清理工作后,当前散列数组中Entry的数量已经达到了列表的扩容阈值(sz >= threshold),就开始执行rehash扩容逻辑。rehash方法一开始还是执行清理工作,清除掉key为null的条目,清理完后,再次判断是否当前Entry数量已经达到了阈值的3/4(size >= threshold - threshold / 4),如果达到了,执行resize方法,进行真正的扩容操作,将容量扩为之前的2倍,并重新进行hash位置计算。

    判断是否需要执行rehash方法时,判断依据是是否达到阈值,而rehash内部再次判断是否需要执行resize方法时,判断依据是是否达到阈值的3/4,为什么这样呢,源码中给出的解释是:Use lower threshold for doubling to avoid hysteresis(使用较低的阈值加倍,以避免迟滞)。

    	// 如果当前数组中的Entry数量已经大于等于阈值,执行rehash方法
    	int sz = ++size;
    	if (!cleanSomeSlots(i, sz) && sz >= threshold)
    		rehash();
    
    	---------------------------------------------------------
    	// 阈值大小规则
    	private void setThreshold(int len) {
    		threshold = len * 2 / 3;
    	}
    
    	---------------------------------------------------------
    	private void rehash() {
    		// 清理过时条目,也就是key被GC回收掉的条目
    		expungeStaleEntries();
    
    		// Use lower threshold for doubling to avoid hysteresis
    		// (使用较低的阈值以避免滞后)
    		if (size >= threshold - threshold / 4)
    			resize();
    	}
    
    	---------------------------------------------------------
    	private void expungeStaleEntries() {
    		Entry[] tab = table;
    		int len = tab.length;
    		for (int j = 0; j < len; j++) {
    			Entry e = tab[j];
    			// e.get得到的是key,如果key不存在,则进行清理
    			if (e != null && e.get() == null)
    				expungeStaleEntry(j);
    		}
    	}
    
    	---------------------------------------------------------
    	private void resize() {
    		Entry[] oldTab = table;
    		int oldLen = oldTab.length;
    		// 新容量扩为之前的2倍
    		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;
    	}
    

    ThreadLocal get 方法详解

    
        public T get() {
            // 获取当前线程对象
            Thread t = Thread.currentThread();
            // 从线程对象t中获取ThreadLocalMap对象
            ThreadLocalMap map = getMap(t);
            if (map != null) {
            	// 通过key(当前ThreadLocal对象)寻找value
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    T result = (T)e.value;
                    return result;
                }
            }
            // 如果获取不到值,则初始化一个值
            return setInitialValue();
        }
    
    	---------------------------------------------------------
    	// 通过key(当前ThreadLocal对象)寻找value
    	private Entry getEntry(ThreadLocal<?> key) {
    		int i = key.threadLocalHashCode & (table.length - 1);
    		Entry e = table[i];
    		if (e != null && e.get() == key)
    			return e;
    		else
    			return getEntryAfterMiss(key, i, e);
    	}
    
    	// 如果hash计算出的位置没有找到,则依据开放定址法去查找
    	private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    		Entry[] tab = table;
    		int len = tab.length;
    
    		while (e != null) {
    			ThreadLocal<?> k = e.get();
    			if (k == key)
    				return e;
    			if (k == null)
    				expungeStaleEntry(i);
    			else
    				i = nextIndex(i, len);
    			e = tab[i];
    		}
    		return null;
    	}
    
    	---------------------------------------------------------
    	// map中确实没有,则初始化一个值
        private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    
    	// ThreadLocal默认初始化返回null,可以自定义具体的返回值
        protected T initialValue() {
            return null;
        }
    

    如上代码所示,当我们调用get方法时,先通过Thread.currentThread()获取当前线程对象t,再获取线程对象t中的ThreadLocalMap类型变量map,如果map不为null,则根据当前threadLocal对象为key去查询,查询遵循开放定址法原则,如果当前hash计算出的位置没有查到,则继续往后查找。如果最终还是从map中没有查到需要的结果,则根据重写的初始化方法初始化一个值。

    初始化示例代码如下:

    public class ThreadLocalTest04_init {
    
        public static void main(String[] args) {
    
            ThreadLocal<String> tl = new ThreadLocal(){
                @Override
                protected String initialValue(){
                    return "default value";
                }
            };
    
            System.out.println(tl.get());
        }
    }
    

    输出结果为:

    default value

    InheritableThreadLocal

    InheritableThreadLocal 简单介绍

    使用ThreadLocal时,子线程获取不到父线程通过set方法保存的数据,要想使子线程也可以获取到,可以使用InheritableThreadLocal类。

    public class ThreadLocalTest03 {
    
        public static void main(String[] args) {
            ThreadLocal<String> threadLocal = new ThreadLocal<>();
            ThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
            threadLocal.set("threadLocal value");
            inheritableThreadLocal.set("inheritableThreadLocal value");
    
            new Thread(() ->  {
                System.out.println("子线程获取父线程threadLocal数据:" + threadLocal.get());
                System.out.println("子线程获取父线程inheritableThreadLocal数据:" + inheritableThreadLocal.get());
            }).start();
        }
    
    }
    

    输出如下:

    子线程获取父线程threadLocal数据:null
    子线程获取父线程inheritableThreadLocal数据:inheritableThreadLocal value

    通过示例,我们可以看到,使用InheritableThreadLocal在父线程set的内容,在子线程中可以由get方法获取到。

    InheritableThreadLocal 原理

    那么,到底是怎样实现子线程可以拿到父线程保存的内容的呢?我们来分析一下原理。

    首先,线程类Thread中,有两个ThreadLocalMap成员变量,一个用来存放普通的ThreadLocal相关信息,一个用来存放InheritableThreadLocal相关信息。

    	// 用来保存ThreadLocal相关信息
    	ThreadLocal.ThreadLocalMap threadLocals = null;
    	
    	// 用来保存InheritableThreadLocal相关信息
        ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    

    InheritableThreadLocal对象在调用set方法保存信息时,调用的是父类ThreadLocal对象的set方法,如下:

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

    而其中getMap方法和createMap方法,已经被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);
        }
    }
    

    看到这里,我们知道了InheritableThreadLocal相关信息,被保存到Thread线程对象的成员变量ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;中了,可是,还没说明白,怎么子线程就能拿到了呢?

    这个关键点在new Thread() 中:

    	public Thread() {
            init(null, null, "Thread-" + nextThreadNum(), 0);
        }
    
    	---------------------------------------------------------
        private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize) {
            init(g, target, name, stackSize, null);
        }
    
    	---------------------------------------------------------
    	// 为了直观,这里我用省略号代替了其它的一些逻辑
        private void init(ThreadGroup g, Runnable target, String name,
                          long stackSize, AccessControlContext acc) {
            ......
    
            Thread parent = currentThread();
            
    		......
    		
            if (parent.inheritableThreadLocals != null)
                this.inheritableThreadLocals =
                    ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
    		
    		......
        }
    

    这里在创建子线程的时候,会获取当前线程对象(也就是父线程对象),然后将当前线程对象inheritableThreadLocals成员变量中的数据,复制一份到要创建的子线程中。所以就可以在子线程中get到内容了。

    InheritableThreadLocal 注意点

    • 一般我们做异步化处理都是使用的线程池,InheritableThreadLocal是在new Thread中的init()方法给赋值的,而线程池是线程复用的逻辑,所以这里会存在问题。
    • 要想在使用线程池等会缓存线程的组件情况下传递ThreadLocal到子线程中,可以使用阿里巴巴开源组件TransmittableThreadLocal
    • 子线程中数据是从父线程拷贝来的,所以,在子线程中重新set的内容,对于父线程是不可见的。

    ThreadLocal 应用案例

    管理数据库连接。

      假如A类的方法a中,会调用B类的方法b和C类的方法c,a方法开启了事务,b方法和c方法会去操作数据库。我们知道,要想实现事务,那么b方法和c方法中所使用的的数据库连接一定是同一个连接,那怎么才能实现所用的是同一个数据库连接呢?答案就是通过ThreadLocal来管理。

    MDC日志链路追踪。

      MDC(Mapped Diagnostic Contexts)主要用于保存每次请求的上下文参数,同时可以在日志输出的格式中直接使用 %X{key} 的方式,将上下文中的参数输出至每一行日志中。而保存上下文信息主要就是通过ThreadLocal来实现的。
      假如在交易流程每个环节的日志中,你都想打印全局流水号transId,流程可能涉及多个系统、多个线程、多个方法。有一些环节中,全局流水号并不能当做参数传递,那你怎么才能获取这个transId参数呢,这里就是利用了Threadlocal特性。每个系统或者线程在接收到请求时,都会将transId存放到ThreadLocal中,在输出日志时,将transId获取出来,进行打印。这样,我们就可以通过transId,在日志文件中查询全链路的日志信息了。

    ThreadLocal 使用注意事项

    内存泄漏或脏数据。 我们在使用线程的时候,多数情况都会通过线程池进行管理,这样有些线程在使用完后,并不会进行销毁,如果我们ThreadLocal也没有执行remove方法,就会导致保存的数据一直存在,造成内存泄漏。如果此时,我们ThreadLocal对象也是一个静态常量,那么在下一次线程被使用的时候,很可能获取到的是之前保存的数据,导致脏数据。所以,在使用ThreadLocal时,一定要记得在最后调用remove方法。


    END

    你可能感兴趣的:(JAVA)