让你真正了解 Threadlocal

90 阅读15分钟

让你真正了解 Threadlocal

1. ThreadLocal

ThreadLocal 这个对象是什么?作用是什么?网络上大部分的解释是说它是 Thread 的本地变量,但是小编觉得它更像一个工具类,作用是为 Thread 对象操作线程自身的成员变量 threadLocals,为什么小编会这样觉得呢?还请大家耐心听小编道来。

1.1 Thread 的成员变量 threadLocals

我们都知道Java中,代表线程的类是 Thread ,如果让每个线程都有一个属于自己可以控制的数据,那最方便的方式,自然是在创建 Thread 这个对象的时候,希望 Thread 有一个属性,这个属性我们可以任意的存储和删除数据。那是不是就完美了。哈哈哈,Thread 类中还真有这么一个属性,这个属性就是 Thread ****类的成员变量 ****threadLocals

以下是 Thread 类的源码, 记住这个线程的成员变量 threadLocals 还有它对应的类型 ThreadLocal.ThreadLocalMap ,我们其实所有的操作都是针对它。

// 这个成员变量就是我们要讨论的核心
public class Thread implements Runnable {
    /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

在Java中如何给一个实例对象的成员变量赋值呢?一般都是根据 set 方法或者构造函数进行初始化赋值。所以按照这个思路,线程的成员变量也按照这样设计赋值不就 OK 了吗?为什么还有弄那么多的东西呢?

思考:
如果 Thread 的成员变量 threadLocals 采用 ConcurrentHashMap 的结构
然后线程提供一个成员方法进行 get 和 set 操作,不也可以到达维护线程本地变量的目的 ?

假设提供一个这个 setThreadLocals 方法 
Thread.currentThread().setThreadLocals(key, value);

对吧,采用以上的方法是不是发现其实也是可以到达维护线程本地变量的目的的,那为什么没采用呢?
【
	主意这里的 key 的安全性问题,如果 key 为引用对象,并且其他线程可以访问,
	可能存在,setThreadLocals(key, value) 的时候 key 的值已经被修改
 】

小编觉得之所以没有采用这种方式,可能就是因为 key 的维护问题,还有一部分应该是历史原因。

那我们就先从历史开始说起

关于 Thread 类的 threadLocals 成员变量,它是 Thread 类的一个私有属性【现在是默认访问修饰符】,用于存储线程的本地变量。threadLocalsThread 类的初始版本中就存在。它是通过一个 ThreadLocal.ThreadLocalMap 实例来实现的,而 ThreadLocal.ThreadLocalMapThreadLocal 类的内部静态类。

在 JDK 1.2 之前,threadLocals 变量是 Thread 类的私有成员变量,除了使用反射操作 threadLocals 变量外,没有提供其他直接的方式来访问和操作它。

简单代码示例:

import java.lang.reflect.Field;

public class ThreadLocalExample {
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException {
        // 创建线程对象
        Thread thread = new Thread();

        // 获取 Thread 类的 threadLocals 字段
        Field threadLocalsField = Thread.class.getDeclaredField("threadLocals");
        threadLocalsField.setAccessible(true);

        // 获取 threadLocals 变量的值
        Object threadLocals = threadLocalsField.get(thread);

        // 操作 threadLocals 变量
        // ...

        // 设置修改后的 threadLocals 值
        threadLocalsField.set(thread, threadLocals);
    }
}

这种方式是非官方推荐的,使用反射可能会引起一些安全和兼容性问题。

所以在早期的时候为了设置和使用线程的本地变量,更多的操作方式是通过继承线程 Thread 来进行扩展子类。如何扩展?请看下面

参考:duanqz.github.io/2018-03-15-…

初始形态

最开始定义线程私有属性的方式:

class ExtandThread extends Thread {
		// 扩展的变量
    private int id;
    public void run() {
        ...// 
    }
}

这里的 id 就是自己进行扩展的线程私有变量。

进化形态

初始形态中我们的变量属性还只有一个,当自定义扩展的属性多了后,我们自然想到了采用对象来封装属性,就变成了这样:【这样一来,每个线程都拥有一个局部变量Session】

class ExtandThread extends Thread {
		// 我们把所有的属性都分装到 Session 对象,这样只要维护这个对象就可以了
    private Session session;
    public void run() {
        ...
    }
}

class Session {
    private int id;
    private String name;
    ... // Other extension
}

到这里,其实我们就可以把 Session 看作线程 Thread 类的 threadLocals 成员变量。那 threadLocals 的变量类型 ThreadLocal.ThreadLocalMap 是个结构怎样的对象呢?

我们看源码:

static class ThreadLocalMap {
		// 实际存储数据的数组
		private Entry[] table;
		static class Entry extends WeakReference<ThreadLocal<?>> {
				Object value;
				Entry(ThreadLocal<?> k, Object v) {
	          super(k);
	          value = v;
		    }
	   }

		private void set(ThreadLocal<?> key, Object value) {
				...
				// 操作数组 table 
		}
}

哦,原来 Thread 类的成员变量 threadLocals 的对象结构, 本质上就是个数组,数组里面保存着 Entry 对象,而 Entry 对象的结构是一个类似 { key : value } 的结构。

Entry 的结构也很有意思,它是一个继承 WeakReference 对象的类】

既然我们都可以自行进行扩展线程的本地变量属性,为什么还需要使用 threadLocals ?

小编的回答是:维护的问题,我们是可以通过扩展的方式实现本地变量,类似上面的 Session 实现,但是在实现的同时,对于 Session 的维护问题也给到了我们自己。同时 JDK1.2 以后,官方提供了可以访问 threadLocals 成员变量的 API,如果非必要,完全可以通过官方提供的方式满足我们的需求。

那我们还是接着讨论线程自身的成员变量 threadLocals 吧,既然JDK1.2后,官方提供了可以访问 threadLocals 成员变量的API,那我们该怎么样访问呢?

1.2 如何操作 Thread 的成员变量 threadLocals

如果进入源码 Thread 类,查询整个类,关于 threadLocals 有被使用到的地方,只有一个方法有使用到:

private void exit() {
    if (threadLocals != null && TerminatingThreadLocal.REGISTRY.isPresent()) {
        TerminatingThreadLocal.threadTerminated();
    }

    threadLocals = null;
}

What ???是不是很吃惊,只有一个地方,还是赋值为空的操作。那这个成员变量的初始化在哪里呢 ?怎么进行维护的 ?

还记得上面我们说到,在JDK1.2 之前,threadLocals 是私有的成员变量,只能通过反射进行操作,但是我们观察现在的访问修饰符,是默认的修饰符,那就说明,在同包下的其他类,是可以访问到 Thread 类的成员变量 threadLocals 的。

1.3 ThreadLocal 登场

查询和 Thread 同包下的类,你会发现这么一个类 ThreadLocal 类 。

我们直接看看 ThreadLocal 类的主要内容有哪些:【这里省略了很多细节,主要关注核心的几个方法和对象结构】

// 这里我省略了部分代码,只看主要的一些方法和关注结构
public class ThreadLocal<T> {
		// 操作 threadLocals 成员变量的 get 方法
		public T get() {
				// 获取当前线程
        Thread t = Thread.currentThread();
				// getMap 就是 t.threadLocals,因为同包下可以直接访问到
        ThreadLocalMap map = getMap(t);
        if (map != null) { 
						// 通过 ThreadLocal 这个 key 获取 ThreadLocalMap 数组中的 Entry 对象,从而获取值
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
				// 如果 getMap(t) ,当前线程的属性 ThreadLocalMap 为空,进行初始化
        return setInitialValue();
    }

		// 操作 threadLocals 成员变量的 set 方法
		public void set(T value) {
        Thread t = Thread.currentThread();
				// 这里的 getMap 是获取 t 这个当前线程的 threadLocals变量
        ThreadLocalMap map = getMap(t);
				// 存在保存变量,不存在创建
        if (map != null) {
						// 将值转换为 Entry 对象,保存到 ThreadLocalMap 的 table 数组中
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

		// 操作 threadLocals 成员变量的初始化方法
		private T setInitialValue() {
		    // 初始化 value 这个值默认为空,(initialValue()方法返回值是null)
		    T value = initialValue();
		    Thread t = Thread.currentThread();
		    ThreadLocalMap map = getMap(t);
		    if (map != null)
		        map.set(this, value);
		     else
					  // 真正给当前线程 Thread 的 ThreadLocalMap 属性创建对象,初始化
		        createMap(t, value);
		     return value;
		}

		void createMap(Thread t, T firstValue) {
				// 通过 new 直接赋值了
		    t.threadLocals = new ThreadLocalMap(this, firstValue);
		}
		
		// 详细内容省略,主要关注结构
		static class ThreadLocalMap {
				static class Entry extends WeakReference<ThreadLocal<?>> {}
		}

}

总结一下 ThreadLocal 类包含了那些内容呢?

从结构上:ThreadLocal 类中包含了一个静态内部类 ThreadLocalMapThreadLocalMap 内部又包含了一个静态内部类 EntryThreadLocalMap 这个静态内部类就是 Thread 成员变量 threadLocals 的类型。

ThreadLocal 类的主要几个方法,都是关于ThreadLocalMap 类的操作,get 和 set 都是用来维护线程 Thread 的成员变量 threadLocals 的。所以小编一开始说 ThreadLocal 更像一个工具类,用于维护 Thread 的 成员变量 threadLocals

1.4 ThreadLocal 有什么特点呢?

ThreadLocal 类的 set 方法我们仔细观察会发现,参数只有一个 value,但是,Thread 类的成员变量 threadLocals 的类型是 ThreadLocal.ThreadLocalMapThreadLocal.ThreadLocalMap 保存的是 {key:value} 的数组对象Entry,那这个 key 是什么呢?

观察下面小编从源码扣出来的 set 方法,看第 5 行你会发现,ThreadLocal 类的 set 方法会获取到线程的 threadLocals 变量,然后调用 ThreadLocalMap 类的 set 方法,这个时候 set 的参数有两个,第一个参数是 key,竟然是 ThreadLocal 对象实例自身。

ThreadLocalMap 的 set 方法里,主要是通过 ThreadLocal 实例的 threadLocalHashCode 哈希码进行与计算,计算出所在数组的下标。【还记得上面我们提到 ThreadLocalMap 类里面保存的是数组类型的 {key: value} Entry 对象】,然后将数据保存到 Entry { key : value } 对象,放入刚才计算出来的下标数组中。至此就结束了

public class ThreadLocal<T> {
		// 操作 threadLocals 成员变量的 set 方法
		public void set(T value) {
        Thread t = Thread.currentThread();
				// 这里的 getMap 是获取 t 这个当前线程的 threadLocals变量
        ThreadLocalMap map = getMap(t);
				// 存在保存变量,不存在创建
        if (map != null) {
						// 将值转换为 Entry 对象,保存到 ThreadLocalMap 的 table 数组中
						// 注意观察 这里的 map.set 的 key 是 ThreadLocal 对象实例自身
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }

	// 那么这个对象如何获取和存储数据?我们看他的 get 和 set 方法,研究研究
	static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
				
				// 这个很重要,这个table,就是真正用于存储数据的数组
				// 数组的类型 Entry 就是上面 static class Entry extends WeakReference<ThreadLocal<?>>
				private Entry[] table;

				// 给 ThreadLocalMap 设置数据
        private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
						
						// 判断 ThreadLocalMap 的 key 如果之前存在,存在就覆盖,不存在,就重新设置
            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();
        }
       
		}
}

是不是还有点懵懵懂懂的感觉?到底有什么特别的地方呢?我们继续看下面

1.5 聊聊 ThreadLocalMap

如果大家还有印象的话, 文章的前面有贴出部分的 ThreadLocalMap 的源码,如下:

static class ThreadLocalMap {
		// 实际存储数据的数组
		private Entry[] table;
		static class Entry extends WeakReference<ThreadLocal<?>> {
				Object value;
				Entry(ThreadLocal<?> k, Object v) {
	          super(k);
	          value = v;
		    }
	   }

		private void set(ThreadLocal<?> key, Object value) {
				...
				// 操作数组 table 
		}
}

ThreadLocalMap 有个成员变量 table ,ThreadLocalMap 主要就是维护和操作这个成员变量 table。我们重点来看看这成员变量的类型 Entry

		

    static class Entry extends WeakReference<ThreadLocal<?>> {
				Object value;
				Entry(ThreadLocal<?> k, Object v) {
	          super(k);
	          value = v;
		    }
	   }

发现没,哈哈哈,是不是很简单,但是却很有意思。

Entry 为什么要继承 WeakReference 呢?有什么作用?有会有什么问题呢?

我们把上面所有的内容总结一下,大概总结一下他们的关系,就是下面这张图,一个线程,单前线程CurrentThread 持有着 Map【这个map就是 localthreads 成员变量】,Map 有持有着一个数组对象Entry,Entry 的 key 指向的是 ThreadLocal 对象实例。

Untitled.png

Untitled 1.png

Untitled 2.png

【关于 WeakReference 对象的作用,这里不进行讲解,请自行查阅,或查看小编之前的文章 】

Entry 为什么要继承 WeakReference 呢?直接引用 ThreadLocal 的实例不行?

答:还真不行,假如采用强引用,看上图,栈内存中的ThreadLocal引用指向了空,堆中实际保存的ThreadLocal 对象将等待被垃圾回收器回收,但是这时候发现它还有一个强引用被 Entry 持有着,Entry 又被单前线程持有,只要当前线程存在,那 ThreadLocal 对象将永远无法被垃圾回收器回收。自然就会产生内存泄漏的问题。

采用 WeakReference 弱引用,当栈内存中的ThreadLocal引用指向了空,堆中实际保存的ThreadLocal 对象只要等待被垃圾回收器回收就可以了,不必要担心被 Entry 对象的引用问题。

等会,好像还是会存在问题吧,继续上面的话题,当采用弱引用后,假如堆内存中的 ThreadLocal 对象被回收了以后,那 Entry 对象的 get 不就为 null 了,就获取不到 Value的值了,但是 Entry 的 Value 实际上对应的引用还是存在的,而且 Entry 对象被 ThreadLocalMap 的 table 数组引用着,如果当前线程一直存在,那不是又有内存泄漏的问题了。哈哈哈,又被细心的你发现了

是的,所以 ThreadLocal 还提供了一个 remove 的方法,我们必须及时的清除不需要的数据

private void remove(ThreadLocal key) {
  //使用hash方式,计算当前ThreadLocal变量所在table数组位置 
  Entry[] tab = table;
  int len = tab.length;
  int i = key.threadLocalHashCode & (len - 1);
  //再次循环判断是否在为ThreadLocal变量所在table数组位置 
  for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    if (e.get() == key) {
      //调用WeakReference的clear方法清除对ThreadLocal的弱引用 
      e.clear(); //清理key为null的元素 
      expungeStaleEntry(i);
      return;
    }
  }
} 

2. 关于性能的分析

ThreadLocal 的性能相对较高,原因如下:

  1. 高效的线程隔离:ThreadLocal 提供了线程级别的数据隔离,每个线程都拥有自己独立的数据副本。线程之间的数据互不干扰,避免了线程安全问题和锁竞争。
  2. 无锁操作:在读写数据时,ThreadLocal 并不需要加锁,因为每个线程独立拥有自己的数据副本,不存在并发访问的问题。因此,ThreadLocal 的读写操作是无锁的,性能较高。
  3. 快速访问数据:由于 ThreadLocalMap 使用线性探测的方式实现,直接通过 ThreadLocal 对象作为键进行访问,不需要遍历整个数据结构。这样可以减少不必要的操作和寻址开销,提高访问速度。

使用 ThreadLocal 的场景包括但不限于以下情况:

  1. 线程上下文信息存储:在多线程环境下,需要将一些上下文信息与线程关联,以便在线程执行过程中访问和使用。例如,用户身份认证信息、请求跟踪ID等。
  2. 线程安全的数据存储:在多线程环境下,需要在每个线程中保存独立的数据副本,以保证线程安全。例如,数据库连接、事务上下文等。
  3. 高效的线程局部变量:对于某些需要频繁访问的临时变量或状态,可以使用 ThreadLocal 存储,避免传递参数或全局变量的开销,提高代码的可读性和性能。

需要注意的是,使用 ThreadLocal 时应当避免滥用,过度使用 ThreadLocal 可能会导致内存泄漏或引发难以调试的问题。同时,应注意及时清理不再需要的数据,避免占用过多的内存资源。

2.1 Java 中使用的事例

  1. Spring实现事务隔离级别

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所:

private static final ThreadLocal<Map<Object, Object>> resources =
   new NamedThreadLocal<>("Transactional resources");

 private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
   new NamedThreadLocal<>("Transaction synchronizations");

 private static final ThreadLocal<String> currentTransactionName =
   new NamedThreadLocal<>("Current transaction name");

  1. SimpleDataFormat
SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat? 所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

3. 总结

至此,关于 ThreadLocal 的介绍就结束啦,小编觉得设计的特别之处主要有两个地方

  1. 对线程 Thread 类的成员变量 threadLocals 的访问控制到最小化,这个最小化体现在,如果我们要访问 Thread 类的成员变量 threadLocals,只能通过先实例化一个 ThreadLocal 对象,然后通过这个实例对象调用他的 get set 方法来操作 Thread 类的成员变量 threadLocals,除此之外没有其他方法【除了反射外,只通过官方提供的API】
  2. ThreadLocalMap 类成员变量 Entry 的设计。Entry 是一个继承了 WeakReference 的对象,采用 { key: value } 的结构,同时 key 又是 ThreadLocal 的实例对象自身, 通过这种巧妙的设计在的实现了线程之间的数据隔离,数据的安全访问。

更多内容欢迎关注 [ 小巫编程室 ] 公众号,喜欢文章的话,也希望能给小编点个赞或者转发,你们的喜欢与支持是小编最大的鼓励,小巫编程室感谢您的关注与支持。good good study day day up