Java高性能编程实战-线程封闭与ThreadLocal

46次阅读

共计 11251 个字符,预计需要花费 29 分钟才能阅读完成。

1 线程封闭

多线程访问共享可变数据时,涉及到线程间数据同步的问题。并不是所有时候,都要用到
共享数据,所以线程封闭概念就提出来了。

数据都被封闭在各自的线程之中,就不需要同步,这种通过将数据封闭在线程中而避免使
用同步的技术称为 线程封闭

避免并发异常最简单的方法就是线程封闭
即 把对象封装到一个线程里, 只有该线程能看到此对象;
那么该对象就算非线程安全, 也不会出现任何并发安全问题.

1.1 栈封闭

局部变量的固有属性之一就是封闭在线程中。
它们位于执行线程的栈中,其他线程无法访问这个栈

1.2 使用 ThreadLocal 是实现线程封闭的最佳实践.

ThreadLocal 是 Java 里一种特殊的变量。
它是一个线程级变量,每个线程都有一个 ThreadLocal, 就是每个线程都拥有了自己独立的一个变量,
竞争条件被彻底消除了,在并发模式下是绝对安全的变量。

  • 用法
ThreadLocal<T> var = new ThreadLocal<T>();

会自动在每一个线程上创建一个 T 的副本,副本之间彼此独立,互不影响。
可以用 ThreadLocal 存储一些参数,以便在线程中多个方法中使用,用来代替方法传参的做法。

实例

ThreadLocal 内部维护了一个 Map,Map 的 key 是每个线程的名称,Map 的值就是我们要封闭的对象.
每个线程中的对象都对应着 Map 中一个值, 也就是 ThreadLocal 利用 Map 实现了对象的线程封闭.

对于 CS 游戏, 开始时, 每个人能够领到一把枪, 枪把上有三个数字: 子弹数、杀敌数、自己的命数,为其设置的初始值分别为 1500、0、10.

设战场上的每个人都是一个线程, 那么这三个初始值写在哪里呢?
如果每个线程都写死这三个值, 万一将初始子弹数统一改成 1000 发呢?
如果共享, 那么线程之间的并发修改会导致数据不准确.
能不能构造这样一个对象, 将这个对象设置为共享变量, 统一设置初始值, 但是每个线程对这个值的修改都是互相独立的. 这个对象就是 ThreadLocal

注意不能将其翻译为线程本地化或本地线程
英语恰当的名称应该叫作:CopyValueIntoEveryThread

示例代码

实在难以理解的,可以理解为,JVM 维护了一个 Map<Thread, T>, 每个线程要用这个 T 的时候,用当前的线程去 Map 里面取。仅作为一个概念理解

该示例中, 无 set 操作, 那么初始值又是如何进入每个线程成为独立拷贝的呢?
首先, 虽然 ThreadLocal 在定义时重写了 initialValue() , 但并非是在BULLET_ NUMBER_ THREADLOCAL 对象加载静态变量的时候执行;
而是每个线程在 ThreadLocal.get() 时都会执行到;
其源码如下

每个线程都有自己的 ThreadLocalMap;
如果 map ==null, 则直接执行setInitialValue();
如果 map 已创建, 就表示 Thread 类的 threadLocals 属性已初始化完毕;
如果 e==null, 依然会执行到 setinitialValue()
setinitialValue() 的源码如下:

这是一个保护方法,CsGameByThreadLocal 中初始化 ThreadLocal 对象时已覆写 value = initialValue() ;

getMap 的源码就是提取线程对象 t 的 ThreadLocalMap 属性: t. threadLocals.

CsGameByThreadLocal 第 1 处,使用了 ThreadLocalRandom 生成单独的Random 实例;
该类在 JDK7 中引入, 它使得每个线程都可以有自己的随机数生成器;
我们要避免 Random 实例被多线程使用, 虽然共享该实例是线程安全的, 但会因竞争同一 seed 而导致性能下降.

我们已经知道了 ThreadLocal 是每一个线程单独持有的;
因为每一个线程都有独立的变量副本, 其他线程不能访问, 所以不存在线程安全问题, 也不会影响程序的执行性能.
ThreadLocal对象通常是由 private static 修饰的, 因为都需要复制到本地线程, 所以非 static 作用不大;
不过,ThreadLocal无法解决共享对象的更新问题, 下面的实例将证明这点.
因为 CsGameByThreadLocal 中使用的是 Integer 不可变对象, 所以可使用相同的编码方式来操作一下可变对象看看

输出的结果是乱序不可控的, 所以使用某个引用来操作共享对象时, 依然需要进行线程同步

