ThreadLocal的复习

ThreadLocal 的笔记

一、ThreadLocal的一些问题

1、和Synchronized 的区别
2、存储在JVM中的哪个区域
3、真的只是当前线程可见吗
4、会导致内存泄漏么
5、为什么用Entry数组而不是Entry对象
6、学习的框架中哪些用到了ThreadLocal
7、ThreadLocal里的对象一定是线程安全吗
问题答疑在最后。

二、ThreadLocal的概述

    THreadLocal类是用来提供线程内部的局部变量。
让这些局部变量在多线程环境下访问(get/set)时能保证各个线程里的变量相对独立于其他线程内的变量。

    通常的情况下,我们创建的成员变量都是线程不安全的,因为他可能被多个线程同时修改,
    此变量对于多个线程之间彼此并不独立,是共享变量。而使用ThreadLocal创建的变量只能被当前线程访问,
    其他线程无法访问与修改。也就是说,讲线程共有化变为线程私有化。
package thread.threadlocal;

import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.atomic.AtomicInteger;

/**
 * @author 超
 * Create by fengc on  2020/8/22 12:35
 * 应用场景:
 * 每个线程都需要一个独立的对象
 * (比如工具类,典型的就是SimpleDateFormat,
 * 每次使用都需要new一次,很浪费性能,直接放到成员变量里又是线性不安全,所以直接放到ThreadLocal 管理)
 */
public class ThreadLocalTimeMain {

    private static final ExecutorService executorService = Executors.newFixedThreadPool(2);

    private final static AtomicInteger countAtomic = new AtomicInteger();

    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            int finalI = i;
            executorService.execute(() -> {

                String dateStr = ThreadLocalDateUtils.dateToStr(new Date(System.currentTimeMillis() + finalI*100));
                //查看输出线程,
                System.out.println("计数器" +countAtomic.incrementAndGet()+",-> " + Thread.currentThread().getName()+":" + dateStr);
            });
        }
        executorService.shutdown();
    }
}


package thread.threadlocal;

import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

/**
 * @author 超
 * Create by fengc on  2020/8/22 13:43
 * 日期转化工具类
 */
public class ThreadLocalDateUtils {
    /**
     * Java8 直接初始化,lambam写法
     */
    private final static ThreadLocal<SimpleDateFormat> ymdSimpleFormatThrread = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyy-MM-dd"));

    private final static ThreadLocal<SimpleDateFormat> simpleFormatThread = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue() {
            SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            System.out.println(Thread.currentThread().getName() + "<-> " + simpleDateFormat);
            return simpleDateFormat;
        }
    };

   /*
   错误
   static {
       simpleFormatThread = new ThreadLocal<>();
       SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
       System.out.println("static<-->" + simpleDateFormat);
       simpleFormatThread.set(simpleDateFormat);
   }*/

    /**
     * 日期转为字符串
     * @param date
     * @return
     */
    public static String dateToStr(Date date) {
        SimpleDateFormat simpleDateFormat = simpleFormatThread.get();
        System.out.println("-->" + simpleFormatThread);
        return simpleDateFormat.format(date);
    }

    /**
     * 字符串转为日期
     * @param dateStr
     * @return
     * @throws ParseException
     */
    public static Date strToDate(String dateStr) throws ParseException {
        SimpleDateFormat simpleDateFormat = simpleFormatThread.get();
        Date date = null;
        try {
            date = simpleDateFormat.parse(dateStr);
        } catch (ParseException pe) {
            pe.printStackTrace();
        }
        return date;
    }
}

上面的代码运行则可发现,就是每个线程去创建一个SimpleDateFormat,当一个请求进来,就是一个线程,假如此线程会贯穿多个转换时间的方法dateToStr(),假如不用ThreadLocal的话,那么则要new三次,产生三个对象,那么使用了ThreadLocal,一个线程仅仅会产生一个对象。

应用场景:

  • 每个线程都需要一个独享的对象,比如工具类,日期转换对象,SimpleDateFormat;
  • 每个线程内需要保存全局变量(比如在登录成功后将用户信息保存到ThreadLocal里,然后当前线程操作的业务逻辑直接get取就完事了,有效地避免了参数的来回传递的麻烦之处);
  • 比如存储交易id等信息,每个线程私有;
  • 比如aop里记录日志需要before记录请求id,end拿出请求id(统计一个方法请求总时间);
  • 比如jdbc连接池。

三、核心知识

  1. 类关系
    每个Thread对象都持有一个ThreadLocalMap的成员变量,每个ThreadLocalMap内部又维护了N个Entry节点,也就是Entry数组,每个Entry代表一个完整的对象,key是ThreadLocal本身,value是代表THreadLocal的泛型值。

  2. 主要类与方法

