0
点赞
收藏
分享

微信扫一扫

java多线程之ThreadLocal

1、 什么是ThreadLocal?


早在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序。


  当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。


         从线程的角度看,每个线程都保持一个对其线程局部变量副本(ThreadLocal)的隐式引用,只要线程是活动的并且 ThreadLocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。


ThreadLocal接口主要由四个方法组成initialValue(),get(),set(T),remove(),其中initialValue()方法是一个protected的方法,只有在重写ThreadLocal的时候有用。


• void set(T t):为调用该方法的线程存入一个本线程变量。


• T get(): 返回本线程存入ThreadLocal中的值,没有返回空。


• void remove(): 将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。


• T initialValue():返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。


ThreadLocal使用的一般步骤:


1. 在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。


2. 在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。


3. 在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。


注:值得一提的是,在JDK5.0中,ThreadLocal已经支持泛型,该类的类名已经变为ThreadLocal<T>。




2、ThreadLocal的原理


在查看了java源码后发现,ThreadLocal通过使用ThreadLocalMap(注:这里的Map非java.util.Map子类)实例来存储”线程局部变量”,当第一次设值的时候,如果“map”为空,则创建一个map并set入值,但是这个储值的“Map”并非ThreadLocal的成员变量,而是java.lang.Thread 类的成员变量。



public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null)
return (T)e.value;
}
return setInitialValue();
}



1)在我们使用ThreadLocal过程中,线程结束后,它的”线程局部变量”是如何回收的呢?


首先,保存”线程局部变量”的map并非是ThreadLocal的成员变量, 而是java.lang.Thread的成员变量。也就是说,线程结束的时候,该map的资源也同时被回收。


解析:


ThreadLocal的set,get方法中均通过如下方式获取Map:


ThreadLocalMap map = getMap(t);


而getMap方法的代码如下:


ThreadLocalMap getMap(Thread t) {


return t.threadLocals;


}


可见:ThreadLocalMap实例是作为java.lang.Thread的成员变量存储的,每个线程有唯一的一个threadLocalMap。这个map以ThreadLocal对象为key,”线程局部变量”为值,所以一个线程下可以保存多个”线程局部变量”。对ThreadLocal的操作,实际委托给当前Thread,每个Thread都会有自己独立的ThreadLocalMap实例,存储的仓库是Entry[] table;Entry的key为ThreadLocal,value为存储内容;因此在并发环境下,对ThreadLocal的set或get,不会有任何问题。以下为”线程局部变量”的存储图:

java多线程之ThreadLocal_成员变量

“线程局部变量”的存储图

由于treadLocalMap是java.util.Thread的成员变量,threadLocal作为threadLocalMap中的key值,在一个线程中只能保存一个”线程局部变量”。将ThreadLocalMap作为Thread类的成员变量的好处是:

a.当线程死亡时,threadLocalMap被回收的同时,保存的”线程局部变量”如果不存在其它引用也可以同时被回收。

b. 同一个线程下,可以有多个treadLocal实例,保存多个”线程局部变量”。

 

2)如果线程在线程池中,一直存在,而threadLocal在多个地方被循环放入,会不会造成threadLocal对象无法回收?

如下所示:



publicclassTestMain {
publicstaticvoidmain(String[] args) {
while(true) {
for(intj = 0; j < 10; j++) {
newThreadLocalDomail(newbyte[1024*1024]).getAndPrint();
}
}
}
}

classThreadLocalDomail{
privateThreadLocal<byte[]> threadLocal=newThreadLocal< byte[]>();

public ThreadLocalDomail(byte[] b){
threadLocal.set(b);
}

publicbyte[] getAndPrint(){
byte[] b=threadLocal.get();
System.out.println(b.length);
returnb;
}
}




因为ThreadLocalMap的Entry是(weakReference)弱引用,在外部不再引用threadLocal对象时,线程map中threadLocal对应的key及其value均会被释放,不会造成内存溢出。以上TestMain代码中的new ThreadLocalDomail在每次循环后即被丢弃,可被垃圾回收器回收,代码可持续运行,不会内存溢出。

注:虽然理论上在线程池中使用Threadlocal由于弱引用不会造成内存溢出,但最好还是在Threadlocal业务周期处理完后显示调用remove()清空“线程局部变量”中的值。

 

3、Threadlocal和线程同步比较:

ThreadLocal和线程同步机制都是为了解决多线程中相同变量的访问冲突问题。

1.在同步机制中,通过对象的锁机制保证同一时间只有一个线程访问变量。这时该变量是多个线程共享的,使用同步机制要求程序慎密地分析什么时候对变量进行读写,什么时候需要锁定某个对象,什么时候释放对象锁等繁杂的问题,程序设计和编写难度相对较大。

2.而ThreadLocal则从另一个角度来解决多线程的并发访问。ThreadLocal会为每一个线程提供一个独立的变量副本,从而隔离了多个线程对数据的访问冲突。因为每一个线程都拥有自己的变量副本,从而也就没有必要对该变量进行同步了。ThreadLocal提供了线程安全的共享对象,在编写多线程代码时,可以把不安全的变量封装进ThreadLocal。

概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。

 

4、ThreadLocal最常用的应用场景:

1)解决数据库连接:

private static ThreadLocal<Connection>connectionHolder = new ThreadLocal<Connection>() {
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};

public static Connection getConnection() {
return connectionHolder.get();
}


2)Session管理:

private static final ThreadLocal threadSession = new ThreadLocal();

public static Session getSession() throws InfrastructureException {
Sessions = (Session) threadSession.get();
try {
if (s == null) {
s= getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}


5、SSH框架中Service层和dao层

在使用Spring时,很多人可能对Spring中为什么DAO和Service对象采用单实例方式很迷惑,这些读者是这么认为的:

       DAO对象必须包含一个数据库的连接Connection,而这个Connection不是线程安全的,所以每个DAO都要包含一个不同的Connection对象实例,这样一来DAO对象就不能是单实例的了。

       上述观点对了一半。对的是“每个DAO都要包含一个不同的Connection对象实例”这句话,错的是“DAO对象就不能是单实例”。其实Spring在实现Service和DAO对象时,使用了ThreadLocal这个类,这个是一切的核心!

Spring中DAO和Service都是以单实例的bean形式存在,Spring通过ThreadLocal类将有状态的变量(例如数据库连接Connection)本地线程化,从而做到多线程状况下的安全。在一次请求响应的处理线程中,该线程贯通展示、服务、数据持久化三层,通过ThreadLocal使得所有关联的对象引用到的都是同一个变量。 



举报

相关推荐

0 条评论