ThreadLocal 有个静态内部类 ThreadLocalMap,它还有一个静态内部类Entry;
在 Thread 中的 ThreadLocalMap 属性的赋值是在 ThreadLocal 类中的createMap.

ThreadLocal ThreadLocalMap 有三组对应的方法: get()、set()和 remove();
ThreadLocal 中对它们只做校验和判断,最终的实现会落在 ThreadLocalMap..
Entry 继承自WeakReference, 只有一个 value 成员变量, 它的 key 是 ThreadLocal 对象

再从栈与堆的内存角度看看两者的关系

一个 Thread 有且仅有一个 ThreadLocalMap 对象
一个 Entry 对象的 key 弱引用指向一个 ThreadLocal 对象
一个 ThreadLocalMap 对象存储多个 Entry 对象
一个 ThreadLocal 对象可被多个线程共享
ThreadLocal 对象不持有 Value,Value 由线程的 Entry 对象持有.

Entry 对象源码如下

所有的 Entry 对象都被 ThreadLocalMap 类实例化对象 threadLocals 持有;
当线程执行完毕时, 线程内的实例属性均会被垃圾回收, 弱引用的 ThreadLocal, 即使线程正在执行, 只要ThreadLocal 对象引用被置成 null,Entry 的 Key 就会自动在下一次 Y – GC 时被垃圾回收;
而在 ThreadLocal 使用 set()/get() 时, 又会自动将那些 key=null 的 value 置为 null, 使 value 能够被 GC, 避免内存泄漏, 现实很骨感, ThreadLocal 如源码注释所述:

ThreadLocal 对象通常作为私有静态变量使用, 那么其生命周期至少不会随着线程结束而结束.

三个重要方法:

  • set()

如果没有 set 操作的ThreadLocal, 很容易引起脏数据问题

  • get()

始终没有 get 操作的 ThreadLocal 对象是没有意义的

  • remove()

如果没有 remove 操作, 则容易引起内存泄漏

如果 ThreadLocal 是非静态的, 属于某个线程实例, 那就失去了线程间共享的本质属性;
那么 ThreadLocal 到底有什么作用呢?
我们知道, 局部变量在方法内各个代码块间进行传递, 而类变量在类内方法间进行传递;
复杂的线程方法可能需要调用很多方法来实现某个功能, 这时候用什么来传递线程内变量呢?
ThreadLocal, 它通常用于同一个线程内, 跨类、跨方法传递数据;
如果没有 ThreadLocal, 那么相互之间的信息传递, 势必要靠返回值和参数, 这样无形之中, 有些类甚至有些框架会互相耦合;
通过将 Thread 构造方法的最后一个参数设置为 true, 可以把当前线程的变量继续往下传递给它创建的子线程

public Thread (ThreadGroup group, Runnable target, String name,long stackSize, boolean inheritThreadLocals) [this (group, target, name,  stackSize, null, inheritThreadLocals) ;
}

parent 为其父线程

if (inheritThreadLocals && parent. inheritableThreadLocals != null)
      this. inheritableThreadLocals = ThreadLocal. createInheritedMap (parent. inheritableThreadLocals) ;

createlnheritedMap()其实就是调用 ThreadLocalMap 的私有构造方法来产生一个实例对象, 把父线程中不为 null 的线程变量都拷贝过来

