一、定义

Thread-Specific Storage就是“线程独有的存储库”,该模式会对每个线程提供独有的内存空间。java.lang.ThreadLocal类提供了该模式的实现,ThreadLocal的实例是一种集合(collection)架构,该实例管理了很多对象,可以想象成一个保管有大量保险箱的房间。

java.lang.ThreadLocal类的方法:

  • public void set()

该方法会检查当前调用线程,默认以该线程的Thread.currentThread()值作为键,来保存指定的值。

  • public Object get()

该方法会检查当前调用线程,默认以该线程的Thread.currentThread()值作为键,获取保存指定的值。

二、模式案例

//实际执行记录日志的类,每个线程都会拥有该类的实例
public class TSLog {private PrintWriter writer = null;public TSLog(String filename){try {writer = new PrintWriter(new FileWriter(filename));} catch (IOException e) {e.printStackTrace();}}public void println(String s){writer.println(s);}public void close(){writer.println("==== End of log ====");writer.close();}
}
public class Log {private static final ThreadLocal<TSLog> tsLogCollection = new ThreadLocal<TSLog>();public static void println(String s){getTSLog().println(s);}private static TSLog getTSLog() {TSLog tsLog = tsLogCollection.get();if(tsLog==null){tsLog = new TSLog(Thread.currentThread().getName()+"-log.txt");tsLogCollection.set(tsLog);}return tsLog;}public static void close(){getTSLog().close();}
}
public class ClientThread extends Thread {public ClientThread(String name) {super(name);}@Overridepublic void run() {System.out.println(getName()+" BEGIN");for (int i = 0; i < 10; i++) {Log.println("i = "+i);try {Thread.sleep(100);} catch (InterruptedException e) {e.printStackTrace();}}Log.close();System.out.println(getName()+" END");}
}
public class Main {public static void main(String[] args) {new ClientThread("Alice").start();new ClientThread("Bobby").start();new ClientThread("Chris").start();}
}

打开硬盘里的项目目录,可以看到三个生成的日志文件

文件内容都是以下图示

Alice、Boddy、Chris三个线程调用Log类的同一个方法,但实际上每个线程都拥有独自的TSLog实例。

三、模式讲解

Thread-Specific Storage模式的角色如下:

  • Client(委托人)

Client会将工作委托给TSObjectProxy。(案例中的ClientThread类就是Client)

  • TSObjectProxy(线程独有对象的代理者)

TSObjectProxy会处理多个Client委托的工作。(案例中的Log类就是TSObjectProxy)

  • TSObjectCollection(线程独有对象的集合)

案例中的java.lang.ThreadLocal类就是TSObjectCollection

  • TSObject(线程独有的对象)

TSObject存放线程所持有的信息,TSObject实例的方法只会由单线程调用,由TSObjectCollection管理,每个线程都拥有独立的TSObject实例。(案例中的TSLog类就是TSObject)

四、ThreadLocal的原理

JDK中有一个类就实现了Thread-Specific Storage模式,即ThreadLocal,ThreadLocal类主要有四个方法:

1、初始化返回值的方法:
该方法实现只返回 null,并且修饰符为protected,很明显,如果用户想返回初始值不为null,则需要重写该方法;

protected T initialValue() {return null;
}

2、get方法,获取线程本地副本变量

public T get() {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null) {ThreadLocalMap.Entry e = map.getEntry(this);if (e != null) {T result = (T)e.value;return result;}}return setInitialValue();
}

3、set方法,设置线程本地副本变量

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);
}

4、remove方法,移除线程本地副本变量

public void remove() {ThreadLocalMap m = getMap(Thread.currentThread());if (m != null)m.remove(this);
}

4.2 实现原理

如果需要我们自己来设计ThreadLocal对象,那么,一般的实现思路:设计一个线程安全的Map,key就是当前线程对象,Value就是线程本地变量的值。

然而,JDK的实现思路:

让每个Thread对象,自身持有一个Map,这个Map的Key就是当前ThreadLocal对象,Value是本地线程变量值。相对于加锁的实现方式,这样做可以提升性能,其实是一种以时间换空间的思路。

