ThreadLocal其实是与线程绑定的一个变量。ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
一句话理解ThreadLocal,向ThreadLocal里面存东西就是向它里面的Map存东西的,然后ThreadLocal把这个Map挂到当前的线程底下,这样Map就只属于这个线程了。
ThreadLocal其实是与线程绑定的一个变量,如此就会出现一个问题:如果没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。通常线程池中对线程管理都是采用线程复用的方法,在线程池中线程很难结束甚至于永远不会结束,这将意味着线程持续的时间将不可预测,甚至与JVM的生命周期一致。举个例字,如果ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,那么内部的集合类和复杂对象所占用的空间可能会开始持续膨胀。
Spring使用ThreadLocal解决线程安全问题。通常只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。一般的Web应用划分为控制层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
所以在JDBCUtils中使用ThreadLocal后,代码如下:
public class JdbcUtils {
private static DruidDataSource dataSource;
private static ThreadLocal<Connection> conns = new ThreadLocal<Connection>();
static {
try {
Properties properties = new Properties();
//读取jdbc.properties属性配置文件
InputStream inputStream = JdbcUtils.class.getClassLoader().getResourceAsStream("jdbc.properties");
//从流中加载数据
properties.load(inputStream);
//创建数据库连接池
dataSource = (DruidDataSource)DruidDataSourceFactory.createDataSource(properties);
// System.out.println(dataSource.getConnection());
} catch (Exception e) {
e.printStackTrace();
}
}
//这是以前不用事务时的写法
// public static Connection getConnection(){
// Connection conn =null;
// try {
// conn= dataSource.getConnection();
// } catch (SQLException throwables) {
// throwables.printStackTrace();
// }
// return conn;
// }
/*
* 获取数据库连接池中的连接
* */
public static Connection getConnection(){
Connection conn =conns.get();
if(conn==null){ //第一次从连接池中取,之后就直接从ThreadLocal中获取
try {
conn = dataSource.getConnection(); //从数据库连接池中获取连接
conns.set(conn); //保存到ThreadLocal对象中,供后面的jdbc操作使用
conn.setAutoCommit(false); //设置为手动管理
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
return conn;
}
/**
* 提交事务,并关闭释放连接
*/
public static void commitAndClose(){
Connection connection = conns.get();
if(connection!=null){ //说明之前已经使用过连接操作过数据库
try {
connection.commit(); //提交事务
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
try {
connection.close(); //关闭连接,释放资源
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
conns.remove(); //一定要执行remove操作,不然会操作,因为Tomcat底层使用了线程池操作
}
/**
* 回滚事务,并关闭释放连接
*/
public static void rollbackAndClose(){
Connection connection = conns.get();
if(connection!=null){ //说明之前已经使用过连接操作过数据库
try {
connection.rollback(); //回滚事务
} catch (SQLException throwables) {
throwables.printStackTrace();
}finally {
try {
connection.close(); //关闭连接,释放资源
} catch (SQLException throwables) {
throwables.printStackTrace();
}
}
}
conns.remove(); //一定要执行remove操作,不然会操作,因为Tomcat底层使用了线程池操作
}
/**
* 关闭连接,放回数据库连接池中
*/
//不用事务之前的处理办法
// public static void close(Connection conn){
// if(conn !=null){
// try {
// conn.close();
// } catch (SQLException throwables) {
// throwables.printStackTrace();
// }
// }
// }
}
不同的线程在使用DAO的时候,会先判断是否为空,如果为null,则新建一个connection,反之则从中获取之前创建的,这样就保证了每一个线程都只有一个Connection,不会去使用其他线程的Connnection,但是这样其实只能保证同一个线程中使用的是同一个Connection,并不能保证不用的线程共享使用Connection。
对于ThreadLocal的存储,就需要提及Thread、 ThreadLocal 及 ThreadLocalMap 三者之间的关系。其关系可以用下图来展示。
每个Thread中有个成员变量ThreadLocalMap,一个Thread只有一个ThreadLocalMap,一个ThreadLocalMap里面存放着多个Entry, key,为ThreadLocal的引用, value为ThreadLocal要存储的值,例如connection或者一个对象等等,如果需要在TheadLocalMap中存放多个与线程相关的值,那么久创建多个ThreadLocal实例就行,每个ThreadLocal对应这一个Value,这样一个线程就可以对应多个value值。
接下来看源码能进一步理解
get()方法
public T get() {
//获取到当前线程
Thread t = Thread.currentThread();
//获取到当前线程内的 ThreadLocalMap 对象,每个线程内都有一个 ThreadLocalMap 对象
ThreadLocalMap map = getMap(t);
if (map != null) {
//获取 ThreadLocalMap 中的 Entry 对象并拿到 Value
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果线程内之前没创建过 ThreadLocalMap,就创建
return setInitialValue();
}
然后就是一个 if ( map != null ) 条件语句,那我们先来看看 if (map == null) 的情况,如果 map == null,则说明之前这个线程中没有创建过 ThreadLocalMap,于是就去调用 setInitialValue 来创建;如果 map != null,我们就应该通过 this 这个引用(也就是当前的 ThreadLocal 对象的引用)来获取它所对应的 Entry,同时再通过这个 Entry 拿到里面的 value,最终作为结果返回。
值得注意的是,这里的 ThreadLocalMap 是保存在线程 Thread 类中的,而不是保存在 ThreadLocal 中的。
getMap()方法
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从这里可以验证,Thread 和 ThreadLocalMap 的关系,可以看出 ThreadLocalMap 是线程的一个成员变量。
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 方法的作用是把我们想要存储的 value 给保存进去。可以看出,首先,它还是需要获取到当前线程的引用,并且利用这个引用来获取到 ThreadLocalMap ;然后,如果 map == null 则去创建这个 map,而当 map != null 的时候就利用 map.set 方法,把 value 给 set 进去。
可以看出,map.set(this, value) 传入的这两个参数中,第一个参数是 this,就是当前 ThreadLocal 的引用,这也再次体现了,在 ThreadLocalMap 中,它的 key 的类型是 ThreadLocal;而第二个参数就是我们所传入的 value,这样一来就可以把这个键值对保存到 ThreadLocalMap 中去了。
内存泄露
查看TheadLocalMap类的源码
static class ThreadLocalMap {
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);
}
static class Entry extends WeakReference<ThreadLocal> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal k, Object v) {
super(k);
value = v;
}
}
}
下面以图片的形式展示ThreadLocal的内存模型
图中的虚线为弱引用,在ThreadLocal有一个比较重要的操作就是必须要在每次用完后,必须要执行ThreadLocal.remove操作来释放value,因为通常我们在jdbc中都会使用线程池,如果用完后不及时回收,那么在GC垃圾回收的时候,会把ThreadLocal实例进行回收,相当于弱引用断开,而线程Thread由于线程池的复用,会造成Thread与ThreadLocalMap之间的强联系会一直保持,那么里面的value一直停留在内存中而不会被清理,所以要避免内存泄露的话,记得每次使用完ThreadLocal取完相应的value后,执行ThreadLocal.remove操作来释放value就行。
参考文章
https://www.jianshu.com/p/1a5d288bdaee
https://www.jianshu.com/p/f956857a8304
https://blog.csdn.net/Rinvay_Cui/article/details/111035071