1.ThreadLocal的概念
ThreadLocal即线程本地变量,或者称为线程变量。它是一个以ThreadLocal对象为key,任意对象为value的存储结构。这个存储结构附属在某个线程上。也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的值。也就是说ThreadLocal为变量在每个线程中都创建了一个副本,这样每个线程可以访问属于自己的变量副本,对这个变量副本的操作只会影响到该线程自身的数据,不会对其他线程中的数据造成影响。
2.ThreadLocal的实现原理
首先来看一下ThreadLocal的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);
}
这个代码很简单,set方法的作用是用来设置附属在线程上的本地数据,首先获取到当前线程,然后获取当前线程的ThreadLocalMap,如果这个map不存在就创建一个,然后设置本地变量的值。这里面的getMap(t)方法返回的是Thread类的一个成员属性threadLocals。
/**
* Get the map associated with a ThreadLocal. Overriden in
* InheritableThreadLocal.
*
* @param t the current thread
* @return the map
*/
ThreadLocalMap getMap(Thread t) { return t.threadLocals; }
在Thread类中,threadLocals的定义如下:
/* ThreadLocal values pertaining to this thread. This Map is maintained * by the ThreadLocal class.*/
ThreadLocal.ThreadLocalMap threadLocals = null;
在Thread类中并没有提供成员属性threadLocals的设置和访问的代码,对于threadLocals的操作需要通过ThreadLocal来实现,换句话说:ThreadLocal是线程Thread中成员属性threadLocals的管理者。对于ThreadLocal的get,set,remove操作对应的都是对线程实例中threadLocals变量的响应操作。
然后在看一下ThreadLocalMap的定义,源码如下:
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least on entry to put in it.
*/
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);
}
跟普通的Map定义有点不同,ThreadLocalMap内部有个Entry数组,将数据包装成静态内部类Entry对象,存储在table数组中,数组的下标是threadLocal的threadLocalHashCode&(INITIAL_CAPACITY-1),因为数组的大小是2的n次方,那其实这个值就是threadLocalHashCode%table.length,用&而不用%,其实是提升效率。只要数组的大小不变,这个索引下标就不会变。
这里的Entry定义如下:
static class Entry extends WeakReference<ThreadLocal> {
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
Entry被继承WeakReference(弱引用),即当与根对象没有可达路径时就会被自动回收掉,从上面Entry的构造函数可以看出来,被WeakReference引用的对象是ThreadLocal本身,也就是说ThreadLocal对象当外界没有对它的引用后就会被GC回收。但是这里需要特别注意的是,Entry并不会被回收,它还在被ThreadLocalMap的table数组强引用着。所以为了避免造成内存泄漏,在使用ThreadLocal时有一个原则,即ThreadLocal使用后务必调用remove方法。
3.ThreadLocal的应用
ThreadLocal在实际中有大量的应用,举个例子,Spring的声明式事务的实现机制,就是基于ThreadLocal来实现的。在实际应用中,ThreadLocal大致在如下场景中使用。
1).在进行对象跨层传递的时,使用ThreadLocal可以避免多次传递,打破层次间的约束;
2).线程间数据隔离;
3).进行事务操作,用于存储线程事务信息;
4).数据库连接,Session回话管理
ThreadLocal实例
下面我们用一个简单的实例来说明ThreadLocal的使用,比如我们编写了一个数据库连接管理类:
class ConnectManager {
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();
}
}
我们使用数据库的时候首先就是建立连接,然后用完之后关闭就可以,这段代码有一个严重的问题,如果有1个客户端频繁使用数据库,那么就需要建立好多次连接和关闭,服务器可能会吃不消。这时候最好用ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之互不影响。
/**
* 负责数据库连接定义的程序类
* 该类可以负责所有操作线程的数据库连接,利用get()方法可以获得连接对象
*/
public class DatabaseConnection {
private static final String DBDRIVER = "oracle.jdbc.driver.OracleDriver" ;
private static final String DBURL = "jdbc:oracle:thin:@localhost:1521:orcl" ;
private static final String USER = "****" ;
private static final String PASSWORD = "****" ;
private static ThreadLocal<Connection> threadLocal = new ThreadLocal<Connection>() ;
/**
* 负责对外部提供获取的数据库连接对象,该对象通过ThreadLocal获取,如果当前线程没有保存的连接对象,则创建新的连接
* @return 连接对象
*/
public static Connection getConnection() {
Connection conn = threadLocal.get() ; // 先判断一下在ThreadLocal里是否有连接对象
if(conn == null) { // 第一次使用,没有连接,没有连接应该创建一个连接
conn = connectionDatabase() ; // 获取连接对象
threadLocal.set(conn); // 将刚刚创建好的连接对象保存在ThreadLocal之中
}
return conn ; // 返回连接对象
}
/**
* 进行数据库的关闭处理。
*/
public static void close() {
Connection conn = threadLocal.get() ;
if (conn != null) { // 现在有连接对象了
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
threadLocal.remove(); // 从当前线程之中删除掉指定连接
}
}
/**
* 负责创建一个数据库连接对象
* @return 数据库连接的实例化对象
*/
private static Connection connectionDatabase() { // 该方法只能本类调用
Connection conn = null ;
try { // 一旦连接出现了错误,整个程序都无发执行
Class.forName(DBDRIVER) ;
conn = DriverManager.getConnection(DBURL, USER, PASSWORD) ;
} catch (Exception e) {
e.printStackTrace();
}
return conn ; // 获得数据库连接对象
}
}