ThreadLoclc初衷是线程并发时,解决变量共享问题,可是因为过分设计,好比弱引用的和哈希碰撞,致使理解难度大、使用成本高,反而成为故障高发点,容易出现内存泄露,脏数据、贡献对象更新等问题。单从ThreadLoacl命名来看人们认为只要用它就对了,包治变量共享问题,然而并非。一下之内存模型、弱引用,哈希算法为铺垫,而后从cs真人游戏的示例代码入手,详细分析Threadlocal源码。咱们从中能够学习到全新的编程思惟方式,并认识到问题的来源,也可以帮助咱们谙熟此类设计之道,扬长避短。java
引用类型算法
对象在堆上建立以后所持有的引用实际上是一种变量类型,引用之间能够经过赋值构成一条引用链。从GC Roots 开始遍历,判断引用是否可达。引用的可达性是判断可否被垃圾回收的基本条件。JVM会据此自动管理内存分配与回收,不须要开发工程师干预。可是在某些场景下,即便引用可达,也但愿根据语义的强弱进行有选择的回收,以保证系统的正常运行。根据引用类型语义的强弱来决定垃圾回收的阶段,咱们能够把引用分为强引用,软引用,弱引用和虚引用四类。后三类引用,本质上可让开发工程师经过代码的方式来决定对象的垃圾回收时机。咱们先简要了解一下这个四类引用。编程
强引用,即Strong Reference , 最为常见,如Object object = new Object();这样的变量声明和定义就会产生该对象的强引用。只要对象有强引用指向,而且GC roots 可达,那么java内存回收时,即便濒临内存耗尽,也不会回收该对象。缓存
软引用,即soft Reference ,引用力度弱于"强引用",是用在非必须对象的场景。在即将OOM以前,垃圾回收器会把这些软引用指向的对象加入回收范围,以得到更多的内存空间,让程序可以继续健康运行。主要用来缓存服务器中间计算结果集不须要试试保存的用户行为等。安全
弱引用,即Weak Reference,引用强度较前二者更弱,也是用来描述非必须对象的。若是弱引用指向的对象只存在弱引用这一条线路,则在下一次YGC的时候被回收。因为YGC时间的不肯定性,弱引用什么时候被回收也有不肯定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用WeakReference.get() 可能返回null,要注意空指针异常。服务器
虚引用,即Phantom Reference ,是极弱的一种引用关系,定义完成后,就没法经过该引用获取指定的对象。为对象设置虚引用的惟一目的就是但愿能在这个对象被回收时收到一个系统通知,虚引用必须与引用队列联合使用,当垃圾回收时,若是发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。session
强引用是最经常使用的,而虚引用在业务中几乎很难用到。下面重点介绍一下软引用和弱引用。先来讲明一下软引用的回收机制。首先设置JVM 参数:-Xms 20m,-Xmx 20m,即只有20m的堆内存空间。多线程
1 public class SoftReferenceHouse { 2 public static void main(String[] args) { 3 //List<House> houses = new ArrayList<>(); //(第1处) 4 List<SoftReference> houses = new ArrayList<>(); 5 6 //剧情反转注释处 7 int i = 0; 8 while (true){ 9 //houses.add(new House()); //(第2处) 10 11 //剧情反转注释处 12 SoftReference<House> buyer2 = new SoftReference<>(new House()); 13 14 //剧情反转注释处 15 houses.add(buyer2); 16 System.out.println("i=" + (++i)); 17 } 18 } 19 } 20 21 class House{ 22 private static final Integer DOOR_NUMBER = 2000; 23 public Door [] doors = new Door[DOOR_NUMBER]; 24 class Door{} 25 }
new House() 是匿名对象,产生以后即赋值给软引用。正常运行一段时间后,内存达到耗尽的临界状态。架构
ThreadLoacl 价值并发
咱们从真人 CS 游戏提及。游戏开始时,每一个人可以领到一把电子枪,枪把上有三个数字,子弹数,杀敌数,本身的命数,为其设置的初始值分别为:1500,0,10.假设战场上每一个人都是一个线程,那么这三个出事值写在哪里呢?若是每一个线程写死这三个值,万一将初始字段数统一改为1000发呢?若是共享,那么线程直接的并发修改会致使数据不许确。能不能构造这样一个对象,将这个对象设置为共享变量,统一设置初始值,可是每一个线程都这个值的修改都是相互独立的。这个对象就是ThreadLoacl。注意不能将其翻译成线程本地化或者本地线程。英语恰当的名称应该叫作:CopyValueIntoEveryThread。具体代码示例以下:
1 /** 2 * @Author: MikeWang 3 * @Date: 2019/1/13 3:38 PM 4 * @Description: 5 */ 6 public class CsGameByThreadLoacl { 7 private static final Integer BULLET_NUMBER = 1500; 8 private static final Integer KILLED_ENEMIES = 0; 9 private static final Integer LIFE_VALUE = 10; 10 private static final Integer TOTAL_PLAYERS = 10; 11 //随机数用来展现每一个对象的不一样的数据(第1处) 12 private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); 13 14 //初始化子弹数 15 private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = new ThreadLocal<Integer>(){ 16 @Override 17 protected Integer initialValue() { 18 return BULLET_NUMBER; 19 } 20 }; 21 //初始化杀敌数 22 private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = new ThreadLocal<Integer>(){ 23 @Override 24 protected Integer initialValue() { 25 return KILLED_ENEMIES; 26 } 27 }; 28 //初始化本身的命数 29 private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = new ThreadLocal<Integer>(){ 30 @Override 31 protected Integer initialValue() { 32 return LIFE_VALUE; 33 } 34 }; 35 36 37 //定义每位队员 38 private static class Player extends Thread{ 39 @Override 40 public void run(){ 41 Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER); 42 Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYERS/2); 43 Integer lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE); 44 45 System.out.println(getName()+", BULLET_NUMBER is "+ bullets); 46 System.out.println(getName()+", KILLED_ENEMIES is "+ killEnemies); 47 System.out.println(getName()+", LIFE_VALUE is "+ lifeValue +"\n"); 48 49 BULLET_NUMBER_THREADLOCAL.remove(); 50 BULLET_NUMBER_THREADLOCAL.remove(); 51 BULLET_NUMBER_THREADLOCAL.remove(); 52 } 53 } 54 55 public static void main(String[] args) { 56 57 for (int i = 0 ; i < TOTAL_PLAYERS;i++){ 58 new Player().start(); 59 } 60 } 61 }
此例中,没有进行set 操做,那么初始值又是如何进入每一个线程成为独立拷贝的呢?首先,虽然ThreadLocal 在定义时覆写了initiaValue() 方法,但并不是是在 BULLET_NUMBER_THREADLOCAL
对象加载静态变量的时候执行的,而是每一个线程在ThreadLoacl.get() 的时候都会执行到,其源码以下:
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 }
每一个线程都有本身的ThreadLoaclMap , 若是 map == null ,则直接执行setInitiaValue()。若是map 已经建立,就表示Thread 类的ThreadLocals 属性已经初始化; 若是 e == null ,依然会执行到setInitiaValue()。setInitiaValue()的源码以下:
1 private T setInitialValue() { 2 T value = initialValue(); 3 Thread t = Thread.currentThread(); 4 ThreadLocalMap map = getMap(t); 5 if (map != null) 6 map.set(this, value); 7 else 8 createMap(t, value); 9 return value; 10 }
在 CsGameByThreadLoacl 类的第1处 ,使用了ThreadLocalRandom 生成单独的Random 实例。此类在JDK7 中引入,它使得每一个线程均可以有本身的随机数生成器。咱们要避免Random 实例被多线程使用,虽然共享实例是线程安全的,可是会因竞争同一seed 而致使性能降低。 咱们已经知道ThreadLoacl是每一个线程单独持有的。由于每一个线程都有独立的变量副本。其余线程不能访问,因此不存在线程安全问题,也不会影响程序执行性能。ThreadLocal 对象一般是由private static 修饰的,由于都须要复制进入本地线程,因此非static 做用不大。须要注意的是,ThreadLocal 没法解决共享对象的更新问题。因此使用某个引用来操做共享对象是,依然须要进行线程同步。
ThreadLocal 有个静态内部类叫ThreadLoaclMap,它还有个静态内部类叫Entry ,在Thread 中的ThreadLocalMap 属性的赋值是在ThreadLocal 类中的createMap() 中进行的,ThreadLoacl 与 ThreadLoclMap 有三组对应的方法:get()、set()、和remove(),在Threadlocal 中对他们只作校验和判断,最终的实现会落在ThreadLocalMap 上。Entry 继承自WeakReference,没有方法,只有一个value 成员变量,它的Key 是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; } }
全部Entry 对象都被ThreadLocalMap 类实例化对象threadLocals 持有。当线程对象执行完毕时,线程对象内的示例属性均会被垃圾回收。源码中weakReference 标识 ThreadLocal 的弱引用,及时线程正在执行中,只要ThreadLoacl对象引用被置成null,Entry 的key 就会在下一次YGC时被垃圾回收。而在ThreadLoacl 使用set() 和get()时,又会自动地将那些 key == null 的Value 置为null,使value 可以被垃圾回收,避免内存泄露,可是理想很丰满,现实很骨感,ThreadLocal 如源码注释所述:
ThreadLocal instances are typically private static fields in classes.
ThreadLocal 对象 一般做为私有静态变量使用,那么其生命周期至少不会随着线程池结束而结束。
线程池使用ThreadLocal 有三个重要方法。
set():若是没有set 操做的ThreadLoacl,容易引发脏读数据问题。
get():始终没有get 操做的ThreadLocal 对象是没有意义的。
remove() : 若是没有remove 操做,容易引发内存泄露。
若是说一个Thread 是非静态的,属于某一个线程实例类,那就失去了线程间共享的本质属性。那么ThreadLocal 到底有什么做用呢?咱们知道,局部变量在方法内各个代码块间进行传递,而类变量在类方法间进行传递。复杂的线程方法可能须要调用多个方法来实现某个功能,这个时候用什么来传递线程内变量呢?答案就是ThreadLocal , 它一般用于同一线程内,跨类,夸方法传递数据。若是没有ThreadLocal ,那么相互之间的信息传递,势必要靠返回值和参数,这样无形之中,有些类甚至有些架构会相互耦合。经过将Thread构造方法的最后一个参数设置为true,能够把当前线程的变量继续往下传递给它建立子线程。
ThreadLocal 反作用
为了使线程安全地共享某个变量,JDK 开出了ThreadLocal 这剂药方,可是药有三分毒。ThreadLocl 主要会产生脏数据和内存泄露。这两个问题一般是在线程池的线程中使用ThreadLocal 引起的,由于线程池有线程复用和内存常驻两个特色。
1.脏数据
线程复用会产生脏数据。因为线程池会重用Thread对象,那么与Thread绑定的类静态属性也会被重用。若是在实现线程run() 方法中不显示的调用remove() 清理与线程相关的ThreadLocal 信息。若是先一个线程不调用set() 设置初始值,那么就get() 到重用信息,包括ThreadLocl 所关联线对象的值。
脏数据问题在实际故障中十分常见。好比 用户A下单后没有看到订单记录,而B却看到了A的订单记录。经过排查发现是经过session 优化引发的。在原来的请求中,用户每次请求Server,都须要去缓存里查询用户的session信息,这样作无疑增长了一次调用。所以开发工程师决定采用某框架来缓存每一个用户对应的SecurityContext,它封装了session 相关信息。优化后虽然为每个用户新建了一个session 相关的上下文,可是由于ThreadLoacl 没有再线程结束是及时进行remove() 清理操做,在高并发场景下,线程池中的线程可能会读取到上一个线程缓存的用户信息。为了便于理解,用一段简要代码来模拟,以下所示:
public class DirtyDataInThreadLocal { private static ThreadLocal<String> threadLocal = new ThreadLocal<>(); public static void main(String[] args) { ExecutorService pool = Executors.newFixedThreadPool(1); for (int i = 0; i < 2; i++) { Mythread mythread = new Mythread(); pool.execute(mythread); } } private static class Mythread extends Thread{ private static boolean flag = true; @Override public void run() { if (flag){ threadLocal.set(this.getName()+". session info ."); flag = false; } System.out.println(this.getName()+" 线程是 "+threadLocal.get()); } } }
执行结果以下:
Thread-0 线程是 Thread-0. session info .
Thread-1 线程是 Thread-0. session info .
内存泄露
在源码注释中提示使用static 关键字来修改ThreadLocal。在此场景下,寄但愿于ThreadLocal对象失去引用后,触发弱引用机制来回收Entry 的Value 就不现实了。在上例中,若是不进行remove() 操做,那么这个线程执行完成后,经过ThreadLocal 对象持有的string对象是不会被释放的。
以上两个问题解决的办法很简单,就是每次用完ThreadLocal 时,必须调用remove() 方法清理。
ThreadLocal 并不解决多线程 共享 变量的问题。