private ThreadLocalMap (ThreadLocalMap parentMap) {
    // table 就是存储
    Entry[] parentTable = parentMap. table;
    int len = parentTable. length;
    setThreshold(len) ;
    table = new Entry[len];

    for (Entry e : parentTable) {if (e != null) {ThreadLocal<object> key = (ThreadLocal<object>) e.get() ;
        if (key != null) {object value = 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++;
        }
    }
}

很多场景下可通过 ThreadLocal 来透传全局上下文的;
比如用 ThreadLocal 来存储监控系统的某个标记位, 暂且命名为 traceld.
某次请求下所有的 traceld 都是一致的, 以获得可以统一解析的日志文件;
但在实际开发过程中, 发现子线程里的 traceld 为 null, 跟主线程的 traceld 并不一致, 所以这就需要刚才说到的 InheritableThreadLocal 来解决父子线程之间共享线程变量的问题, 使整个连接过程中的 traceld 一致.
示例代码如下

import org.apache.commons.lang3.StringUtils;

/**
 * @author sss
 * @date 2019/1/17
 */
public class RequestProcessTrace {

    private static final InheritableThreadLocal<FullLinkContext> FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL
            = new InheritableThreadLocal<FullLinkContext>();

    public static FullLinkContext getContext() {FullLinkContext fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();
        if (fullLinkContext == null) {FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.set(new FullLinkContext());
            fullLinkContext = FULL_LINK_CONTEXT_INHERITABLE_THREAD_LOCAL.get();}
        return fullLinkContext;
    }

    private static class FullLinkContext {
        private String traceId;

        public String getTraceId() {if (StringUtils.isEmpty(traceId)) {FrameWork.startTrace(null, "JavaEdge");
                traceId = FrameWork.getTraceId();}
            return traceId;
        }

        public void setTraceId(String traceId) {this.traceId = traceId;}
    }

}

使用 ThreadLocalInheritableThreadLocal透传上下文时, 需要注意线程间切换、异常传输时的处理, 避免在传输过程中因处理不当而导致的上下文丢失.

最后,SimpleDateFormat 是非线程安全的类, 定义为 static, 会有数据同步风险.
通过源码可以看出,SimpleDateFormat 内部有一个 Calendar 对象;
在日期转字符串或字符串转日期的过程中, 多线程共享时很可能产生错误;
推荐使用 ThreadLocal, 让每个线程单独拥有这个对象.

ThreadLocal 的副作用

为了使线程安全地共享某个变量,JDK 给出了 ThreadLocal.
ThreadLocal的主要问题是会产生脏数据和内存泄漏;
这两个问题通常是在线程池的线程中使用 ThreadLocal 引发的,因为线程池有线程复用和内存常驻两是在线程池的线程中使用 ThreadLocal 引发的,因为线程池有线程复用和内存常驻两个特点

1 脏数据

线程复用会产生脏数据;
由于线程池会重用 Thread 对象, 与 Thread 绑定的静态属性 ThreadLoca l 变量也会被重用.
如果在实现的线程 run()方法中不显式调用 remove() 清理与线程相关的 ThreadLocal 信息, 那么若下一个线程不调用 set(), 就可能get() 到重用的线程信息;
包括 ThreadLocal 所关联的线程对象的 value 值.

脏读问题其实十分常见.
比如, 用户 A 下单后没有看到订单记录, 而用户 B 却看到了用户 A 的订单记录.
通过排查发现是由于 session 优化引发.
在原来的请求过程中, 用户每次请求 Server, 都需要通过 sessionId 去缓存里查询用户的 session 信息, 这样无疑增加了一次调用.
因此, 工程师决定采用某框架来缓存每个用户对应的 SecurityContext, 它封装了 session 相关信息.
优化后虽然会为每个用户新建一个 session 相关的上下文, 但由于 Threadlocal 没有在线程处理结束时及时 remove();
在高并发场景下, 线程池中的线程可能会读取到上一个线程缓存的用户信息.

  • 示例代码



## 2 内存泄漏
在源码注释中提示使用 static 关键字来修饰 ThreadLocal.
在此场景下, 寄希望于 ThreadLocal 对象失去引用后, 触发弱引用机制来回收 EntryValue就不现实了.
在上例中, 如果不进行 remove(), 那么当该线程执行完成后, 通过ThreadLocal 对象持有的 String 对象是不会被释放的.

  • ** 以上两个问题的解决办法很简单

每次用完 ThreadLocal 时, 及时调用 remove() 清理 **

What is ThreadLocal

该类提供了线程局部 (thread-local) 变量;
这些变量不同于它们的普通对应物, 因为访问某变量(通过其 get /set 方法)的每个线程都有自己的局部变量, 它独立于变量的初始化副本.

ThreadLocal 实例通常是类中的 private static 字段, 希望将状态与某一个线程(e.g. 用户 ID 或事务 ID)相关联.

一个以 ThreadLocal 对象为键、任意对象为值的存储结构;
有点像 HashMap, 可以保存 ”key : value” 键值对, 但一个ThreadLocal 只能保存一个键值对, 各个线程的数据互不干扰.
该结构被附带在线程上, 也就是说一个线程可以根据一个 ThreadLocal 对象查询到绑定在这个线程上的一个值.

ThreadLocal<String> localName = new ThreadLocal();
localName.set("JavaEdge");
String name = localName.get();

在线程 A 中初始化了一个 ThreadLocal 对象 localName,并 set 了一个值 JavaEdge;
同时在线程 A 中通过 get 可拿到之前设置的值;
但是如果在线程 B 中, 拿到的将是一个 null.

因为 ThreadLocal 保证了各个线程的数据互不干扰
看看 set(T value)和 get()方法的源码


可见, 每个线程中都有一个ThreadLocalMap

  • 执行 set 时, 其值是保存在当前线程的 threadLocals 变量
  • 执行 get 时, 从当前线程的 threadLocals 变量获取

所以在线程 A 中 set 的值, 是线程 B 永远得不到的
即使在线程 B 中重新 set, 也不会影响 A 中的值;
保证了线程之间不会相互干扰.

追寻本质 – 结构

从名字上看猜它类似 HashMap, 但在 ThreadLocal 中, 并无实现 Map 接口

  • ThreadLoalMap 中, 也是初始化一个大小为 16 的 Entry 数组

  • Entry 节点对象用来保存每一个 key-value 键值对


这里的 key 恒为 ThreadLocal;
通过 ThreadLocalset(), 把 ThreadLocal 对象自身当做 key, 放进 ThreadLoalMap

ThreadLoalMapEntry继承 WeakReference
和 HashMap 很不同,Entry中没有 next 字段, 所以不存在链表情形.

hash 冲突

无链表, 那发生 hash 冲突时何解?

先看看 ThreadLoalMap 插入一个 key/value 的实现

  • 每个 ThreadLocal 对象都有一个 hash 值 – threadLocalHashCode

  • 每初始化一个 ThreadLocal 对象,hash 值就增加一个固定大小

在插入过程中, 根据 ThreadLocal 对象的 hash 值, 定位至 table 中的位置 i.
过程如下

  • 若当前位置为空, 就初始化一个 Entry 对象置于 i;
  • 位置 i 已有对象

    • 若该 Entry 对象的 key 正是将设置的 key, 覆盖其 value(和 HashMap 处理相同);
    • 若和即将设置的 key 无关, 则寻找下一个空位

如此, 在 get 时, 也会根据 ThreadLocal 对象的 hash 值, 定位到 table 中的位置. 然后判断该位置 Entry 对象中的 key 是否和 get 的 key 一致, 如果不一致, 就判断下一个位置.

可见,set 和 get 如果冲突严重的话, 效率很低, 因为 ThreadLoalMap 是 Thread 的一个属性,所以即使在自己的代码中控制了设置的元素个数,但还是不能控制其它代码的行为

内存泄露

ThreadLocal 可能导致内存泄漏,为什么?
先看看 Entry 的实现:

static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {super(k);
        value = v;
    }
}

