一、ThreadLocal两大使用场景
场景一
-
每个线程需要一个独享的对象,即该对象在线程间隔离(通常是工具类,典型需要使用的类有
SimpleDateFormat
和Random
)下面程序中用
ThreadSafeFormatter
初始化了SimpleDateFormat
实例,在每个线程调用时,都是一个独立的对象,做到了线程间的隔离public class ThreadLocalNormalUsage05 { public static ExecutorService threadPool = Executors.newFixedThreadPool(10); public static void main(String[] args) throws InterruptedException { for (int i = 0; i < 1000; i++) { int finalI = i; threadPool.submit(new Runnable() { @Override public void run() { String date = new ThreadLocalNormalUsage05().date(finalI); System.out.println(date); } }); } threadPool.shutdown(); } public String date(int seconds){ Date date = new Date(1000 * seconds); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get(); return dateFormat.format(date); } } class ThreadSafeFormatter { public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; }
场景二
-
每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
下面程序中,用
ThreadLocal
来保存了User对象,避免了在每个Service中都要传递参数的麻烦public class ThreadLocalNormalUsage06 { public static void main(String[] args) { new Service1().process(""); } } class Service1 { public void process(String name) { User user = new User("超哥"); UserContextHolder.holder.set(user); new Service2().process(); } } class Service2 { public void process() { User user = UserContextHolder.holder.get(); ThreadSafeFormatter.dateFormatThreadLocal.get(); System.out.println("Service2拿到用户名:" + user.name); new Service3().process(); } } class Service3 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service3拿到用户名:" + user.name); } } class UserContextHolder{ public static ThreadLocal<User> holder = new ThreadLocal<>(); } class User { String name; public User (String name){ this.name = name; } }
二、两种用法的分析
- 根据共享对象的生成时机不同,选择
initialValue
或set
来保存对象- initialValue:在 ThreadLocal 第一次 get 的时候把对象初始化出来,对象的初始化时机可以由我们控制
- set:如果需要保存到ThreadLocal里的对象的生成时机不由我们控制,例如拦截器生成的用户信息,用ThreadLocal.set 直接放到ThreadLocal中去,以便后续使用
三、使用ThreadLocal的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效的利用内存,节省开销
- 免去传参的繁琐,降低耦合度
四、原理
- 每个Thread对象中都持有一个ThreadLocalMap对象,在ThreadLocalMap中则保存了多个ThreadLocal对象,是一种和HashMap类似的存储方法,键是
ThreadLocal
,值就是实际需要的成员变量
五、重要方法
initialValue、set
-
该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
我们在初始化时重写了initialValue(),但调用ThreadLocal对象使用的是get()
查看ThreadLocal的源码:
//initialValue默认返回null protected T initialValue() { return null; }
/** * Returns the value in the current thread's copy of this * thread-local variable. If the variable has no value for the * current thread, it is first initialized to the value returned * by an invocation of the {@link #initialValue} method. * * @return the current thread's value of this thread-local */ public T get() { //获取到当前线程的ThreadLocalMap Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //判断map是否为null,不是则setInitialValue() if (map != null) { //获取当前ThreadLocal对象的键值对 ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") //返回在初始化时,设置的value T result = (T)e.value; return result; } } return setInitialValue(); } /** * Variant of set() to establish initialValue. Used instead * of set() in case user has overridden the set() method. * * @return the initial value */ private T setInitialValue() { //得到的value就是我们重写的initialValue方法设置的value T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //map不为空,就设置键值对 if (map != null) map.set(this, value); else createMap(t, value); return value; } /** * Sets the current thread's copy of this thread-local variable * to the specified value. Most subclasses will have no need to * override this method, relying solely on the {@link #initialValue} * method to set the values of thread-locals. * * @param value the value to be stored in the current thread's copy of * this thread-local. */ 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方法访问变量时,将调用initialValue方法,除非线程先前调用了set方法,这时将不会为线程调用initialValue方法
-
通常,每个线程最多调用一次initialValue方法,但如果调用了remove()后,再调用get(),则可以再次调用此方法
get
- get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,将本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
- 这个map以及map中的key和value都是保存在线程中,而不是保存在ThreadLocal中
remove
-
获得当前线程的ThreadLocalMap,然后将当前ThreadLocal对于的键值对删除
public void remove() { ThreadLocalMap m = getMap(Thread.currentThread()); if (m != null) m.remove(this); }
六、注意点
内存泄漏
内存泄漏是指某个对象不再有用,但是占用的内存却不能回收
-
在ThreadLocal中可能会存在Value的泄漏
-
ThreadLocalMap的每个Entry都是一个对key的弱引用,同时,每个Entry都包含了一个对value的强引用
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pJuN0KgY-1611129909135)(note/entry.png)]
-
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
-
但是如果线程不终止,那么key对应的value就不能被回收,因为有以下调用链:
Thread --> ThreadLocalMap --> Entry(key 为 null) --> value
-
JDK在set、remove、rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
-
如何避免内存泄漏?
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal对象后,应该调用remove方法
针对上面的场景二:在Service3中调用完ThreadLocal对象后,应该使用remove方法
class Service3 { public void process() { User user = UserContextHolder.holder.get(); System.out.println("Service3拿到用户名:" + user.name); UserContextHolder.holder.remove(); } }
-