前言:
相信有一些开发经验的童鞋应该都听过threadlocal,但是可能有一些只是知道threadlocal的使用,并没有真正理解threadlocal的工作的原理,以及在使用threadlocal中可能会遇到的问题,今天会从源码的角度跟大家一起学习threadlocal使用的场景、常见的源码中如何使用它,以及使用threadlocal应该注意什么—内存泄露。
- 1、Threadlocal的使用
- 2、Threadlocal的源码解析
- 3、ThreadLocal内存泄漏的原因
T hreadLocal的使用
Threadlocal和synchronized都是用于解决多线程并发访问。然而ThreadLocal与synchronized有本质的区别。Synchronized是利用锁的机制,使变量或者代码块在某一时刻仅能被一个线程访问。而ThreadLocal使用了线程隔离的方式为每一个线程都提供了变量的副本,使得每一个线程在某一时刻访问到的并非同一个对象。先来看下一个简单的例子热身一下,现在有创建三个线程,每个线程对threadlocal中存的值自增,看看打印结果
package threadLocal;
public class CounterTest {
private final ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(){
@Override
protected Integer initialValue() {
return 0;
}
};
private int getCounter(){
threadLocal.set(threadLocal.get()+1);
return threadLocal.get();
}
public static void main(String[] args) {
CounterTest counterTest = new CounterTest();
Thread t1 = new Thread(new MyTask(counterTest));
Thread t2 = new Thread(new MyTask(counterTest));
Thread t3 = new Thread(new MyTask(counterTest));
t1.start();
t2.start();
t3.start();
}
static class MyTask implements Runnable{
private final CounterTest counterTest;
public MyTask(CounterTest counterTest) {
this.counterTest = counterTest;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
// ④每个线程打出3个序列值
System.out.println("thread[" + Thread.currentThread().getName() + "] --> counterTest["
+ counterTest.getCounter() + "]");
}
}
}
}
执行结果:
很和谐是不是,这就是用线程隔离的方法,使得每个线程都是使用了自己的线程内部的变量。那么我们在哪些地方使用过threadlocal呢?其实我们使用的spring事务就是使用了Threadlocal类,spring会从数据库连接池中获取一个connection,该connection就是放在threadlocal中,也就是和线程绑定了,事务的提交或者回滚,只要从threadlocal中拿到connection进行操作。为何Spring的事务要借助ThreadLocal类?
以JDBC为例,正常的事务代码可能如下:
dbc = new DataBaseConnection();//第1行
Connection con = dbc.getConnection();//第2行
con.setAutoCommit(false);// //第3行
con.executeUpdate(…);//第4行
con.executeUpdate(…);//第5行
con.executeUpdate(…);//第6行
con.commit();第7行
上述代码,可以分成三个部分:
事务准备阶段:第1~3行
业务处理阶段:第4~6行
事务提交阶段:第7行
可以很明显的看到,不管我们开启事务还是执行具体的sql都需要一个具体的数据库连接。
现在我们开发应用一般都采用三层结构,如果我们控制事务的代码都放在DAO(DataAccessObject)对象中,在DAO对象的每个方法当中去打开事务和关闭事务,当Service对象在调用DAO时,如果只调用一个DAO,那我们这样实现则效果不错,但往往我们的Service会调用一系列的DAO对数据库进行多次操作,那么,这个时候我们就无法控制事务的边界了,因为实际应用当中,我们的Service调用的DAO的个数是不确定的,可根据需求而变化,而且还可能出现Service调用Service的情况。
如果不使用ThreadLocal,代码大概就会是这个样子:
但是需要注意一个问题,如何让三个DAO使用同一个数据源连接呢?我们就必须为每个DAO传递同一个数据库连接,要么就是在DAO实例化的时候作为构造方法的参数传递,要么在每个DAO的实例方法中作为方法的参数传递。这两种方式无疑对我们的Spring框架或者开发人员来说都不合适。为了让这个数据库连接可以跨阶段传递,又不显示的进行参数传递,就必须使用别的办法。
Web容器中,每个完整的请求周期会由一个线程来处理。因此,如果我们能将一些参数绑定到线程的话,就可以实现在软件架构中跨层次的参数共享(是隐式的共享)。而JAVA中恰好提供了绑定的方法–使用ThreadLocal。
结合使用Spring里的IOC和AOP,就可以很好的解决这一点。
只要将一个数据库连接放入ThreadLocal中,当前线程执行时只要有使用数据库连接的地方就从ThreadLocal获得就行了。
ThreadLocal源码解析
其实threadlocal工作原理非常的简单,我们先简单的描述一下,然后再从一起阅读分析源码。
- threadlocal本身并不存储内容
- 存储内容的thread对象中的ThreadLocalMap(可以当作Map理解)
- threadLocal是充当Thread对象中ThreadLocalMap中的key值
简单的用下面一幅图表示一下,所以每个线程取到的值,其实是他自己线程内部的值
如果结合上面的文字和图,你能看懂的话,那么恭喜你,threadLocal的工作原理,你基本上已经了解了。下面我们就来详细的看看threadLocal的源码,这里主要介绍threadLocal中的4个方法:
public void set(T value)
设置当前线程的线程局部变量的值
public T get()
返回当前线程所对应的线程局部变量
public void remove()
将当前线程局部变量的值删除,目的是为了减少内存占用,该方法是JDK5.0新增的方法,需要注意的是,当线程结束后(那如果线程不结束呢?),对应该线程的局部变量将会自动被垃圾回收,所以显示调用该方法清除线程局部变量并不是必须的操作,但它可以加快内存的回收速度。
pretected T initialValue()
返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的,这个方法是一个延迟调用方法,在线程第一次调用get()或者set(T t)时才运行,并且仅执行一次,ThreadLocalz中缺省实现直接返回一个null
1、get()方法
public T get() {
Thread t = Thread.currentThread();
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();
}
get方法非常的简单,就是获取当前线程的ThreadLocalMap,然后将this(表示该ThreadLocal)当作key值,获取相应的value,如果获取不到,那么就返回初始设置的值。
2、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方法也是简单的不要不要的啦,首先也是获取当前的线程啦,然后获取当前线程的ThreadLocalMap,如果map不为null那么就往该线程的ThreadLocalMap中set一条数据,key为this(该threadLocal),value为传进去的值,这里解释一下,ThreadLocalMap可以简单的理解成Map(虽然和Map不一样),它里面使用一个table[]存储键值对(Entry)。
3、remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
remove也简单,同样也是获取当前线程的ThreadLocalMap,然后删除该key(ThreadLocal)对应的键值对
4、initialValue()方法
4、protected T initialValue() {
return null;
}
initialValue一般用于我们自己初始化,比如,我们需要存储Integer类型的值,那么可以重写initialValue方法,然后返回0对应的Integer值。
看到上面的四个方法的源码是不是惊呆啦,不过如此嘛,是不是so easy!但是我们今天学习可不仅仅到这里就结束了哦,我们需要继续学习使用ThreadLocal可能会存在什么样的问题—内存泄露,这是我们在面试过程中经常被问到的内容,同时也让我们学的代码更加安全、健壮、稳定
ThreadLocal内存泄漏原因及解决方法
在分析ThreadLocal内存泄漏原因之前,希望大家最好先了解强引用、软引用、弱引用、虚引用。
强引用:就是指在程序代码之中普遍存在的,类似“Object obj=new Object()”这类的引用,只要强引用还存在,垃圾收集器永远不会回收掉被引用的对象实例。
软引用:是用来描述一些还有用但并非必需的对象。对于软引用关联着的对象,在系统将要发生内存溢出异常之前,将会把这些对象实例列进回收范围之中进行第二次回收。如果这次回收还没有足够的内存,才会抛出内存溢出异常。在JDK 1.2之后,提供了SoftReference类来实现软引用。
弱引用:也是用来描述非必需对象的,但是它的强度比软引用更弱一些,被弱引用关联的对象实例只能生存到下一次垃圾收集发生之前。当垃圾收集器工作时,无论当前内存是否足够,都会回收掉只被弱引用关联的对象实例。在JDK 1.2之后,提供了WeakReference类来实现弱引用。
虚引用:也称为幽灵引用或者幻影引用,它是最弱的一种引用关系。一个对象实例是否有虚引用的存在,完全不会对其生存时间构成影响,也无法通过虚引用来取得一个对象实例。为一个对象设置虚引用关联的唯一目的就是能在这个对象实例被收集器回收时收到一个系统通知。在JDK 1.2之后,提供了PhantomReference类来实现虚引用。
了解上述内容之后,我们来看下ThreadLocal中的内部类ThreadLocalMap的定义及描述
static class ThreadLocalMap {
/**
* The entries in this hash map extend WeakReference, using
* its main ref field as the key (which is always a
* ThreadLocal object). Note that null keys (i.e. entry.get()
* == null) mean that the key is no longer referenced, so the
* entry can be expunged from table. Such entries are referred to
* as "stale entries" in the code that follows.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
//...
}
可以看到ThreadLocalMap中的Entry的key是用弱引用,那么到下一次垃圾回收就会被垃圾收集器回收掉,然而我们就永远获取不到该key对应的value,然而这个value又被thread中的threadlocal.threadLocalMap给强引用(如果线程没有结束),那么value将不会被回收,又访问不了,那么这个时候就产生了内存泄漏啦,用下面的图来简单的描述一下。
然而,在前面的描述中已经提到过,当线程结束时,对应value也会被删除,这个时候不会产生内存泄漏,那什么时候我们不会让线程结束呢?—就是我们在使用线程池的时候,我们是会保留一部分线程放在线程池中,这个时候使用threadlocal就要注意啦,如果操作不注意就会产生内存泄漏啦。这个时候我们使用完之后可以手动调用threadlocal.remove()就可以将对应value删除掉,防止内存泄漏。
从上面的分析中貌似是使用了弱引用导致的内存泄漏,那为什么jdk不使用强引用呢?
使用强引用:threadlocal使用完被置null时,我们结合上面的图来分析一下,theadlocal=null,但是currentThread还是持有key-value的强引用,那么key和value都不会被回收,但是又访问不了,同样是产生了内存泄漏。
使用弱引用:当threadlocal对应实例被置null时,那么currentthread中key(threadlocal)会被垃圾回收器回收掉,其对应的value在下一次threadlocal调用你set、get、remove方法时都有机会被回收掉。
比较上述两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障。
因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。
好了,threadlocal就分享到这里,如有错误,欢迎指正!!!
发布者:全栈程序员-用户IM,转载请注明出处:https://javaforall.cn/111217.html原文链接:https://javaforall.cn
【正版授权,激活自己账号】: Jetbrains全家桶Ide使用,1年售后保障,每天仅需1毛
【官方授权 正版激活】: 官方授权 正版激活 支持Jetbrains家族下所有IDE 使用个人JB账号...