通过之前的分析已经知道,当使用 ThreadLocal 保存一个 value 时,会在 ThreadLocalMap 中的数组插入一个 Entry 对象,按理说 key-value 都应该以强引用保存在 Entry 对象中,但在 ThreadLocalMap 的实现中,key 被保存到了 WeakReference 对象中

这就导致了一个问题,ThreadLocal 在没有外部强引用时,发生 GC 时会被回收,如果创建 ThreadLocal 的线程一直持续运行,那么这个 Entry 对象中的 value 就有可能一直得不到回收,发生内存泄露。

避免内存泄露

既然发现有内存泄露的隐患,自然有应对策略,在调用 ThreadLocal 的 get()、set()可能会清除 ThreadLocalMap 中 key 为 null 的 Entry 对象,这样对应的 value 就没有 GC Roots 可达了,下次 GC 的时候就可以被回收,当然如果调用 remove 方法,肯定会删除对应的 Entry 对象。

如果使用 ThreadLocal 的 set 方法之后,没有显示的调用 remove 方法,就有可能发生内存泄露,所以养成良好的编程习惯十分重要,使用完 ThreadLocal 之后,记得调用 remove 方法。

    ThreadLocal<String> localName = new ThreadLocal();
    try {localName.set("JavaEdge");
        // 其它业务逻辑
    } finally {localName.remove();
    }

题外小话

首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的.
一般情况下,通过 set() 到线程中的对象是该线程自己使用的对象, 其他线程是不需要访问的, 也访问不到的;
各个线程中访问的是不同的对象.

** 另外,说 ThreadLocal 使得各线程能够保持各自独立的一个对象;
并不是通过 set()实现的, 而是通过每个线程中的 new 对象的操作来创建的对象, 每个线程创建一个,不是什么对象的拷贝或副本。**
通过 set()将这个新创建的对象的引用保存到各线程的自己的一个 map 中, 每个线程都有这样一个 map;
执行 get()时, 各线程从自己的 map 中取出放进去的对象, 因此取出来的是各自线程中的对象.
ThreadLocal 实例是作为 map 的 key 来使用的.

如果 set()进去的东西本来就是多个线程共享的同一个对象;
那么多个线程的 get()取得的还是这个共享对象本身,还是有并发访问问题。

Hibernate 中典型的 ThreadLocal 应用

private static final ThreadLocal threadSession = new ThreadLocal();  
  
public static Session getSession() throws InfrastructureException {Session s = (Session) threadSession.get();  
    try {if (s == null) {s = getSessionFactory().openSession();  
            threadSession.set(s);  
        }  
    } catch (HibernateException ex) {throw new InfrastructureException(ex);  
    }  
    return s;  
}  

