ThreadLocal的官方API解释为:

"该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。"

大概的意思有两点:

  1. ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。
  2. 如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。

应用场景

ThreadLocal通常用来共享数据,当你想在多个方法中使用某个变量,这个变量是当前线程的状态,其它线程不依赖这个变量,你第一时间想到的就是把变量定义在方法内部,然后再方法之间传递参数来使用,这个方法能解决问题,但是有个烦人的地方就是,每个方法都需要声明形参,多处声明,多处调用。影响代码的美观和维护。有没有一种方法能将变量像private static形式来访问呢?这样在类的任何一处地方就都能使用。这个时候ThreadLocal大显身手了。

实践

我们首先来看一段代码

package com.test1;import java.util.HashMap;
import java.util.Map;public class Test1 implements Runnable {private final static Map map = new HashMap();int id;/*static ThreadLocal<HashMap> threadLocal = new ThreadLocal<HashMap>(){@Overrideprotected HashMap initialValue() {System.out.println(Thread.currentThread().getName()+"initialValue");return new HashMap();}};*/public Test1(int id) {super();this.id = id;}@Overridepublic void run() {//Map map = threadLocal.get();for (int i = 0; i < 20; i++) {map.put(i, i + id * 100);try {Thread.sleep(100);} catch (Exception ex) {}}System.out.println(Thread.currentThread().getName() + "# map.size()="+ map.size() + " # " + map);}public static void main(String[] args) {Thread[] runs = new Thread[15];Test1 t = new Test1(1);for (int i = 0; i < runs.length; i++) {runs[i] = new Thread(t);}for (int i = 0; i < runs.length; i++) {runs[i].start();}}
}

这段程序的本意是,启动15个线程,线程向map中写入20个整型值,然后输出map。运行该程序,观察结果,我们会发现,map中压根就不止20个元素,这说明程序产生了线程安全问题。

我们都知道HashMap是非线程安全的,程序启动了15个线程,他们共享了同一个map,15个线程都往map写对象,这势必引起线程安全问题。

我们有两种方法解决这个问题:

  1. 将map的声明放到run方法中,这样map就成了方法内部变量,每个线程都有一份new HashMap(),无论多少个线程执行run方法,都不会有线程安全问题。这个方法也正如应用场景中提到的,如果有多处地方使用到map,传值是个烦人的地方。
  2. 将HashMap换成Hashtable。用线程同步来解决问题,然而我们的程序只是想向一个map中写入20个整型的KEY-VALUE而已,并不需要线程同步,同步势必影响性能,得不偿失。
  3. ThreadLocal提供另外一种解决方案,即在解决方案a上边,将new HashMap()得到的实例变量,绑定到当前线程中。之后从任何地方,都可以通过ThreadLocal获取到该变量。将程序中的注释代码恢复,再将 private final static Map map = new HashMap();注释掉,运行程序,结果就是我们想要的。

实现原理

程序调用了get()方法,我们来看一下该方法的源码:

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

getMap方法的源码:

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

该方法返回的是当前线程中的ThreadLocalMap实例。阅读Thread的源码我们发现Thread中有如下变量声明:

/* ThreadLocal values pertaining to this thread. This map is maintained* by the ThreadLocal class. */ThreadLocal.ThreadLocalMap threadLocals = null;

我们暂时可以将ThreadLocalMap理解为一个类似Map的这么个类,之后再讲解它。

get()方法的大致意思就是从当前线程中拿到ThreadLocalMap的实例threadLocals,如果threadLocals不为空,那么就以当前ThreadLocal实例为KEY从threadLocals中拿到对应的VALUE。如果不为空,那么就调用 setInitialValue()方法初始化threadLocals,最终返回的是initialValue()方法的返回值。下面是 setInitialValue()方法的源码

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

我们看到map.set(this, value);这句代码将ThreadLocalMap的实例作为KEY,将initialValue()的返回值作为VALUE,set到了threadLocals中。

程序在声明ThreadLocal实例的时候覆写了initialValue(),返回了VALUE,当然我们可以直接调用set(T t)方法来设置VALUE。下面是set(T t)方法的源码:

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

我们看到它比setInitialValue()方法就少了个return语句。这两种方式都能达到初始化ThreadLocalMap实例的效果。

我们再来看一下ThreadLocal类的结构。

ThreadLocal类只有三个属性,如下:

/*ThreadLocal的hash值,map用它来存储值*/private final int threadLocalHashCode = nextHashCode();/*改类能以原子的方式更新int值,这里主要是在产生新的ThreadLocal实例时用来产生一个新的hash值,map用该值来存储对象*/private static AtomicInteger nextHashCode =new AtomicInteger();/*该变量标识每次产生新的ThreadLocal实例时,hash值的增量*/private static final int HASH_INCREMENT = 0x61c88647;

剩下的就是一些方法。最关键的地方就是ThreadLocal定义了一个静态内部类ThreadLocalMap。我们在下一章节再来分析这个类。从ThreadLocal的类结构,我们可以看到,实际上问题的关键先生是ThreadLocalMap,ThreadLocal只是提供了管理的功能,我们也可以说ThreadLocal只是代理了ThreadLocalMap而已。

ThreadLocalMap源码分析

既然ThreadLocalMap实现了类似map的功能,那我们首先来看看它的set方法源码:

private void set(ThreadLocal key, Object value) {// We don’t use a fast path as with get() because it is at// least as common to use set() to create new entries as// it is to replace existing ones, in which case, a fast// path would fail more often than not.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)]) {ThreadLocal k = e.get();if (k == key) {e.value = value;return;}if (k == null) {replaceStaleEntry(key, value, i);return;}}tab[i] = new Entry(key, value);int sz = ++size;if (!cleanSomeSlots(i, sz) && sz >= threshold)rehash();}

这个方法的主要功能就是讲KEY-VALUE存储到ThreadLocalMap中,这里至少我们看到KEY实际上是 key.threadLocalHashCode,ThreadLocalMap同样维护着Entry数组,这个Entry我们在下一节会讲解。这里涉及到了Hash冲突的处理,这里并不会向HashMap一样冲突了以链表的形式往后添加。如果对这个Hash冲突解决方案有兴趣,可以再进一步研究源码。

既然ThreadLocalMap也是用Entry来存储对象,那我们来看看Entry类的声明,Entry被定义在ThreadLocalMap的内部:

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

这里我们看到Entry集成了WeakReference类,泛型声明了ThreadLocal,即每一个Entry对象都保留了对 ThreadLocal实例的弱引用,之所以这么干的原因是,线程在结束之后需要将ThreadLocal实例从map中remove调,以便回收内存空间。

总结

首先,ThreadLocalMap并不是为了解决线程安全问题,而是提供了一种将实例绑定到当前线程的机制,类似于隔离的效果,实际上自己在方法中new出来变量也能达到类似的效果。ThreadLocalMap跟线程安全基本不搭边,绑定上去的实例也不是多线程公用的,而是每个线程new一份,这个实例肯定不是共用的,如果共用了,那就会引发线程安全问题。ThreadLocalMap最大的用处就是用来把实例变量共享成全局变量,在程序的任何方法中都可以访问到该实例变量而已。网上很多人说ThreadLocalMap是解决了线程安全问题,其实是望文生义,两者不是同类问题。

Java ThreadLocal 使用详解相关推荐

  1. 5W字高质量java并发系列详解教程(上)-附PDF下载

    文章目录 第一章 java.util.concurrent简介 主要的组件 Executor ExecutorService ScheduledExecutorService Future Count ...

  2. JUC第六讲:ThreadLocal/InheritableThreadLocal详解/TTL-MDC日志上下文实践

    本文是JUC第六讲:ThreadLocal/InheritableThreadLocal详解.ThreadLocal无论在项目开发还是面试中都会经常碰到,本文就 ThreadLocal 的使用.主要方 ...

  3. ThreadLocal原理详解--终于弄明白了ThreadLocal

    ThreadLocal原理详解 在我看到ThreadLocal这个关键字的时候我是懵逼的,我觉得我需要弄明白,于是,我就利用搜索引擎疯狂查找,试图找到相关的解答,但是结果不尽人意. 首先说一下我的理解 ...

  4. Java内存溢出详解之Tomcat配置

    Java内存溢出详解 转自:http://elf8848.iteye.com/blog/378805 一.常见的Java内存溢出有以下三种: 1. java.lang.OutOfMemoryError ...

  5. java基础(十三)-----详解内部类——Java高级开发必须懂的

    java基础(十三)-----详解内部类--Java高级开发必须懂的 目录 为什么要使用内部类 内部类基础 静态内部类 成员内部类 成员内部类的对象创建 继承成员内部类 局部内部类 推荐博客 匿名内部 ...

  6. Java类加载机制详解【java面试题】

    Java类加载机制详解[java面试题] (1)问题分析: Class文件由类装载器装载后,在JVM中将形成一份描述Class结构的元信息对象,通过该元信息对象可以获知Class的结构信息:如构造函数 ...

  7. Java线程池详解学习:ThreadPoolExecutor

    Java线程池详解学习:ThreadPoolExecutor Java的源码下载参考这篇文章:Java源码下载和阅读(JDK1.8) - zhangpeterx的博客 在源码的目录java/util/ ...

  8. Java 线程池详解学习:FixedThreadPool,CachedThreadPool,ScheduledThreadPool...

    Java常用的线程池有FixedThreadPool和CachedThreadPool,我们可以通过查看他们的源码来进行学习. Java的源码下载参考这篇文章:Java源码下载和阅读(JDK1.8) ...

  9. 关于Java的Classpath详解

    关于Java的Classpath详解 Java 的新入门者对classpath往往比较困惑,为何在开发环境中能运行的东东出去就不好,或在外面运行的东东挺溜的进了开发环境就死菜. java的优点就是他是 ...

最新文章

  1. 当AI黑科技撞上大数据日:清华大学第四届大数据日成功举办
  2. .Net CF下精确的计时器
  3. python设计模式-观察者
  4. 如何快速实现Mathematica和Mathtype之间的切换
  5. SAP Kyma能像SAP BYD那样做field extension吗
  6. scanf(“%s“)真的只开读入字符串大小就可以了吗??
  7. 2022中国供应链物流创新科技报告
  8. ehcache缓存的详细配置
  9. 使用C#的unsafe加快处理图像速度
  10. [转]有关TinyXML使用的简单总结
  11. 空格在科技类文章中对阅读体验的影响
  12. SICP 第二章的练习
  13. 英国加入亚投行是顺从中国还是想玩无间道?
  14. word表格分开快捷键_word文档如何快速拆分表格,干货!怎样快速拆分表格以及快速合并表格技巧介绍...
  15. 计算机工程怎么评,美国电气与计算机工程专业怎么样?评价如何
  16. 如何关闭苹果Mac开机启动声音
  17. 车载人机交互语音android,手机与车机如何实现语音智能交互?
  18. 前端开发:JavaScript---ECMAScript
  19. 【7.0】 数学建模 | 相关系数详解 | Person相关系数、Spearman相关系数
  20. 创建可启动 WinPE 介质|使用 DISM 修改 Windows 映像

热门文章

  1. Android开发一 什么是3G
  2. WF(9):本地服务之事件处理
  3. redis根据通配符去批量删除指令
  4. 怎么利用Excel统计各分数段的人数?(亲测sum函数可用)
  5. MySQL 全文索引实现简单版搜索引擎
  6. linux php安装memcached扩展
  7. 不用FTP使用SecureCRT上传下载文件,并解决rz、sz command not found异常
  8. u盘往linux考文件过大,U盘拷贝时提示文件过大怎么办,教您如何解决
  9. git丢弃本地修改的所有文件(新增、删除、修改)
  10. SQL Error (1130): Host IP is not allowed to connect to this MySQL server