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

java多线程之ThreadLocal

2017-09-01 15:56 211 查看
在线程同步中,我们可以使用锁机制,或者通过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;
}
@Override
public 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;
}
@Override
public 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 2017
991:Mon Apr 03 17:10:31 CST 2017
989:Mon Apr 03 17:10:29 CST 2017
987:Mon Apr 03 17:10:27 CST 2017
986:Mon Apr 03 17:10:26 CST 2017
984:Mon Apr 03 17:10:24 CST 2017
983:Mon Apr 03 17:10:23 CST 2017
999: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
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签:  ThreadLocal