ThreadLocal简单分析与实现原理

《Java并发编程之美》读书笔记 ThreadLocal 多线程在访问同一个共享变量的时候容易出现并发的问题,特别是在多个线程对同一个共享变量进行写入的时候,一般都要对共享变量进行适当的同步。
同步的措施一般都是加锁,这就需要使用者对锁有一定的了解,这显然增加了使用者的负担,那么有没有一种方式可以做到,当创建一个变量后,每个线程对其进行访问的时候访问的是自己线程的变量呢->ThreadLocal
ThreadLocal是JDK包提供的,它提供了线程本地变量,也就是如果你创建了一个ThreadLocal,那么访问这个变量的每个线程都会有这和个变量本地的一个副本。当多个线程操作这个ThreadLocal变量时,实际上是在操作自己本地内存里面的变量,从而避免了线程安全问题。创建了一个ThreadLocal变量后,每个线程都会复制一个变量复制到自己的本地内存
ThreadLocal使用示例

public class ThreadLocalTest { static void print(String str){ //获取到当前线程本地内存中的localVariable值 System.out.println(str+":"+localVariable.get()); //删除当前线程本地内存中的localVariable值 localVariable.remove(); } //创建ThreadLocal变量 static ThreadLocal localVariable=new ThreadLocal<>(); public static void main(String[] args) { Thread threadOne=new Thread(new Runnable() { @Override public void run() { //设置当前线程本地内存中的localVariable值 localVariable.set("threadOne local variable"); print("threadOne"); System.out.println("threadOne remove after"+":"+localVariable.get()); } }); Thread threadTwo=new Thread(new Runnable() { @Override public void run() { localVariable.set("threadTwo local variable"); print("threadTwo"); System.out.println("threadTwo remove after"+":"+localVariable.get()); } }); threadOne.start(); threadTwo.start(); } }

本例子开启了两个线程,在每个线程内部设置了本地变量的值,然后调用print函数打印当前本地变量的值,如果打印后调用了本地变量的remove方法之后,则会删除本地内存中的共享变量。

ThreadLocal简单分析与实现原理
文章图片
线程One run方法通过设置localvariable的值,这其实是设置的是线程one本地内存中的一个副本,这个副本线程two是访问不了的。
ThreadLocal实现原理
Thread类内部会有一个threadLocals和inheritableThreadLocals,他们都是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的hashMap,在默认的情况下,每个线程中的两个变量都为null,只有当前线程第一次调用ThreadLocal的set或者get方法之后才会创建他们,其实每个线程的本地变量并不是存在ThreadLocal实例里面,而是存放在调用线程的threadLocals里面,也就是说ThreadLocal类型的本地变量存放在具体的线程的内存空间中,ThreadLocal就是一个空壳,它通过set方法把value值放入线程的threadlocals里面存放起来,当调用线程使用它的get方法时,再从当前线程的threadlocals变量里面将其拿出来,如果调用线程一直不终止,那么这个本地变量会一直存放在调用线程的threadlocals里面,所以当不需要使用本地变量的时候,可以通过调用ThreadLocal变量里面的remove方法,从当前线程的threadlocals里面删除该本地变量
另外 Thread里面的threadlocals为什么设置为map结构?很明显是因为多个线程可以关联多个ThreadLocal变量。

ThreadLocal简单分析与实现原理
文章图片

简单分析ThreadLocal的set,get以及remove方法的是实现逻辑
  1. public void set(T value)
public void set(T value) { //获取到当前线程 Thread t = Thread.currentThread(); //将当前线程作为key去找对应的线程变量,找到则设置 ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { //如果是第一次调用就创建当前线程对应的HashMap createMap(t, value); } }

代码中首先获取调用线程,然后使用当前线程作为参数调用getMap(t)方法

ThreadLocal简单分析与实现原理
文章图片
可以看到,getMap(t)的作用是获取线程自己的变量threadlocals,threadlocals变量被绑定到了线程的成员变量上。
如果getMap(t)返回值不为空,则把value值设置到threadLocals中,也就是把变量值放入到当前线程的内存变量threadLocals中。threadLocals是一个HashMap结构,其中的key就是当前ThreadLocal的实例对象的引用,value是通过set方法传递的值
如果getMap(t)返回空值则说明是第一次调用set方法,这时用 createMap(t, value)创建当前线程的threadLocals变量。

ThreadLocal简单分析与实现原理
文章图片

createMap创建当前线程的threadLocals变量。
  1. T get()
public T get() { //获取当前线程 Thread t = Thread.currentThread(); //获取当前线程的threadLocals变量 是一个ThreadLocalMap结构 ThreadLocalMap map = getMap(t); //如果threadLocals不为空,则返回对应本地变量的值 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } //threadLocals为空的话就初始化当前线程的threadLocals变量 return setInitialValue(); }

上诉代码首先获取当前线程的实例,在获取当前线程的threadLocals变量,如果不为null则直接返回当前线程绑定的本地变量,否则执行代码初始化。
private T setInitialValue() { //初始化为null T value = https://www.it610.com/article/initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //如果当前线程的threadLocals变量不为空 if (map != null) { //value为null map.set(this, value); } else { //如果当前线程的threadLocals变量为空 createMap(t, value); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal) this); } return value; }

如果当前线程的threadLocals变量不为空,则设置为当前线程的本地变量值为null,否则调用createMap(t, value)方法创建当前线程的threadLocals变量。
  1. 【ThreadLocal简单分析与实现原理】void remove()

    ThreadLocal简单分析与实现原理
    文章图片
如果当前线程的threadLocals变量不为空,则删除当前线程中指定ThreadLocal实例即this的本地变量。
总结
在每个线程内部都有一个名为threadLocals的成员变量,这个变量的类型是HashMap,其中key就为我们定义的ThreadLocal类型的变量的this引用,value则为我们用set方法设置的值,每个线程的本地变量存放在线程自己的内存变量threadLocals里面,如果当前线程一直不消亡,那么这些本地变量就会一直存在,所以可能会造成内存溢出,因此使用完毕后记得调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。
注:在JUC包里面的ThreadLocalRandom,就是借鉴这中思想实现的。

ThreadLocal简单分析与实现原理
文章图片

ThreadLocal不支持继承性
public class ThreadLocalDemo { //创建线程变量 private static ThreadLocal threadLocal=new ThreadLocal<>(); public static void main(String[] args) { threadLocal.set("helloworld"); Thread thread=new Thread(new Runnable() { @Override public void run() { System.out.println("thread:"+threadLocal.get()); } }); thread.start(); System.out.println("main:"+threadLocal.get()); } }

ThreadLocal简单分析与实现原理
文章图片
也就是说,同一个ThreadLocal变量在父线程中设置值后,在子线程中是获取不到的,如之前所说,这是很正常的现象,因为子线程thread里面调用get方法时当前线程为thread线程,而这里调用set方法设置线程变量的是main线程,两者是不同的线程所以子线程访问时为null;
Inheritable ThreadLocal类
为了解决ThreadLocal类不支持继承性的这个问题,InheritableThreadLocal应运而生,它继承自ThreadLocal,提供了一个属性,就是让子线程可以访问可以访问在父线程中设置的本地变量,
InheritableThreadLocal源代码:
public class InheritableThreadLocal extends ThreadLocal {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继承ThreadLocal类,并且重写了三个方法。重写了createMap方法,所以现在第一次调用set方法的时候,创建的是当前线程的t.inheritableThreadLocals变量的实例而不再是threadLocals实例。当调用get方法获取当前线程内部的map变量时,获取的是t.inheritableThreadLocals而不再是threadLocals。
综上,在InheritableThreadLocal世界里,变量由inheritableThreadLocals代替了threadLocals
观察如何让子线程可以访问父线程的本地变量。
public Thread(Runnable target) { this(null, target, "Thread-" + nextThreadNum(), 0); } private Thread(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { //获取当前线程 Thread parent = currentThread(); //如果父线程的inheritThreadLocals不为null if (inheritThreadLocals && parent.inheritableThreadLocals != null) //设置子线程中的inheritThreadLocals变量 this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); this.stackSize = stackSize; this.tid = nextThreadID(); }

如上的代码在创建的过程中,在构造函数中会调用私有的构造方法,先获取当前的线程,这里是main函数所在的线程,也就是main线程,然后再判断main函数所在的线程里面inheritableThreadLocals是否为空,然后就会执行createInheritedMap方法

ThreadLocal简单分析与实现原理
文章图片
可以看到createInheritedMap的内部使用的是父线程的inheritableThreadLocals变量作为构造函数创建了一个新的ThreadLocalMap变量。
private ThreadLocalMap(ThreadLocalMap parentMap) { Entry[] parentTable = parentMap.table; int len = parentTable.length; setThreshold(len); table = new Entry[len]; for (Entry e : parentTable) { if (e != null) { @SuppressWarnings("unchecked") ThreadLocal key = (ThreadLocal) e.get(); if (key != null) { Object value = https://www.it610.com/article/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++; } } } }
在该构造函数的内部将父线程的inheritableThreadLocals成员变量复制到新的ThreadLocalMap变量当中
总结,InheritableThreadLocal通过重写ThreadLocal类的代码让本地变量保存到了具体的inheritableThreadLocals里面,那么线程在通过InheritableThreadLocal实例的set或者get方法设置变量时,就会创建当前线程的inheritableThreadLocals变量。当父线程创建子线程时,构造函数会把父线程中的inheritableThreadLocals变量里面的本地变量复制一份保存到子线程的inheritableThreadLocals里面
把之前的代码改为:
private static ThreadLocal threadLocal=new InheritableThreadLocal<>();

ThreadLocal简单分析与实现原理
文章图片
可见,现在可以从子线程正常获取到线程变量的值了。
在什么情况下需要子线程可以获取到父线程的thredLocal变量呢?
比如子线程需要使用存放在threadLocal变量中的用户信息,再比如说一些中间件需要把统一的id追踪的整个调用链路记录下来。其实子线程使用父线程中的threadLocal方法有很多种,比如创建线程时候,使用父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程。但是这些方法都改变了我们的使用习惯,所以在这些情况下InheritableThreadLocal就显得比较有用。
参考资料:
《Java并发编程之美》

    推荐阅读