JUC之ThreadLocal

若有不对之处欢迎大家指出,这个也是在学习工作中的一些总结,侵删!
得之在俄顷,积之在平日 。
1、使用场景
每个线程需要独享的对象(通常是工具类,典型需要使用的类有和),每个内有自己的实例副本,不共享 。
每个线程内需要保存全局变量(例如在拦截器中获取用户信息) , 可以在不同的地方直接使用,避免参数传递的麻烦,例如:当前用户信息需要被线程内的所有方法共享,一个比较繁琐的解决方案是把user作为参数层层传递,从一个传递到另一个,以此类推,但是这样会导致代码冗余且不易维护;解决方法:如果用保存一些业务内容(用户权限、信息,从用户系统获取到的用户名、等) , 这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不同的,在线程的生命周期内 , 都通过这个静态的实例的get()方法取得自己set过的那个对象,避免了将这个对象(例如:User对象)作为参数传递的麻烦 。强调的是同一个请求内(同一个线程内)不同方法的共享 。
2、的两个作用:
让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)
在任何方法中都可以轻松的获取到该对象
3、示例

注:采用java8新日期不会出现线程安全问题
//打印1000个日期用10个线程来执行//创建线程池public static ExecutorService executorService = Executors.newFixedThreadPool(10);//工具类,进行日期转换public static String DateTransition(long seconds) {SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");Date date = new Date(1000*seconds);return dateFormat.format(date);}public static void main(String[] args) {for (int i = 0; i < 1000; i++) {//i的十倍作为毫秒数long scond = i*10;executorService.submit(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubString date = new ThreadLocalTest01().DateTransition(scond);System.out.println("--->"+date);}});}executorService.shutdown();}
这样做看似没什么问题 , 实际上当执行1000次的时候则会创建1000个,如下图:
然后进行再次升级 , 将提取出来作为静态常量 , 所有线程共享这一个 , 就会发生线程安全问题
static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");//工具类,进行日期转换publicString DateTransition(long seconds) {Date date = new Date(1000*seconds);return dateFormat.format(date);}
在这里我们可以选择加锁来解决线程安全问题 。
//工具类,进行日期转换publicString DateTransition(long seconds) {Date date = new Date(1000*seconds);String endDate = null;synchronized (ThreadLocalTest01.class){endDate = dateFormat.format(date);}return endDate;}
加锁能解决问题,但是会有性能开销,如果使用就不会存在这个问题

JUC之ThreadLocal

文章插图
//打印1000个日期用10个线程来执行//创建线程池public static ExecutorService executorService = Executors.newFixedThreadPool(10);//static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");static ThreadLocal> threadLocaldateFormat = new ThreadLocal>(){@Overrideprotected SimpleDateFormat initialValue() {return new SimpleDateFormat("yyyy-MM-DD HH:mm:ss");}};//工具类,进行日期转换publicString DateTransition(long seconds) {Date date = new Date(1000*seconds);SimpleDateFormat simpleDateFormat = threadLocaldateFormat.get();return simpleDateFormat.format(date);}public static void main(String[] args) {for (int i = 0; i < 1000; i++) {//i的十倍作为毫秒数long scond = i*10;executorService.submit(new Runnable() {@Overridepublic void run() {// TODO Auto-generated method stubString date = new ThreadLocalTest01().DateTransition(scond);System.out.println("--->"+date);}});}executorService.shutdown();}

一个请求 , 调用多个业务 , 这样就会将这个请求所带的数据进行一层一层的传递 , 会导致代码冗余且不易维护
解决方案:采用将参数放在一个map里面 , 然后各层都能取到map里面的数据,这样每个线程里面的map都不一样
public class ThreadLocalTest02 {//一个请求,调用多个业务public static void main(String[] args) {UserRequest UserRequest = new UserRequest();UserRequest.request("123");}}class CreateThreadLocal{public static ThreadLocal> threadLocal = new ThreadLocal<>();}class UserRequest{void request(String id){//请求CreateThreadLocal.threadLocal.set(id);UserService01 userService01 = new UserService01();userService01.Service01();}}class UserService01{void Service01(){//业务一System.out.println("Service01拿到数据:"+CreateThreadLocal.threadLocal.get());UserService02 userService02 = new UserService02();userService02.Service02();}}class UserService02{void Service02(){//业务二System.out.println("Service02拿到数据:"+CreateThreadLocal.threadLocal.get());UserService03 userService03 = new UserService03();userService03.Service03();}}class UserService03{void Service03(){//业务三System.out.println("Service03拿到数据:"+CreateThreadLocal.threadLocal.get());CreateThreadLocal.threadLocal.remove();}}
4、使用场景:
在ThreadLocal第一次get的时候把对象初始化出来,对象的初始化时机可以由我们控制(常用于工具类)
5、set的使用场景
如果需要保存到里面的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用.set直接放到我们的中去,以便后续使用 。
6、使用带来的好处
达到线程安全 。
不需要加锁,提高执行效率 。
更高的利用内存,节省开销:相比于每个任务都新建一个,显然用可以节省内存和开销 。
免去传参的繁琐,使得代码耦合度低,更优雅 。
7、解析
类:
也就是ThreadLocals, ThreadLocalMap类是每个线程Thread里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map,键值对:键:这个ThreadLocal值:实际需要的成员变量,比如我们放进去的idThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个位置,而不是使用链表拉链,不想map那样采用链表和红黑树
8、主要方法介绍:
T ():初始化。
【该方法会返回当前线程对应的“初始值”,这是一个延迟加载的方法,只有在调用get的时候才会触发,当线程第一次使用get方法访问变量时调用此方法,除非线程先前调用了set方法,这种情况下,不会为线程调用本方法;通常 , 每个线程最多调用一次此方法,但如果调用了()后再调用get(),则可再次调用此方法;如果不重写本方法 , 这个方法会返回null,一般使用匿名内部类的方法来重写()方法,以便在后续使用过程中可以初始化副本对象 。】
void set(T t):为这个线程设置新值 。
T get():得到这个线程对应的value,如果是首次调用get , 则会调用来得到这个值 。
void ():删除对应这个线程的值 。
9、注意点:
内存泄漏
内存泄漏是指某个对象不再有用,但是占有的内存却不能被回收
key的泄漏:ThreadLocalMap中的Entry继承自 weakReference,是弱引用(弱应用的特点是,如果这个对象只被弱应用关 联,没有任何强引用关联,那么这个对象就可以被回收,所以弱引用不会 阻止GC)![在这里插入图片描述](https://img-blog.csdnimg.cn/20200223135951481.png)Value泄漏: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的内存泄漏 。
如何避免内存泄漏
调用remove()方法就会删除对应的Entry对象,可以避免内存泄漏,所以在使用完ThreadLocal之后应该调用remove()方法
空指针异常
基本数据类型《==》包装类
共享对象
不能将static放入ThreadLocal中
优先使用框架支持
【JUC之ThreadLocal】例如:在Spring中 , 如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal , 因为自己可能会忘记调用remove等方法造成内存泄漏 。