为什么建议多用下ThreadLocal
小说通常会有个起承转合,本片文章也打算用这样的方式来。先卖个关子,讲讲synchronized的那些事。
synchronized锁,平常在代码里面随处可见,它可以用在类上,可以用在方法上面,也可以用在代码块上面,似乎任何地方想到加锁,synchronized都是信手拈来。
但是被synchronized修饰过的方法或者对象,当线程需要调用方法或者获取对象时,其他线程会被丢进线程队列里面,等待锁被释放。由于synchronized是一个重量级的锁,在高并发的场景问题就会凸显出来。
当然了,随着最近几个版本的更新,JVM对synchronized做了不少优化,在并发和非并发场景下分别用来不同的锁,感兴趣的读者可以去百度一下相关文章,这里就不展开描述了,内容比较深奥,不是一时半会能讲清楚,弄明白。
因此总结一下synchronized使用带来的一些问题:
但是我们都知道线程执行任务都是在一定的CPU时间片里面进行的。如果真的遇到高并发的场景,大量线程都都阻塞等待了,其他事情就没办法执行了。
好了,说了这么多,接下来才是引入今天的主角,ThreadLocal。
作为线程安全的一个替代方案,ThreadLocal有可见的优势。假如我们需要并发操作的是一份全局变量,而这份全局变量需要线程隔离的(即多个线程自己保持一份数据,不是由多个线程同时对全局变量进行累加操作的行为),ThreadLocal可以提出来作为一种替代。
ThreadLocal的使用很简单,主要有两个方法,get() 和 set()
ThreadLocal<String> CONTEXT_HOLDER = new ThreadLocal<>(); CONTEXT_HOLDER.set(setvalue) String getvalue = CONTEXT_HOLDER.get();
下面重点从原理上分析,ThreadLocal内部是怎么实现的,为什么它有这样或者那样的特点,再从特点出发,总结一下它的适用场景。
ThreadLocal 可以形象描述为各扫门前雪,为什么说它是各扫门前雪呢?道理很简单,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(); }
public void set(T value) { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); }
从代码不难看出,ThreadLocal内部实际上是定义了一个叫做ThreadLocalMap 的 HashMap,用于存储当前线程的数据
再往下一步,源码对TreadLocalMap有一个比较详尽的描述:
/**
* ThreadLocalMap is a customized hash map suitable only for
* maintaining thread local values. No operations are exported
* outside of the ThreadLocal class. The class is package private to
* allow declaration of fields in class Thread. To help deal with
* very large and long-lived usages, the hash table entries use
* WeakReferences for keys. However, since reference queues are not
* used, stale entries are guaranteed to be removed only when
* the table starts running out of space.
*/
意思大概是,由于Map使用WeakReference弱引用对象作为key,因为没有使用WeakRefrence的弱引用队列(即key具有唯一性)
WeakRefrence这个东东也是一个很奇妙的东西,有兴趣可以在园子里面逛逛,看看相关介绍啦,我自己也是一步步追溯下来理解的。这里就不展开了。
所以只有当Map的数据非常大,已经超出规定的内存空间的时候才会被GC掉。这样保障了单个复杂操作过程,线程中ThreadLocal数据的稳定。
static class ThreadLocalMap { /** * The entries in this hash map extend WeakReference, using * its main ref field as the key (which is always a * ThreadLocal object). Note that null keys (i.e. entry.get() * == null) mean that the key is no longer referenced, so the * entry can be expunged from table. Such entries are referred to * as "stale entries" in the code that follows. */ // 只有当key是null的时候,才会被定义为陈旧的数据被移除
// static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
// ThreadLocalMap的构造
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
...
...
};
关于陈旧数据的移除:
可以看到ThreadLocalMap实际上是一个数组结构,用于存储ThreadLocal数据。ThreadLocal内部定义了一个水位线,当内存大于这个水位线的时候,会先把陈旧的数据线GC掉;
如果GC的效果并不理想,会把水位线值升高,增大内存。
这样做的好处当然是为了避免频繁的GC和保持数据的稳定性啦。
/** * Re-pack and/or re-size the table. First scan the entire * table removing stale entries. If this doesn't sufficiently * shrink the size of the table, double the table size. */ private void rehash() {
// 先清理陈旧数据 expungeStaleEntries(); // Use lower threshold for doubling to avoid hysteresis if (size >= threshold - threshold / 4) resize(); }
讲了这么一些内容,总结一下:
1. ThreadLocal实际上是用在线程内部存储数据,避免Thread争夺资源导致产生了脏的数据。
2. 其内部运用水位线的机制来控制什么时候进行内存的GC,保障数据稳定性的同时也减小系统开销
至此,ThreadLocal为什么线程内部的一个副本数据,大概有一个比较好的解释了。简单来说,就是我管我自己的事情,跟你其他人(线程)一点关系都没有!各扫门前雪,大家互不干扰,各干各的。
那么ThreadLocal在实际应用中会怎么用呢?
总结一下使用的场景:
1. 对于SpringMVC 多线程单实例的特点,如果想在Controller中定义变量,那么这个变量会是全局性的,可以用ThreadLocal解决并发时产生的问题。
2. 分布式数据库配置,每个线程内部保留自己一份数据库连接
我会在下一篇:聊聊Spring的主从数据库配置 里面进行介绍
3. 其他需要内部线程独立访问全局变量的地方
世界上有些东西就是这么美妙,当你一层层剥开它的时候,会忍不住说,哇塞,太棒了吧。
献上杨宗纬的《洋葱》:
如果你愿意一层一层
一层地剥开我的心
你会鼻酸
你会流泪
只要你能
听到我
看到我的全心全意