这是ThreadLocal系列的最后一篇文章。
前几篇文章更多的是在使用层面去介绍ThreadLocal,并没有深入去理解原理。
其实学任何技术都是这样一个过程,我们最先接触到的可能是一个框架的API,然后你可能就会开始使用它;再然后会看看别人是怎么使用它的,有没有值得借鉴之处,再然后就是深入原理,看看它的底层是如何实现的,对它做一个深入的了解。
下面我们进入正题,先分析一下ThreadLocal几个重要的方法。
set
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); } }
先拿到当前的线程,然后通过它去拿到一个Map,如果这个Map存在,就把value塞进去,否则就创建一个新的。
ThreadLocalMap
是在ThreadLocal类里面实现的一个Map,它的Entry是一个弱引用的实现。
static class Entry extends WeakReference<ThreadLocal<?>>
每个线程对应一个自己线程私有的ThreadLocalMap,它被Thread对象持有:
// 类Thread里面定义了ThreadLocalMap的引用 ThreadLocal.ThreadLocalMap threadLocals = null;
从set方法的代码可以看到,最开始线程的threadLocals
可能是空,这个时候就创建一个新的,赋值给当前线程对象:
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue);
get
看完上面的分析后,get方法就很好理解了。仍然是先通过getMap
方法拿到当前线程对应的Map,然后从里面取出value。如果没有value,就调用ThreadLocal提供的初始化方法,初始化一个值。
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); }
初始化
先来看在ThreadLocal定义的的初始化方法,看起来就是一个很简单的protected
方法:
protected T initialValue() { return null; }
而为了更方便用户使用,ThreadLocal自己内部有一个ThreadLocal的实现类,它提供了一个函数式编程的方式来让客户端更方便地使用:
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> { private final Supplier<? extends T> supplier; SuppliedThreadLocal(Supplier<? extends T> supplier) { this.supplier = Objects.requireNonNull(supplier); } @Override protected T initialValue() { return supplier.get(); } }
我们甚至可以依样画葫芦,自己新建一个FunctionedThreadLocal,实现更多的定制化。
remove
remove方法不得不提。首先我们思考一下,既然已经有了弱引用,按理说,如果线程没有持有某个value的时候,会在GC的时候自动清理掉对应的Entry,为什么会有remove方法存在?
因为我们在开发一个多线程的程序时,往往会使用线程池。而线程池的功能就是线程的复用。那如果线程池和ThreadLocal在一起就可能会造成一个问题:
- job A和job B共用了同一个线程,
- job A使用完ThreadLocal,ThreadLocal里面还有job A保存的值,而这个时候可能还没有清理掉,
- job B复用线程进来了,取出来是 job A的值,可能就会造成问题。
所以在有必要的时候,可以在使用完ThreadLocal的时候,显式调用一下remove方法。remove方法的源码也比较简单,就是调用对应的entry的clear
方法。
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) { m.remove(this); } } private void remove(ThreadLocal<?> key) { 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)]) { if (e.get() == key) { e.clear(); expungeStaleEntry(i); return; } } }
学习借鉴
这里总结出我们从ThreadLocal的设计中可以学习借鉴的一些点。
避免并发
这个其实挺有意思的。如果让我们自己来设计一个ThreadLocal,想要拿到当前线程对应的ThreadLocalMap,可能就会用一个Map来存这个关系:
Map<Thread, ThreadLocalMap> threadLocalMaps = new ConcurrentHashMap<>();
因为可能会有多个线程同时调用get/set方法,所以还需要对这个Map来做一些措施来保证线程安全,比如使用ConcurrentHashMap,甚至复杂的原子操作可能还需要上锁。这样其实对性能是不利的。
而JDK巧妙地把这个引用直接放到了Thread
对象里面,使得多个线程不需要同时操作同一个对象。所以我们在设计代码的时候,也要有这种思维的转变。并不是说我想实现一个工具类,就一定要把所有的代码都写在这个工具类里面。要充分考虑怎样设计更合理,性能更高。
弱引用
使用弱引用可以让GC及时回收掉程序中不需要使用的对象。这个刚好适用于ThreadLocal的场景。因为很多时候线程执行完后,就销毁了。如果让我们显示去调用一个方法,就会变得非常麻烦。而且一旦忘记回收,还有可能撑满内存。
所以这点ThreadLocal做得很好,利用了弱引用的特性,与Java的设计哲学一致:你只管用,回收的事情我帮你做了!
❝Tips: 这里需要注意上面提到的与线程池一起使用可能存在的问题哦。
❞
简单设计
ThreadLocal中自己定义了一个很简单的可以自动扩容的Map。它处理冲突的方式与HashMap不一样,HashMap是数组 + 链表/红黑树的方式来处理哈希冲突,而ThreadLocal实现得更简单,使用的是「开放地址法」,如果发生了冲突,就寻找下一个有空的位置。
开放地址法虽然效率不一定高,但胜在实现起来很简单,用在这里绰绰有余。我们在设计数据结构和算法的时候,甚至是在设计程序的时候,也有遵循够用、简单就行的原则,不用太过度设计。也就是我们常说的KISS原则:Keep it stupid and simple。
函数式编程
使用函数式编程可以让客户端更简单地实现定制化。比如ThreadLocal中的初始化方法,如果没有函数式编程,我们首先得新建一个ThreadLocal的继承类,然后复写它的initialValue
方法,用起来特别不方便。
我们在设计自己的工具类的时候,想要实现一定程度的灵活性和定制化,就可以考虑利用函数式编程的便利。
巧用this
this
其实我们平时用的还算比较多,最多的地方应该是POJO类了。但ThreadLocal进行了一个骚操作。
我们看ThreadLocalMap的源码可以发现,它的key类型就是ThreadLocal。我们在调用get/set方法的时候,就会使用this。
为什么要这么设计?你会发现Thread和ThreadLocal其实是「多对多的关系」。一个Thread可能会用到多个ThreadLocal,而一个ThreadLocal又同时给多个Thread用。那么问题来了,我们的入口是ThreadLocal对象,那如何能够快速地拿到当前Thread,当前ThreadLocal的value?
这就是this
的关键之处了,我先拿到当前Thread,然后通过Thread里面保存的引用,拿到ThreadLocalMap,这个Map里面保存了此线程对应的所有ThreadLocal的对象,key就是这个对象本身,所以用this
作为key,可以快速找到当前ThreadLocal对应的value。
假如我们要实现一个多对多的场景,比如一个学生有多个老师,一个老师有多个学生。通过学生类作为入口进去,如何能够快速获取一个学生指定老师的分数?我们写个程序来模拟一下:
// 教师类 public class Teacher { // 每个教师保存了自己每个学生的分数 Map<Student, Integer> scores = new HashMap<>(); public Map<Student, Integer> getScores() { return scores; } } // 学生类 public class Student { public int get(Teacher teacher) { Map<Student, Integer> scores = teacher.getScores(); return scores.get(this); } public void set(Teacher teacher, int score) { teacher.getScores().put(this, score); Map<Student, Integer> scores = teacher.getScores(); } }
当然了,这种场景其实并不多见。但ThreadLocal有它的特殊性,首先当前Thread对象是可以通过全局直接获取到的,然后我们的操作入口一般是ThreadLocal,使用而不是Thread。
试想一下,其实如果JDK开放权限,通过Thread也能拿到最后的ThreadLocal,无非就是麻烦一些:大概长这样:
Thread thread = Thread.currentThread(); // 如果jdk提供下面这个方法 ThreadLocalMap threadLocalMap = thread.getThreadLocals(); threadLocalMap.set(threadLocal, value); // set Object value = threadLocalMap.get(threadLocal); // get
但是这样一看,显然不如现在这样设计得优雅:
threadLocal.set(value); //set Object value = threadLocal.get(); // get
所以这就是程序设计的哲学,大佬设计出来的东西,就是好用!JDK把ThreadLocal的引用放到了Thread里面,让它能够避免多个线程争用资源,再巧妙利用了this关键字,让你可以很简单地使用它。然后还考虑到了内存回收的问题,用弱引用帮你解决。
看完ThreadLocal源码不禁惊呼:只怪自己没文化,一句卧槽走天下!