JUC-ThreadLocal
ThreadLocal 的使用
ThreadLocal的用途—两大使用场景
- 典型场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random )
- 典型场景2 :每个线程内需要保存全局变量(例如: 在拦截器中获取用户信息) , 可以让不同方法直接使用,避免参数传递的麻烦
场景1:每个Thread内有自己的实例副本,不共享
比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题。
-
2个线程分别用自 己的SimpleDateFormat ,这没问题
-
后来延伸出10个 ,那就有10个线程和10个 SimpleDateFormat ,这虽然写法不优雅(应该复用对 象) ,但勉强可以接受
-
但是当需求变成了 1000个,那么必然要用线程池(否则 消耗内存太多)
-
所有的线程都共用同一个simpleDateFormat对象,有线程安全问题
用synchronized 加锁解决的话,效果正常,但是效率低
用ThreadLocal解决问题,可以让每个线程独享simpleDateFormat对象
当多个不同线程用同一个SimpleDateFormat会引发线程安全问题
利用ThreadLocal解决线程安全问题,并且不怎么影响性能。
代码实现:通过在类中实现ThreadLocal,重写initialValue,用的时候用ThreadLocal.get()
package threadlocal; import java.text.SimpleDateFormat; import java.util.Date; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; /** * 描述: 利用ThreadLocal,给每个线程分配自己的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) { //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); // SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal2.get(); return dateFormat.format(date); } } class ThreadSafeFormatter { //实现ThreadLocal,泛型用我们需要的对象,实现initialValue方法 public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; //Java8 public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal .withInitial(() -> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); }
场景2: 每个线程内需要保存全局变量
实例:当前用户信息需要被线程内所有方法共享
- 个比较繁琐的解决方案是把user作为参数层层传递,从service-1()传 到service- 2(),再从service- 2()传到service-3() ,以此类推,但是这样 做会导致代码冗余且不易维护
方法:用ThreadLocal保存一些业务内容(用户权限信息,从用户系统获取用户名,userId等)
这些信息在同一个线程中相同,但是在不同线程中使用业务内容时不相同的。但是我们需要他们相同。
当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以使用ConcurrentHashMap,但无论用什么对性能都有一定的影响。
更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,无序层层传递参数,就可达到保存当前线程对应用户信息的目的。
在线程生命周期内,都通过这个静态ThreadLocal实例的get() 方法取得自己set()过的那个对象,避免将这个对象(例如user对象) 作为参数传递的麻烦
和场景1的代码实现方式不同,我们不需要实现**initialValue()方法,但是必须手动调用Set()**方法。
package threadlocal;
/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process("牛叠为");
}
}
class Service1 {
public void process(String name) {
User user = new User(name);
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);
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
使用总结
- 让某个需要用到的对象在线程间隔离
- 在任何方法中都可以轻松获取到该对象
- 达到线程安全,无需加锁,提高执行效率,更高效的利用内存,节省开销。 如: 不用频繁创建SimpleDateFormat
- 免去传参的繁琐:无论是场景一的工具类,还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传同样的参数。ThreadLocal使得代码耦合度更低,更优雅。
场景1:在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们来控制。
场景2:set 需要保存到ThreadLocal里的对象生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中,以便后续使用。
ThreadLocal原理源码分析
Thread,ThreadLocal,ThreadLocalMap之间的关系
Thread类中都持有一个ThreadLocalMap成员变量
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
即ThreadLocalMap可以保存多个ThreadLocal
主要方法
T initialValue()
初始化 需要重写
-
该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
protected T initialValue() { return null; }
-
当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
-
通常,每个线程最多调用一次此方法,但如果已经调用了remove()后,再调用get(),则可以再次调用此方法
-
如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象。
void set(T t)
: 为这个线程设置一个新值
//拿到当前线程的ThreadLocalMap 第一次设置就创建Map,把本ThreadLocal的引用作为参数传入,并把value设置进去
//最后将ThreadLocal存进ThreadLocalMap中
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
-
T get()
: 得到这个线程对应的value。如果是首次调用get(),则会调用initialize来得到这个值/* get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value 注意,这个map以及map中的key和value都是保存在线程中的,而不是保存在ThreadLocal中 */ public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") 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() { T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) { map.set(this, value); } else { createMap(t, value); } if (this instanceof TerminatingThreadLocal) { TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this); } return value; }
-
void remove()
: 删除对应这个线程的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
ThreadLocalMap 类分析
也就是Thread.threadLocals
ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[]table,可以认为是一个map,键值对
键: 这个ThreadLocal 值: 实际需要的成员变量,比如user或者simpleDateFormat对象
ThreadLocalMap这里采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
通过源码分析可以看出,setInitialValue和直接set最后都是利用map.set()方法来设置值 也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过是起点和入口不一样
ThreadLocal注意点
-
内存泄露
某个对象不再有用,但是占用的内存却不能被回收。
-
ThreadLocalMap的Key的泄漏: ThreadLocalMap中的Entry继承自WeakReference,是弱引用
-
弱引用的特点是,如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收。
所以弱引用不会阻止GC,因此这个弱引用的机制
-
ThreadLocalMap的每个Entry都是一个对key的弱引用,同时每个Entry都包含了一个对value的强引用。
正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了。
但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,因为有以下的调用链.
Thread -> ThreadLocalMap -> Entry(key为null) ->value
因为value和Thread之间还存在这个强引用链路,所以导致value无法回收,就可能会出现OOM
-
JDK已经考虑到了这个问题,所以在set, remove,rehash方法中会扫描key为null的Entry,并把对应的value设置为null,这样value对象就可以被回收
但是如果一个ThreadLocal不被使用,那么实际上set, remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏
注意:当你使用完ThreadLocal时,调用remove方法
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal之后,应该调用remove方法
-
-
空指针异常
- 在进行get之前,必须先set,否则可能会报空指针异常。
-
共享对象
-
如果在每个线程中ThreadLocal.set()进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题
-
如果可以不使用ThreadLocal就解决问题,那么不要强行使用 例如在任务数很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal
-
优先使用框架的支持,而不是自己创造 例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏
- DateTimeContextHolder类,看到里面用了ThreadLocal
- 每次HTTP请求都对应一个线程,线程之间相互隔离,这就是ThreadLocal的典型应用场景