ThreadLocal在数据库连接和session管理下有广泛的应用,了解ThreadLocal对struts、spring等开源代码的理解有很大的帮助。
ThreadLocal如果单纯从名字上来看像是“本地线程”这么个意思,只能说这个名字起的确实不太好,很容易让人产生误解,ThreadLocalVariable(线程本地变量)应该是个更好的名字。我们先看一下官方对ThreadLocal的描述:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
- 每个线程都有自己的局部变量,每个线程都有一个独立于其他线程的上下文来保存这个变量,一个线程的本地变量对其他线程是不可见的
- 独立于变量的初始化副本,ThreadLocal可以给一个初始值,而每个线程都会获得这个初始化值的一个副本,这样才能保证不同的线程都有一份拷贝。
- 状态与某一个线程相关联, ThreadLocal 不是用于解决共享变量的问题的,不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制,理解这点对正确使用ThreadLocal至关重要。
ThreadLocal的理解
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
先来看一个例子(引用Java并发编程:深入剖析ThreadLocal):
class ConnectionManager {
private static Connection connect = null;
public static Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public static void closeConnection() {
if(connect!=null)
connect.close();
}
}
很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
如果在connect使用锁概念,这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。
那么就对代码进一步修改,每次都new一下:
class ConnectionManager {
private Connection connect = null;
public Connection openConnection() {
if(connect == null){
connect = DriverManager.getConnection();
}
return connect;
}
public void closeConnection() {
if(connect!=null)
connect.close();
}
}
class Dao{
public void insert() {
ConnectionManager connectionManager = new ConnectionManager();
Connection connection = connectionManager.openConnection();
//使用connection进行操作
connectionManager.closeConnection();
}
}
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。
那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
解析ThreadLocal源码
ThreadLocal有一个内部类ThreadLocalMap,这个类的实现占了整个ThreadLocal类源码的一多半。这个ThreadLocalMap的作用非常关键,它就是线程真正保存线程自己本地变量的容器。每一个线程都有自己的单独的一个ThreadLocalMap实例,其所有的本地变量都会保存到这一个map中。
ThreadLocal主要提供了:
public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
- get()方法是用来获取ThreadLocal在当前线程中保存的变量副本,
- set()用来设置当前线程中变量的副本,
- remove()用来移除当前线程中变量的副本,
- initialValue()是一个protected方法,一般是用来在使用时进行重写的,它是一个延迟加载方法。
首先我们来看一下ThreadLocal类是如何为每个线程创建一个变量的副本的。
get方法
public T get() {
// 取得当前线程
Thread t = Thread.currentThread();
// 取得当前线程的ThreadLocalMap实例
ThreadLocalMap map = getMap(t);
// 如果map不为空,说明该线程已经有了一个ThreadLocalMap实例
if (map != null) {
// map中保存线程的所有的线程本地变量,我们要去查找当前线程本地变量
ThreadLocalMap.Entry e = map.getEntry(this);
// 如果当前线程本地变量存在这个map中,则返回其对应的值
if (e != null)
return (T)e.value;
}
// 如果map不存在或者map中不存在当前线程本地变量,返回初始值
return setInitialValue();
}
强调一下:Thread对象都有一个ThreadLocalMap类型的属性threadLocals,这个属性是专门用于保存自己所有的线程本地变量的。这个属性在线程对象初始化的时候为null。所以对一个线程对象第一次使用线程本地变量的时候,需要对这个threadLocals属性进行初始化操作。注意要区别 “线程第一次使用本地线程变量”和“第一次使用某一个线程本地线程变量”。
getMap方法
//直接返回线程对象的threadLocals属性
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
setInitialValue方法
private T setInitialValue() {
// 获取初始化值,initialValue 就是我们之前覆盖的方法
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 如果map不为空,将初始化值放入到当前线程的ThreadLocalMap对象中
if (map != null)
map.set(this, value);
else
// 当前线程第一次使用本地线程变量,需要对map进行初始化工作
createMap(t, value);
// 返回初始化值
return value;
}
createMap方法:
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
至此,可能大部分朋友已经明白了ThreadLocal是如何为每个线程创建变量的副本的:
首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前线程ThreadLocal变量,value为变量副本(即T类型的变量)。
初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前线程ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。
然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
set方法
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
//说明线程第一次使用线程本地变量(注意这里的第一次含义)
else
createMap(t, value);
}
remove方法
public void remove() {
//获取当前线程的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
//如果map不为空,则删除该本地变量的值
if (m != null)
m.remove(this);
}
实例
证明通过ThreadLocal能达到在每个线程中创建变量副本的效果
public class Test {
ThreadLocal<Long> longLocal = new ThreadLocal<Long>();
ThreadLocal<String> stringLocal = new ThreadLocal<String>();
public void set() {
longLocal.set(Thread.currentThread().getId());
stringLocal.set(Thread.currentThread().getName());
}
public long getLong() {
return longLocal.get();
}
public String getString() {
return stringLocal.get();
}
public static void main(String[] args) throws InterruptedException {
final Test test = new Test();
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
Thread thread1 = new Thread(){
public void run() {
test.set();
System.out.println(test.getLong());
System.out.println(test.getString());
};
};
thread1.start();
thread1.join();
System.out.println(test.getLong());
System.out.println(test.getString());
}
}
结果:
1
main
12
Thread-0
1
main
可以理解为:
longLocal 的 ThreadLocals中
key:main线程的ThreadLocal 变量, value:1
key:Thread-0线程的当前ThreadLocal 变量, value:12
StringLocal 的 ThreadLocals中
key:main线程的ThreadLocal变量 ,value:main
key:Thread-0线程的当前ThreadLocal变量 ,value:Thread-0
应用场景
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
与synchronized对比
synchronized关键字主要解决多线程共享数据同步问题 。
ThreadLocal使用场合主要解决多线程中数据因并发产生不一致问题 。
ThreadLocal和Synchonized都用于解决多线程并发访问 。但是ThreadLocal与synchronized有本质的区别:
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问 。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享 。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
对象释放问题
在我们使用ThreadLocal过程中,线程结束后,它的”线程局部变量”是如何回收的呢?
首先,保存”线程局部变量”的map并非是ThreadLocal的成员变量, 而是java.lang.Thread的成员变量。也就是说,线程结束的时候,该map的资源也同时被回收。
解析:
ThreadLocal的set,get方法中均通过如下方式获取Map:
ThreadLocalMap map = getMap(t);
而getMap方法的代码如下:
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
代码片段2
可见:ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。以下为”线程局部变量”的存储图:
“线程局部变量”的存储图
由于treadLocalMap是java.util.Thread的成员变量,threadLocal作为threadLocalMap中的key值,在一个线程中只能保存一个”线程局部变量”。将ThreadLocalMap作为Thread类的成员变量的好处是:
a. 当线程死亡时,threadLocalMap被回收的同时,保存的”线程局部变量”如果不存在其它引用也可以同时被回收。
b. 同一个线程下,可以有多个treadLocal实例,保存多个”线程局部变量”。
如果线程在线程池中,一直存在,而threadLocal在多个地方被循环放入,会不会造成threadLocal对象无法回收?
public class TestMain {
public static void main(String[] args) {
while (true) {
for (int j = 0; j < 10; j++) {
new ThreadLocalDomail(new byte[1024*1024]).getAndPrint();
}
}
}
}
class ThreadLocalDomail{
private ThreadLocal<byte[]> threadLocal=new ThreadLocal< byte[]>();
public ThreadLocalDomail(byte[] b){
threadLocal.set(b);
}
public byte[] getAndPrint(){
byte[] b=threadLocal.get();
System.out.println(b.length);
return b;
}
}
代码片段3
因为ThreadLocalMap的Entry是(weakReference)弱引用,在外部不再引用threadLocal对象时,线程map中 threadLocal对应的key及其value均会被释放,不会造成内存溢出。以上TestMain代码中的new ThreadLocalDomail在每次循环后即被丢弃,可被垃圾回收器回收,代码可持续运行,不会内存溢出。
参考: