java多线程之ThreadLocal

来源:互联网 发布:淘宝家具安装服务 编辑:程序博客网 时间:2024/05/16 14:05

在线程同步中,我们可以使用锁机制,或者通过CAS。但是还有一种方法就是ThreadLocal。这里先举一个生活中的例子,

比如,让100个人填写个人信息表,如果只有一支笔的话,那么大家就得挨个填写,为了让每个人都能完成的填写,我们就需要

保证大家不能哄抢这一支笔,否则谁也填不玩,这时候可能大家可以想到利用锁机制来控制这支笔。其实从另外一种角度出发,

我们可以每人发一支笔让他们填写信息表。如果锁是第一种思路的话,那么ThreadLocal就是第二种思路。接下来先看ThreadLocal

的简单使用:


下面先来看一个简单的示例:

public class ThreadLocalDemo {private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");public static class ParseDate implements Runnable{int i = 0;public ParseDate(int i){this.i = i;}@Overridepublic void run() {try{Date t = sdf.parse("2017-03-34 17:10:"+i%60);System.out.println(i+":"+t);}catch(ParseException e){e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(10);//这里使用到线程池,创建含有十个线程的线程池for(int i =0;i<1000;i++){es.execute(new ParseDate(i));}}}
上述代码在多线程中使用SimpleDateFormat来解析字符串类型的日志。如果你执行上面代码,一般来说,你很可能得到一些异常

这里截取一部分:

Exception in thread "pool-1-thread-116" java.lang.NumberFormatException: For input string: ""at java.lang.NumberFormatException.forInputString(NumberFormatException.java:65)at java.lang.Long.parseLong(Long.java:453)
出现这些问题的原因,是SimpleDateFormat.parse()方法并不是线程安全的。因此在线程池中共享这个对象必然导致错误。

下面我们看看ThreadLocal的解决方法:

public class ThreadLocalDemo {static ThreadLocal<SimpleDateFormat> t1 = new ThreadLocal<SimpleDateFormat>();public static class ParseDate implements Runnable{int i = 0;public ParseDate(int i){this.i = i;}@Overridepublic void run() {try{if(t1.get()==null){t1.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));}Date t = t1.get().parse("2017-03-34 17:10:"+i%60);System.out.println(i+":"+t);}catch(ParseException e){e.printStackTrace();}}}public static void main(String[] args) {ExecutorService es = Executors.newFixedThreadPool(10);//这里使用到线程池,创建含有十个线程的线程池for(int i =0;i<1000;i++){es.execute(new ParseDate(i));}}}

结果截取一部分如下:

992:Mon Apr 03 17:10:32 CST 2017991:Mon Apr 03 17:10:31 CST 2017989:Mon Apr 03 17:10:29 CST 2017987:Mon Apr 03 17:10:27 CST 2017986:Mon Apr 03 17:10:26 CST 2017984:Mon Apr 03 17:10:24 CST 2017983:Mon Apr 03 17:10:23 CST 2017999:Mon Apr 03 17:10:39 CST 2017
在threadlocal的解决方案中10-14行,如果当前线程不持有SimpleDateFormat对象实例。那么就新建一个并把它设置到当前线程中,

如果已经持有,则直接使用。


ThreadLocal的实现原理:

我们需要关注的是ThreadLocal中的set()方法和get()方法。从set()方法开始说,首先看看set()方法的源码:

    public void set(T value) {        Thread t = Thread.currentThread();        ThreadLocalMap map = getMap(t);        if (map != null)            map.set(this, value);        else            createMap(t, value);    }
我们剖析一下,我们在调用set方法时,首先获得当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,

(这里的ThreadLocalMap其实就是线程中的一个成员变量)并将值设入hreadLocalMap中。其实ThreadLocalMap的

实现使用了弱引用,这个概念在《深入理解java虚拟机》有提过,这里解释一下,类似Object obj = new Object() 这类引用表示强引用,

而弱引用的强度顾名思义比强引用弱,带来的结果就是在GC的时候,无论当前内存是否够用,都会回收掉被弱引用关联的对象。

ThreadLocalMap之所以采用弱引用其实好处在于垃圾回收。你看上面的代码可以发现,我们设置在ThreadLocal中的数据,

其实就是写入到ThreadLocalMap这个map中,这里的key表示的就是ThreadLocal这个对象实例,value就是我们需要写入的值。

说到这,我们再提一下ThreadLocalMap采用弱引用为什么好处在于垃圾回收。我们一但在我们程序中将ThreadLocal设置为null的话,

那么ThreadLocal这个强引用没了,而ThreadLocalMap中的key也就是ThreadLocal实例又都是弱引用,自然里面的value就会被垃圾回收。

这其实也是清理ThreadLocal的一种措施。后面还会讲到其他清理措施。

下面再来看看get()方法的源码:

 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();    }
首先,get()方法先取得当前线程的ThreadLocalMap对象。然后通过将自己作为key取得内部的实际数据。


在了解了ThreadLocal的内部实现后,我们自然后会引出一个问题。那就是这些变量时维护在Thread类内部的,

这也意味着只要线程不退出,对象的引用就会一直存在。这里先说明一下,线程在退出时会做一些清理工作,其中

就包括ThreadLocalMap的清理。

如果我们使用线程池,那就意味着当前线程未必会退出。如果这样,将一些打打的对象设置到ThreadLocal中

(它实际是保存在当前线程持有的ThreadLocalMap中),最后可能会造成内存泄漏问题。如果你设置在ThreadLocal

中的对象不再使用了,你需要清理它。有以下两种方法:

  • 通过ThreadLocal.remove()
  • 就是前面说的设置ThreadLocal的实例为null




原创粉丝点击