ThreadLoclc初衷是线程并发时,解决变量共享问题,但是由于过度设计,比如弱引用的和哈希碰撞,导致理解难度大、使用成本高,反而成为故障高发点,容易出现内存泄露,脏数据、贡献对象更新等问题。单从ThreadLoacl命名来看人们认为只要用它就对了,包治变量共享问题,然而并不是。一下以内存模型、弱引用,哈希算法为铺垫,然后从cs真人游戏的示例代码入手,详细分析Threadlocal源码。我们从中可以学习到全新的编程思维方式,并认识到问题的来源,也能够帮助我们谙熟此类设计之道,扬长避短。

引用类型

对象在堆上创建之后所持有的引用其实是一种变量类型,引用之间可以通过赋值构成一条引用链。从GC Roots 开始遍历,判断引用是否可达。引用的可达性是判断能否被垃圾回收的基本条件。JVM会据此自动管理内存分配与回收,不需要开发工程师干预。但是在某些场景下,即使引用可达,也希望根据语义的强弱进行有选择的回收,以保证系统的正常运行。根据引用类型语义的强弱来决定垃圾回收的阶段,我们可以把引用分为强引用,软引用,弱引用和虚引用四类。后三类引用,本质上可以让开发工程师通过代码的方式来决定对象的垃圾回收时机。我们先简要了解一下这个四类引用。

强引用,即Strong Reference , 最为常见,如Object object = new Object();这样的变量声明和定义就会产生该对象的强引用。只要对象有强引用指向,并且GC roots 可达,那么java内存回收时,即使濒临内存耗尽,也不会回收该对象。

软引用,即soft Reference ,引用力度弱于"强引用",是用在非必须对象的场景。在即将OOM之前,垃圾回收器会把这些软引用指向的对象加入回收范围,以获得更多的内存空间,让程序能够继续健康运行。主要用来缓存服务器中间计算结果集不需要试试保存的用户行为等。

弱引用,即Weak Reference,引用强度较前两者更弱,也是用来描述非必须对象的。如果弱引用指向的对象只存在弱引用这一条线路,则在下一次YGC的时候被回收。由于YGC时间的不确定性,弱引用何时被回收也有不确定性。弱引用主要用于指向某个易消失的对象,在强引用断开后,此引用不会劫持对象。调用WeakReference.get() 可能返回null,要注意空指针异常。

虚引用,即Phantom Reference ,是极弱的一种引用关系,定义完成后,就无法通过该引用获取指定的对象。为对象设置虚引用的唯一目的就是希望能在这个对象被回收时收到一个系统通知,虚引用必须与引用队列联合使用,当垃圾回收时,如果发现存在虚引用,就会在回收对象内存前,把这个虚引用加入与之关联的引用队列中。

强引用是最常用的,而虚引用在业务中几乎很难用到。下面重点介绍一下软引用和弱引用。先来说明一下软引用的回收机制。首先设置JVM 参数:-Xms 20m,-Xmx 20m,即只有20m的堆内存空间。

 public class SoftReferenceHouse {
public static void main(String[] args) {
//List<House> houses = new ArrayList<>(); //(第1处)
List<SoftReference> houses = new ArrayList<>(); //剧情反转注释处
int i = 0;
while (true){
//houses.add(new House()); //(第2处) //剧情反转注释处
SoftReference<House> buyer2 = new SoftReference<>(new House()); //剧情反转注释处
houses.add(buyer2);
System.out.println("i=" + (++i));
}
}
} class House{
private static final Integer DOOR_NUMBER = 2000;
public Door [] doors = new Door[DOOR_NUMBER];
class Door{}
}

new House() 是匿名对象,产生之后即赋值给软引用。正常运行一段时间后,内存达到耗尽的临界状态。

ThreadLoacl 价值

我们从真人 CS 游戏说起。游戏开始时,每个人能够领到一把电子枪,枪把上有三个数字,子弹数,杀敌数,自己的命数,为其设置的初始值分别为:1500,0,10.假设战场上每个人都是一个线程,那么这三个出事值写在哪里呢?如果每个线程写死这三个值,万一将初始字段数统一改成1000发呢?如果共享,那么线程直接的并发修改会导致数据不准确。能不能构造这样一个对象,将这个对象设置为共享变量,统一设置初始值,但是每个线程都这个值的修改都是相互独立的。这个对象就是ThreadLoacl。注意不能将其翻译成线程本地化或者本地线程。英语恰当的名称应该叫做:CopyValueIntoEveryThread。具体代码示例如下:

 /**
* @Author: MikeWang
* @Date: 2019/1/13 3:38 PM
* @Description:
*/
public class CsGameByThreadLoacl {
private static final Integer BULLET_NUMBER = 1500;
private static final Integer KILLED_ENEMIES = 0;
private static final Integer LIFE_VALUE = 10;
private static final Integer TOTAL_PLAYERS = 10;
//随机数用来展示每个对象的不同的数据(第1处)
private static final ThreadLocalRandom RANDOM = ThreadLocalRandom.current(); //初始化子弹数
private static final ThreadLocal<Integer> BULLET_NUMBER_THREADLOCAL = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return BULLET_NUMBER;
}
};
//初始化杀敌数
private static final ThreadLocal<Integer> KILLED_ENEMIES_THREADLOCAL = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return KILLED_ENEMIES;
}
};
//初始化自己的命数
private static final ThreadLocal<Integer> LIFE_VALUE_THREADLOCAL = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return LIFE_VALUE;
}
}; //定义每位队员
private static class Player extends Thread{
@Override
public void run(){
Integer bullets = BULLET_NUMBER_THREADLOCAL.get() - RANDOM.nextInt(BULLET_NUMBER);
Integer killEnemies = KILLED_ENEMIES_THREADLOCAL.get() + RANDOM.nextInt(TOTAL_PLAYERS/2);
Integer lifeValue = LIFE_VALUE_THREADLOCAL.get() - RANDOM.nextInt(LIFE_VALUE); System.out.println(getName()+", BULLET_NUMBER is "+ bullets);
System.out.println(getName()+", KILLED_ENEMIES is "+ killEnemies);
System.out.println(getName()+", LIFE_VALUE is "+ lifeValue +"\n"); BULLET_NUMBER_THREADLOCAL.remove();
BULLET_NUMBER_THREADLOCAL.remove();
BULLET_NUMBER_THREADLOCAL.remove();
}
} public static void main(String[] args) { for (int i = 0 ; i < TOTAL_PLAYERS;i++){
new Player().start();
}
}
}

此例中,没有进行set 操作,那么初始值又是如何进入每个线程成为独立拷贝的呢?首先,虽然ThreadLocal 在定义时覆写了initiaValue() 方法,但并非是在 BULLET_NUMBER_THREADLOCAL

对象加载静态变量的时候执行的,而是每个线程在ThreadLoacl.get() 的时候都会执行到,其源码如下:

 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();
}

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

 private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}

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对象。两者简要关系如下:

  • 1个Thread 有且仅有一个ThreadLoaclMap 对象;
  • 1个Entry 对象的key 弱应用指向一个ThreadLocal对象;
  • 1个ThreadLocalMap 对象存储多个Entry 对象;
  • 1个ThreadLocal 对象可以被多个线程共享;
  • ThreadLocal 对象不持有Value,Value 由线程的Entry 对象持有。

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 并不解决多线程 共享 变量的问题。

  

