多线程学习笔记九之ThreadLocal

简介

  ThreadLocal顾名思义理解为线程本地变量,这个变量只在这个线程内,对于其他的线程是隔离的,JDK中对ThreadLocal的介绍:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its{@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g.,a user ID or Transaction ID).

大意是ThreadLocal提供了线程局部变量,只能通过ThreadLocal的set方法和get方法来存储和获得变量。

类结构

  ThreadLocal类结构如下:

可以看到ThreadLocal有内部类ThradLocalMap,ThreadLocal存储线程局部对象就是利用了ThreadLocalMap数据结构,在下面的源码分析也会先从这里开始。

源码分析

ThreadLocalMap

  ThreadLocalMap静态内部类Entry是存储键值对的基础,Entry类继承自WeakReference(为什么用弱引用在后面解释),通过Entry的构造方法表明键值对的键只能是ThreadLocal对象,值是Object类型,也就是我们存储的线程局部对象,通过super调用父类WeakReference构造函数将ThreadLocal<?>对象转换成弱引用对象

  ThreadMap存储键值对的原理与HashMap是类似的,HashMap依靠的是数组+红黑树数据结构和哈希值映射,ThreadMap依靠Entry数组+散列映射,ThreadLocalMap使用了Entry数组来保存键值对,Entry数组的初始长度为16,键值对到Entry数组的映射依靠的是int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);,通过ThreadLocal对象的threadLocalHashCode与(INITIAL_CAPACITY - 1)按位相与将键值对均匀散列到Entry数组上。

  1. static class ThreadLocalMap {
  2. // 键值对对象
  3. static class Entry extends WeakReference<ThreadLocal<?>> {
  4. /** The value associated with this ThreadLocal. */
  5. Object value;
  6. Entry(ThreadLocal<?> k, Object v) {
  7. super(k);
  8. value = v;
  9. }
  10. }
  11. //初始Entry数组大小
  12. private static final int INITIAL_CAPACITY = 16;
  13. //Entry数组
  14. private Entry[] table;
  15. //ThreadLocalMap实际存储键值对的个数
  16. private int size = 0;
  17. //数组扩容阈值
  18. private int threshold; // Default to 0
  19. //阈值为数组长度的2/3
  20. private void setThreshold(int len) {
  21. threshold = len * 2 / 3;
  22. }
  23. private static int nextIndex(int i, int len) {
  24. return ((i + 1 < len) ? i + 1 : 0);
  25. }
  26. /**
  27. * Decrement i modulo len.
  28. */
  29. private static int prevIndex(int i, int len) {
  30. return ((i - 1 >= 0) ? i - 1 : len - 1);
  31. }
  32. //构造一个ThreadLocalMap对象,并把传入的第一个键值对存储
  33. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  34. table = new Entry[INITIAL_CAPACITY];
  35. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  36. table[i] = new Entry(firstKey, firstValue);
  37. size = 1;
  38. setThreshold(INITIAL_CAPACITY);
  39. }
  40. }

  ThreadLocal作为做为键值对的键通过常量threadLocalHashCode映射到Entry数组,threadLocalHashCode初始化时会调用nextHashCode()方法,就是在nextHashCode的基础上加上0x61c88647,实际上每个ThreadLocal对象的threadLocalHashCode值相差0x61c88647,这样生成出来的Hash值可以较为均匀的散列到2的幂次方长度的数组中,具体可见这篇文章为什么使用0x61c88647

  由于采用的是散列算法,就需要考虑Hash冲突的情况,HashMap解决Hash冲突的方法是链表+红黑树,ThreadLocalMap解决方法是linear-probe(线性探测),简单来说如果散列对应的位置已经有键值对占据了,就把散列位置加/减一找到符合条件的位置放置键值对。

  1. // final常量,一旦确定不再改变
  2. private final int threadLocalHashCode = nextHashCode();
  3. /**
  4. * The next hash code to be given out. Updated atomically. Starts at
  5. * zero.
  6. */
  7. private static AtomicInteger nextHashCode =
  8. new AtomicInteger();
  9. /**
  10. * The difference between successively generated hash codes - turns
  11. * implicit sequential thread-local IDs into near-optimally spread
  12. * multiplicative hash values for power-of-two-sized tables.
  13. */
  14. private static final int HASH_INCREMENT = 0x61c88647;
  15. /**
  16. * Returns the next hash code.
  17. */
  18. private static int nextHashCode() {
  19. return nextHashCode.getAndAdd(HASH_INCREMENT);
  20. }
  21. //构造方法
  22. public ThreadLocal() {
  23. }

set(T value)

  简要介绍完了内部类ThreadLocalMap后,set方法属于ThreadLocal,首先获得与线程Thread绑定的ThreadLocalMap对象,再将ThreadLocal和传入的value封装为Entry键值对存入ThreadLocalMap中。注意,ThreadLocalMap对象是在线程Thread中声明的:

ThreadLocal.ThreadLocalMap threadLocals = null;

  1. public void set(T value) {
  2. //获得当前线程对象
  3. Thread t = Thread.currentThread();
  4. //获得线程对象的ThreadLocalMap
  5. ThreadLocalMap map = getMap(t);
  6. // 如果map存在,则将键值对存到map里面去
  7. if (map != null)
  8. map.set(this, value);
  9. //如果不存在,调用ThreadLocalMap构造方法存储键值对
  10. else
  11. createMap(t, value);
  12. }
  13. //返回线程t中声明的Thread
  14. ThreadLocalMap getMap(Thread t) {
  15. return t.threadLocals;
  16. }
  17. void createMap(Thread t, T firstValue) {
  18. t.threadLocals = new ThreadLocalMap(this, firstValue);
  19. }
  • set(ThreadLocal<?> key, Object value)

      在ThreadLocalMap存在的情况下,调用ThreadLocal类的set方法存储键值对,set方法需要考虑散列的位置已经有键值对:如果已经存在的键值对的键当存入的键,覆盖键值对的值;如果键值对的键ThreadLocal对象已经被回收,调用replaceStaleEntry方法删除table中所有陈旧的元素(即entry的引用为null)并插入新元素。
  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. //利用ThreadLocal的threadLocalHahsCode值散列
  5. int i = key.threadLocalHashCode & (len-1);
  6. //如果散列的位置不为空,判断是否是哈希冲突
  7. for (Entry e = tab[i];
  8. e != null;
  9. e = tab[i = nextIndex(i, len)]) {
  10. ThreadLocal<?> k = e.get();
  11. //如果此位置的键值对的键与传入的键相同,覆盖键值对的值
  12. if (k == key) {
  13. e.value = value;
  14. return;
  15. }
  16. //键值对的键为空,说明键ThreadLocal对象被回收,用新的键值对代替过时的键值对
  17. if (k == null) {
  18. replaceStaleEntry(key, value, i);
  19. return;
  20. }
  21. }
  22. //散列位置为空,直接存储键值对
  23. tab[i] = new Entry(key, value);
  24. int sz = ++size;
  25. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  26. rehash();
  27. }

get()

  获得当前线程中保存的以ThreadLocal对象为键的键值对的值。首先获取当前线程关联的ThreadLocalMap,再获得以当前ThreadLocal对象为键的键值对,map为空的话返回初始值null,即线程局部变量为null,

  1. public T get() {
  2. //获取与当前线程绑定的ThreadLocalMap
  3. Thread t = Thread.currentThread();
  4. ThreadLocalMap map = getMap(t);
  5. //map不为空,获取键值对对象
  6. if (map != null) {
  7. ThreadLocalMap.Entry e = map.getEntry(this);
  8. if (e != null) {
  9. @SuppressWarnings("unchecked")
  10. T result = (T)e.value;
  11. return result;
  12. }
  13. }
  14. return setInitialValue();
  15. }
  16. private Entry getEntry(ThreadLocal<?> key) {
  17. //散列
  18. int i = key.threadLocalHashCode & (table.length - 1);
  19. Entry e = table[i];
  20. //判断散列位置的键值对是否符合条件:e.get()==key
  21. if (e != null && e.get() == key)
  22. return e;
  23. else
  24. return getEntryAfterMiss(key, i, e);
  25. }
  26. //线性探测寻找key对应的键值对
  27. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
  28. Entry[] tab = table;
  29. int len = tab.length;
  30. while (e != null) {
  31. ThreadLocal<?> k = e.get();
  32. if (k == key)
  33. return e;
  34. if (k == null)
  35. expungeStaleEntry(i);
  36. else
  37. i = nextIndex(i, len);
  38. e = tab[i];
  39. }
  40. return null;
  41. }

remove()

  从ThreadLocalMap中移除键值对,一般在get方法取出保存的线程局部变量后调用remove方法防止内存泄露。

  1. public void remove() {
  2. ThreadLocalMap m = getMap(Thread.currentThread());
  3. if (m != null)
  4. m.remove(this);
  5. }

为什么ThreadLocalMap的键是WeakReferrence?

  键值对对象Enry的键是ThreadLocal对象,但是使用WeakReferrence虚引用包装了的,虚引用相对于我们经常使用的String str = "abc"这种强引用来说对GC回收对象的影响较小,以下是虚引用的介绍:

WeakReference是Java语言规范中为了区别直接的对象引用(程序中通过构造函数声明出来的对象引用)而定义的另外一种引用关系。WeakReference标志性的特点是:reference实例不会影响到被应用对象的GC回收行为(即只要对象被除WeakReference对象之外所有的对象解除引用后,该对象便可以被GC回收),只不过在被对象回收之后,reference实例想获得被应用的对象时程序会返回null。

  如果Entry的键使用强引用,那么我们存入的键值对即使线程之后不再使用也不会被回收,生命周期将变得和线程的生命周期一样。而使用了虚引用之后,作为键的虚引用并不影响ThreadLocal对象被GC回收,当ThreadLocal对象被回收后,键值对就会被标记为stale entry(过期的键值对),再下一次调用set/get/remove方法后会进行  ThreadLocalMap层面对过期键值对进行回收,防止发生内存泄漏。

注意:当我们使用了set方法存入局部变量后,如果不进行get/remove,那么过期的键值对无法被回收,所以建议在get取出存储变量后手动remove,可以有效防止内存泄漏。

总结

  ThreadLocal实现了存储线程局部变量,ThreadLocal的实现并不是HashMap<Thread,Object>以线程对象为键,而是在线程内部关联了一个ThreadLocalMap用于存储键值对,键值对的键是ThreadLocal对象,所以ThreadLocal对象本身是不存储内容的,而是作为键与存储内容构成键值对。

多线程学习笔记九之ThreadLocal的更多相关文章

  1. java进阶-多线程学习笔记

    多线程学习笔记 1.什么是线程 操作系统中 打开一个程序就是一个进程 一个进程可以创建多个线程 现在系统中 系统调度的最小单元是线程 2.多线程有什么用? 发挥多核CPU的优势 如果使用多线程 将计算 ...

  2. java多线程学习笔记——详细

    一.线程类  1.新建状态(New):新创建了一个线程对象.        2.就绪状态(Runnable):线程对象创建后,其他线程调用了该对象的start()方法.该状态的线程位于可运行线程池中, ...

  3. JAVA多线程学习笔记(1)

    JAVA多线程学习笔记(1) 由于笔者使用markdown格式书写,后续copy到blog可能存在格式不美观的问题,本文的.mk文件已经上传到个人的github,会进行同步更新.github传送门 一 ...

  4. MDX导航结构层次:《Microsoft SQL Server 2008 MDX Step by Step》学习笔记九

    <Microsoft SQL Server 2008 MDX Step by Step>学习笔记九:导航结构层次   SQL Server 2008中SQL应用系列及BI笔记系列--目录索 ...

  5. python3.4学习笔记(九) Python GUI桌面应用开发工具选择

    python3.4学习笔记(九) Python GUI桌面应用开发工具选择 Python GUI开发工具选择 - WEB开发者http://www.admin10000.com/document/96 ...

  6. Go语言学习笔记九: 指针

    Go语言学习笔记九: 指针 指针的概念是当时学C语言时了解的.Go语言的指针感觉与C语言的没啥不同. 指针定义与使用 指针变量是保存内存地址的变量.其他变量保存的是数值,而指针变量保存的是内存地址.这 ...

  7. Java多线程学习笔记(一)——多线程实现和安全问题

    1. 线程.进程.多线程: 进程是正在执行的程序,线程是进程中的代码执行,多线程就是在一个进程中有多个线程同时执行不同的任务,就像QQ,既可以开视频,又可以同时打字聊天. 2.线程的特点: 1.运行任 ...

  8. go微服务框架kratos学习笔记九(kratos 全链路追踪 zipkin)

    目录 go微服务框架kratos学习笔记九(kratos 全链路追踪 zipkin) zipkin使用demo 数据持久化 go微服务框架kratos学习笔记九(kratos 全链路追踪 zipkin ...

  9. java 多线程学习笔记

    这篇文章主要是个人的学习笔记,是以例子来驱动的,加深自己对多线程的理解. 一:实现多线程的两种方法 1.继承Thread class MyThread1 extends Thread{ public ...

随机推荐

  1. SpringMVC——SpringMVC简介

    Spring web mvc 和Struts2 都属于表现层的框架,它是Spring 框架的一部分,我们可以从Spring 的整体结构中看得出来:

  2. CentOS7配置网络

    #进入目录 cd /etc/sysconfig/network-scripts/ #编辑ifcfg-XXX vi ifcfg-eno167777 #把onboot=no 改为yes #重启 shutd ...

  3. kali linux 安装QQ

    之前在kali上尝试过Wineqq2012,显示版本过低,放弃了.最近听说crossover比wine的支持要好,再次尝试. 1.下载 https://www.codeweavers.com/ 选择d ...

  4. ubuntu 下抓包

    笔记本安装了ubuntu 14.04, 利用笔记本的网卡进行抓包时,需要将网卡配置为monitor模式. (1)关闭无线网卡 sudo ifconfig wlan0 down (2)将无线网卡配置为m ...

  5. tomcat生产环境JDK部署及虚拟主机等常用配置详解

    jdk和tomcat环境部署: 1.删除系统自带的openjdk # java -version java version "1.7.0_45" OpenJDK Runtime E ...

  6. Ext需要的文件目录

    使用ext版本信息:ext-4.1.1a <!-- 下面是引入文件需要导入的文件信息 ext-all.css ext-all.js --><link rel="styles ...

  7. 电信运营商 IT 系统介绍

    业务支撑系统 BSS: Business support system  运营支撑系统 OSS: Operation support system  管理支撑系统 MSS: Management Su ...

  8. mysql语句判断是否存在记录,没有则插入新纪录否则不执行

    1 前言 由于项目需要,当某个表如果有记录,就不执行加入语句,否则加入新纪录(测试数据).思路是:判断表的记录是否为空,然后再决定是否插入 2 代码 DROP PROCEDURE IF EXISTS ...

  9. Dotfuscator使用

    参考:https://www.cnblogs.com/xiezunxu/articles/7228741.html

  10. n个月后兔子的个数问题(for循环)