本文对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对象不能被回收。

ThreadLocal的应用与实现原理的更多相关文章

  1. 深入解析ThreadLocal 详解、实现原理、使用场景方法以及内存泄漏防范 多线程中篇(十七)

    简介 从名称看,ThreadLocal 也就是thread和local的组合,也就是一个thread有一个local的变量副本 ThreadLocal提供了线程的本地副本,也就是说每个线程将会拥有一个 ...

  2. 【java】ThreadLocal线程变量的实现原理和使用场景

    一.ThreadLocal线程变量的实现原理 1.ThreadLocal核心方法有这个几个 get().set(value).remove() 2.实现原理 ThreadLocal在每个线程都会创建一 ...

  3. ThreadLocal类详解:原理、源码、用法

    以下是本文目录: 1.从数据库连接探究 ThreadLocal 2.剖析 ThreadLocal 源码 3. ThreadLocal 应用场景 4. 通过面试题理解 ThreadLocal 1.从数据 ...

  4. ThreadLocal用法详解和原理

    一.用法 ThreadLocal用于保存某个线程共享变量:对于同一个static ThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量. 1.Thr ...

  5. Java并发编程:ThreadLocal的使用以及实现原理解析

    前言 前面的文章里,我们学习了有关锁的使用,锁的机制是保证同一时刻只能有一个线程访问临界区的资源,也就是通过控制资源的手段来保证线程安全,这固然是一种有效的手段,但程序的运行效率也因此大大降低.那么, ...

  6. ThreadLocal用法详解和原理(转)

    本文转自https://www.cnblogs.com/coshaho/p/5127135.html 感谢作者 一.用法 ThreadLocal用于保存某个线程共享变量:对于同一个static Thr ...

  7. volatile、ThreadLocal的使用场景和原理

    并发编程中的三个概念 原子性 一个或多个操作.要么全部执行完成并且执行过程不会被打断,要么不执行.最常见的例子:i++/i--操作.不是原子性操作,如果不做好同步性就容易造成线程安全问题. 可见性 多 ...

  8. 从源码看Thread&ThreadLocal&ThreadLocalMap的关系与原理

    1.三者的之间的关系 ThreadLocalMap是Thread类的成员变量threadLocals,一个线程拥有一个ThreadLocalMap,一个ThreadLocalMap可以有多个Threa ...

  9. ThreadLocal的正确使用与原理

    ThreadLocal是什么 ThreadLocal是线程Thread中属性threadLocals即ThreadLocal.ThreadLocalMap的管理者,ThreadLocal用于给每个线程 ...

随机推荐

  1. Day3_函数

    为啥要用到函数: 复杂度增大 组织结构不清晰 可读性差 工具就是具备某一种功能的物件,就是程序中函数的概念. 事先准备工具的过程称为函数的定义 遇到特定的场景拿来用就称为函数的调用 函数的分类: 内置 ...

  2. Java虚拟机-垃圾收集器

    垃圾收集器(Garbage Collection, GC)的诞生引导出了三个问题: 哪些内存需要回收? 什么时候回收? 如何回收? 对于线程独占的三个区域(程序计数器.虚拟机栈.本地方法栈)不用过多的 ...

  3. [Java算法分析与设计]--线性结构与顺序表(List)的实现应用

    说到线性结构,我们应该立马能够在脑子里蹦出"Array数组"这个词.在Java当中,数组和对象区别基本数据类型存放在堆当中.它是一连串同类型数据存放的一个整体.通常我们定义的方式为 ...

  4. SpringMVC+GSON 对象序列化--日期格式的处理

    Gson异常强大因此使用它代替了Jackson作为SpringMVC消息转换器. 在自己的项目中,发现对象在序列化后,日期格式出现了问题. 先看问题 在员工表中有一列是生日,字段类型为Date,也就是 ...

  5. RabbitMQ在windows系统安装部署文档

    1.RabbitMQ简介 MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法.应用程序通过读写出入队列的消息(针对应用程序的数据)来通信,而无需专用连接来链接它 ...

  6. cocos2d-x 开发常见问题:

    更改Andriod项目的显示横屏还是竖屏问题: 打开项目中的proj.android/AndroidManifest.xml文件中,更改screenOrientation配置信息: screenOri ...

  7. 使用pypi-server搭建简单的PyPI源

    pypiserver 是一个最基本的PyPI服务器实现, 可以用来上传和维护python包. 本文介绍 pypiserver 在ubuntu上的基本安装, 配置和使用. 1. 基本安装和使用 1.1 ...

  8. 实验效果展示(会声会影+FSCapture)

    第一步,视频录制: 利用屏幕录制软件(Eg:FSCapture,可设定矩形区域)录制信号采集过程,存储. 第二步,视频叠加制作 1)导入视频 2)主轨,复叠轨视频安插&时序调整 3)两个视频图 ...

  9. JDK安装遇见的问题及解决方案

    问题描述: Jdk安装完成后从新启动电脑,打开eclipse报找 不到JRE或JDK,在cmd中输入  java -version 也不显示JDK信息. 后把JDK配置的环境变量path的JAVA_H ...

  10. Redis数据结构简介

    Redis可以存储键与5种不同数据结构类型之间的映射,这5种数据结构类型分别为STRING(字符串).LIST(列表).SET(集合).HASH(散列)和ZSET(有序集合).有一部分Redis命令对 ...