线程池-Threadlocal的更多相关文章

  1. 线程池 Threadlocal 使用注意

    线程池中的线程是重复使用的,即一次使用完后,会被重新放回线程池,可被重新分配使用. 因此,ThreadLocal线程变量,如果保存的信息只是针对一次请求的,放回线程池之前需要清空这些Threadloc ...

  2. 0041 Java学习笔记-多线程-线程池、ForkJoinPool、ThreadLocal

    什么是线程池 创建线程,因为涉及到跟操作系统交互,比较耗费资源.如果要创建大量的线程,而每个线程的生存期又很短,这时候就应该使用线程池了,就像数据库的连接池一样,预先开启一定数量的线程,有任务了就将任 ...

  3. 线程池与Threadlocal

    线程池与Threadlocal 线程池: 线程池是为了使线程能够得到循环的利用,线程池里面养着一些线程,有任务需要使用线程的时候就往线程池里抓线程对象出来使用.线程池里的线程能够重复使用,所以在资源上 ...

  4. ThreadLocal 遇上线程池的问题及解决办法

    ThreadLocal 称为线程本地存储,它为每一个使用它的线程提供一个其值(value)的副本.可以将 ThreadLocal<T> 理解成 Map<Thread, T>,即 ...

  5. ThreadLocal与线程池使用的问题

    感谢博主的这篇分享,见 https://www.cnblogs.com/qifenghao/p/8977378.html 在今天的面试中,突然被考官问了这个问题,当时脱口而出的是 threadloca ...

  6. ThreadLocal 定义,以及是否可能引起的内存泄露(threadlocalMap的Key是弱引用,用线程池有可能泄露)

    ThreadLocal 也可以跟踪一个请求,从接收请求,处理请求,到返回请求,只要线程不销毁,就可以在线程的任何地方,调用这个参数,这是百度二面的题目,参考: Threadlocal 传递参数(百度二 ...

  7. 在使用线程池时应特别注意对ThreadLocal的使用

    使用ThreadLocal并且有线程池时要特别注意,ThreadLocal是以线程为key的,而线程池里面的线程是会被重新利用的,所以如果有使用线程池并且使用ThreadLocal来保存状态信息时要特 ...

  8. ThreadLocal遇到线程池时, 各线程间的数据会互相干扰, 串来串去

    最近遇到一个比较隐蔽而又简单地问题,在使用ThreadLocal时发现出现多个线程中值串来串去,排查一番,确定问题为线程池的问题,线程池中的线程是会重复利用的,而ThreadLocal是用线程来做Ke ...

  9. 当ThreadLocal碰上线程池

    ThreadLocal使用 ThreadLocal可以让线程拥有本地变量,在web环境中,为了方便代码解耦,我们通常用它来保存上下文信息,然后用一个util类提供访问入口,从controller层到s ...

随机推荐

  1. CodeForces Round #527 (Div3) D1. Great Vova Wall (Version 1)

    http://codeforces.com/contest/1092/problem/D1 Vova's family is building the Great Vova Wall (named b ...

  2. mysql 慢查询,查询缓存,索引,备份,水平分割

    1.开启慢查询 在mysql的配置文件my.ini最后增加如下命令 [mysqld]port=3306slow_query_log =1long_query_time = 1 2.查看慢查询记录 默认 ...

  3. react-自定义事件

    没有嵌套关系的组件(如兄弟组件)之间的通信,只能通过自定义事件的方式来进行. var EventEmitter = require('events').EventEmitter; import Rea ...

  4. Centos 7 环境下,如何使用 Apache 实现 SSL 虚拟主机 双向认证 的详细教程:

    1. testing ! ... 1 1 原文参考链接: http://showerlee.blog.51cto.com/2047005/1266712 很久没有更新LAMP的相关文档了,刚好最近单位 ...

  5. 25个Java机器学习工具&库--转载

    本列表总结了25个Java机器学习工具&库: 1. Weka集成了数据挖掘工作的机器学习算法.这些算法可以直接应用于一个数据集上或者你可以自己编写代码来调用.Weka包括一系列的工具,如数据预 ...

  6. BZOJ 1066:[SCOI2007]蜥蜴(最大流)

    蜥蜴Description在一个r行c列的网格地图中有一些高度不同的石柱,一些石柱上站着一些蜥蜴,你的任务是让尽量多的蜥蜴逃到边界外. 每行每列中相邻石柱的距离为1,蜥蜴的跳跃距离是d,即蜥蜴可以跳到 ...

  7. [BZOJ2432][Noi2011]兔农 矩阵乘法+exgcd

    2432: [Noi2011]兔农 Time Limit: 10 Sec  Memory Limit: 256 MB Description 农夫栋栋近年收入不景气,正在他发愁如何能多赚点钱时,他听到 ...

  8. 【刷题】BZOJ 4573 [Zjoi2016]大森林

    Description 小Y家里有一个大森林,里面有n棵树,编号从1到n.一开始这些树都只是树苗,只有一个节点,标号为1.这些树都有一个特殊的节点,我们称之为生长节点,这些节点有生长出子节点的能力.小 ...

  9. [NOI2017]蚯蚓排队 hash

    题面:洛谷 题解: 我们暴力维护当前所有队伍内的所有子串(长度k = 1 ~ 50)的出现次数. 把每个子串都用一个hash值来表示,每次改变队伍形态都用双向链表维护,并暴力更新出现次数. 现在考虑复 ...

  10. 使用StoryBoard执行动画

    在WPF动画编程中,最常用的动画处理方式是DoubleAnimation动画,但是随着你的开发经验越来越多,你会发现,有时候使用这个动画类会很麻烦,因为这个动画是封闭动画,也就是说在动画的时间间隔内, ...