您的位置:首页 > 编程语言 > Java开发

[Java] Java ThreadLocal 学习

2013-02-01 15:53 543 查看
早在Java 1.2推出之时,Java平台中就引入了一个新的支持:java.lang.ThreadLocal,给我们在编写多线程程序时提供了一种新的选择。使用这个工具类可以很简洁地编写出优美的多线程程序,虽然ThreadLocal非常有用,但是似乎现在了解它、使用它的朋友还不多。 

   ThreadLocal是什么 

   ThreadLocal是什么呢?其实ThreadLocal并非是一个线程的本地实现版本,它并不是一个Thread,而是thread local variable(线程局部变量)。也许把它命名为ThreadLocalVar更加合适。线程局部变量(ThreadLocal)其实的功用非常简单,就是为每一个使用该变量的线程都提供一个变量值的副本,是每一个线程都可以独立地改变自己的副本,而不会和其它线程的副本冲突。从线程的角度看,就好像每一个线程都完全拥有该变量。线程局部变量并不是Java的新发明,在其它的一些语言编译器实现(如IBM
XL FORTRAN)中,它在语言的层次提供了直接的支持。因为Java中没有提供在语言层次的直接支持,而是提供了一个ThreadLocal的类来提供支持,所以,在Java中编写线程局部变量的代码相对比较笨拙,这也许是线程局部变量没有在Java中得到很好的普及的一个原因吧。 

   ThreadLocal的设计 

   首先看看ThreadLocal的接口: 

Object get() ; // 返回当前线程的线程局部变量副本
protected Object initialValue(); // 返回该线程局部变量的当前线程的初始值
void set(Object value); // 设置当前线程的线程局部变量副本的值


ThreadLocal有3个方法,其中值得注意的是initialValue(),该方法是一个protected的方法,显然是为了子类重写而特意实现的。该方法返回当前线程在该线程局部变量的初始值,这个方法是一个延迟调用方法,在一个线程第1次调用get()或者set(Object)时才执行,并且仅执行1次。ThreadLocal中的确实实现直接返回一个null: 

protected Object initialValue() { return null; }

  ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单,在ThreadLocal类中有一个Map,用于存储每一个线程的变量的副本。

可以看一下ThreadLocal的源码:

ThreadLocal.set()方法: 以该Thread对象为key,将给定的value存放到该Thread的ThreadLocalMap对象中.

/**
* Sets the current thread's copy of this thread-local variable
* to the specified value.  Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
*        this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取该Thread的ThreadLocalMap对象
if (map != null)
map.set(this, value);
else
createMap(t, value);       //如果ThreadLocalMap对象为空,则创建
}
ThredLocal.createMap(t,value)方法:

/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
*
* @param t the current thread
* @param firstValue value for the initial entry of the map
* @param map the map to store.
*/
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}


看看ThreadLocalMap的构造函数:  将给定的<firstKey,firstValue>作为第一条entry存放到map中。

        /**
         * Construct a new map initially containing (firstKey, firstValue).
         * ThreadLocalMaps are constructed lazily, so we only create
         * one when we have at least one entry to put in it.
         */
        ThreadLocalMap(ThreadLocal firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }


//在看看ThreadLocal.get()方法