ThreadLocal类有个getMap()方法,其实就是返回Thread对象自身的Map——threadLocals。

ThreadLocalMap getMap(Thread t) {return t.threadLocals;
}

set方法如下:

public void set(T value) {Thread t = Thread.currentThread();ThreadLocalMap map = getMap(t);if (map != null)map.set(this, value);elsecreateMap(t, value);}

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

看了源码就能懂了,假如线程有变量A和变量B,那么变量A会使用ThreadLocalA的set方法存放,变量B会使用ThreadLocalB的set方法存放,而set方法实际上是将变量A存放在线程自己的Map里,key为ThreadLocalA,value是变量A。

threadLocals是一种ThreadLocal.ThreadLocalMap类型的数据结构,作为内部类定义在ThreadLocal类中,其内部采用一种WeakReference(弱引用)的方式保存键值对。

Entry继承了WeakReference:

static class Entry extends WeakReference<ThreadLocal<?>> {/** The value associated with this ThreadLocal. */Object value;Entry(ThreadLocal<?> k, Object v) {super(k);value = v;}
}

4.3 使用注意

Hash冲突

ThreadLocalMap中解决Hash冲突采用线性探测的方式。所谓线性探测:

就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

ThreadLocalMap采用线性探测的方式解决Hash冲突的效率很低(简单地步长+1),所以如果有大量不同的ThreadLocal对象放入map中时发送冲突,则效率很低。

使用建议

每个线程只存一个变量,这样的话所有的线程存放到map中的Key都是相同的ThreadLocal,如果一个线程要保存多个变量,就需要创建多个ThreadLocal,多个ThreadLocal放入Map中时会极大的增加Hash冲突的可能。

内存泄漏

ThreadLocal在ThreadLocalMap中是以一个弱引用类型被Entry中的Key引用的,因此如果ThreadLocal没有外部强引用来引用它,那么ThreadLocal会在下次JVM垃圾收集时被回收。

这个时候就会出现Entry中Key已经被回收,出现一个null Key的情况,外部读取ThreadLocalMap中的元素是无法通过null Key来找到Value的。

因此如果当前线程的生命周期很长,一直存在,那么其内部的ThreadLocalMap对象也一直生存下来,这些null key就存在一条强引用链的关系一直存在:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。

但JVM团队已经考虑到这样的情况,并做了一些措施来保证ThreadLocal尽量不会内存泄漏:
在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程的ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。

最好的解决方案:

每次使用完ThreadLocal,都调用它的remove()方法,清除数据。

/*** Remove the entry for key.*/private void remove(ThreadLocal<?> key) {Entry[] tab = table;int len = tab.length;int i = key.threadLocalHashCode & (len-1);for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {if (e.get() == key) {e.clear();expungeStaleEntry(i);return;}}}

借鉴学习自https://segmentfault.com/a/1190000015558915

