简析ThreadLocal

看到ThreadLocal的时候多少总会跟线程安全关联在一起,因为在线程安全中涉及到共享数据,但是如果不使用共享数据如何来保证线程安全呢?网上有文章分析说,ThreadLocal的出现是为了从另外的一个角度来解决线程安全的问题,以空间换时间,每个线程拥有一份属于自己的数据副本,线程在运行过程中彼此不互相打扰,进水不犯河水。
这是对ThreadLocal的一个初步感性的认识,但是真正去理解的时候,又发现了ThreadLocalMap这个玩意,它到底和ThreadLocal是什么关系呢,一刚开始接触的时候确实会半天不知道在说什么,希望本文能够整理出一份清晰的脉络,以飨自己和其他人。

ThreadLocal的应用场景

很多时候当我们知道知识、技术或者其他等等在什么时候会用到的时候,往往会理解的更加迅速与透彻。这一小节会给出两个应用案例,一个是JDK注释文档上的官方案例,一个是借鉴的网上的资料。
在这之前先来看看,JDK源代码ThreadLocal类最开始的英文注释:

This class provides thread-local variables. These variables differ from their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g. a user ID or Transaction ID).

翻译过来大概意思就是ThreadLocal提供了线程本地的变量(可以理解为数据),不同“线程同行”有不同的变量,那怎么拿到自己的那一份呢?就是通过通过ThreadLocal对象的get方法获取每个线程自己的数据,当然了,设置的话通过set方法。
好了,那这个ThreadLocal对象一般怎么用呢,怎么玩呢?最后一句说了,ThreadLocal实例对象一般典型的是作为一个类的私有的静态field,与线程的一些状态(这里的状态是个广义的状态,意思应该就是跟线程相关的数据)关系起来。更好的是官方JDK注释给了使用案例。

case x0

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
public class ThreadId {
// Atomic integer containing the next thread ID to be assigned
private static final AtomicInteger nextId = new AtomicInteger(0);
// ThreadLocal作为静态私有变量,就是用来获取和存储每个线程自己的线程ID,帮你屏蔽了内部的实现,压根不用管为什么每个线程能获取到自己的不同ID。
private static final ThreadLocal<Integer> threadId = new ThreadLocal<Integer>() {
//第一次get时候,会默认调用该方法初始化,暂时不理解没关系,你可以当成是默认值,后面会继续从源码角度分析。
protected Integer initialValue() {
return nextId.getAndIncrement();
}
};
// Returns the current thread's unique ID, assigning it if necessary
public static int get() {
return threadId.get();
}
//还可以实现set方法,手动设置线程本地变量
}

这个官方给出的案例,相当于ThreadId类包裹了ThreadLocal,给我们提供了一种方便的获取和设置线程本地数据的途径。