public class Thread implements Runnable {
    //每个线程Thread 都持有一个ThreadLocalMap对象
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

public class ThreadLocal<T> {

      //此类的实现是由子类继承实现,否则返回为空对象
    protected T initialValue() {
        return null;
    }

    //得到这个线程设置ThreadLocal对应的value,如果调用get之前没有进行set,则会内部执行initialValue()进行初始化。
    public T get() {
        //获取当前的线程对象
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap对象。
        ThreadLocalMap map = getMap(t);
        //当当前线程的ThreadLocalMap不为空的时候,那么则进入
        if (map != null) {
            //获取当前线程ThreadLocalMap的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //当拿到了entry,则可以拿到了value了
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 直接获取entry对应的value返回。
                T result = (T)e.value;
                return result;
            }
        }
        //初始化的方法,是延迟加载,只有当get的时候才会用到,当能获取到ThreadLocal的value值的时候,那么则不就执行到。
        //一般来说,此初始化方法也只是会执行一次,但是当执行过remove方法后,再执行get的话,也会执行
        return setInitialValue();
    }
    
    /**
     * 设置当前线程的线程局部变量的值
     * 此方法所做的事情跟initialValue()所做的事情是一样的,都是set值,只是当调用的时机不同。
     * 实际上ThreadLocal的值是放入到当前线程的一个ThreadLocalMap的实例Entry[]的tables的,所以只能本地线程访问。
     * @param value
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap实例,把当前线程的对象作为传输值,那么获取到的ThreadLcoalMap就是当前线程的。
        ThreadLocalMap map = getMap(t);
        //若是当前线程对应的ThreadLocalMap实例不为空,那么则把当前的ThreadLocal作为key,value作为值,设置到当前线程的ThreadLocalMap的数组Entrey中
        if (map != null)
            map.set(this, value);
        else
            //若是当前线程没有对应的ThreadLocalMap,则创建ThreadLocalMap,并绑定到当前线程中
            createMap(t, value);
    }
    
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             //直接删除当前ThreadLocal为key的value值
             m.remove(this);
     }
    
    
    static class ThreadLocalMap {
    
        private static final int INITIAL_CAPACITY = 16;
        
        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
         
         //ThreadLocalMap内部有Entry静态内部类,Entry的内部key是ThreadLocal本身,value是泛型
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
    }
}

  • initialValue() : 初始化。在get方法里面懒加载的。
  • get() : 得到这个线程设置ThreadLocal对应的value,如果调用get之前没有进行set,则会内部执行initialValue()进行初始化。
  • set() : 为这个线程设置一个value的新值
  • remove() : 删除这个线程对应的value值,防止内存泄漏的最佳手段

3.线程中的ThreadLocalMap

此类虽然是ThreadLocal里面的静态,但是这个类是在Thread类里面创建的,归线程Thread所有。ThreadLocalMap类的内部是一个Entry数组,并不是一个Entry对象。因为在平时业务代码使用过程中,会创建很对各ThreadLocald对象,各司其职。但是在一次请求,就是一个线程,所以一个线程也就只有一个ThreadLocalMap,所以不管ThreadLocald创建多少个,ThreadLocalMap在一个线程里面也就只有一个,所以ThreadLocalMap里面是放一个Entry[]数组,存放的是一个线程里new出来的多个ThreadLocal对象。

四、重要的小结

ThrealLocal理解,主要是把几个类的关系了解清楚。
Thread、ThreadLocal、ThreadLocalMap、Entry

总结就是:线程Thread 维护了ThreadLocalMap,而ThreadLocalMap里面维护了数组Entry,在数组Entry里面存的是以ThreadLocal作为key,传入的值作为value的键值对。

五、面试题答疑(自己的白话答疑)

1. 和Synchronized的区别。(他是否和线程同步机制Synchronized提供同样的功能。)
    其实不是。同步机制(如Synchronized)保证的是多线程同时操作共享变量并且能正确
输出结果的。ThreadLocal则是不行的,他仅仅是可以把共享变量变为线程私有,每个线程都有
独立的一个变量。
比如:网站计数器,每个请求计算一次,那么变量count++,带上Synchronized即可解决,
使用THreadLocal 是无法统计的,他只能用来保存一个线程使用到的用户登录信息,
方法跟着id等。

2、ThreadLcoal存储在jvm的哪个区域。(线程私有,是不是ThreadLocal的实例和他的值

就放在栈上)

    不是。ThreadLocal的对象也是对象,还是放在堆上的。JVM通过一些技巧将其可见性变为线程可见。    
3、ThreadLocal真的只是当前线程可见吗
    不是单单是当前线程可见的,可以其他线程操作访问同一个ThreadLocal对象,但是这样操作,是不安全的。
4、ThreadLocal会导致内存泄漏么?
   ThreadLocal的内存泄漏主要是说线程Thread存在ThreadLocalMap.Entry所存的key与values是否会内存泄漏。
   那么可以分析的是:
- ThreadLocalMap.Entry 的key会内存泄漏吗?
- ThreadLocalMap.Entry 的value会内存泄漏吗?
看源代码:

// Entry,里面保存在ThreadLocal变量,也就是key,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}
    

可以看到源代码,发现是继承了弱引用,而且key是直接交给父类处理的,父类是一个弱引用,所以key是完全不存在内存泄漏问题,因为他不是强引用,他完全可以被GC回收的。

弱引用的特点:
如果这个对象只被弱引用关联,没有任何的强引用关联,那么这个对象就可以被GC回收掉,弱引用是不会阻止GC回收的。

可以再看value值,value值是一个强引用,但是不管强引用或者弱引用,线程终止了,那么都可以被GC回收掉的,引用引用链断了。(JVM的可达性分析法,
线程终止了,根结点Root就断了,下面的对象都会被回收)。
那么如此分析,ThreadLocal的使用并不会内存泄漏,但是一般使用线程的时候,都是会使用线程池的。线程池的存在核心线程是不会销毁的,只要创建出来就会反复地被利用,生命周期不会结束掉。
但是key是弱引用,会被GC回收掉,value是强引用,根结点还在,不会被回收掉,就会形成下面情形:

Thread -> ThreadLocalMap -> Entry(key=null)->value

由于value和Thread还存在着链路关系,还是可达的,所以不会被回收,假如线程中反复被创建如此多的垃圾对象无法被回收,那么内存就会存在泄漏,时间久了,就会OOM。
解决方案:
ThreadLocal已经为我们提供了remove的方法,所以在每个线程用完之后,最后记得调remove方法,把value移除。

5、ThreadLocalMap里面为什么是Entry数组,而不是Entry对象。

此问题主要是考ThreadLocalMap是在Thread里面持有的。
因为ThreadLocalMap是线程里面持有的,ThreadLocalMap的引用是在线程Thread里面的,所以在同一个线程中,也仅仅有只有一个ThreadLocalMap,而一个线程中,ThreadLocal的对象是会创建多个的,每个都有不同的用法。因为ThreadLocalMap中的Entry管理着以ThreadLocal为key,以泛型传进来值作为value的数据,所以ThreadLocalMap里面的Entry只能设置为数据吗,存储着所创建的多个ThreadLocal。

6、学习的开源框架中,哪里用到了ThreadLocal了
Spring框架
DateTimeContextHolder
RequestContextHolder
7、ThreadLocal里面的对象一定是线程安全的吗
不是。如果每个线程中的ThreadLocal.set()进去的对象,就是多线程共享的同一个对象,比如static,那么多线程的ThreadLcoal.get()获取的还是这个共享对象的本身,还是有并发访问线性不安全的问题。

六 ThreadLocal的一个简单的工具类

package thread.threadlocal;

import java.util.HashMap;
import java.util.Map;
import java.util.Objects;

/**
 * @author 超
 * Create by fengc on  2020/8/23 00:00
 * 以自己喜欢的key存入某个自己想要的对象到ThreadLocal中去
 */
public class ThreadLocalMapUtil<T> {