/**
* Returns the value in the current thread's copy of this
* thread-local variable.  If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
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的例子:

public class TestWithoutThreadLocal extends Thread {

static  Integer num;
//private static ThreadLocal<Integer> num=new ThreadLocal<Integer>();

@Override
public void run(){
for(int i=0;i<5;i++){

if(num==null){
System.out.println("thread="+this.getName()+" is initing num!");
num=new Integer(0);
}
num=num+1;

System.out.println("thread="+this.getName()+",num="+num);
}

}

public static void main(String[] args) {

TestWithoutThreadLocal t1=new TestWithoutThreadLocal();
t1.setName("t1");
TestWithoutThreadLocal t2=new TestWithoutThreadLocal();
t2.setName("t2");

t1.start();
t2.start();
}

}


执行结果如下,线程t1,和t2都对变量进行了初始化,结果并没有向程序设计的那样,打印出1到10, 而是发生了不安全的情况。

thread=t1 is initing num!
thread=t2 is initing num!
<span style="color:#FF0000;">thread=t1,num=1</span>
thread=t1,num=2
thread=t1,num=3
thread=t1,num=4
thread=t1,num=5
<span style="color:#FF0000;">thread=t2,num=1</span>
thread=t2,num=6
thread=t2,num=7
thread=t2,num=8
thread=t2,num=9


2.使用了ThreadLocal的例子

public class TestWithoutThreadLocal extends Thread {

//static  Integer num;
private static ThreadLocal<Integer> num=new ThreadLocal<Integer>();

@Override
public void run(){
for(int i=0;i<5;i++){
try {
if(this.getName().equalsIgnoreCase("t1")){
Thread.sleep(10);
}else{
Thread.sleep(3);
}
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}

if(num==null){
System.out.println("null");
num=new Integer(0);
}
num=num+1;

System.out.println("thread="+this.getName()+",num="+num);
}

}

public static void main(String[] args) {

TestWithoutThreadLocal t1=new TestWithoutThreadLocal();
t1.setName("t1");
TestWithoutThreadLocal t2=new TestWithoutThreadLocal();
t2.setName("t2");

t1.start();
t2.start();
}

}
执行结果:线程t1,t2各自管理自己的静态变量num,结果符合预期

thread=t1 is initing num!
thread=t2 is initing num!
thread=t1,num=1
thread=t2,num=1
thread=t2,num=2
thread=t2,num=3
thread=t2,num=4
thread=t2,num=5
thread=t1,num=2
thread=t1,num=3
thread=t1,num=4
thread=t1,num=5


本文主要参考了这篇文章:http://www.jcwcn.com/article-30276-1.html

还有一篇文章,关于ThreadLocal的体会,写的很好:http://lkf520java.iteye.com/blog/617368

写这篇帖子的目的不是为了来剖析ThreadLocal,因为坛子里有许多高手已经深入浅出的把ThreadLocal讲解的很清楚了。

特别是lujh99正确理解ThreadLocal这篇帖子,通过JDK源代码把ThreadLocal讲得非常深入浅出,让我深受启发。我写这篇帖子的目的只是为再此作一个补充,想以另外一种通俗易懂的表达方式把自己对ThreadLocal理解写出来。

lujh99 写道

总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点: 

1。每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。 

2。将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

 正如lujh99 所言,ThreadLocal不是用来解决对象共享访问问题的,而是为了处理在多线程环境中,某个方法处理一个业务,需要递归依赖其他方法时,而要在这些方法中共享参数的问题。例如有方法a(),在该方法中调用了方法b(),而在b方法中又调用了方法c(),即a-->b--->c,如果a,b,c都需要使用用户对象,那么我们常用做法就是a(User user)-->b(User user)---c(User user)。但是如果使用ThreadLocal我们就可以用另外一种方式解决:
  在某个接口中定义一个静态的ThreadLocal 对象,例如 public static ThreadLocal  threadLocal=new ThreadLocal ();
  然后让a,b,c方法所在的类假设是类A,类B,类C都实现1中的接口
  在调用a时,使用A.threadLocal.set(user) 把user对象放入ThreadLocal环境
  这样我们在方法a,方法b,方法c可以在不用传参数的前提下,在方法体中使用threadLocal.get()方法就可以得到user对象。

   上面的类A,类B ,类C就可以分别对应我们做web开发时的 web层的Action--->业务逻辑层的Service-->数据访问层的DAO,当我们要在这三层中共享参数时,那么我们就可以使用ThreadLocal 了。

    那么user对象是如何存放到ThreadLocal 中的?

lujh99 写道

将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。 

    经过我的测试,正是如此,当我们调用A.threadLocal.set(user) 时,set()做了以下几件事:
得到当前线程Thread 对象
通过当前线程对象得到当前线程的ThreadLocalMap 对象
将ThreadLocal静态实例作为key,user对象作值,存放到ThreadLocalMap 中。

PS:因为ThreadLocal是静态的,所以每个线程中的ThreadLocalMap的key都是相同的,不同的只是存放的容器ThreadLocalMap。

 

总结:

   其实ThreadLocal 跟我们做web开发时使用的session对象的作用很类似,每当我们向服务器发送一个请求时,web服务器会为该请求创建一个线程,同时会为该请求创建一系列对象,其中包括session(当然在同一个浏览器发送的请求都获得是同一个session对象),所以当我们做web开发时,可以不用担心线程安全问题,自由的往session中存取变量,保存用户状态。同理,当我们在程序中第一次使用A.threadLocal.set(user)
存放参数时,不管在程序的哪个地方,我们都可以通过ThreadLocal  所在的接口访问静态threadLocal对象,同时来共享threadLocal存放的参数,threadLocal就相当于session的作用,来保存当前线程的状态。在我们开发实际开发中,可以任意往threadLocal中共享和存取自己需要的变量,只不过web中的session的生命周期在于客户端的浏览器,而threadLocal中存储的变量的生命周期只在于当前线程,当前结束,threadLocal中存放的参数也被销毁。

 
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息