case x1

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;
public class ConnectionManager {
//静态私有的ThreadLocal对象
private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
//设置默认值
@Override
protected Connection initialValue() {
Connection conn = null;
try {
Class.forName("oracle.jdbc.driver.OracleDriver");
conn = DriverManager.getConnection(
"jdbc:oracle:thin:@localhost:13002:orcl", "root",
"root");
} catch (SQLException e) {
e.printStackTrace();
} catch (ClassNotFoundException e) {
e.printStackTrace();
}
return conn;
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static void setConnection(Connection conn) {
connectionHolder.set(conn);
}
}

按照我们前面的思路来看的话,不同的线程通过getConnection()获取到的connection是不同的,各自使用各自不同的链接来操作数据库,而每个线程总是会用自己一开始获取的connection,只要这个connection不被清掉。

多个线程

我们先不管内部实现,先来测试看看,用多个线程去获取链接看是什么情况:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public static void main(String[] args) throws SQLException {
for (int i=0;i<10;i++){
new Thread(new Runnable() {
@Override
public void run() {
Connection conn=ConnectionManager.getConnection();
System.out.println(conn.toString());
try {
conn.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}).start();
}
}

测试数据结果如下:

1
2
3
4
5
6
7
8
9
10
oracle.jdbc.driver.T4CConnection@5b51da40
oracle.jdbc.driver.T4CConnection@3b0b482d
oracle.jdbc.driver.T4CConnection@37acaf5
oracle.jdbc.driver.T4CConnection@6f2f3e6a
oracle.jdbc.driver.T4CConnection@2b7e572f
oracle.jdbc.driver.T4CConnection@42d1696b
oracle.jdbc.driver.T4CConnection@2b8f0eac
oracle.jdbc.driver.T4CConnection@87d61dc
oracle.jdbc.driver.T4CConnection@a84b73
oracle.jdbc.driver.T4CConnection@747870b0

单个线程
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) throws SQLException {
new Thread(new Runnable() {
@Override
public void run() {
while (true){
Connection conn=ConnectionManager.getConnection();
System.out.println(conn.toString());
try {
conn.close();
//ConnectionManager.removeConnection();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}).start();
}

测试数据:

1
2
3
oracle.jdbc.driver.T4CConnection@3975fb62
oracle.jdbc.driver.T4CConnection@3975fb62
oracle.jdbc.driver.T4CConnection@3975fb62

通过上面的测试正面我们前面的猜想是正确的,这样也就实现了Connection对象在多个线程中的完全隔离。据说在Spring容器中管理多线程环境下的Connection对象时,采用的思路和以上代码非常相似,但是还没有进行验证。

ThreadLocal源码探析

get方法

在ThreadLocal的使用过程,用到的最多的就是get和set方法,其实就是存取每个线程自己的本地变量数据。先看get方法的源码是如何实现的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public T get() {
Thread t = Thread.currentThread();
//获得当前这个线程自己的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}

在这里我们一开始所说的ThreadLocalMap浮出水面,这个到底是什么呢?其实很简单,说白了就是当前这个线程自己内部的一个属性变量threadLocals,在Thread线程类中,代码如下:

1
2
3
4
public class Thread implements Runnable {
...
ThreadLocal.ThreadLocalMap threadLocals = null;
}

拿到这个map之后(假设已经被初始化过不为null),这个时候还没结束,真正取出这个存放的值是通过this取出来的,this是什么?this在前两个案例中就是这个静态的私有的nextId和connectionHolder,也就是ThreadLocal对象,通过它作为key,在每个线程自己的ThreadLocalMap中取出了线程本地的变量值。
所以思路还是很清晰的,每个线程有个map,我们在类似于connectionManager这些类中可以定义很多个ThreadLocal对象,所以根据不同的ThreadLocal对象作为key值,可以在map拿到对应的值。
到这儿其实get方法的分析可以结束了,但是其实可以继续看看getEntry的构造:

1
2
3
4
5
6
7
8
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}

setInitialValue设置默认值

在刚才的get方法中,是假设线程所拥有的ThreadLocalMap已经被初始化,但是如果当第一次调用get方法时候,还没有初始化呢?根据上面的代码片段可以看到是调用setInitialValue()方法,方法源代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
private T setInitialValue() {
//调用我们覆盖重写的initialValue(),返回一个默认值
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
//创建一个新的ThreadLocalMap,并且赋值给当前的线程
createMap(t, value);
return value;
}
void createMap(Thread t, T firstValue) {
//以this作为key值,放进map中
t.threadLocals = new ThreadLocalMap(this, firstValue);
}

以上两小节就是关于get方法的这个流程中主要代码实现。

set方法

set方法就更简单了,跟setInitialValue相比就是自己手动设置线程本地变量,而不是通过默认值的方式。

1
2
3
4
5
6
7
8
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

关于ThreadLocal是否存在内存泄漏问题

从上面源码分析中,我们大致可以看出引用关系,当前线程>ThreadLocalMap>table>Entry>弱引用ThreadLocal和强引用value。当线程运行结束的时候,线程对象会被回收,线程内部的ThreadLocalMap也会被回收,table和Entry更不用说的,也会被回收,因此value回收。
但是也有一种情况值得考虑,就是当时线程池的时候,这个时候线程池里面的线程有的是经常活跃的,就不能这么来了,具体可以参考这篇文章,博主做了一个关于用线程池的测验,就是在最后不使用线程本地变量的时候,通过ThreadLocal的remove方法清除变量,这样也就解决了线程池可能存在的内存泄漏问题。

小结

其实问题也没我们想象的那么复杂,我们使用线程本地变量就是跟ThreadLocal打交道就行了,至于ThreadLocalMap只是每个线程Thread内部维护的一个map属性。
内部get实现的时候就是通过当前线程拿到自己的map,然后以ThreadLocal的实例为key值拿到属于自己线程的value值,就这样了。

-EOF-