(十三)Thread-Specific Storage(ThreadLocal)模式相关推荐

  1. Thread Specific Storage

    这个模式是这样的:所有线程都有一个入口,但是内部为每个线程分配了特有的存储空间. 这里需要用到ThreadLocal类,这个类的实例可以想象成保管箱间,他有大量的保管箱房间.ThreadLocal的实 ...

  2. TLS(Thread Local Storage)问题demo

      C++11中的thread_local是C++存储期的一种,属于线程存储期.存储期定义C++程序中变量/函数的范围(可见性)和生命周期.C++程序中可用的存储期包括auto.register.st ...

  3. 二十三种设计模式之原型模式

    今天继续探讨GOF二十三种设计模式的原型模式,原型模式也是属于创建型模式的一种 原型模式通俗的讲就是对象复制的过程,即通过一个原型对象,我可以得到一个该对象的克隆. 下面来看下原型模式的第一种写法-- ...

  4. thread local storage

    有时会需要这种模式,一个全局变量,需要在程序的任何地方都可以使用它,但是当这个变量出现在不同线程时,就要求系统将这个变量拷贝到各个线程中,这样的话,每个线程内部也可以随时访问本线程的全局变量,但是线程 ...

  5. ThreadLocal模式的一点小理解

    垃圾回收: 在Thread里面有个ThreadLocalMap对象,ThreadLocalMap里面的EnTry继承了WeakReference<ThreadLocal>,垃圾回收的核心就 ...

  6. .net开发笔记(十三) Winform常用开发模式第一篇

    上一篇博客最后我提到"异步编程模型"(APM),之后本来打算整理一下这方面的材料然后总结一下写篇文章与诸位分享,后来在整理的过程中不断的延伸不断地扩展,发现完全偏离了"异 ...

  7. java线程本地存储_[并发并行]_[C/C++]_[使用线程本地存储Thread Local Storage(TLS)-win32和pthread比较]...

    场景: 1.  需要统计某个线程的对象上创建的个数. 2. 当创建的堆空间需要根据线程需要创建和结束时销毁时. 3. 因为范围是线程只能看到自己的存储数据,所以不需要临界区或互斥量来维护自己的堆内存. ...

  8. Java设计模式菜鸟系列(十三)建模和实现状态模式

    转载请注明出处:http://blog.csdn.net/lhy_ycu/article/details/39829859 状态模式(State):同意对象在内部状态改变时改变它的行为,对象看起来好像 ...

  9. Java的二十三种设计模式(原型模式(Prototype))

    原型模式虽然是创建型的模式,但是与工程模式没有关系,从名字即可看出,该模式的思想就是将一个对象作为原型,对其进行复制.克隆,产生一个和原对象类似的新对象.本小结会通过对象的复制,进行讲解.在Java中 ...

  10. Java的二十三种设计模式(建造者模式(Builder))

    工厂类模式提供的是创建单个类的模式,而建造者模式则是将各种产品集中起来进行管理,用来创建复合对象,所谓复合对象就是指某个类具有不同的属性,其实建造者模式就是前面抽象工厂模式和最后的Test结合起来得到 ...

最新文章

  1. MySQL(MariaDB)常用DOM命令
  2. html div套div,Web前端开发技术之Div+Css基础
  3. [异常处理]class kafka.common.UnknownTopicOrPartitionException (kafka.server.ReplicaFetcherThread)
  4. PL/SQL Developer中,存储过程无法调试的问题解决办法
  5. 转行程序员深漂的这三年 #1
  6. Qt C++中 Map 和 List 转换到 QML 中使用
  7. Vue使用axios提交表单数据
  8. 第四讲 Python3中的int型和浮点型
  9. GDCM:智能指针的测试程序
  10. 阿里技术:万级规模K8s如何管理?
  11. 64位内核开发第二讲.内核编程注意事项,以及UNICODE_STRING
  12. macbook装双系统多分区其实很简单,你只要把macbook当作一台普通pc就可以了!
  13. pandas不显示index_pandas层级索引
  14. 2021深育杯-网络安全大赛专业竞赛部分wp
  15. Trickbot 年度版本变化情况
  16. 深度学习笔记 摘抄笔记
  17. python3 录屏
  18. 太阳直射点纬度计算公式_高中地理——每日讲1题(极昼、极夜、太阳高度角、太阳辐射)...
  19. element table表头设置换行
  20. 1年19款,款款口碑爆棚,Cocos 插件大佬的真面目竟是?

热门文章

  1. 用IDM怎么批量下载视频
  2. meltdown linux检测,Linux操作系统已拥有自动化的Spectre/Meltdown检查器
  3. 【黄冈市中级人民法院在湖北行星传动设备有限公司的强制清算案件中的违法问题给投资者的启示】
  4. 个人职业生涯规划书-职业生涯规划书
  5. 内网信息收集(手动收集本机信息)
  6. RecyclerView的横向展示、item滑动居中
  7. 由2003年的一篇讲座笔记
  8. git命令将项目克隆到本地
  9. java 腾讯微博模拟登陆_腾讯微博模拟登录
  10. git 误删文件如何恢复