本文对 ThreadLocal 的分析基于 JDK 8.
本文大纲
1. ThreadLocal 快速上手
2. ThreadLocal 应用场景
3. TheadLocal set 与 get 方法简析
4. TheadLocal 与内存泄漏
1. ThreadLocal 快速上手
ThreadLocal 是 java.lang 包下的一个类, 它可以为每个线程维护一份独立的变量副本. 当线程运行结束后, 线程内部的引用的指向的实例副本都会被回收.
对于初次接触 ThreadLocal 的同学来说, 看了上面这段话可能还是蒙的, 下面我们通过简单的例子快速上手 ThreadLocal.
我们先看看不使用 ThreadLocal 的情况下, 让两个线程共享一个打印 Task 进行打印输出:
- public class ThreadLocalTest1 {
- public static void main(String[] args) {
- Runnable task = new Task();
- new Thread(task, "t1").start();
- new Thread(task, "t2").start();
- }
- static class Task implements Runnable {
- Integer counter = 0; // 多个线程共享的实例
- @Override
- public void run() {
- while (true) {
- System.out.println(Thread.currentThread().getName() + "->" + counter++);
- try {
- Thread.sleep(1000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
毫无疑问, 上面这段代码对 counter 的操作不是线程安全的, 因为 counter 是两个线程间共享的, 所以一个线程对 counter 的修改操作可能会影响另一个线程对 counter 的输出, 下面我节选了部分输出结果:
- t2 -> 0
- t1 -> 0 // t1 线程打印 0
- t2 -> 1
- t1 -> 2 // t1 线程打印 couter 从 0 直接跳到了 2, 因为 t0 线程对 counter 做了修改
- t2 -> 3
- t1 -> 3
可以从下图看出两个线程共享 counter 大致模型:
假设, 现在有一个需求, 要求 t1 和 t2 各自分别进行计数并打印, 那么这时我们就可以使用 ThreadLocal 了, 代码如下:
- public class ThreadLocalTest1 {
- public static void main(String[] args) {
- Runnable task = new Task();
- new Thread(task, "t1").start();
- new Thread(task, "t2").start();
- }
- static class Task implements Runnable {
- ThreadLocal<Integer> cntTl = new ThreadLocal<Integer>() {
- protected Integer initialValue() {
- return 0; // 设置初始值为 0
- }
- };
- @Override
- public void run() {
- while (true) {
- Integer counter = cntTl.get(); // 获取值
- System.out.println(Thread.currentThread().getName() + "->" + counter++);
- cntTl.set(counter); // counter++ 后, 将 counter 值设置回去
- try {
- Thread.sleep(1000L);
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- }
- }
- }
- }
运行上面代码的部分输出结果:
- t1 -> 0
- t2 -> 0
- t1 -> 1
- t2 -> 1
- t1 -> 2
- t2 -> 2
- t1 -> 3
- t2 -> 3
可以看到, t1 和 t2 两个线程分别按顺序输出了 1,2,3...... 这就是因为上面提到过的 ThreadLocal 为每个线程都维护了一份数据的副本, 在本例中的体现就是两个线程 t1,t2 中都各自有一个 counter,t1 和 t2 线程各自操作自己的 counter, 因此对其中一个 counter 的数据进行修改不会对另一个 counter 产生影响.
使用 ThreadLocal 后的模型:
我们再理解深入一点, 每个线程都有一个 ThreadLocalMap 对象, ThreadLocalMap 是 ThreadLocal 的一个内部类:
- /* ThreadLocal values pertaining to this thread. This map is maintained
- * by the ThreadLocal class. */
- ThreadLocal.ThreadLocalMap threadLocals = null; // Thread 类中的 threadLocals 属性
线程 t1 和 t2 各自都有一个 ThreadLocalMap 对象, 暂且就把它看成一个 Map 就行, 这个 Map 以当前 ThreadLocal 对象为 key,value 为我们要保存的值. 当使用 cntTl 调用 get 方法时, 其实是以当前 ThreadLocal 对象为 key 去获取对应的 value.
2. ThreadLocal 应用场景
ThreadLocal 主要有如下两种应用场景:
1. 每个线程需要单独维护一个对象实例, 就像在快速上手提到的那样;
2. 在同一线程执行的不同方法中共享对象实例.
下面将重点分析第 2 种应用场景. 熟悉 web 开发的同学都知道 MVC 模型, C(Controller) 会调用 Service,Service 调用 DAO,DAO 会使用 Connection 去连接数据库. 在直接使用 JDBC 和数据库通信的情况下, 我们需要在 Service 中创建 Connection 对象, 然后打开事务, 并将 Connection 以参数的形式传递给 DAO,DAO 使用 Connection 对象与数据库进行 (开启事务的 Connection 和执行 SQL 的 Connection 必须是同一个), 交互完成后我们在 Service 层进行事务的提交或者回滚. 在不使用 ThreadLocal 的情况下, 我们可能会这样写代码:
一个 SqlRunner 类用于执行 SQL:
- public class SqlRunner {
- public void save(Connection connection, String sql, Object data) {
- System.out.println("sql:" + sql + "executed successfully");
- }
- }
Dao 调用 SqlRunner:
- public class Dao {
- public void save(Connection connection, Object data) { // 接收 Connection
- SqlRunner sqlRunner = new SqlRunner();
- sqlRunner.save(connection, "insert into ...", data);
- }
- }
Service 调用 Dao:
- public class Service {
- Dao dao = new Dao();
- public void save(Object data) {
- Connection connection = new Connection(); // 创建 Connection
- connection.beginTransaction(); // 开启事务
- dao.save(connection, data); // 传入 connection 对象
- connection.commit(); // 提交事务
- }
- }
测试类:
- public class ServiceTest {
- public static void main(String[] args) {
- Service service = new Service();
- service.save("test data");
- }
- }
控制台输出:
- transaction begin
- data: test data, sql: insert into ... executed successfully
- transaction commit
因为开启事务的 Connection 和执行 SQL 的 Connection 必须是同一个, 所以可以看到 Service 中将创建的 Connection 以参数的方式传给了 Dao, 但是这种以传参的方式共享 Connection 会导致每个调用 Dao 方法的 Service 都必须传递 Connection, 显得太不优雅, 下面我们将使用 ThreadLocal 来改变这种局面.
SqlRunner 和上面的一样, 这里不再贴出代码.
新增一个 DataSource 类:
- public class DataSource {
- private static ThreadLocal<Connection> tl = new ThreadLocal<>(); // 使用 ThreadLocal 包装 Connection
- public static void beginTransaction() {
- getCurrentConnection().beginTransaction(); // 开启事务
- }
- public static void commit() {
- getCurrentConnection().commit(); // 提交事务
- }
- public static Connection getCurrentConnection() {
- Connection connection = tl.get(); // 从 ThreadLocal 对象 tl 获取 connection
- if (connection == null) {
- connection = getConnection(); // 没有和当前线程绑定的 connection, 则新建一个
- tl.set(connection); // 将新建的 connection 与当前线程绑定
- }
- return connection;
- }
- private static Connection getConnection() {
- return new Connection(); // 创建线程
- }
- }
- Dao:
- public class Dao {
- public void save(Object data) {
- SqlRunner sqlRunner = new SqlRunner();
- Connection connection = DataSource.getCurrentConnection(); // 获取与当前线程绑定的 connection
- System.out.println("connection in dao:" + connection); // 打印 Dao 中的 connection 对象
- sqlRunner.save(connection, "insert into ...", data);
- }
- }
- Service:
- public class Service {
- Dao dao = new Dao();
- public void save(Object data) {
- DataSource.beginTransaction(); // 使用 Connection 开启事务
- dao.save(data);
- System.out.println("connection in dao:" + DataSource.getCurrentConnection()); // 打印 Service 中 connection 对象
- DataSource.commit(); // 提交事务
- }
- }
测试类和上面的 ServiceTest 相同, 这里不再贴出.
控制台输出:
- transaction begin
- connection in Dao: com.andywooh.texplore.demo.concurrency.threadlocal.Connection@15db9742
- data: test data, sql: insert into ... executed successfully
- connection in Service: com.andywooh.texplore.demo.concurrency.threadlocal.Connection@15db9742
- transaction commit
可以看到, 在 Service 中的 connection 和 Dao 中的 Connection 是同一个对象.
简单对 ThreadLocal 方式在同一个线程中, 不同方法间共享 connection 对象做一个分析: 调用 Service 的 save 方法, 在开启事务前会先使用 DataSource 的 getCurrentConnection 去获得一个连接, 由于是第一次获取 connection, 此时还没有和当前线程绑定的 connection 对象, 所以会调用 getConnection 方法区创建一个 connection 对象, 并将这个 connection 对象和当前线程进行绑定. 当在同一个线程中在 Dao 里再一次调用 getCurrentConnection 时, 由于已经有一个 connection 和当前线程绑定, 所以就会直接返回该 connection 对象, 这样就实现了不传参但是却在 Service 和 Dao 中使用同一个 Connectiond 的功能.
3. TheadLocal set 与 get 方法简析
下面对 ThreadLocal 的 set 和 get 方法进行分析. 再次说明一下, 每个线程都包含一个 ThreadLocalMap, 我们先将其当成一个 Map 就行, ThreadLocalMap 是 ThreadLocal 的一个内部类, 这个 Map 中存储了我们想要和当前线程绑定的值, 其中 key 是当前 ThreadLocal 对象, value 是我们想要保存的值.
set 方法:
- public void set(T value) {
- Thread t = Thread.currentThread(); // 获取当前线程
- ThreadLocalMap map = getMap(t); // 获取当前线程内的 map
- if (map != null)
- map.set(this, value); // map 不为空, 则以当前 ThreadLocal 对象为 key,value 为我们想要保存的值设置到 map 中
- else
- createMap(t, value); // map 为空, 创建一个 map 来保存 value, 当然 key 还是当前 ThreadLocal
- }
get 方法:
- public T get() {
- Thread t = Thread.currentThread(); // 获取当前线程
- ThreadLocalMap map = getMap(t); // 获取当前线程内的 map
- if (map != null) {
- ThreadLocalMap.Entry e = map.getEntry(this); // 以当前 ThreadLocal 对象为 key 取 entry 对象
- if (e != null) {
- @SuppressWarnings("unchecked")
- T result = (T)e.value; // 获取 entry 中包装的值, 也就是我们之前设置进来的 value
- return result;
- }
- }
- return setInitialValue(); // map 为空, 创建一个 map 并给 map 设置一个初始值 entry; 或者 map 中没有 entry, 给已有的 map 添加一个初始值的 entry
- }
4. TheadLocal 与内存泄漏
前面提到过, 当线程销毁的时候, 与线程绑定的相关的对象将会被 GC. 下面的代码展示了 Thread 类中的 exit 方法, 可以看到这里将 threadLocals(就是 ThreadLocalMap) 进行了置空, 方便虚拟机对 ThreadLocalMap 对象进行回收.
- private void exit() {
- if (group != null) {
- group.threadTerminated(this);
- group = null;
- }
- /* Aggressively null out all reference fields: see bug 4006245 */
- target = null;
- /* Speed the release of some of these resources */
- threadLocals = null; // 把 ThreadLocalMap 引用置空
- inheritableThreadLocals = null;
- inheritedAccessControlContext = null;
- blocker = null;
- uncaughtExceptionHandler = null;
- }
但是在一些线程不会死亡的场景, 比如在线池, 因为线程不会结束, 如果处理的不好, 那么和线程绑定的对象就会一直存在, 从而造成内存泄漏.
因为这里涉及到强, 弱引用的知识, 这里简单介绍一下: 我们平常写的 Object obj = new Object() 中的 obj 就是强引用, 只要还有强引用指向一个对象, 这个对象不会被回收. 而对于弱引用, 一旦发现只被弱引用引用的对象, 不管当前内存空间足够与否, 这个对象都会被回收.
ThreadLocalMap 中的 Entry 的 key 就是一个弱引用:
- static class Entry extends WeakReference<ThreadLocal<?>> {
- /** The value associated with this ThreadLocal. */
- Object value;
- Entry(ThreadLocal<?> k, Object v) {
- super(k); // 创建一个弱引用的 key
- value = v;
- }
- }
下图展示了 Thread 对象, ThreadLocal 对象, ThreadLocalMap 对象以及 Entry 对象之间的联系, 其中虚线箭头表示 Entry 中的弱引用 key 指向了 ThreadLocal 对象:
Entry 对象中弱引用 key 指向了我们的 ThreadLocal 对象, 当我们将 ThreadLocal 对象的引用置为 null 后, 就没有强用用指向它, 只剩这个弱引用指向 ThreadLocal 对象, 那么 JVM 会在 GC 的时候回收 ThreadLocal 对象. 然而 Entry 对象中 value 引用指向的 value 对象还是存活的, 这样就会导致 value 对象一直得不到回收. 但是, 在我们调用 ThreadLocal 对象的 get,set,remove 方法时, 会将上述提到的 key 为 nul 对应的 value 对象进行清除, 从而避免了内存泄漏. 值得注意的是, 如果我们在创建一个 ThreadLocal 对象并 set 了一个 value 对象到 ThreadLocalMap, 然后不再调用前面提到的 get,set,remove 方法中的任意一个, 此时就可能会导致这个 value 对象不能被回收.
来源: https://www.cnblogs.com/pedlar/p/10785542.html