简介
一般情况下,我们创建的变量是可以被任意一个线程访问和修改的,但是有时候我们需要每个线程中都含有一个属于自己的变量,每个线程中的get和set方法不会影响到其他线程,这个时候就需要用到JDK提供的ThreadLocal
类,ThreadLocal类主要解决的就是让每个线程都绑定一个自己的值,防止线程对可变的单实例变量或全局变量进行共享,实现了线程封闭
。
ThreadLocal实例一
该例子出自《Java并发编程实战》,例举JDBC的Connection
对象。
在典型的服务器应用程序中,线程从连接池中获得一个获得一个Connection对象,并且用该对象处理请求,使用完后再将对象返还给连接池。如果是在单线程应用中,为了避免每一次调用方法都要传递一个Connection
对象,通常会在程序启动时就初始化这个连接对象。由于JDBC的连接对象不一定是线程安全的,因此,当多线程应用程序在没有协同的情况下使用这个全局变量时,就不是线程安全的。
通过将JDBC的连接保存在ThreadLocal对象中,每个线程都会拥有属于自己的连接,代码如下。
private static ThreadLocal<Connection> connectionHolder =
new ThreadLocal<Connection>() {
public Connection initialValue(){
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection(){
return connectionHolder.get();
}
当某个线程初次调用getConnection()
时,就会调用ThreadLocal的initialValue()
来获得初始值,你可以大致将ThreadLocal< T>视为包含了Map< Thread,T>对象,其中保存了特定于该线程的值,当线程终止后,这些值会作为垃圾回收,后面分析源码再详细讲解。
ThreadLocal实例二
相信经过上面的例子,大家应该都知道ThreadLocal类是个什么东西了,下面再来一个例子,
首先,我们要知道SimpleDateFormat
不是线程安全的,至于为什么有兴趣的童鞋可以去查一查,下面上代码
- 正常情况下使用
SimpleDateFormat
public class ThreadLocalTest2 implements Runnable{
private static SimpleDateFormat formatter = new SimpleDateFormat("yyyyMMdd HHmm");
public static void main(String[] args) throws InterruptedException {
ThreadLocalTest2 obj = new ThreadLocalTest2();
for (int i = 0;i < 10;i++){
Thread t = new Thread(obj, "" + i);
Thread.sleep(new Random().nextInt(1200));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter " + formatter.toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//在该线程中改名formatter的pattern
formatter = new SimpleDateFormat();
System.out.println("Thread Name=" + Thread.currentThread().getName() + " changed Formatter " + formatter.toPattern());
}
}
通过创建10个线程去改变全局变量SimpleDateFormat
,可以看到,当第一个线程改变了SimpleDateFormat
的pattern后,其他线程的pattarn也被改变了。这显然不是我们希望看到的。
- 通过
ThreadLocal
创建线程安全的SimpleDateFormat
。
public class ThreadLocalTest implements Runnable{
private static final ThreadLocal<SimpleDateFormat> formatter
= new ThreadLocal<SimpleDateFormat>(){
public SimpleDateFormat initialValue(){
return new SimpleDateFormat("yyyyMMdd HHmm");
}
};
public static void main(String[] args) throws InterruptedException {
ThreadLocalTest obj = new ThreadLocalTest();
for (int i = 0;i < 10;i++){
Thread t = new Thread(obj, "" + i);
Thread.sleep(new Random().nextInt(1000));
t.start();
}
}
@Override
public void run() {
System.out.println("Thread Name=" + Thread.currentThread().getName() + " default Formatter " + formatter.get().toPattern());
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
//在该线程中改名formatter的pattern
formatter.set(new SimpleDateFormat());
System.out.println("Thread Name=" + Thread.currentThread().getName() + " changed Formatter " + formatter.get().toPattern());
}
}
同样是创建10个线程去改变共享变量SimpleDateFormat
的值,可以看到,虽然0号线程改变了pattern,但其他线程并没有改变。
源码分析
从Thread
源代码入手
public class Thread implements Runnable {
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
}
从上面的代码中,我们可以看到,Thread
类维护了两个ThreadLocal
的内部类ThreadLocalMap
,且都是默认为null。那么这个ThreadLocalMap
到底是什么东西?我们可以把它理解为是一个为ThreaLocal类定制化的hashMap
(不在本篇讲解),其中的key就是ThreadLocal
,而value是一个Object
。
static class ThreadLocalMap {
....
//可以看到,它的构造器参数一个是ThreadLocal,一个是Object
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
}
到现在为止,我们理解了每个线程都维护着一个ThreadLocalMap
,这个Map可以理解为是一个存放以key为ThreadLocal
,value为Object
的hashMap
。
那么什么时候对这个map进行存取呢?我们来看一下ThreadLocal
的几个主要方法。
//保存当前线程的副本变量
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
//判断当前线程的ThreadLocalMap初始化没有
if (map != null)
//在map中再插入一个副本变量
map.set(this, value);
else
//延迟初始化,但要set时才创建ThreadLocalMap
createMap(t, value);
}
//返回当前线程的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
//从当前线程中获得现在这个ThreadLocal的副本变量
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
//创建一个value为null的entry
private T setInitialValue() {
T value = initialValue();//初始化的value为null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
结论
通过上面那些内容,我们足以通过得出结论:
- 每个线程的副本变量是存放在当前线程的ThreadLocalMap中,并不是存在ThreadLoca中的,ThreadLocal可以理解为只是一个Thread和ThreadLocalMap的关联量,作为map的key值。
- ThreadLocal类中通过Thread.currentThread()获取到当前线程对象,再通过当前线程调用getMap(Thread T)访问到当前线程中的ThreadLocalMap对象,该对象中存放这共享数据的副本变量。
- 同一个线程可以声明多个ThreadLocal对象,map的key的就是ThreadLocal对象,value就是该对象调用set方法设置的值。
ThreadLocal内存泄漏问题
ThreadLocalMap
中使用的key为ThreadLocal
的弱引用,而value是强引用,如果ThreadLocal
在没有外部强引用的情况下,在垃圾回收时,key就会被清理掉,而value不会被清理掉。这样一来,ThreadLocalMap
中就会出现key为null的Entry。假如我们不做任何措施的话,value永远无法被GC回收,这个时候就可能会产生内存泄漏。ThreadLocalMap
实现中已经考虑了这种情况,再调用remove
方法的时候,会清理key为null的记录。所以使用完ThreadLocal
方法后,最好手动调用remove
方法。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}