首先判断当前线程中有没有放入 session, 如果还没有, 那么通过 sessionFactory().openSession() 来创建一个 session;
再将 session set()到线程中, 实际是放到当前线程的 ThreadLocalMap;
这时, 对于该 session 的唯一引用就是当前线程中的那个 ThreadLocalMap;
threadSession 作为这个值的 key,要取得这个 session 可以通过 threadSession.get();
里面执行的操作实际是先取得当前线程中的 ThreadLocalMap;
然后将 threadSession 作为 key 将对应的值取出.
这个 session 相当于线程的私有变量, 而不是 public 的.

显然,其他线程中是取不到这个 session 的,他们也只能取到自己的 ThreadLocalMap 中的东西。要是 session 是多个线程共享使用的,那还不乱套了.

如果不用 ThreadLocal 怎么实现呢?

可能就要在 action 中创建 session,然后把 session 一个个传到 service 和 dao 中,这可够麻烦的;
或者可以自己定义一个静态的 map,将当前 thread 作为 key,创建的 session 作为值,put 到 map 中,应该也行,这也是一般人的想法.
但事实上,ThreadLocal 的实现刚好相反,它是在每个线程中有一个 map,而将 ThreadLocal 实例作为 key,这样每个 map 中的项数很少,而且当线程销毁时相应的东西也一起销毁了

总之,ThreadLocal不是用来解决对象共享访问问题的;
而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式

  • 每个线程中都有一个自己的 ThreadLocalMap 类对象;

可以将线程自己的对象保持到其中, 各管各的, 线程可以正确的访问到自己的对象.

  • 将一个共用的 ThreadLocal 静态实例作为 key, 将不同对象的引用保存到不同线程的 ThreadLocalMap 中, 然后在线程执行的各处通过这个静态 ThreadLocal 实例的 get()方法取得自己线程保存的那个对象, 避免了将这个对象作为参数传递的麻烦.

当然如果要把本来线程共享的对象通过 set()放到线程中也可以,可以实现避免参数传递的访问方式;
但是要注意 get()到的是那同一个共享对象,并发访问问题要靠其他手段来解决;
但一般来说线程共享的对象通过设置为某类的静态变量就可以实现方便的访问了,似乎没必要放到线程中

ThreadLocal 的应用场合

我觉得最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。

可以看到 ThreadLocal 类中的变量只有这 3 个 int 型:

private final int threadLocalHashCode = nextHashCode();  
private static AtomicInteger nextHashCode =
        new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647; 

而作为 ThreadLocal 实例的变量只有 threadLocalHashCode
nextHashCodeHASH_INCREMENT 是 ThreadLocal 类的静态变量
实际上

  • HASH_INCREMENT 是一个常量,表示了连续分配的两个 ThreadLocal 实例的 threadLocalHashCode 值的增量
  • nextHashCode 表示了即将分配的下一个 ThreadLocal 实例的 threadLocalHashCode 的值

看一下创建一个 ThreadLocal 实例即 new ThreadLocal()时做了哪些操作,构造方法 ThreadLocal() 里什么操作都没有,唯一的操作是这句

private final int threadLocalHashCode = nextHashCode();  

那么 nextHashCode()做了什么呢

private static int nextHashCode() {return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

就是将 ThreadLocal 类的下一个 hashCode 值即 nextHashCode 的值赋给实例的 threadLocalHashCode,然后 nextHashCode 的值增加 HASH_INCREMENT 这个值。.

因此 ThreadLocal 实例的变量只有这个 threadLocalHashCode,而且是 final 的,用来区分不同的 ThreadLocal 实例;
ThreadLocal 类主要是作为工具类来使用,那么 set()进去的对象是放在哪儿的呢?

看一下上面的 set()方法,两句合并一下成为

ThreadLocalMap map = Thread.currentThread().threadLocals;  

这个 ThreadLocalMap 类是 ThreadLocal 中定义的内部类,但是它的实例却用在 Thread 类中:

public class Thread implements Runnable {  
    ......  
  
    /* ThreadLocal values pertaining to this thread. This map is maintained 
     * by the ThreadLocal class. */  
    ThreadLocal.ThreadLocalMap threadLocals = null;    
    ......  
} 

再看这句:

if (map != null)  
    map.set(this, value);  

也就是将该 ThreadLocal 实例作为 key,要保持的对象作为值,设置到当前线程的 ThreadLocalMap 中,get()方法同样看了代码也就明白了.

参考

《码出高效:Java 开发手册》

正文完
 0