    //创建一个ThreadLocalMap
    private static final ThreadLocal<Map<String,Object>> threadLcoal = ThreadLocal.withInitial(HashMap::new);
    //直接拿出Map
    public static Map<String,Object> getThreadLocalMap() {
        return threadLcoal.get();
    }

    /**
     * 通过key拿出对象
     * @param key
     * @param <T>
     * @return
     */
    public static <T> T get(String key) {
        Map<String,Object> map = threadLcoal.get();
        @SuppressWarnings("unchecked")
        T t = (T)map.get(key);
        return t;
    }

    /**
     * 获取不到返回默认值
     * @param key
     * @param defaulValue
     * @param <T>
     * @return
     */
    public static <T> T get(String key,T defaulValue) {
        Map<String,Object> map = threadLcoal.get();
        @SuppressWarnings("unchecked")
        T t = Objects.nonNull(map) ? (T)map.get(key) : defaulValue;
        return t;
    }

    /**
     * 设置值
     * @param key
     * @param value
     */
    public static void set(String key,Object value) {
        Map<String,Object> map = threadLcoal.get();
        map.put(key,value);
    }

    /**
     * 直接设置一个Map
     * @param map
     */
    public static void set(Map<String,Object> map) {
        threadLcoal.get().putAll(map);
    }

    /**
     * 移除当前的
     */
    public static void remove() {
        threadLcoal.remove();
    }
}

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值