简单分析Java线程编程中ThreadLocal类的使用
一、概述
ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是threadlocalvariable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是Java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且ThreadLocal实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过ThreadLocal存取的数据,总是与当前线程相关,也就是说,JVM为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而ThreadLocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
二、API说明
ThreadLocal()
创建一个线程本地变量。
Tget()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
protected TinitialValue()
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用get()方法访问变量的时候。如果线程先于get方法调用set(T)方法,则不会在线程中再调用initialValue方法。
若该实现只返回null;如果程序员希望将线程局部变量初始化为null以外的某个值,则必须为ThreadLocal创建子类,并重写此方法。通常,将使用匿名内部类。initialValue的典型实现将调用一个适当的构造方法,并返回新构造的对象。
voidremove()
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其initialValue。
voidset(Tvalue)
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于initialValue()方法来设置线程局部变量的值。
在程序中一般都重写initialValue方法,以给定一个特定的初始值。
三、一.对ThreadLocal的理解
ThreadLocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。
我们还是先来看一个例子:
classConnectionManager{
privatestaticConnectionconnect=null;
publicstaticConnectionopenConnection(){
if(connect==null){
connect=DriverManager.getConnection();
}
returnconnect;
}
publicstaticvoidcloseConnection(){
if(connect!=null)
connect.close();
}
}
假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openConnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeConnection关闭链接。
所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。
这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。
到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:
classConnectionManager{
privateConnectionconnect=null;
publicConnectionopenConnection(){
if(connect==null){
connect=DriverManager.getConnection();
}
returnconnect;
}
publicvoidcloseConnection(){
if(connect!=null)
connect.close();
}
}
classDao{
publicvoidinsert(){
ConnectionManagerconnectionManager=newConnectionManager();
Connectionconnection=connectionManager.openConnection();
//使用connection进行操作
connectionManager.closeConnection();
}
}
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。
那么这种情况下使用ThreadLocal是再适合不过的了,因为ThreadLocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
但是要注意,虽然ThreadLocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用ThreadLocal要大。
四、实例
创建一个Bean,通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。
/**
*CreatedbyIntelliJIDEA.
*User:leizhimin
*Date:2007-11-23
*Time:10:45:02
*学生
*/
publicclassStudent{
privateintage=0;//年龄
publicintgetAge(){
returnthis.age;
}
publicvoidsetAge(intage){
this.age=age;
}
}
/**
*CreatedbyIntelliJIDEA.
*User:leizhimin
*Date:2007-11-23
*Time:10:53:33
*多线程下测试程序
*/
publicclassThreadLocalDemoimplementsRunnable{
//创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
privatefinalstaticThreadLocalstudentLocal=newThreadLocal();
publicstaticvoidmain(String[]agrs){
ThreadLocalDemotd=newThreadLocalDemo();
Threadt1=newThread(td,"a");
Threadt2=newThread(td,"b");
t1.start();
t2.start();
}
publicvoidrun(){
accessStudent();
}
/**
*示例业务方法,用来测试
*/
publicvoidaccessStudent(){
//获取当前线程的名字
StringcurrentThreadName=Thread.currentThread().getName();
System.out.println(currentThreadName+"isrunning!");
//产生一个随机数并打印
Randomrandom=newRandom();
intage=random.nextInt(100);
System.out.println("thread"+currentThreadName+"setageto:"+age);
//获取一个Student对象,并将随机数年龄插入到对象属性中
Studentstudent=getStudent();
student.setAge(age);
System.out.println("thread"+currentThreadName+"firstreadageis:"+student.getAge());
try{
Thread.sleep(500);
}
catch(InterruptedExceptionex){
ex.printStackTrace();
}
System.out.println("thread"+currentThreadName+"secondreadageis:"+student.getAge());
}
protectedStudentgetStudent(){
//获取本地线程变量并强制转换为Student类型
Studentstudent=(Student)studentLocal.get();
//线程首次执行此方法的时候,studentLocal.get()肯定为null
if(student==null){
//创建一个Student对象,并保存到本地线程变量studentLocal中
student=newStudent();
studentLocal.set(student);
}
returnstudent;
}
}
运行结果:
aisrunning! threadasetageto:76 bisrunning! threadbsetageto:27 threadafirstreadageis:76 threadbfirstreadageis:27 threadasecondreadageis:76 threadbsecondreadageis:27
可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,游兼顾数据的安全性。
五、ThreadLocal使用的一般步骤
1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。