出自
> [聊一聊ThreadLocal](http://www.importnew.com/21043.html)
[TOC=1,2]
对于ThreadLocal感兴趣是从一个问题开始的:ThreadLocal在何种情况下会发生内存泄露?对于这个问题的思考不得不去了解ThreadLocal本身的实现以及一些细节问题等。接下去依次介绍ThreadLocal的功能,实现细节,使用场景以及一些使用建议。
## 概述
ThreadLocal不是用来解决对象共享访问问题的,而主要提供了线程保持对象的方法和避免参数传递的方便的对象访问方式。一般情况下,通过ThreadLocal.set()到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致的问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,但大大减少了线程同步所带来的线程消耗,也介绍了线程并发控制的复杂度。
另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new对象的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map(Thread类中的ThreadLocal.ThreadLocalMap的变量)中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
【代码1】
~~~
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
~~~
很多人会有这样的无解:感觉这个ThreadLocal对象建立了一个类似于全局的map,然后每个线程作为map的key来存取对应的线程本地的value。其实是ThreadLocal类中有一个ThreadLocalMap静态内部类,可以简单的理解为一个map,这个map为每个线程复制一个变量的“拷贝”存储其中。下面是ThreadLocalMap的部分源码:
【代码2】
~~~
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
private static final int INITIAL_CAPACITY = 16;
private Entry[] table;
private int size = 0;
private int threshold; // Default to 0
//部分省略
}
~~~
ThreadLocal类中一共有4个方法:
* T get()
* protected T initialValue()
* void remove()
* void set(T value)
就以get()方法为例
【代码3】
~~~
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();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
~~~
get()方法的源码如上所示,可以看到map中真正的key是线程ThreadLocal实例本身(ThreadLocalMap.Entry e = map.getEntry(this);中的this)。可以看一下getEntry(ThreadLocal key)的源码.
【代码4】
~~~
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);
}
~~~
那么map中的value是什么呢?我们继续来看源码:
【代码5】
~~~
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
~~~
代码5中只能够观察到通过[protected T initialValue()]方法设置了一个初始值,当然也可以通过set方法来赋值,继续看源码:
【代码6】
~~~
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
~~~
ThreadLocal设置值有两种方案:1\. Override其initialValue方法;2\. 通过set设置。
关于重写initialValue方法可以参考下面这个例子简便的实现:
【代码7】
~~~
private static final ThreadLocal<Long> TIME_THREADLOCAL = new ThreadLocal<Long>(){
@Override
protected Long initialValue()
{
return System.currentTimeMillis();
}
};
~~~
## 内存泄露
通过代码1和代码2的片段可以看出,在Thread类中保有ThreadLocal.ThreadLocalMap的引用,即在一个Java线程栈中指向了堆内存中的一个ThreadLocal.ThreadLocalMap的对象,此对象中保存了若干个Entry,每个Entry的key(ThreadLocal实例)是弱引用,value是强引用(这点类似于WeakHashMap)。
用到弱引用的只是key,每个key都弱引用指向threadLocal,当把threadLocal实例置为null以后,没有任何强引用指向threadLocal实例,所以threadLocal将会被gc回收,但是value却不能被回收,因为其还存在于ThreadLocal.ThreadLocalMap的对象的Entry之中。只有当前Thread结束之后,所有与当前线程有关的资源才会被GC回收。所以,如果在线程池中使用ThreadLocal,由于线程会复用,而又没有显示的调用remove的话的确是会有可能发生内存泄露的问题。
其实在ThreadLocal.ThreadLocalMap的get或者set方法中会探测其中的key是否被回收(调用expungeStaleEntry方法),然后将其value设置为null,这个功能几乎和WeakHashMap中的expungeStaleEntries()方法一样。因此value在key被gc后可能还会存活一段时间,但最终也会被回收,但是若不再调用get或者set方法时,那么这个value就在线程存活期间无法被释放。
【代码8】
~~~
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;
}
~~~
其实ThreadLocal本身可以看成是没有内存泄露问题的,通过显示的调用remove方法即可。
## 使用场景及方式
ThreadLocal的应用场景,最适合的是按线程多实例(每个线程对应一个实例)的对象的访问,并且这个对象很多地方都要用到。
对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,比如定义一个static变量,同步访问,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
在多线程的开发中,经常会考虑到的策略是对一些需要公开访问的属性通过设置同步的方式来访问。这样每次能保证只有一个线程访问它,不会有冲突。但是这样做的结果会使得性能和对高并发的支持不够。在某些情况下,如果我们不一定非要对一个变量共享不可,而是给每个线程一个这样的资源副本,让他们可以独立都各自跑各自的,这样不是可以大幅度的提高并行度和性能了吗?
还有的情况是有的数据本身不是线程安全的,或者说它只能被一个线程使用,不能被其它线程同时使用。如果等一个线程使用完了再给另一个线程使用就根本不现实。这样的情况下,我们也可以考虑用ThreadLocal。
> ThreadLocal建议:
> 1\. ThreadLocal类变量因为本身定位为要被多个线程来访问,它通常被定义为static变量。
> 2\. 能够通过值传递的参数,不要通过ThreadLocal存储,以免造成ThreadLocal的滥用。
> 3\. 在线程池的情况下,在ThreadLocal业务周期处理完成时,最好显示的调用remove()方法,清空“线程局部变量”中的值。
> 4\. 在正常情况下使用ThreadLocal不会造成OOM, 弱引用的知识ThreadLocal,保存值依然是强引用,如果ThreadLocal依然被其他对象应用,线程局部变量将无法回收。
## InheritableThreadLocal
InheritableThreadLocal是ThreadLocal的子类,代码量很少,可以看一下:
【代码9】
~~~
public class InheritableThreadLocal<T> extends ThreadLocal<T> {
protected T childValue(T parentValue) {
return parentValue;
}
ThreadLocalMap getMap(Thread t) {
return t.inheritableThreadLocals;
}
void createMap(Thread t, T firstValue) {
t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
}
}
~~~
这里主要的还是一个childValue这个方法。
在代码7中示范了ThreadLocal的方法,而使用类InheritableThreadLocal可以在子线程中取得父线程继承下来的值。可以采用重写childValue(Object parentValue)方法来更改继承的值。
查看案例:
【代码10】
~~~
public class InheriableThreadLocal
{
public static final InheritableThreadLocal<?> itl = new InheritableThreadLocal<Object>(){
@Override protected Object initialValue()
{
return new Date().getTime();
}
@Override protected Object childValue(Object parentValue)
{
return parentValue+" which plus in subThread.";
}
};
public static void main(String[] args)
{
System.out.println("Main: get value = "+itl.get());
Thread a = new Thread(new Runnable(){
@Override public void run()
{
System.out.println(Thread.currentThread().getName()+": get value = "+itl.get());
}
});
a.start();
}
}
~~~
运行结果:
~~~
Main: get value = 1467100984858
Thread-0: get value = 1467100984858 which plus in subThread.
~~~
如果去掉@Override protected Object childValue(Object parentValue)方法运行结果:
~~~
Main: get value = 1461585396073
Thread-0: get value = 1461585396073
~~~
参考资料
1. [Java多线程知识小抄集(一)](http://blog.csdn.net/u013256816/article/details/51325246#t11)
2. [深入JDK源码之ThreadLocal类](http://my.oschina.net/xianggao/blog/392440?fromerr=CLZtT4xC)
3. [Java集合框架:WeakHashMap](http://blog.csdn.net/u013256816/article/details/50916504)
- JVM
- 深入理解Java内存模型
- 深入理解Java内存模型(一)——基础
- 深入理解Java内存模型(二)——重排序
- 深入理解Java内存模型(三)——顺序一致性
- 深入理解Java内存模型(四)——volatile
- 深入理解Java内存模型(五)——锁
- 深入理解Java内存模型(六)——final
- 深入理解Java内存模型(七)——总结
- Java内存模型
- Java内存模型2
- 堆内内存还是堆外内存?
- JVM内存配置详解
- Java内存分配全面浅析
- 深入Java核心 Java内存分配原理精讲
- jvm常量池
- JVM调优总结
- JVM调优总结(一)-- 一些概念
- JVM调优总结(二)-一些概念
- VM调优总结(三)-基本垃圾回收算法
- JVM调优总结(四)-垃圾回收面临的问题
- JVM调优总结(五)-分代垃圾回收详述1
- JVM调优总结(六)-分代垃圾回收详述2
- JVM调优总结(七)-典型配置举例1
- JVM调优总结(八)-典型配置举例2
- JVM调优总结(九)-新一代的垃圾回收算法
- JVM调优总结(十)-调优方法
- 基础
- Java 征途:行者的地图
- Java程序员应该知道的10个面向对象理论
- Java泛型总结
- 序列化与反序列化
- 通过反编译深入理解Java String及intern
- android 加固防止反编译-重新打包
- volatile
- 正确使用 Volatile 变量
- 异常
- 深入理解java异常处理机制
- Java异常处理的10个最佳实践
- Java异常处理手册和最佳实践
- Java提高篇——对象克隆(复制)
- Java中如何克隆集合——ArrayList和HashSet深拷贝
- Java中hashCode的作用
- Java提高篇之hashCode
- 常见正则表达式
- 类
- 理解java类加载器以及ClassLoader类
- 深入探讨 Java 类加载器
- 类加载器的工作原理
- java反射
- 集合
- HashMap的工作原理
- ConcurrentHashMap之实现细节
- java.util.concurrent 之ConcurrentHashMap 源码分析
- HashMap的实现原理和底层数据结构
- 线程
- 关于Java并发编程的总结和思考
- 40个Java多线程问题总结
- Java中的多线程你只要看这一篇就够了
- Java多线程干货系列(1):Java多线程基础
- Java非阻塞算法简介
- Java并发的四种风味:Thread、Executor、ForkJoin和Actor
- Java中不同的并发实现的性能比较
- JAVA CAS原理深度分析
- 多个线程之间共享数据的方式
- Java并发编程
- Java并发编程(1):可重入内置锁
- Java并发编程(2):线程中断(含代码)
- Java并发编程(3):线程挂起、恢复与终止的正确方法(含代码)
- Java并发编程(4):守护线程与线程阻塞的四种情况
- Java并发编程(5):volatile变量修饰符—意料之外的问题(含代码)
- Java并发编程(6):Runnable和Thread实现多线程的区别(含代码)
- Java并发编程(7):使用synchronized获取互斥锁的几点说明
- Java并发编程(8):多线程环境中安全使用集合API(含代码)
- Java并发编程(9):死锁(含代码)
- Java并发编程(10):使用wait/notify/notifyAll实现线程间通信的几点重要说明
- java并发编程-II
- Java多线程基础:进程和线程之由来
- Java并发编程:如何创建线程?
- Java并发编程:Thread类的使用
- Java并发编程:synchronized
- Java并发编程:Lock
- Java并发编程:volatile关键字解析
- Java并发编程:深入剖析ThreadLocal
- Java并发编程:CountDownLatch、CyclicBarrier和Semaphore
- Java并发编程:线程间协作的两种方式:wait、notify、notifyAll和Condition
- Synchronized与Lock
- JVM底层又是如何实现synchronized的
- Java synchronized详解
- synchronized 与 Lock 的那点事
- 深入研究 Java Synchronize 和 Lock 的区别与用法
- JAVA编程中的锁机制详解
- Java中的锁
- TreadLocal
- 深入JDK源码之ThreadLocal类
- 聊一聊ThreadLocal
- ThreadLocal
- ThreadLocal的内存泄露
- 多线程设计模式
- Java多线程编程中Future模式的详解
- 原子操作(CAS)
- [译]Java中Wait、Sleep和Yield方法的区别
- 线程池
- 如何合理地估算线程池大小?
- JAVA线程池中队列与池大小的关系
- Java四种线程池的使用
- 深入理解Java之线程池
- java并发编程III
- Java 8并发工具包漫游指南
- 聊聊并发
- 聊聊并发(一)——深入分析Volatile的实现原理
- 聊聊并发(二)——Java SE1.6中的Synchronized
- 文件
- 网络
- index
- 内存文章索引
- 基础文章索引
- 线程文章索引
- 网络文章索引
- IOC
- 设计模式文章索引
- 面试
- Java常量池详解之一道比较蛋疼的面试题
- 近5年133个Java面试问题列表
- Java工程师成神之路
- Java字符串问题Top10
- 设计模式
- Java:单例模式的七种写法
- Java 利用枚举实现单例模式
- 常用jar
- HttpClient和HtmlUnit的比较总结
- IO
- NIO
- NIO入门
- 注解
- Java Annotation认知(包括框架图、详细介绍、示例说明)