1. ThreadLocal是什么?
首先,学过操作系统的都应该知道,同一个进程中的线程之间的头是相互独立的,数据部分是共享的。现在如果我们想让每个线程都有自己的数据,从而实现线程数据隔壁,那么如何实现?ThreadLocal
就是来做这个事情的,通过ThreadLocal
可以给线程设置自己的局部变量,也可取出自己的局部变量。说白了,ThreadLocal就是想在多线程环境下去保证成员变量的安全。
另外,通过ThreadLocal
的字面意思(线程的局部变量)也可以初步了解其作用。
注意:本文中说的线程的局部变量是指线程自己的数据,不是指方法中的局部变量。
2. ThreadLocal如何实现对线程局部变量的操作
2.1 分析
首先,我们查看一下ThreadLocal
类中的源码:
以下是ThreadLocal
类中的部分源码:
public class ThreadLocal<T> {
ThreadLocal.ThreadLocalMap threadLocals = null;
public T get() {
//得到当前的线程对象
Thread t = Thread.currentThread();
//获取该线程的属性:ThreadLocalMap;ThreadLocalMap是用来存放该线程所有的局部变量的容器,是一个map结构
ThreadLocalMap map = getMap(t);
//如果map存在,则将当前ThreadLocal对象作为key,根据key获取内容
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
public void set(T value) {
//得到当前的线程对象
Thread t = Thread.currentThread();
//获取该线程的属性:ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,则以当前ThreadLocal对象为key,在map中设置key-value
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//获取该线程的成员变量:threadLocals
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
//ThreadLocalMap类,是ThreadLocal的内部类
static class ThreadLocalMap {
// Entry类,是ThreadLocalMap的内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
...
tab[i] = new Entry(key, value);
...
}
}
}
注意:如果以上源码看不懂没关系,接着往下看,我详细解释。
public void set(T value) {
//得到当前的线程对象
Thread t = Thread.currentThread();
//获取该线程的属性:ThreadLocalMap
ThreadLocalMap map = getMap(t);
//如果map存在,则以当前ThreadLocal对象为key,从Entry中设置key-value
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
//获取该线程的属性:ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
从getMap
这个方法,我们可以得出结论,threadLocals
是线程的一个成员变量。
从set
方法,我们可以知道,threadLocals
是一个ThreadLocalMap
类型的变量,并且以当前的ThreadLocal
对象为key
,线程要存放的局部变量为value
存放于map中。
由此可见,线程的成员变量threadLocals
就是用来存放线程的局部变量的容器,是一个map结构。为线程存放局部变量时,是以当前的ThreadLocal
对象为key,要存放的局部变量为value存放数据的。另外,线程的局部变量是有线程对象管理的,而不是交给ThreadLocal管理的,因为threadLocals
是线程对象的属性。
现在,我们知道了ThreadLocalMap
是用来存放线程局部变量的容器,那么我们接下来来了解ThreadLocalMap
:
//ThreadLocalMap类,是ThreadLocal的内部类
static class ThreadLocalMap {
// Entry类,是ThreadLocalMap的内部类
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
...
tab[i] = new Entry(key, value);
...
}
}
}
通过以上代码可知,ThreadLocalMap
是ThreadLocal
的内部类,Entry类是ThreadLocalMap
的内部类。ThreadLocalMap
的map结构实际上是通过Entry
类型的数组实现的。
2.2 小结
- 每个Thread维护着一个存放线程局部变量的容器:
ThreadLocalMap
,其是一个map结构。 ThreadLocalMap
是ThreadLocal
的内部类,其map结构是用Entry
数组实现的,也就是数据最终存放在Entry
对象中。- 调用
ThreadLoca
l的set()
给线程设置局部变量时,实际上就是往ThreadLocalMap
设置值,key
是ThreadLocal
对象,值是传递进来的要设置局部变量。 - 调用
ThreadLocal
的get()
方法时,实际上就是往ThreadLocalMap
获取值,key是ThreadLocal
对象。 ThreadLocal
本身并不存储值,它只是作为一个key来让线程从ThreadLocalMap
获取value。
其关系,可以用如下一张图表示:
3. 举一个例子帮助理解
比如,我们在实现银行转账的时候,A给B转500元,具体步骤如下:
1. 从数据库中读取A的钱
2. 从数据库中读取B的钱
3. A的钱 - 500
4. B的钱 + 500
5. 将A现在的钱写入数据库
6. 将B现在的钱写入数据库
这6步中操作数据库时,应该用的是同一个Connection
连接对象,因为这样能够保证这1,2,5,6
操作数据库时如果有一个没有操作成功则整个6步都无效,从而保证了不会一方加钱,另一方不减钱的情况。
那么如何实现这6步中操作数据库时,应该用的是同一个Connection
连接对象?
其实,我们只需要为每个线程对象的局部变量中存放同一个Connection
连接对象对象就可以实现。具体代码如下:
public class DBUtil {
//数据库连接池
private static BasicDataSource source;
//为不同的线程管理连接
private static ThreadLocal<Connection> local;
static {
try {
//加载配置文件
Properties properties = new Properties();
//获取读取流
InputStream stream = DBUtil.class.getClassLoader().getResourceAsStream("连接池/config.properties");
//从配置文件中读取数据
properties.load(stream);
//关闭流
stream.close();
//初始化连接池
source = new BasicDataSource();
//设置驱动
source.setDriverClassName(properties.getProperty("driver"));
//设置url
source.setUrl(properties.getProperty("url"));
//设置用户名
source.setUsername(properties.getProperty("user"));
//设置密码
source.setPassword(properties.getProperty("pwd"));
//初始化线程本地
local = new ThreadLocal<>();
} catch (IOException e) {
e.printStackTrace();
}
}
public static Connection getConnection() throws SQLException {
if(local.get()!=null){
return local.get();
}else{
//获取Connection对象
Connection connection = source.getConnection();
//把Connection放进ThreadLocal里面
local.set(connection);
//返回Connection对象
return connection;
}
}
//关闭数据库连接
public static void closeConnection() {
//从线程中拿到Connection对象
Connection connection = local.get();
try {
if (connection != null) {
//恢复连接为自动提交
connection.setAutoCommit(true);
//这里不是真的把连接关了,只是将该连接归还给连接池
connection.close();
//既然连接已经归还给连接池了,ThreadLocal保存的Connction对象也已经没用了
local.remove();
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
4. 内存泄露的问题
首先,简单介绍一下一些相关术语:
-
内存泄漏:指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。
-
强引用:指如果一个对象=null, 但是该对象在被其他对象使用,则该对象不会被垃圾回收机制回收。
-
弱引用:指如果一个对象=null, 但是该对象在被其他对象使用,则该对象会被垃圾回收机制回收。
看如下一段源码:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
可以看见,实际存储类Entry
对ThreadLocal对象是弱引用关系,也就是ThreadLocalMap对于key是弱引用关系。
为什么要设置成若引用关系?
你可以这样想,如果ThreadLocal = null时,ThreadLocalMap中存放以ThreadLocal为key的键值对是否应该删除,ThreadLocal 是否应该删除???
答案显然是都应该删除。如果是强引用,则都不能删除,只有在该线程被回收时才都被删除。那么至少我们应该把能删除的删除了,弱引用关系能够将ThreadLocal删除,ThreadLocalMap中存放以ThreadLocal为key的键值对通过其他手段删除。
弱引用关系会造成,ThreadLocal = null时,ThreadLocal被回收,但是ThreadLocalMap中存放以ThreadLocal为key的键值对没有被回收,且无法被访问,这样就造成了内存泄漏,事实上早期是这样的,现在这个问题被解决了。
解决的方法就是:在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。当然,我们也可以手动的通过调用ThreadLocal的remove方法进行释放!
参考文件:
https://blog.csdn.net/qq_42862882/article/details/89820017
https://www.jianshu.com/p/ee8c9dccc953