1、ThreadLocal的用途
经典场景1:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
经典场景2:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦
2、经典场景1
- 每个Thread内有自己的实例副本,不共享
- 比喻:教材只有一本,一起做笔记有线程安全问题。复印后没问题
我们使用线程池来帮助我们创建线程
/** * @Classname ThreadLocalNormalUsage02 * @Description 使用线程池来优化 * @Date 2021/4/11 12:19 * @Created by WangXiong */ public class ThreadLocalNormalUsage02 { public String date(int seconds){ //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 1000; i++) { final int finalI = i; executorService.submit(new Runnable() { public void run() { //我们创建了1000个工具类对象,能否优化只创建一次? String date = new ThreadLocalNormalUsage02().date(finalI); System.out.println(date); } }); } executorService.shutdown(); } }
我们将创建1000次的工具类进行优化,将他提取出来
/** * @Classname ThreadLocalNormalUsage03 * @Description 使用线程池优化,将工具类静态提取出来,防止创建1000次 * @Date 2021/4/11 12:19 * @Created by WangXiong */ public class ThreadLocalNormalUsage01 { public String date(int seconds){ //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); return sdf.format(date); } public static void main(String[] args) { for (int i = 0; i < 30; i++) { final int finalI = i; new Thread(new Runnable() { public void run() { String date = new ThreadLocalNormalUsage01().date(finalI); System.out.println(date); } }).start(); } } }
这个时候,我们发现,出现了问题。这是为啥呢?这是因为两个任务指向了同一个SimpleDateFormat对象,这个对象也不是线程安全导致的。
我们可以采用加锁(synchronize)来解决线程安全问题
但是我们使用了synchronize加锁,那1000个线程执行这个方法,他们是一个一个排队执行的,在高并发中,这显然是不可取的。
所以我们在这种情况下,应该使用ThreadLocal,下面我们来看下如何操作
/** * @Classname ThreadLocalNormalUsage05 * @Description 利用ThreadLocal给每个线程分配自己的SimpleDateFormat对象, * 同时保证了线程安全,高效利用内存 * @Date 2021/4/11 12:19 * @Created by WangXiong */ public class ThreadLocalNormalUsage05 { public String date(int seconds){ //参数的单位是毫秒,从1970.1.1 00:00:00 GMT计时 Date date = new Date(1000 * seconds); // SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); SimpleDateFormat sdf = ThreadSafeFormatter.simpleDateFormatThreadLocal.get(); return sdf.format(date); } public static void main(String[] args) { ExecutorService executorService = Executors.newFixedThreadPool(10); for (int i = 0; i < 1000; i++) { final int finalI = i; executorService.submit(new Runnable() { public void run() { //我们创建了1000个工具类对象,能否优化只创建一次? String date = new ThreadLocalNormalUsage05().date(finalI); System.out.println(date); } }); } executorService.shutdown(); } } class ThreadSafeFormatter{ public static ThreadLocal<SimpleDateFormat> simpleDateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){ @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; }
使用ThreadLocal是线程安全的,且不会有性能问题
3、经典场景2
在我们的业务需求中,很多时候,都是需要用户信息的,这就导致我们在调用方法传递参数的时候,需要层层传递我们的user信息。
我们希望 每个线程内需要保存全局变量,可以让不同方法直接使用,避免参数传递的麻烦
那么我们是否可以设置一个全局静态变量,存储用户信息呢?
这是不行的,因为我们一个请求对应一个用户信息,第一个和第二个线程之间不能使用相同的用户对象
那我们是否可以顶一个map集合来存储呢?也是不行的,因为他不是map不是线程安全的,如果使用线程安全集合,那么或多或少还是会影响我们的性能
更好的方案是使用ThreadLocal,在线程生命周期内,都可以通过静态ThreadLocal实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
/** * @Classname ThreadLocalNormalUsage06 * @Description ThreadLocal的用法2,避免传递参数的麻烦 * @Date 2021/4/11 15:48 * @Created by WangXiong */ public class ThreadLocalNormalUsage06 { public static void main(String[] args) { new Service1().process(); } } class Service1 { public void process(){ User user = new User("超哥"); //将我们的user信息传递到ThreadLocal中 UserContextHolder.holder.set(user); new Service2().process(); } } class Service2 { public void process(){ User user = UserContextHolder.holder.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); } } /** * @Description: 定义我们的ThradLocal持有的类,需要我们直接获取即可 * @Param: * @returns: * @Author: WangXiong * @Date: 2021/4/11 15:52 */ class UserContextHolder { public static ThreadLocal<User> holder = new ThreadLocal<User>(); } class User { public String name; public User(String name) { this.name = name; } }
4、ThreadLocal总结
- 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
- 在任何方法中都可以轻松获取到对象
根据共享对象的生成时机不同,选择initiaValue或set来保存对象
- initiaValue:在ThreadLocal第一次get的时候把对象给初始化出阿里,对象的初始化时机可以由我们控制
- set:如果我们需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set()进去,以便我们后续使用
使用ThreadLocal带来的好处
- 达到线程安全
- 不需要加锁,提高执行效率
- 更高效的利用内存、节省开销:相比于每个任务都需要新建一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
- 免去传参的繁琐
5、ThreadLocal原理
我们一个Thread线程对应一个ThreadLocalMap,一个ThreadLocalMap里面有多个TreadLocal对象
5.1、initialValue()方法
- 该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候,才会触发
- 当线程第一个使用get()方法访问变量时,将调用此方法,除非线程当前调用了set()方法,在这种情况下,不会为线程调用initialValue()方法
public T get() { Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); //如果线程调用了set方法,那么这个map就不为空 if (map != null) { ThreadLocalMap.Entry e = map.getEntry(this); if (e != null) { @SuppressWarnings("unchecked") T result = (T)e.value; return result; } } return setInitialValue(); } private T setInitialValue() { //调用我们重写的initialValue方法 T value = initialValue(); Thread t = Thread.currentThread(); ThreadLocalMap map = getMap(t); if (map != null) map.set(this, value); else createMap(t, value); return value; } //如果不重写这个方法,默认返回null protected T initialValue() { return null; }
- 每个线程只会调用一次initialValue方法,如果已经调用了remove后,在调用get,则可以再次调用此方法
- 如果不重写这个方法,返回的就是null
5.2、set方法
为线程设置一个新值
5.3、get方法
得到这个线程对应的value。如果是首次调用get(),则会调用initialValue方法
5.4、remove方法
删除线程中这个对象
5.5、ThreadLocalMap
类似于HashMap,可以当成HashMap来理解
- 键:这个ThreaLocal
- 值:实际需要的成员变量,比如user对象
6、内存溢出
我们的ThreadLocalMap中的entry,我们可以看下他的构造方法
static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { //使用弱引用进行赋值了 super(k); //强引用,不会被垃圾回收 value = v; } } public WeakReference(T referent) { super(referent); }
如果是弱引用,是可以被垃圾回收器回收的,但是我们的value是一个强引用,他是不会被回收的。
使用线程池,线程不终止,那么它的key对应的value就不能被回收,因为有以下的引用链
Thread -> ThreadLocalMap -> Entry(key为null) -> value
所以我们要在使用完value后手动remove,删除对应的entry对象
7、TreadLocal空指针
/** * @Classname ThreadLocalNPE * @Description 线程空指针 * @Date 2021/4/12 21:41 * @Created by WangXiong */ public class ThreadLocalNPE { ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>(); public void set(){ longThreadLocal.set(Thread.currentThread().getId()); } public Long get(){ return longThreadLocal.get(); } public static void main(String[] args) { final ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE(); System.out.println(threadLocalNPE.get()); new Thread(new Runnable() { @Override public void run() { threadLocalNPE.set(); System.out.println(threadLocalNPE.get()); } }).start(); } }
空指针异常是装箱拆箱导致的
8、小结
ThreadLocal中不要使用static对象,因为对象使静态的,如果不是线程安全的,那么我们使用ThreadLocal包装,还是无法保证线程安全。