为什么建议多用下ThreadLocal

小说通常会有个起承转合,本片文章也打算用这样的方式来。先卖个关子,讲讲synchronized的那些事。

synchronized锁,平常在代码里面随处可见,它可以用在类上,可以用在方法上面,也可以用在代码块上面,似乎任何地方想到加锁,synchronized都是信手拈来。

但是被synchronized修饰过的方法或者对象,当线程需要调用方法或者获取对象时,其他线程会被丢进线程队列里面,等待锁被释放。由于synchronized是一个重量级的锁,在高并发的场景问题就会凸显出来。

当然了,随着最近几个版本的更新,JVM对synchronized做了不少优化,在并发和非并发场景下分别用来不同的锁,感兴趣的读者可以去百度一下相关文章,这里就不展开描述了,内容比较深奥,不是一时半会能讲清楚,弄明白。

 

因此总结一下synchronized使用带来的一些问题:

1.  带来了不小的时间消耗。加锁、解锁都需要额外操作。
我曾经写过一篇关于python的的锁机制(写得比较简单,有兴趣可以出门左转),大致描述了一下python的锁是怎么获得和释放的。
语言之间也是有共通之处的,虽然Java对我们隐藏了Synchronized的实现逻辑,但道理还是差不多的。
2、带来了不少的系统开销。互斥同步对性能最大的影响是阻塞的实现,因为阻塞涉及到的挂起线程和恢复线程的操作都需要转入内核态中完成
用户态与内核态的切换的性能代价是比较大的,这里就不展开描述了,我自己理解也不是很深刻
 
话虽这么说,synchronized如此方便,当然会有它的一席用武之地。例如在之前的项目中,获取Jedis连接库的初始化的时候会用到,读取配置文件的时候也会用到,即使并发情况下也不碍于它的正常运行。

但是我们都知道线程执行任务都是在一定的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. 其他需要内部线程独立访问全局变量的地方

 

世界上有些东西就是这么美妙,当你一层层剥开它的时候,会忍不住说,哇塞,太棒了吧。

献上杨宗纬的《洋葱》:

如果你愿意一层一层
一层地剥开我的心
你会鼻酸
你会流泪
只要你能
听到我
看到我的全心全意

 

posted @ 2022-07-23 20:51  2015夏  阅读(343)  评论(0编辑  收藏  举报