ThreadLocal笔记
ThreadLocal理解
通过和synchonized比较来理解threadlocal
threadlocal和synchonized区别:
我们知道它俩都是为了解决多线程的并发访问,但是可是有本质区别的。synchonized主要是利用锁的机制,让变量或者代码块在某一时刻仅仅能被一个线程访问。threadlocal能够为每个小城提供变量的副本,使得每个线程在某一时间访问到的并非同一个对象。用于解决多线程并发访问
例如:spring中的事务
它就是借助了ThreadLocal类实现spring会从数据库连接池中获得一个connection,然后把它放入到ThreadLocal,也就和线程绑定了,事务需要提交或者回滚,只需要从threadlocal中拿connection进行操作。
为什么要这么做呢?因为我们开发一般采用三层架构,我们一般通过dao对象在每个方法中打开或关闭事务,但是我们一个service通常需要调多个dao,为了能让多个dao使用同一个数据源连接,实现跨层次的参数共享,我们每个完整的请求周期都是通过线程来处理,通过线程绑定参数,java提供的threadlocal正好解决这点。
ThreadLocal使用
- void set(Object value)
设置当前线程的线程局部变量值 - public Object get()
获取当前线程的线程局部变量值 - public void remove()
删除当前线程的线程局部变量值(该方法是jdk5.0新增方法,虽然对应线程结束会被垃圾回收,但是显示调用它可以加快内存的回收) - protected Object initialValue()
返回对应线程的线程局部变量初始值(这个方法是一个延迟调用方法,也就是线程第一次调用get()或set()时才会执行,并且只执行一次,缺省为null)
贴上小源码帮助理解
获取的时候先获取当前线程,然后通过getMap方法得到一个ThreadLocalMap对象,它是ThreadLocal的静态内部类。
然后可以理解map中ThreadLocal作为键值,然后我们获取到对应的value值(其中的用数组保存了entry,因为可能有多个变量需要线程隔离访问)
其实我们的get方法就是拿到了每个线程独有的ThreadLocalMap,然后再拿当前的ThreadLocald的当前实例,拿到map中对应的entry,然后拿到对应的值返回。(如果map为空,会先进行创建初始化)
内存泄漏引发
我们先来了解一下java中的四个引用
- 强引用
例如“Object o = new Object();”,这就是我们很常见的强引用,在栈中的o指向堆中objecet的实例,这样的引用垃圾回收器是永远不会回收掉被引用的对象实例的。 - 软引用
指一些还有用但并非必需的对象。如果内存将要溢出,那么垃圾回收器就会把这些对象的实例列入回收的范围内。如果内存还不够,那么就会抛出内存溢出异常。(jdk1.2之后,提供了SoftReference类来实现软引用) - 弱引用
用来描述非必需对象的,但是程度比软引用更软,就是无论当前内存是否足够,它都会被回收。(jdk1.2之后,提供WeakReference来实现) - 虚引用
它地位更低,会被垃圾回收器立马回收。(jdk1.2后,提供PhantomReference来实现)
首先我们要明白,我们知道每个Thread维护一个TheadLocalMap,然后它的key是ThreadLocal,对应的value是我们需要的值。也就是ThreadLocal本身是并不储存值得,它只是作为一把钥匙供我们取值。
如图我们看到,ThreadLocalRef指向堆中得ThreadLocal实例,是强引用,而当前线程中有的ThreadLocalMap的Entry需要ThreadLocal作为key取value,但是因为Entry继承了WeakReference,是个弱引用,也就是这个map是使用了ThreadLocal的弱引用作为key,弱引用的对象在GC时会被回收。
这样如果ThreadLocal为null时,那么在ThreadLocalMap中的key中的value就会无法取到,而线程迟迟不结束,就会存在一条强引用线路:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value,但是value却永远不会被访问到,所以存在内存泄漏。
只有当Thread结束,currentThread就不会存在栈中,那么强引用断开,GC才会全部回收。
那么为避免这种状况,我们在使用ThreadLocal变量之后,及时remove()。
但是我看可以看一下TheadLocal的源码,其中的get(),set()在某些时候,调用了expungeStaleEntry 方法来清楚Entry中key为null的value,但是并不是每次都会执行,所以会发生内存泄漏,只有remove()显示调用了expungeStaleEntry 方法。
错误使用ThreadLocal导致线程不安全
public class MyThread extends Thread{
private static ThreadLocal<Number> threadLocal = new ThreadLocal<Number>();
public static Number number = new Number(0);
@Override
public void run() {
number.setI(number.getI()+1);
threadLocal.set(number);
System.out.println(Thread.currentThread().getName()+":"+threadLocal.get().getI());
}
public static void main(String[] args) {
for (int i = 0 ; i<=5;i++){
new MyThread().start();
}
}
public static class Number{
public Number(int i){
this.i = i;
}
private int i;
public int getI() {
return i;
}
public void setI(int i) {
this.i = i;
}
}
}
为什么输出的结果一样呢?不应该每个线程都单独拥有一个ThreadLocal的变量的副本吗?
其实我们回顾一下,其中ThreadLocalMap中保存的是一个对象的引用,当其他线程对这个线程中指向的对象实例做修改时,本质上其实也影响了其他线程所持有的对象引用所指向的值,所以会输出同样的结果。6个线程中保存的都是同一个Number对象的引用。
正确的方式应该是我们要让每个线程中的ThreadLocal都拥有一个新的Number对象。