Java多线程同步问题的探究(五)

五、你有我有全都有—— ThreadLocal如何解决并发安全性?

前面我们介绍了Java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些 方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。

在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望 文生义:本地线程?

什么是本地线程?

本地线程开玩笑的说:不要迷恋哥,哥只是个传说。

其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local STorage,即线程本地存储技术。不仅仅是Java ,在C++、C#、.NET、Python、Ruby、Perl等开发平台上,该技术都已经得以实现。

当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题 ,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但 资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。

说了这么多,来看看如何正确使用ThreadLocal。

通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:

返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的 值、返回此线程局部变量的当前线程的“初始值”。

还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常, 我们需要对Student的对象进行同步。

下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:

/**  *  * @author x-spirit  */public class ThreadDemo3 extends Thread{     private ThreadLocal stuLocal = new ThreadLocal();     public ThreadDemo3(Student stu){         stuLocal.set(stu);     }     public static void main(String[] args) {         Student stu = new Student();         ThreadDemo3 td31 = new ThreadDemo3(stu);         ThreadDemo3 td32 = new ThreadDemo3(stu);         ThreadDemo3 td33 = new ThreadDemo3(stu);         td31.start();         td32.start();         td33.start();     }     @Override     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 = stuLocal.get();         student.setAge(age);         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());         try {             Thread.sleep(5000);         } catch (InterruptedException ex) {             ex.printStackTrace();         }         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());     }}

貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器 当中把Student对象 set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?

带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。

原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用 java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视 为HashMap。

而在每一个Thread对象中,都有一个ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的 set方法就是首先尝试从当前 线程中取得ThreadLocalMap(以下简称Map)对象。如果取到的不为null,则以ThreadLocal对象自身为key,来取Map中的value。如果取不 到Map对象,则首先为当前线程创建一个ThreadLocalMap,然后以ThreadLocal 对象自身为key,将传入的value放入该Map中。

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

而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在当前线 程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为 key,把initialValue()方法 产生的对象作为value放入新创建的ThreadLocalMap中。

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();     }     private T setInitialValue() {         T value = initialValue();         Thread t = Thread.currentThread();         ThreadLocalMap map = getMap(t);         if (map != null)             map.set(this, value);         else             createMap(t, value);         return value;     }     protected T initialValue() {         return null;     }

这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入 Student对 象,而此时,以ThreadLocal对象为key,Student对象为value的Map是被放入当前的活动线程内的。也就是 Main线程。而当我们的3个 ThreadDemo3线程运行起来以后,调用get()方法,都是试图从当前的活动线程中取得 ThreadLocalMap对象,但当前的活动线程显然已经不 是Main线程了,于是,程序最终执行了ThreadLocal原生的 initialValue()方法,返回了null。

讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。

于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供 线程拥有的本地变量值。

这样,我们就有了下面的正确例程:

/**  *  * @author x-spirit  */public class ThreadDemo3 extends Thread{     private ThreadLocal stuLocal = new ThreadLocal(){         @Override         protected Student initialValue() {             return new Student();         }     };     public ThreadDemo3(){     }     public static void main(String[] args) {         ThreadDemo3 td31 = new ThreadDemo3();         ThreadDemo3 td32 = new ThreadDemo3();         ThreadDemo3 td33 = new ThreadDemo3();         td31.start();         td32.start();         td33.start();     }     @Override     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 = stuLocal.get();         student.setAge(age);         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());         try {             Thread.sleep(5000);         } catch (InterruptedException ex) {             ex.printStackTrace();         }         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());     }}

********** 补疑 ******************

有的童鞋可能会问:“你这个Demo根本没体现出来,每个线程里都有一个ThreadLocal对象;应该是一个ThreadLocal对象对应多个线程 ,你这变成了一对一,完全没体现出ThreadLocal的作用。”

那么我们来看一下如何用一个ThreadLocal对象来对应多个线程:

/** *//**  *  * @author x-spirit  */public class ThreadDemo3 implements Runnable{     private ThreadLocal stuLocal = new ThreadLocal(){         @Override         protected Student initialValue() {             return new Student();         }     };     public ThreadDemo3(){     }     public static void main(String[] args) {         ThreadDemo3 td3 = new ThreadDemo3();         Thread t1 = new Thread(td3);         Thread t2 = new Thread(td3);         Thread t3 = new Thread(td3);         t1.start();         t2.start();         t3.start();     }     @Override     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 = stuLocal.get();         student.setAge(age);         System.out.println("thread " + currentThreadName + " first  read age is:" +  student.getAge());         try {             Thread.sleep(5000);         } catch (InterruptedException ex) {             ex.printStackTrace();         }         System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge ());     }}

这里,多个线程对象都使用同一个实现了Runnable接口的ThreadDemo3对象来构造。这样,多个线程使用的ThreadLocal对象就是同一个 。结果仍然是正确的。但是仔细回想一下,这两种实现方案有什么不同呢?

答案其实很简单,并没有本质上的不同。对于第一种实现,不同的线程对象当中ThreadLocalMap里面的KEY使用的是不同的 ThreadLocal对象。而对于第二种实现,不同的线程对象当中ThreadLocalMap里面的KEY是同一个ThreadLocal对象。但是从本质上讲,不同 的线程对象都是利用其自身的ThreadLocalMap对象来对各自的Student对象进行封装,用ThreadLocal对象作为该ThreadLocalMap的KEY。所 以说,“ThreadLocal的思想精髓就是为每个线程创建独立的资源副本。”这句话并不应当被理解成:一定要使用同一个ThreadLocal对象 来对多个线程进行处理。因为真正用来封装变量的不是ThreadLocal。就算是你的程序中所有线程都共用同一个ThreadLocal对象,而你真 正封装到ThreadLocalMap中去的仍然是.hashCode()方法返回不同值的不同对象。就好比线程就是房东,ThreadLocalMap就是房东的房子。 房东通过ThreadLocal这个中介去和房子里的房客打交道,而房东不管要让房客住进去还是搬出来,都首先要经过ThreadLocal这个中介。

所以提到ThreadLocal,我们不应当顾名思义的认为JDK里面提供ThreadLocal就是提供了一个用来封装本地线程存储的容器,它本身并 没有Map那样的容器功能。真正发挥作用的是ThreadLocalMap。也就是说,事实上,采用ThreadLocal来提高并发行,首先要理解,这不是 一种简单的对象封装,而是一套机制,而这套机制中的三个关键因素(Thread、ThreadLocal、ThreadLocalMap)之间的关系是值得我们引 起注意的。

**************** 补疑完毕 ***************************

可见,要正确使用ThreadLocal,必须注意以下几点:

1. 总是对ThreadLocal中的initialValue()方法进行覆盖。

2. 当使用set()或get()方法时牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程!

3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。

运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多 个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。

现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性 ,线程安全也得以保证。

当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。

把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了 全新的思路。它为每个线程创建一个独立的资源副本,从而将多个线程中的数据隔离开来,避免了同步所产生的性能问题,是一种“以空 间换时间”的解决方案。

但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非 常有意义的。

好了,本系列到此马上就要划上一个圆满的句号了。不知大家有什么意见和疑问没有。希望看到你们的留言。

下一讲中我们就来对之前的内容进行一个总结,顺便讨论一下被遗忘的volatile关键字。敬请期待。

第一个青春是上帝给的;第二个的青春是*自己努力的

Java多线程同步问题的探究(五)

相关文章:

你感兴趣的文章:

标签云: