ThreadLocal原理分析
概述
ThreadLocal是面试非常高频的问题,在很多框架源码中都可以看到他的身影,比如Spring,ReentrantReadWriteLock,然后在平时的工作使用的却并不多,ThreadLocal要解决并不是多线程修改共享变量保证线程安全的问题,这个是通过悲观锁(比如synchronized)或者乐观锁(比如CAS)实现的,它要解决的问题是多线程环境下修改变量,每个线程修改自己的变量副本,线程之间互相不影响的问题。本文就介绍一下ThreadLocal是如何实现线程之间隔离的。
举例
为了方便ThreadLocal的理解,这里先举一个ThreadLocal的使用小例子,通过例子来分析它的原理。
public class SeqCount { // 一般使用private static修饰 private static ThreadLocal<Integer> seqCount = new ThreadLocal<Integer>(){ // 实现initialValue() public Integer initialValue() { return 0; } }; public int nextSeq(){ seqCount.set(seqCount.get() + 1); return seqCount.get(); } public static void main(String[] args){ SeqCount seqCount = new SeqCount(); SeqThread thread1 = new SeqThread(seqCount); SeqThread thread2 = new SeqThread(seqCount); SeqThread thread3 = new SeqThread(seqCount); SeqThread thread4 = new SeqThread(seqCount); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } private static class SeqThread extends Thread{ private SeqCount seqCount; SeqThread(SeqCount seqCount){ this.seqCount = seqCount; } public void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " seqCount值为 :" + seqCount.nextSeq()); } } } }
运行结果
Thread-0 seqCount值为 :1 Thread-0 seqCount值为 :2 Thread-0 seqCount值为 :3 Thread-1 seqCount值为 :1 Thread-1 seqCount值为 :2 Thread-1 seqCount值为 :3 Thread-2 seqCount值为 :1 Thread-2 seqCount值为 :2 Thread-2 seqCount值为 :3 Thread-3 seqCount值为 :1 Thread-3 seqCount值为 :2 Thread-3 seqCount值为 :3
为了对比,把上面的例子修改一下,不使用ThreadLocal看一下执行结果是怎么样的。
public class SeqCount1 { private static AtomicInteger seqCount1 = new AtomicInteger(0); public int nextSeq(){ return seqCount1.incrementAndGet(); } public static void main(String[] args){ SeqCount1 seqCount = new SeqCount1(); SeqThread thread1 = new SeqThread(seqCount); SeqThread thread2 = new SeqThread(seqCount); SeqThread thread3 = new SeqThread(seqCount); SeqThread thread4 = new SeqThread(seqCount); thread1.start(); thread2.start(); thread3.start(); thread4.start(); } private static class SeqThread extends Thread{ private SeqCount1 seqCount; SeqThread(SeqCount1 seqCount){ this.seqCount = seqCount; } public void run() { for(int i = 0 ; i < 3 ; i++){ System.out.println(Thread.currentThread().getName() + " seqCount值为 :" + seqCount.nextSeq()); } } } }
执行结果为:
Thread-0 seqCount值为 :1 Thread-0 seqCount值为 :2 Thread-0 seqCount值为 :3 Thread-1 seqCount值为 :4 Thread-1 seqCount值为 :5 Thread-1 seqCount值为 :6 Thread-2 seqCount值为 :7 Thread-2 seqCount值为 :9 Thread-2 seqCount值为 :10 Thread-3 seqCount值为 :8 Thread-3 seqCount值为 :11 Thread-3 seqCount值为 :12
通过上面两个例子大家可以清楚的看到,不使用ThreadLocal就变成了一个线程同步的问题,而使用了ThreadLocal之后线程之间就没有协作的问题,而是每个线程修改自己的变量副本,变量变成了线程内部私有的变量。
ThreadLocalMap
在上面的例子中,大家会发现使用ThreadLocal的get()、set()方法,而这些方法最后要操作就是ThreadLocalMap,所以这里先介绍一下这个东东,这个map是联系ThreadLocal和Thread的桥梁,当分析完这个map,大家对Thread,ThreadLocal,ThreadLocalMap之间的关系就会变得非常清晰。
ThreadLocalMap属性分析
//ThreadLocalMap是通过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; } } //ThreadLocalMap初始容量 private static final int INITIAL_CAPACITY = 16; //保存Entry的数组 private Entry[] table; //ThreadLocalMap中元素个数 private int size = 0; //ThreadLocalMap的负载因子 private int threshold;
针对上面的属性,做下面几点解读:
- 看过HashMap源码的应该有印象,HashMap实现了Map接口,而且在Map接口中也有一个Entry接口,HashMap是通过Node来保存key-value的,Node实现了Entry接口。ThreadLocalMap却完全不同,它既没有实现Map接口,在Entry中也没有类似next的指针指向下一个节点,说明ThreadLocalMap中没有使用链表,就直接存储在数组上,除此之外,ThreadLocalMap是ThreadLocal的内部类,没有使用public修饰,默认是只有当前包下面的类才可以使用,也就是说这个Map我们自己写的代码中是不能直接创建的。
- Entry中的key就是ThreadLocal,而且这个ThreadLocal还被WeakReference包装了一下,也就是说ThreadLocal在这里是弱引用,如果ThreadLocal为null,可以直接被gc垃圾回收,关于弱引用,后面会举一个简单的例子,大家看一下即可。具体可以参考:用弱引用堵住内存泄漏
- 下面几个属性和HashMap中类似,这里有意思的一点是HashMap的负载因子是0.75,而ThreadLocalMap的负载因子是2/3。
弱引用使用举例
public class FinalizeTest { @Override protected void finalize() throws Throwable { super.finalize(); System.out.println("finalize methode executed"); } public static void main(String[] args) { FinalizeTest finalizeTest = new FinalizeTest(); WeakReference<FinalizeTest> weak = new WeakReference(finalizeTest); Map<WeakReference<FinalizeTest>,Integer> map = new HashMap<>(); map.put(weak,1); System.out.println("====第一次gc"); System.gc(); finalizeTest = null; System.out.println("====第二次gc"); System.gc(); } }
执行结果
====第一次gc ====第二次gc finalize methode executed
这里为了模拟ThreadLocalMap,也搞了一个Map,这个map的key也是一个使用WeakReference包装的类,事实上这个map中key的引用并没有影响gc垃圾回收,只要将对象finalizeTest设置为null,就可以正常垃圾回收,所以ThreadLocalMap中Entry节点的key的垃圾回收也是如此。ThreadLocalMap使用弱引用是为了解决内存泄漏的问题,至于什么是内存泄漏,参考:对ThreadLocal实现原理的一点思考。
ThreadLocalMap构造方法分析
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) { //初始化数组,容量大小为16 table = new Entry[INITIAL_CAPACITY]; //通过key的hash值和15做与运算得到桶的位置 int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1); //将key-value封装到Entry中插入数组 table[i] = new Entry(firstKey, firstValue); size = 1; //设置阈值,达到这个阈值就扩容,阈值为16 * (2/3),当然这里要取整 setThreshold(INITIAL_CAPACITY); }
构造方法很简单,就不过多介绍了。
ThreadLocalMap常用方法分析
//由于ThreadLocalMap不像HashMap,发生Hash冲突时使用链表解决,ThreadLocalMap的做法就是发生hash冲突 //会找当前位置的下一个桶 private static int nextIndex(int i, int len) { return ((i + 1 < len) ? i + 1 : 0); } //当前位置的上一个位置 private static int prevIndex(int i, int len) { return ((i - 1 >= 0) ? i - 1 : len - 1); } private Entry getEntry(ThreadLocal<?> key) { //确定桶的位置 int i = key.threadLocalHashCode & (table.length - 1); Entry e = table[i]; //如果找的位置entry不为null,并且entry正好是要找的key,就返回 if (e != null && e.get() == key) return e; else //这一步其实就是发生了hash冲突,本来应该是这个key占用的位置,却被别的key给占用了 //所以这里就要去数组挨个找了 return getEntryAfterMiss(key, i, e); } //通过key的hash定位到桶中entry,entry中的key和自己的key不相同,就会调用这个方法 //参数中的i就是key通过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) //在这个方法中会把key对应的value给置为null,同时将entry移除 expungeStaleEntry(i); else //如果当前桶中的entry不符合,就找后一个节点 i = nextIndex(i, len); e = tab[i]; } return null; } //向map中插入元素 private void set(ThreadLocal<?> key, Object value) { // We don't use a fast path as with get() because it is at // least as common to use set() to create new entries as // it is to replace existing ones, in which case, a fast // path would fail more often than not. Entry[] tab = table; int len = tab.length; int i = key.threadLocalHashCode & (len-1); //这里使用了一个for循环,寻找定位到的桶,如果定位到的桶中有元素 //就寻找该桶之后没有存放元素的桶用来存放当前的key for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) { ThreadLocal<?> k = e.get(); //如果key重复,用新的value覆盖旧的value if (k == key) { e.value = value; return; } if (k == null) { //在这个方法中会检测key是否为null,如果为null就把value也置为null //同时移除Entry节点 replaceStaleEntry(key, value, i); return; } } tab[i] = new Entry(key, value); int sz = ++size; if (!cleanSomeSlots(i, sz) && sz >= threshold) rehash(); } 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) { //上面介绍key通过弱引用包装,可以正常GC,但是value没有使用弱引用 //所以在key被垃圾回收之后,value并不会被回收,所以这里手动设置为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; }
上面都有注释,这里提几点需要注意的地方
- 由于ThreadLocalMap没有使用散列表的结构,所以发生hash冲突的时候是寻找下一个桶
- 把key使用弱引用,可以使得gc正常回收,但是value并不是弱引用,所以在扩容的时候,把value置为null,方便value垃圾回收,在平时写代码的时候,如果某个ThreadLocal不在使用了,最好直接调用ThreadLoalMap的remove方法把当前的key,value都移除,防止内存泄漏
- 在getEntry方法和set方法中当key为null,就把value也置为null,同时把Entry也移除了。
- 里面有些方法没有详细注释,因为并不是重要方法,所以就没有仔细看
Thread、ThreadLocal、ThreadLocalMap三者之间的的关系
在我的另一篇分析Thread的文章有提到在Thread源码中有这么一个字段,如下:
ThreadLocal.ThreadLocalMap threadLocals = null;
这个字段就是保存TreadLocalMap的,也就是说每个线程都有一个ThreadLocalMap,ThreadLocalMap中保存这个ThreadLocal和ThreadLocal封装的成员变量的值,同一个父线程的子线程的ThreadLocalMap中保存的ThreadLocal都是一样的,只是value不同,具体三者之间的关系可以用下图表示。
ThreadLocal常用方法分析
get方法
public T get() {
//获取当前线程引用 Thread t = Thread.currentThread();
//获取当前线程的ThreadLocalMap,就是上面介绍的Thread类中的threadLocals字段 ThreadLocalMap map = getMap(t); if (map != null) {
//拿到ThreadLocalMap之后,根据key获取Entry ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } }
//如果是首次插入,map没有创建,创建ThreadLocalMap return setInitialValue(); }
进入#getMap()方法
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
进入#setInitialValue()方法
private T setInitialValue() { //这个方法在最开始举例的时候重写了 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else //创建map createMap(t, value); return value; }
进入#initialValue()方法
protected T initialValue() { return null; }
这个返回的泛型T就是ThreadLocal要包装的成员变量
进入#createMap()方法
void createMap(Thread t, T firstValue) { t.threadLocals = new ThreadLocalMap(this, firstValue); }
直接调用上面分析的ThreadLocalMap的构造方法创建,并且给Thread中threadLocals赋值,从这里开始Thread就和ThreadLocal还有ThreadLocalMap联系起来了。
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); }
这个方法很简单,就不分析了。
remove方法
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
这个也很简单。
常见应用场景
由于工作中基本没有使用过,所以在网上看到几个常见的使用场景,如下:
- 把session保存到ThreadLocal中,但是现在session一般保存在redis中,用于分布式共享,使用ThreadLocal只能在一个节点的线程中共享,无法做到分布式共享,所以这个场景目前来看并不合适。
- 由于SimpleDateFormat在格式化时间的时候,线程不安全,所以在高并发的时候格式化出来的日期可能是错误的,可以使用ThreadLocal封装SimpleDateFormat,避免每次重新创建这个对象,这个确实是一个使用场景,但是现在是java8的天下,完全可以不用这个格式化类,java8可以通过LocalDateTime获取日期时间,通过DateTimeFormatter进行格式化,这个是一个线程安全的类
没有找到具体日常开发中使用ThreadLocal的场景,所以找到了源码中使用ThreadLocal的例子,就是ReentrantReadWriteLock,是一个读写锁,大家有兴趣可以看一下我的另一篇文章:ReentrantReadWriteLock原理分析
参考: