java中ThreadLocal类的详细介绍(详解)
ThreadLocal简介
变量值的共享可以使用public static的形式,所有线程都使用同一个变量,如果想实现每一个线程都有自己的共享变量该如何实现呢?JDK中的ThreadLocal类正是为了解决这样的问题。
ThreadLocal类并不是用来解决多线程环境下的共享变量问题,而是用来提供线程内部的共享变量,在多线程环境下,可以保证各个线程之间的变量互相隔离、相互独立。在线程中,可以通过get()/set()方法来访问变量。ThreadLocal实例通常来说都是private static类型的,它们希望将状态与线程进行关联。这种变量在线程的生命周期内起作用,可以减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
我们先通过一个例子来看一下ThreadLocal的基本用法:
public class ThreadLocalTest {
static class MyThread extends Thread {
private static ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
@Override
public void run() {
super.run();
for (int i = 0; i < 3; i++) {
threadLocal.set(i);
System.out.println(getName() + " threadLocal.get() = " + threadLocal.get());
}
}
}
public static void main(String[] args) {
MyThread myThreadA = new MyThread();
myThreadA.setName("ThreadA");
MyThread myThreadB = new MyThread();
myThreadB.setName("ThreadB");
myThreadA.start();
myThreadB.start();
}
}
运行结果
ThreadA threadLocal.get() = 0
ThreadB threadLocal.get() = 0
ThreadA threadLocal.get() = 1
ThreadA threadLocal.get() = 2
ThreadB threadLocal.get() = 1
ThreadB threadLocal.get() = 2
虽然两个线程都在向threadLocal对象中set()数据值,但每个线程都还是能取出自己设置的数据,确实可以达到隔离线程变量的效果。
ThreadLocal源码解析
ThreadLocal常用方法介绍
**get()方法:**获取与当前线程关联的ThreadLocal值。
**set(T value)方法:**设置与当前线程关联的ThreadLocal值。
**initialValue()方法:**设置与当前线程关联的ThreadLocal初始值。
当调用get()方法的时候,若是与当前线程关联的ThreadLocal值已经被设置过,则不会调用initialValue()方法;否则,会调用initialValue()方法来进行初始值的设置。通常initialValue()方法只会被调用一次,除非调用了remove()方法之后又调用get()方法,此时,与当前线程关联的ThreadLocal值处于没有设置过的状态(其状态体现在源码中,就是线程的ThreadLocalMap对象是否为null),initialValue()方法仍会被调用。
initialValue()方法是protected类型的,很显然是建议在子类重载该函数的,所以通常该方法都会以匿名内部类的形式被重载,以指定初始值,例如:
public class ThreadLocalTest {
public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(1);
}
};
}
remove()方法:将与当前线程关联的ThreadLocal值删除。
实现原理
ThreadLocal最简单的实现方式就是ThreadLocal类内部有一个线程安全的Map,然后用线程的ID作为Map的key,实例对象作为Map的value,这样就能达到各个线程的值隔离的效果。
JDK最早期的ThreadLocal就是这样设计的,但是,之后ThreadLocal的设计换了一种方式,我们先看get()方法的源码,然后进一步介绍ThreadLocal的实现方式:
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();
}
get()方法主要做了以下事情:
1、调用Thread.currentThread()获取当前线程对象t;
2、根据当前线程对象,调用getMap(Thread)获取线程对应的ThreadLocalMap对象:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
threadLocals是Thread类的成员变量,初始化为null:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
3、如果获取的map不为空,则在map中以ThreadLocal的引用作为key来在map中获取对应的value e,否则转到步骤5;
4、若e不为null,则返回e中存储的value值,否则转到步骤5;
5、调用setInitialValue()方法,对线程的ThreadLocalMap对象进行初始化操作,ThreadLocalMap对象的key为ThreadLocal对象,value为initialValue()方法的返回值。
从上面的分析中,可以看到,ThreadLocal的实现离不开ThreadLocalMap类,ThreadLocalMap类是ThreadLocal的静态内部类。每个Thread维护一个ThreadLocalMap映射表,这个映射表的key是ThreadLocal实例本身,value是真正需要存储的Object。这样的设计主要有以下几点优势:
这样设计之后每个Map的Entry数量变小了:之前是Thread的数量,现在是ThreadLocal的数量,能提高性能;
当Thread销毁之后对应的ThreadLocalMap也就随之销毁了,能减少内存使用量。
ThreadLocalMap源码分析
ThreadLocalMap是用来存储与线程关联的value的哈希表,它具有HashMap的部分特性,比如容量、扩容阈值等,它内部通过Entry类来存储key和value,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继承自WeakReference,通过上述源码super(k);可以知道,ThreadLocalMap是使用ThreadLocal的弱引用作为Key的。
分析到这里,我们可以得到下面这个对象之间的引用结构图(其中,实线为强引用,虚线为弱引用):
我们知道,弱引用对象在Java虚拟机进行垃圾回收时,就会被释放,那我们考虑这样一个问题:
ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部关联的强引用,那么在虚拟机进行垃圾回收时,这个ThreadLocal会被回收,这样,ThreadLocalMap中就会出现key为null的Entry,这些key对应的value也就再无妨访问,但是value却存在一条从Current Thread过来的强引用链。因此只有当Current Thread销毁时,value才能得到释放。
该强引用链如下:
CurrentThread Ref -> Thread -> ThreadLocalMap -> Entry -> value
因此,只要这个线程对象被gc回收,那些key为null对应的value也会被回收,这样也没什么问题,但在线程对象不被回收的情况下,比如使用线程池的时候,核心线程是一直在运行的,线程对象不会回收,若是在这样的线程中存在上述现象,就可能出现内存泄露的问题。
那在ThreadLocalMap中是如何解决这个问题的呢?
在获取key对应的value时,会调用ThreadLocalMap的getEntry(ThreadLocal<?> key)方法,该方法源码如下:
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
通过key.threadLocalHashCode & (table.length - 1)来计算存储key的Entry的索引位置,然后判断对应的key是否存在,若存在,则返回其对应的value,否则,调用getEntryAfterMiss(ThreadLocal<?>, int, Entry)方法,源码如下:
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
ThreadLocalMap采用线性探查的方式来处理哈希冲突,所以会有一个while循环去查找对应的key,在查找过程中,若发现key为null,即通过弱引用的key被回收了,会调用expungeStaleEntry(int)方法,其源码如下:
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
通过上述代码可以发现,若key为null,则该方法通过下述代码来清理与key对应的value以及Entry:
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
此时,CurrentThread Ref不存在一条到Entry对象的强引用链,Entry到value对象也不存在强引用,那在程序运行期间,它们自然也就会被回收。expungeStaleEntry(int)方法的后续代码就是以线性探查的方式,调整后续Entry的位置,同时检查key的有效性。
在ThreadLocalMap中的set()/getEntry()方法中,都会调用expungeStaleEntry(int)方法,但是如果我们既不需要添加value,也不需要获取value,那还是有可能产生内存泄漏的。所以很多情况下需要使用者手动调用ThreadLocal的remove()函数,手动删除不再需要的ThreadLocal,防止内存泄露。若对应的key存在,remove()方法也会调用expungeStaleEntry(int)方法,来删除对应的Entry和value。
其实,最好的方式就是将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,可以防止内存泄露。
InheritableThreadLocal
InheritableThreadLocal继承自ThreadLocal,使用InheritableThreadLocal类可以使子线程继承父线程的值,来看一段示例代码:
public class ThreadLocalTest {
private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(10);
}
};
static class MyThread extends Thread {
@Override
public void run() {
super.run();
System.out.println(getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
}
}
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName() + " inheritableThreadLocal.get() = " + inheritableThreadLocal.get());
MyThread myThread = new MyThread();
myThread.setName("线程A");
myThread.start();
}
}
运行结果:
main inheritableThreadLocal.get() = 10
线程A inheritableThreadLocal.get() = 10
可以看到子线程成功继承了父线程的值。
父线程还可以设置子线程的初始值,只需要重写InheritableThreadLocal类的childValue(T)方法即可,将上述代码的inheritableThreadLocal 定义修改为如下方式:
private static InheritableThreadLocal<Integer> inheritableThreadLocal = new InheritableThreadLocal<Integer>() {
@Override
protected Integer initialValue() {
return Integer.valueOf(10);
}
@Override
protected Integer childValue(Integer parentValue) {
return Integer.valueOf(5);
}
};
运行结果:
main inheritableThreadLocal.get() = 10
线程A inheritableThreadLocal.get() = 5
可以看到,子进程成功获取到了父进程设置的初始值。
使用InheritableThreadLocal类需要注意的一点是,如果子线程在取得值的同时,主线程将InheritableThreadLocal中的值进行更改,那子线程获取的还是旧值。
线程中用来实现上述功能的ThreadLocalMap类变量为
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
InheritableThreadLocal类的实现很简单,主要是重写了ThreadLocal类的getMap(Thread)方法和createMap(Thread, T)方法,将其中操作的ThreadLocalMap变量修改为了inheritableThreadLocals,这里不再进一步叙述。
java中ThreadLocal类的详细介绍(详解)的更多相关文章
- java中ReentrantLock类的详细介绍(详解)
博主如果看到请联系小白,小白记不清地址了 简介 ReentrantLock是一个可重入且独占式的锁,它具有与使用synchronized监视器锁相同的基本行为和语义,但与synchronized关键字 ...
- java中Timer类的详细介绍(详解)
一.概念 定时计划任务功能在Java中主要使用的就是Timer对象,它在内部使用多线程的方式进行处理,所以它和多线程技术还是有非常大的关联的.在JDK中Timer类主要负责计划任务的功能,也就是在指定 ...
- Java中Iterator类的详细介绍
迭代器模式:就是提供一种方法对一个容器对象中的各个元素进行访问,而又不暴露该对象容器的内部细节. 概述 Java集合框架的集合类,我们有时候称之为容器.容器的种类有很多种,比如ArrayList.Li ...
- java中Condition类的详细介绍(详解)
已找不到原文了,还望原文博主看到能告诉小白一下,一定标注原文地址 一 condition 介绍及demo Condition是在java 1.5中才出现的,它用来替代传统的Object的wait(). ...
- JUC中的AQS底层详细超详解
摘要:当你使用java实现一个线程同步的对象时,一定会包含一个问题:你该如何保证多个线程访问该对象时,正确地进行阻塞等待,正确地被唤醒? 本文分享自华为云社区<JUC中的AQS底层详细超详解,剖 ...
- Java中的equals和hashCode方法详解
Java中的equals和hashCode方法详解 转自 https://www.cnblogs.com/crazylqy/category/655181.html 参考:http://blog.c ...
- 转:Java中的equals和hashCode方法详解
转自:Java中的equals和hashCode方法详解 Java中的equals方法和hashCode方法是Object中的,所以每个对象都是有这两个方法的,有时候我们需要实现特定需求,可能要重写这 ...
- Java中堆内存和栈内存详解2
Java中堆内存和栈内存详解 Java把内存分成两种,一种叫做栈内存,一种叫做堆内存 在函数中定义的一些基本类型的变量和对象的引用变量都是在函数的栈内存中分配.当在一段代码块中定义一个变量时,ja ...
- java中List的用法和实例详解
java中List的用法和实例详解 List的用法List包括List接口以及List接口的所有实现类.因为List接口实现了Collection接口,所以List接口拥有Collection接口提供 ...
随机推荐
- 1026 Table Tennis (30分) 难度不高 + 逻辑复杂 +细节繁琐
题目 A table tennis club has N tables available to the public. The tables are numbered from 1 to N. Fo ...
- node的http模块
node中的几个常用核心模块的api返回的都是eventEmitter的实例,也就是说都继承了on和emit方法,用以监听事件并触发回调来处理事件. http模块处理网络请求通常是创建一个server ...
- Java基础之抽象类和接口
今天来说说抽象类和接口的实现以及它们的区别.我们知道抽象类和接口都是对具体事物的抽象,接口在实现上比抽象类更加抽象,抽象类中可以有普通方法和变量,而接口中只有抽象方法和不可变常量.但是从另一个角度看, ...
- 字符串 kmp算法 codeforce 625B 题解(模板)
题解:kmp算法 代码: #include <iostream>#include <algorithm>#include <cstring>#include < ...
- [前端开发]Vue mixin
两个非常相似的组件,他们的基本功能是一样的,但他们之间又存在着足够的差异性,此时的你就像是来到了一个分岔路口:我是把它拆分成两个不同的组件呢?还是保留为一个组件,然后通过props传值来创造差异性从而 ...
- Kubernetes学习笔记(七):访问Pod元数据与Kubernetes API
Downward API 我们已经了解到,使用ConfigMap和Secret向应用传递配置数据,这对于运行前预设的数据是可行的.但是对于那些不能预先知道的,就需要使用Downward API. Do ...
- 第一章-初识AngularJS
完全使用javascript编写的客户端技术.同其他历史悠久的Web技术配合使用,使Web应用开发比以往更简单,更快捷. Angularjs主要用于构建单页面Web应该.它通过增加开发人员和常见Web ...
- 【GAN】GAN设计与训练集锦
以下内容纯属经验之谈,无公式推断!部分内容源自其他博客或课程,并已标注来源. 问题篇[1] 1.模式崩溃 在某个模式(mode)下出现大量重复样本,如左图中,生成的样本分布靠得很近,较聚集,可视化如右 ...
- [PHP学习教程 - 类库]001.全局唯一ID(GUID)
GUID: 即Globally Unique Identifier(全球唯一标识符) 也称作 UUID(Universally Unique IDentifier) . GUID 是一个通过特定算法产 ...
- [SD心灵鸡汤]000.每月一则 - 索引
[SD心灵鸡汤]001.每月一则 - 2015.05 [SD心灵鸡汤]002.每月一则 - 2015.06 [SD心灵鸡汤]003.每月一则 - 2015.07 [SD心灵鸡汤]004.每月一则 - ...