基于OpenJDK 12
引 本文主要想了解两个地方:
ThreadLocal实例看起来是在多个线程共享,但实际上是彼此独立的,这个是怎么实现的?
ThreadLocal使用不当真的会OOM吗?如果会,那么原因是啥?
先看一下ThreadLocal的官方API 解释为:
该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本[原文:These variables differ from their normal counterparts in that each thread that accesses one (via its get or set method) has its own, independently initialized copy of the variable.]。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。
大概的意思有两点:
ThreadLocal提供了一种访问某个变量的特殊方式:访问到的变量属于当前线程,即保证每个线程的变量不一样,而同一个线程在任何地方拿到的变量都是一致的,这就是所谓的线程隔离。
如果要使用ThreadLocal,通常定义为private static类型,在我看来最好是定义为private static final类型。
看一段代码:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 // 代码来自: // http://tutorials.jenkov.com/java-concurrency/threadlocal.html public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>(); @Override public void run() { //注意这里 set的值是run函数的内部变量,如果是MyRunnable的全局变量 //则无法起到线程隔离的作用 threadLocal.set((int) (Math.random() * 100D)); try { //sleep两秒的作用是让thread2 set操作在thread1的输出之前执行 //如果线程之间是共用threadLocal,则thread2 set操作会覆盖掉thread1的set操作 //从而两者的输出都是thread2 set的值 Thread.sleep(2000); } catch (InterruptedException e) { System.out.println(e); } System.out.println(threadLocal.get()); } } public static void main(String[] args) throws InterruptedException { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); thread2.start(); thread1.join(); //wait for thread 1 to terminate thread2.join(); //wait for thread 2 to terminate } }
输出结果:
1 2 3 4 5 6 thread1 start thread2 start 38 thread1 join 78 thread2 join
MyRunnable run中sleep两秒的作用是让thread2 set操作在thread1的输出之前执行,如果线程之间是共用threadLocal,则thread2 set操作会覆盖掉thread1的set操作,两者的输出都是thread2 set的值,从而输出的应该是同一个值。
但从代码执行结果来看,thread1、thread2的threadLocal是不同的,也就是实现了线程隔离。
ThreadLocal实例在线程间是如何独立的? 看一眼ThreadLocal set方法:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 public void set(T value) { //currentThread是个native方法,会返回对当前执行线程对象的引用。 Thread t = Thread.currentThread(); //getMap 返回线程自身的threadLocals ThreadLocalMap map = getMap(t); if (map != null) { //把value set到线程自身的ThreadLocalMap中了 map.set(this, value); } else { //线程自身的ThreadLocalMap未初始化,则先初始化,再set createMap(t, value); } } ThreadLocalMap getMap(Thread t) { return t.threadLocals; } //Thread类中 //ThreadLocalMapset的set方法未执行深拷贝,需要注意传递值的类型 ThreadLocal.ThreadLocalMap threadLocals = null;
从代码中可以看到,在set的时候,会根据Thread对象的引用来将值添加到各自线程中。但set的值value还是同一个对象,既然传递的是同一个对象,那就涉及到另一个问题:参数值传递、引用传递的问题了。
基本类型 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Object> threadLocal = new ThreadLocal<>(); // MyRunnable 全局变量 int random; @Override public void run() { random = (int) (Math.random() * 100D); threadLocal.set(random); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println(e); } System.out.println(threadLocal.get()); } } public static void main(String[] args) throws InterruptedException { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); System.out.println("thread1 start"); thread2.start(); System.out.println("thread2 start"); thread1.join(); //wait for thread 1 to terminate System.out.println("thread1 join"); thread2.join(); //wait for thread 2 to terminate System.out.println("thread2 join"); } }
输出结果:
1 2 3 4 5 6 7 thread1 start thread2 start //两个值不同 16 thread1 join 75 thread2 join
从输出可以看出两者隔离了。
引用类型 全局引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Object> threadLocal = new ThreadLocal<>(); // MyRunnable 全局变量 Obj obj = new Obj(); @Override public void run() { obj.value = (int) (Math.random() * 100D); threadLocal.set(obj); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println(e); } System.out.println(((Obj) threadLocal.get()).value); } class Obj { int value; } } public static void main(String[] args) throws InterruptedException { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); System.out.println("thread1 start"); thread2.start(); System.out.println("thread2 start"); thread1.join(); //wait for thread 1 to terminate System.out.println("thread1 join"); thread2.join(); //wait for thread 2 to terminate System.out.println("thread2 join"); } }
输出结果:
1 2 3 4 5 6 7 thread1 start thread2 start //两个值相同 36 36 thread1 join thread2 join
从输出结果来看,当set操作的值是MyRunnable的全局变量,并且是引用类型的时候,无法起到隔离的作用。
局部引用 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 public class ThreadLocalExample { public static class MyRunnable implements Runnable { private ThreadLocal<Object> threadLocal = new ThreadLocal<>(); //Obj obj = new Obj(); @Override public void run() { Obj obj = new Obj(); obj.value = (int) (Math.random() * 100D); threadLocal.set(obj); try { Thread.sleep(2000); } catch (InterruptedException e) { System.out.println(e); } System.out.println(((Obj) threadLocal.get()).value); } class Obj { int value; } } public static void main(String[] args) throws InterruptedException { MyRunnable sharedRunnableInstance = new MyRunnable(); Thread thread1 = new Thread(sharedRunnableInstance); Thread thread2 = new Thread(sharedRunnableInstance); thread1.start(); System.out.println("thread1 start"); thread2.start(); System.out.println("thread2 start"); thread1.join(); //wait for thread 1 to terminate System.out.println("thread1 join"); thread2.join(); //wait for thread 2 to terminate System.out.println("thread2 join"); } }
输出结果:
1 2 3 4 5 6 7 thread1 start thread2 start //两个值不同 12 19 thread1 join thread2 join
从输出结果看,局部引用,可以相互隔离。
到这里可以看出ThreadLocal,只是把set值或引用绑定到了当前线程,但却没有进行相应的深拷贝,所以ThreadLocal要想做的线程隔离,必须是基本类型或者run的局部变量。
ThreadLocal OOM ? 看一下ThreadLocalMap内部Entry:
1 2 3 4 5 6 7 8 9 static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }
从代码中看到,Entry继承了WeakReference,并将ThreadLocal设置为了WeakReference,value设置为强引用。也就是:当没有强引用指向ThreadLocal变量时,它可被回收。
但是,还有一个问题:ThreadLocalMap维护ThreadLocal变量与具体实例的映射,当ThreadLocal变量被回收后,该映射的key变为 null,而该Entry还是在ThreadLocalMap中,从而这些无法清理的Entry,会造成内存泄漏。
ThreadLocal自带的remove、set方法,都无法处理ThreadLocal自身为null的情况,因为代码中都直接取ThreadLocal的threadLocalHashCode属性了,所以如果ThreadLocal自身已经是null,这时调用remove、set会报空指针异常(java.lang.NullPointerException)的。
所以,在使用ThreadLocal的时候,在使用完毕记得remove(remove方法会将Entry的value及Entry自身设置为null并进行清理)。
JDK 12 ThreadLocal代码地址:https://github.com/jiankunking/openjdk12/blob/master/src/java.base/share/classes/java/lang/ThreadLocal.java