分析ThreadLocal如何做到单个线程独享

分析ThreadLocal如何做到单个线程独享

前情概要

  • 我们可能都知道SimpleDateFormat这个类的实例它不是线程安全的,如果不知道,我把代码贴这儿:

        // 类的成员变量
    	protected Calendar calendar;
    
    	// 这个私有方法会对calendar对象进行赋值,但是没有加锁,在多并发场景下,就造成了问题。
    	// Called from Format after creating a FieldDelegate
        private StringBuffer format(Date date, StringBuffer toAppendTo,
                                    FieldDelegate delegate) {
            // Convert input date to time field list
            calendar.setTime(date);
            // ....其他代码
        }
    
  • 我们往往会用ThreadLocal进行解决这种一个线程应该持有一个对象的问题。

    使用示例代码如下:摘自拉勾教育的《Java 并发编程 78 讲》

    public class ThreadLocalDemo06 {
    
        public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
    
        public static void main(String[] args) throws InterruptedException {
            for (int i = 0; i < 1000; i++) {
                final int finalI = i;
                threadPool.submit(new Runnable() {
                    @Override
                    public void run() {
                        String date = new ThreadLocalDemo06().date(finalI);
                        System.out.println(Thread.currentThread().getName() + "  " + 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 final static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal 是不是会更好呢!
        public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
            @Override
            protected SimpleDateFormat initialValue() {
                return new SimpleDateFormat("mm:ss");
            }
        };
    }
    
    • 这里使用了dateFormatThreadLocal.get()来保证获取到的一定是一个线程一个。

代码分析

java.lang.ThreadLocal#initialValue

  • 这是一个protected 修饰的方法,默认返回null。具体实现交给子类,返回类型T。
  protected T initialValue() {    return null;}

java.lang.ThreadLocal#get

  • 这是获取对象的方法,为示例中使用,做到一个线程返回一个对象。

       /**
         * 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() {
            // 1. 获取当前线程的引用
            Thread t = Thread.currentThread();
            // 2. 获取这个线程内部维护的ThreadLocalMap
            ThreadLocalMap map = getMap(t);
            if (map != null) {
                // 3. 以当前的ThreadLocal对象作为key去获取键值对Entry
                ThreadLocalMap.Entry e = map.getEntry(this);
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    // 4. 获取值
                    T result = (T)e.value;
                    return result;
                }
            }
            return setInitialValue();
        }
    
    • 从这个方法中可以看到,当已经存在对象时,获取对象分四步走。
    • 当这个对象还未存在时,调用setInitialValue()进行初始化。

java.lang.ThreadLocal#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();
        // 1. 获取当前线程
        Thread t = Thread.currentThread();
        // 2. 获取线程中维护的ThreadLocalMap对象。
        ThreadLocalMap map = getMap(t);
        if (map != null)
            // 3. 将该线程中的map,以ThreadLoacal本身作为key,将Value进行设置。
            map.set(this, value);
        else
            // 如果map都不存在,则在该线程中创建该map,并进行set value。
            // 真是节省空间,都是用到才去创建!
            createMap(t, value);
        return value;
    }
  • 从这个方法看到,是这个方法去真正的调用了initialValue方法进行初始化的,所以ThreadLocal的对象获取其实也是一个懒加载的方式,用到了才去进行初始化。
  • 也看到了这里是对Thread中的map进行赋值的位置。

小结

  • 每个线程的default级别的成员变量ThreadLocal.ThreadLocalMap threadLocals都是由ThreadLocal进行维护的。

在这里插入图片描述

  • threadLocalsThreadLocal对象作为key,保证了一个线程中,同一个ThreadLocal的实例去执行get方法去获取到这个的对象都是唯一的。这也解释了为什么ThreadLocal的对象在示例中使用了static修饰,为了保证它的唯一性,当然,我更乐意在 static 前再加上 final 进行修饰

Debug实操

  • 代码分析看过了,进行debug实际查看一下,就用以上的示例代码。
  • 示例代码中,线程池的核心线程数是16,最大线程数也是16.[暂且不论创建线程池的姿势不丝滑],那么在线程启动后,应当:每个线程在执行get()方法前,threadLocals这个map应该为null,执行后,每个线程保证只有一个。

初始化之前

  • 第一次执行get时,没有获取到需要进行setInitialValue()。可以观察到this对象的确不存在,他去进行set初始化。

在这里插入图片描述

在这里插入图片描述

初始化之后

  • 第二次这个线程再去获取时,就会去map中获取。

在这里插入图片描述

总结

要理清ThreadThreadLocalThreadLocal.ThreadLocalMap三者的关系,结合debug,不难看到它究竟是怎么实现线程独享的一个ThreadLocal对象的。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值