ThreadLocal简介
ThreadLocal 的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。
当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
ThreadLocal的使用
ThreadLocal类接口很简单,只有4个方法:
- public void set(Object value):设置当前线程的线程局部变量的值。
- public Object get():该方法返回当前线程所对应的线程局部变量。
- public void remove():将当前线程局部变量的值删除,目的是为了减少内存的占用。不过调用remove不是必要的,因为在线程结束后系统会自动回收ThreadLocal。
- protected Object initialValue():用来设置ThreadLocal的初始值,该函数在调用get函数的时候会第一次调用。通常该函数只会被调用一次,除非手动调用了remove函数之后又调用get函数,这种情况下,get函数中还是会调用initialValue函数。
在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。API方法也相应进行了调整,新版本的API方法分别是void set(T value)
、T get()
以及T initialValue()
。
public class ThisUser {
protected static ThreadLocal<User> instance = new ThreadLocal<>();
public static User get(){
return (instance.get());
}
public static void set(User user){
instance.set(user);
return ;
}
public static void remove(){
instance.remove();
return;
}
}
ThreadLocal实现原理
每个Thread
维护一个ThreadLocalMap
映射表,这个映射表的key就是ThreadLocal
本身,value是真正需要存储的Object。也就是说ThreadLocal本身不存储值,它只是作为一个key来让线程从ThreadLocalMap获取value。
值得注意的是图中的虚线,表示 ThreadLocalMap是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。为什么使用弱引用呢?下面我们分两种情况讨论:
-
key 使用强引用 :引用的
ThreadLocal
的对象被回收了,但是ThreadLocalMap
还持有ThreadLocal
的强引用,如果没有手动删除,ThreadLocal
不会被回收,导致 Entry 内存泄漏。 -
key 使用弱引用 :引用的
ThreadLocal
的对象被回收了,由于ThreadLocalMap
持有ThreadLocal
的弱引用,即使没有手动删除,ThreadLocal
也会被回收。 value 在下一次ThreadLocalMap
调用 set,get,remove 的时候会被清除。
因此,ThreadLocal如果内存泄漏了,那么它的的根源就是:由于 ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏,而不是因为弱引用。而我们使用弱引用,还可以加多一层内存不会泄露的保障。
ThreadLocal 的应用场景
最常见的ThreadLocal使用场景为 用来解决 数据库连接、Session管理等。
举一个在实际中应用的例子,例如,我们有一个银行的BankDAO类和一个个人账户的PeopleDAO类,现在需要个人向银行进行转账,在PeopleDAO类中有一个账户减少的方法,BankDAO类中有一个账户增加的方法,那么这两个方法在调用的时候必须使用同一个Connection数据库连接对象,如果他们使用两个Connection对象,则会开启两段事务,可能出现个人账户减少而银行账户未增加的现象。
使用同一个Connection对象的话,在应用程序中可能会设置为一个全局的数据库连接对象,从而避免在调用每个方法时都传递一个Connection对象。问题是当我们把Connection对象设置为全局变量时,你不能保证是否有其他线程会将这个Connection对象关闭,这样就会出现线程安全问题。
解决办法就是在进行转账操作这个线程中,使用ThreadLocal中获取Connection对象,这样,在调用个人账户减少和银行账户增加的线程中,就能从ThreadLocal中取到同一个Connection对象,并且这个Connection对象为转账操作这个线程独有,不会被其他线程影响,保证了线程安全性。
public class TransactionThreadLocal {
private static ThreadLocal<Connection> tc = new ThreadLocal<>();
/**
* 返回当前线程的数据库连接
* @return
* @throws SQLException
*/
public static Connection getConnection() throws SQLException{
Connection connection = tc.get();
if(connection == null){
connection = C3P0Utils.getConnection();
tc.set(connection);
}
return connection;
}
/**
* 开启事务
* @throws SQLException
*/
public static void startTransaction() throws SQLException{
getConnection().setAutoCommit(false);
}
/**
* 提交事务
* @throws SQLException
*/
public static void commit() throws SQLException{
getConnection().commit();
}
/**
* 事务回滚
* @throws SQLException
*/
public static void rollback() throws SQLException{
getConnection().rollback();
}
/**
* 关闭数据库
* @throws SQLException
*/
public static void close() throws SQLException{
getConnection().close();//关闭数据库
tc.remove();//将该线程的connection对象移除
}
}
使用ThreadLocal对象来保存Connection对象有一个好处,它可以保证当前线程中任何地方的Connection数据库连接都是相同的。ThreadLocal类中有一个get()方法,该方法用来获取当前线程的保存的ThreadLocal中的对象,每个线程保存的是该对象的一个副本,不同线程之间不会互相影响,一个线程对该对象的修改不会影响其他线程中该对象的值。