ThreadLocal
Posted joe-go
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了ThreadLocal相关的知识,希望对你有一定的参考价值。
除了控制资源的访问外,我们还可以通过增加资源来保证所有对象的线程安全。比如,让100个人填写个人信息表,如果只有一支笔,大家就得挨个填,对于管理人员来说,必须保证大家不会去哄抢仅存的一支笔,否则,谁也填不完。当然我们还可以从另外一个角度出发,就是准备100支笔,人手一支,那么所有人都可以各自为营,很快就能完成表格的填写工作。
如果说锁是一种思路,那么ThreadLocal就是第二种思路。
ThreadLocal的简单使用
ThreadLocal是线程的局部变量,也就是说,只有当前线程才能访问。既然是只有当前线程才能访问的数据,自然就是线程安全的,下面是一个简单的示例,了解ThreadLocal的基本用法:
1 public class ThreadLocalDemo { 2 static ThreadLocal<SimpleDateFormat> threadLocal = new ThreadLocal<SimpleDateFormat>(); 3 4 public static class ParseDate implements Runnable{ 5 int i = 0; 6 public ParseDate(int i){ 7 this.i = i; 8 } 9 10 @Override 11 public void run() { 12 try { 13 if (threadLocal.get() == null){ 14 threadLocal.set(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); 15 } 16 Date t = threadLocal.get().parse("2018-10-16 09:13:"+ i%60); 17 System.out.println(i + ":" + t); 18 }catch (ParseException e) { 19 e.printStackTrace(); 20 } 21 } 22 } 23 //测试 24 public static void main(String[] args){ 25 ExecutorService es = Executors.newFixedThreadPool(10); 26 for (int i = 0;i < 1000;i++){ 27 es.execute(new ParseDate(i)); 28 } 29 } 30 }
上述代码第13,14行,如果当前线程不持有SimpleDateformat对象实例。那么就新建一个并把它设置到当前线程中,如果持有,则直接使用。
从上面的例子可以看出,为每一个线程人手分配一个对象的工作并不是由ThreadLocal来完成的,而是需要在应用层面保证的。如果在应用上为不同的线程分配了相同的对象实例,那么ThreadLocal也不能保证线程安全。ThreadLocal只是起到了容器的作用。
ThreadLocal的实现原理
ThreadLocal又是如何保证这些对象只被当前线程所访问呢?下面来看看ThreadLocal的内部实现。
set():
1 public void set(T value) { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) 5 map.set(this, value); 6 else 7 createMap(t, value); 8 }
在set时,首先获得了当前线程对象,然后通过getMap()拿到当前线程的ThreadLocalMap,并将值设置到ThreadLocalMap中。而ThreadLocalMap可以理解为一个Map(虽然不是,但是可以把它简单的理解成HashMap),但是它是定义在Thread内部的成员。
ThreadLocal.ThreadLocalMap threadLocals = null;
上述代码是在Thread类中定义的。可以看出设置到ThreadLocal中的数据,就是写入到了threadLocals这个Map中。其中,key为ThreadLocal当前对象,value就是我们需要的值。而threadLocals本身就保存了当前自己所在线程所有的“局部变量”,也就是一个ThreadL变量的集合。
get():
1 public T get() { 2 Thread t = Thread.currentThread(); 3 ThreadLocalMap map = getMap(t); 4 if (map != null) { 5 ThreadLocalMap.Entry e = map.getEntry(this); 6 if (e != null) { 7 @SuppressWarnings("unchecked") 8 T result = (T)e.value; 9 return result; 10 } 11 } 12 return setInitialValue(); 13 }
首先,get()方法也是先取得当前线程的ThreadLocalMap对象,然后,通过将自己作为key取得内部的实际数据。否则通过setInitialValue()方法,返回null。
在了解了ThreadLocal的set()和get()方法后,就会想到一个问题。那就是这些变量是维护在Thread类内部的,这也就意味着只要线程不退出,对象的引用将一直存在。
线程退出
当线程退出时,Thread类会进行一些清理工作,其中就包括清理ThreadLocalMap,下面方法就是线程退出的源码:
1 private void exit() { 2 if (group != null) { 3 group.threadTerminated(this); 4 group = null; 5 } 6 /* Aggressively null out all reference fields: see bug 4006245 */ 7 target = null; 8 /* Speed the release of some of these resources */ 9 threadLocals = null; 10 inheritableThreadLocals = null; 11 inheritedAccessControlContext = null; 12 blocker = null; 13 uncaughtExceptionHandler = null; 14 }
可以看出在线程退出时,会将threadLocals设置为null,这样会让GC在下一次回收时,回收这个引用。
注意:
如果我们使用线程池,那就意味着当前线程未必会退出(比如固定大小的线程池,线程总是存在),如果这样,将一些大的对象设置到了ThreadLocal中(实际上是保存在ThreadLocalMap中),可能会导致内存泄露的可能(意思就是:你设置了大的对象到ThreadLocal中,但是不清理它,在你使用几次后,这个对象就不再有用了,但是它却无法被回收),此时,如果你希望及时回收对象,最好使用ThreadLocal.remove()方法将这个变量移除。还有一种方法是将ThreadLocal的变量手动设置为null(比如:threadLocal = null),那么这个ThreadLocal对应的局部变量会更加容易被垃圾回收器发现,从而加速回收。
ThreadLocal的回收机制
要了解ThreadLocal的回收机制,需要更进一步了解ThreadLocal.ThreadLocalMap的实现。之前说过,ThreadLocalMap类似于HashMap,其实,更准确的说是,ThreadLocalMap更加类似于WeakHashMap。
ThreadLocalMap的实现使用了弱引用。弱引用是比强引用弱得多的引用。Java虚拟机在垃圾回收时,若发现弱引用,就会立即回收。ThreadLocalMap是由一系列的Entry构成的,下面看一下Entry源码:
1 static class Entry extends WeakReference<ThreadLocal<?>> { 2 /** The value associated with this ThreadLocal. */ 3 Object value; 4 5 Entry(ThreadLocal<?> k, Object v) { 6 super(k); 7 value = v; 8 } 9 }
可以看出,每一个Entry都是WeakReference<ThreadLocal>。这里的参数k就是Map的key,v就是Map的value。其中k就是ThreadLocal的实例,作为弱引用使用(super(k)就是调用了WeakReference的构造函数),因此,虽然这里使用ThreadLocal作为Map的key,但是实际上它并不是真的持有ThreadLocal的引用。而当ThreadLocal的外部强引用被回收时,ThreadLocalMap中的key就会变为null。当系统进行ThreadLocalMap清理时(比如将新的变量加入表中,就会自动进行一次清理),就会将这些垃圾数据回收。
ThreadLocal 的性能
为每一个线程分配一个独立的对象对系统性能也许是有帮助的。当然,这不是一定的。这取决于共享对象的内部逻辑。如果共享对象对于竞争的处理容易引起性能损失,这样我们就可以考虑使用ThreadLocal为每个线程分配单独的对象。下面举一个典型的案例:多线程下产生随机数
1 public class RandomDemo { 2 public static final int GEN_COUNT = 10000000;//每个线程要产生的随机数 3 public static final int THREAD_COUNT = 4;//线程数 4 static ExecutorService es = Executors.newFixedThreadPool(THREAD_COUNT);//线程数量为4的固定线程池 5 public static Random random = new Random(123);//共享的Random实例用于产生随机数 6 7 //ThreadLocal 封装了Random 8 public static ThreadLocal<Random> tRnd = new ThreadLocal<Random>(){ 9 @Override 10 protected Random initialValue() { 11 return new Random(123); 12 } 13 }; 14 15 public static class RndTask implements Callable<Long>{ 16 private int mode = 0; 17 public RndTask(int mode){ 18 this.mode = mode; 19 } 20 //mode=0:共享一个Random实例;mode=1:每个线程分配一个实例 21 public Random getRandom(){ 22 if (mode == 0){ 23 return random; 24 }else if (mode == 1){ 25 return tRnd.get(); 26 }else { 27 return null; 28 } 29 } 30 //每个线程产生100000个随机数,完成工作后,记录并返回所消耗的时间 31 @Override 32 public Long call() { 33 long b = System.currentTimeMillis(); 34 for (long i = 0;i < GEN_COUNT;i++){ 35 getRandom().nextInt(); 36 } 37 long e = System.currentTimeMillis(); 38 System.out.println(Thread.currentThread().getName() + " spend " + (e - b) + "ms"); 39 return e-b; 40 } 41 } 42 //测试 43 public static void main(String[] args) throws ExecutionException, InterruptedException { 44 Future<Long>[] futs = new Future[THREAD_COUNT]; 45 for (int i = 0;i < THREAD_COUNT;i++){ 46 futs[i] = es.submit(new RndTask(0)); 47 } 48 long totalTime = 0; 49 for (int i = 0;i < THREAD_COUNT;i++){ 50 totalTime += futs[i].get(); 51 } 52 System.out.println("多线程访问同一个Random实例:"+ totalTime + "ms"); 53 //ThreadLocal情况 54 for (int i = 0;i < THREAD_COUNT;i++){ 55 futs[i] = es.submit(new RndTask(1)); 56 } 57 totalTime = 0; 58 for (int i = 0;i < THREAD_COUNT;i++){ 59 totalTime += futs[i].get(); 60 } 61 System.out.println("使用ThreadLocal包装Random实例:" + totalTime + "ms"); 62 es.shutdown(); 63 } 64 }
输出结果:
pool-1-thread-2 spend 978ms pool-1-thread-1 spend 983ms pool-1-thread-3 spend 808ms pool-1-thread-4 spend 1216ms 多线程访问同一个Random实例:3985ms pool-1-thread-4 spend 109ms pool-1-thread-2 spend 111ms pool-1-thread-3 spend 115ms pool-1-thread-1 spend 130ms 使用ThreadLocal包装Random实例:465ms
可以看出:多线程在共享一个Random实例的情况下,总共耗时3985ms,而在ThreadLocal模式下,仅仅耗时465ms。注意:不是所有的并发情况下都适合ThreadLocal,只是在锁资源竞争激烈的情况下,ThreadLocal才有可能适合。
最后
ThreadLocal的key就是线程本身的弱引用,一个ThreadLocalMap只能存储一种特定的数据类型。如果想让ThreadLocalMap里存储不同数据类型的数据,你如一个String,一个Integer,一个Date,那么就只能多定义几个ThreadLocal,ThreadLocal支持泛型"public class ThreadLocal<T>"。
参考:《Java高并发程序设计》 葛一鸣 郭超 编著:
以上是关于ThreadLocal的主要内容,如果未能解决你的问题,请参考以下文章