简单分析Java线程编程中ThreadLocal类的使用
一、概述
threadlocal是什么呢?其实threadlocal并非是一个线程的本地实现版本,它并不是一个thread,而是threadlocalvariable(线程局部变量)。也许把它命名为threadlocalvar更加合适。线程局部变量(threadlocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是java中一种较为特殊的线程绑定机制,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。
从线程的角度看,每个线程都保持一个对其线程局部变量副本的隐式引用,只要线程是活动的并且 threadlocal 实例是可访问的;在线程消失之后,其线程局部实例的所有副本都会被垃圾回收(除非存在对这些副本的其他引用)。
通过threadlocal存取的数据,总是与当前线程相关,也就是说,jvm 为每个运行的线程,绑定了私有的本地实例存取空间,从而为多线程环境常出现的并发访问问题提供了一种隔离机制。
threadlocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在threadlocal类中有一个map,用于存储每一个线程的变量的副本。
概括起来说,对于多线程资源共享的问题,同步机制采用了“以时间换空间”的方式,而threadlocal采用了“以空间换时间”的方式。前者仅提供一份变量,让不同的线程排队访问,而后者为每一个线程都提供了一份变量,因此可以同时访问而互不影响。
二、api说明
threadlocal()
创建一个线程本地变量。
t get()
返回此线程局部变量的当前线程副本中的值,如果这是线程第一次调用该方法,则创建并初始化此副本。
protected t initialvalue()
返回此线程局部变量的当前线程的初始值。最多在每次访问线程来获得每个线程局部变量时调用此方法一次,即线程第一次使用 get() 方法访问变量的时候。如果线程先于 get 方法调用 set(t) 方法,则不会在线程中再调用 initialvalue 方法。
若该实现只返回 null;如果程序员希望将线程局部变量初始化为 null 以外的某个值,则必须为 threadlocal 创建子类,并重写此方法。通常,将使用匿名内部类。initialvalue 的典型实现将调用一个适当的构造方法,并返回新构造的对象。
void remove()
移除此线程局部变量的值。这可能有助于减少线程局部变量的存储需求。如果再次访问此线程局部变量,那么在默认情况下它将拥有其 initialvalue。
void set(t value)
将此线程局部变量的当前线程副本中的值设置为指定值。许多应用程序不需要这项功能,它们只依赖于 initialvalue() 方法来设置线程局部变量的值。
在程序中一般都重写initialvalue方法,以给定一个特定的初始值。
三、一.对threadlocal的理解
threadlocal,很多地方叫做线程本地变量,也有些地方叫做线程本地存储,其实意思差不多。可能很多朋友都知道threadlocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
这句话从字面上看起来很容易理解,但是真正理解并不是那么容易。
我们还是先来看一个例子:
class connectionmanager { private static connection connect = null; public static connection openconnection() { if(connect == null){ connect = drivermanager.getconnection(); } return connect; } public static void closeconnection() { if(connect!=null) connect.close(); } }
假设有这样一个数据库链接管理类,这段代码在单线程中使用是没有任何问题的,但是如果在多线程中使用呢?很显然,在多线程中使用会存在线程安全问题:第一,这里面的2个方法都没有进行同步,很可能在openconnection方法中会多次创建connect;第二,由于connect是共享变量,那么必然在调用connect的地方需要使用到同步来保障线程安全,因为很可能一个线程在使用connect进行数据库操作,而另外一个线程调用closeconnection关闭链接。
所以出于线程安全的考虑,必须将这段代码的两个方法进行同步处理,并且在调用connect的地方需要进行同步处理。
这样将会大大影响程序执行效率,因为一个线程在使用connect进行数据库操作的时候,其他线程只有等待。
那么大家来仔细分析一下这个问题,这地方到底需不需要将connect变量进行共享?事实上,是不需要的。假如每个线程中都有一个connect变量,各个线程之间对connect变量的访问实际上是没有依赖关系的,即一个线程不需要关心其他线程是否对这个connect进行了修改的。
到这里,可能会有朋友想到,既然不需要在线程之间共享这个变量,可以直接这样处理,在每个需要使用数据库连接的方法中具体使用时才创建数据库链接,然后在方法调用完毕再释放这个连接。比如下面这样:
class connectionmanager { private connection connect = null; public connection openconnection() { if(connect == null){ connect = drivermanager.getconnection(); } return connect; } public void closeconnection() { if(connect!=null) connect.close(); } } class dao{ public void insert() { connectionmanager connectionmanager = new connectionmanager(); connection connection = connectionmanager.openconnection(); //使用connection进行操作 connectionmanager.closeconnection(); } }
这样处理确实也没有任何问题,由于每次都是在方法内部创建的连接,那么线程之间自然不存在线程安全问题。但是这样会有一个致命的影响:导致服务器压力非常大,并且严重影响程序执行性能。由于在方法中需要频繁地开启和关闭数据库连接,这样不尽严重影响程序执行效率,还可能导致服务器压力巨大。
那么这种情况下使用threadlocal是再适合不过的了,因为threadlocal在每个线程中对该变量会创建一个副本,即每个线程内部都会有一个该变量,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
但是要注意,虽然threadlocal能够解决上面说的问题,但是由于在每个线程中都创建了副本,所以要考虑它对资源的消耗,比如内存的占用会比不使用threadlocal要大。
四、实例
创建一个bean,通过不同的线程对象设置bean属性,保证各个线程bean对象的独立性。
/** * created by intellij idea. * user: leizhimin * date: 2007-11-23 * time: 10:45:02 * 学生 */ public class student { private int age = 0; //年龄 public int getage() { return this.age; } public void setage(int age) { this.age = age; } } /** * created by intellij idea. * user: leizhimin * date: 2007-11-23 * time: 10:53:33 * 多线程下测试程序 */ public class threadlocaldemo implements runnable { //创建线程局部变量studentlocal,在后面你会发现用来保存student对象 private final static threadlocal studentlocal = new threadlocal(); public static void main(string[] agrs) { threadlocaldemo td = new threadlocaldemo(); thread t1 = new thread(td, "a"); thread t2 = new thread(td, "b"); t1.start(); t2.start(); } public void run() { accessstudent(); } /** * 示例业务方法,用来测试 */ public void accessstudent() { //获取当前线程的名字 string currentthreadname = thread.currentthread().getname(); system.out.println(currentthreadname + " is running!"); //产生一个随机数并打印 random random = new random(); int age = random.nextint(100); system.out.println("thread " + currentthreadname + " set age to:" + age); //获取一个student对象,并将随机数年龄插入到对象属性中 student student = getstudent(); student.setage(age); system.out.println("thread " + currentthreadname + " first read age is:" + student.getage()); try { thread.sleep(500); } catch (interruptedexception ex) { ex.printstacktrace(); } system.out.println("thread " + currentthreadname + " second read age is:" + student.getage()); } protected student getstudent() { //获取本地线程变量并强制转换为student类型 student student = (student) studentlocal.get(); //线程首次执行此方法的时候,studentlocal.get()肯定为null if (student == null) { //创建一个student对象,并保存到本地线程变量studentlocal中 student = new student(); studentlocal.set(student); } return student; } }
运行结果:
a is running! thread a set age to:76 b is running! thread b set age to:27 thread a first read age is:76 thread b first read age is:27 thread a second read age is:76 thread b second read age is:27
可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用threadlocal,既实现多线程并发,游兼顾数据的安全性。
五、threadlocal使用的一般步骤
1、在多线程的类(如threaddemo类)中,创建一个threadlocal对象threadxxx,用来保存线程间需要隔离处理的对象xxx。
2、在threaddemo类中,创建一个获取要隔离访问的数据的方法getxxx(),在方法中判断,若threadlocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
3、在threaddemo类的run()方法中,通过getxxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。