ThreadLocal的原理以及在Spring中的应用

前言

问题的引出是在项目的时候使用spring框架,在spring中bean都是单例的,因此在想这样会不会导致多线程的问题呢?然后再网上看了很多blog,都是说是使用ThreadLocal来避免多线程问题的,但其实读起来都是很难理解,说的最多的就是,threadLocal是多线程中使用共享资源而避免线程安全问题的另一个解决办法(一个是采用同步)。但其实我的理解不是如此:
ThreadLocal是对线程间共享资源不通信的时候,即不是(生产者-消费者模式),那就没必要把它定义为共享资源(即成员变量),而是采用ThreadLocal来处理这个变量,使得它拥有成员变量的特性(类中甚至线程中全局可用)。
引用其他blog中的一句话:
ThreadLocal的作用是提供线程内的局部变量,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或者组件之间一些公共变量的传递的复杂度。

public class Resource{
	private int num=0;  //成员变量,资源。
}

java内存知识的普及:

由于涉及到多线程的问题,所以有必要提到内存方面的知识,但这里不做详细讲解。
为什么我把资源称作为成员变量呢?这要理解jvm在处理多线程的时候的内存模型,以及jvm中内存的分配:先来说内存的分配,java中把内存主要分成程序计数器,堆,栈,方法区,本地方法区。

  1. 堆:存放new出来的对象,注意,这里的对象存放的是成员变量,而其中的方法不使用时不占用内存。(根据对象的状态分为年老代而年轻代)
  2. 方法区:存放加载的类信息、静态变量以及常量池。
  3. 栈:存放局部变量表(各种基本数据类型以及引用实例),运行时使用的方法帧(含有各种局部变量)。

而在多线程中java的内存模型把堆称作主线程、每个线程各自的栈称为工作线程。

Q:那么为什么把资源称作成员变量呢?
A:因为成员变量是放在堆中的,多个线程通过拷贝主内存的成员变量到工作内存中使用,使用完后又加载回去,即:多个线程是对堆中同一个成员变量操作的。

想详细研究内存,可以看这个博客。
http://blog.csdn.net/shimiso/article/details/8595564

问题的思考

Q1:spring中Dao对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的(每一个线程有应该有自己的Connection),因此这个Connection不能是单例的,但是Dao又怎么可以是单例的呢?
Answer:
第一,我们必须明确,Dao类是单例的那么说明它一定不能有成员变量!!
然后继续分析web分为三层架构,web、service、dao三层,所以数据库连接这个资源Connection一般放在Dao层初始化(new Connection)但是,有这样一个问题,dao层中有很多方法,不可能每一个方法中都new一个Connection来处理数据库的连接问题,这样会造成资源开销过大,所以,好像我们不可避免的必须在dao层把Connection变成成员变量,上面也说了,对于成员变量如果要避免并发问题,两种方法:

  1. 多例
  2. 单例中,关于成员变量的代码保持原子性或者同步,但是这种方法在对于编码的技巧和开销很难。

那只有使用多例吗?但是spring中dao层都是单例的,那是如何实现dao类是单例且Connection具有全局性质呢?
没错,就是使用ThreadLocal把Connection变成拥有成员变量的特性(可以在全局使用)。

Q2:多例是可以解决把Connection当作成员变量而造成的多线程安全问题的,为什么不使用多例?
Answer:
如果非常非常多的并发量,有可能会造成内存方面的问题,且性能非常差。所以spring用单例多线程模式来解决这个问题了。

什么是ThreadLocal?

通过上面的问题,应该明白了spring为什么要使用ThreadLocal了,以及ThreadLocal的作用:把一个局部变量变成拥有成员变量的属性。
那么ThreadLocal是怎么工作的呢?
这里写图片描述
如上图可以看出,ThreadLocal实际上,是一个中介,通过这个中介,可以为每一个线程都创建自己独有的ThreadLocalMap 集合,并为当前线程设置一个自己的变量,存到每个线程对应的ThreadLocalMap 集合(key为当前ThreadLocal)中。这样就实现了,线程隔离使用变量。
且,ThreadLocal 是一个泛型类,因此,最后存入ThreadLocalMap中key类型为ThreadLocal ,value类型为 T

这篇blog主要还是讲ThreadLocal到底作用是什么,以纠正网上很多说用来处理共享资源问题的错误,具体ThreadLocal源码就不分析了。可对比下面的源码结合上面的图分析.

//ThreadLocal的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();
    }
//ThreadLocal的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);
    }

ThreadLocal真的会造成内存泄漏吗?:

网上很多都说由于弱引用,导致value值不能被释放,所以ThreadLocal会内存泄漏。总之我看了很多博客都没有说明白什么情况下会内存泄漏。这里写一下自己的理解,其实感觉还是不是很明白-。-

Q1:为什么jdk源码里有一段注释是推荐 使用ThreadLocal的时候加上private static
Answer:一般来说,ThreadLocal是运用在***多实例多线程***的场景下的,如果不把它设置成static的话,那么每一个线程都会实例化一个ThreadLocal对象,但是实际上每个ThreadLocal对象的作用是一样的,所以重复的对象是浪费。但是ThreadLocal对象用static的话,那么它的生命周期会特别长,这样更容易导致内存泄漏。但是好处是不会重复产生对象。

Q2:ThreadLocalMap为什么对ThreadLocal的引用要设置成弱引用?
Answer: 如果在多实例多线程的场景下,没有static的话,会创建多个ThreadLocal。

  • 如果是强引用的话,那么在使用ThreadLocal的对象被回收后,ThreadLocalMap仍然会对ThreadLocal有一个强引用,导致threadLocal一直不能被回收,且此时有多个ThreadLocal对象,这样更容易导致内存泄漏。
  • 如果是弱引用的话,那么在使用那么在使用ThreadLocal的对象被回收后,ThreadLcoalMap对ThreadLocal的引用由于是弱引用,因此ThreadLocal也会被挥手,ThreadLocalMap中的key变成null,而Value仍然存在,但是,ThreadLocal在每一次set过程中,都会把key=null的value回收掉,所以弱引用的好处就是使得ThreadLocal可以回收一部分value。但是这仍然会导致多个ThreadLocal对象以及可能发生的内存泄漏。

因此,最好的办法就是使用static,然后在使用完ThreadLocal的时候,采用它的remove方法。

Q1:什么时候最容易发生内存泄漏呢?
Answer:在ThreadLocal和线程池一起使用的时候,因为线程池中的线程用完之后只是放回线程池,而不是关闭,那么static化的ThreadLocal就会一直存在,ThreadLocalMap中的value就会一直存在。所以,在使用连接池的时候,线程close的同时,ThreaddLocal.remove();

//ThreadLocal.ThreadLocalMap 的remove方法
private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

题外话

单实例多线程场景:

比如spring中Dao层就属于单实例多线程,Tomcat服务器启动的servlet也是。这种场景要求,单实例在处理成员变量的时候一定要同步,或者干脆没有成员变量。

多实例多线程场景:

就是根本不用考虑线程安全问题,反正一个线程一个实例不会有线程间共享资源,Struts2中的Action接收一个请求就产生一个实例,就是这种场景。

总结

1.一个ThreadLocal对应一个对象。  ThreadLocal<User> threadLocal = new ThreadLocal();    
User user =  new User();
   threadLocal.set(user)
 2. ThreadLocal 对于同一个线程的作用:在同一个线程中的任何地方都可以取到设置的user
     ThreadLocal对于不同线程: 比如原来在t1线程设置了,现在多了一个线程t2,那么首先要在t2中
                            通过threadLocal.set(user)设置。最后才能达到多个线程共享一个user的目的。
3. 2中线程t1到线程t2,需要通过方法参数,把user传递过去,t2才能设置值。
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值