别再问我ThreadLocal是什么?一文带你剖析ThreadLocal(真实案例加源码分析)

点击上方"悟能之能",选择"设为星标"

回复"面经"获取面试资料

目录:

  1. 什么是ThreadLocal

  2. ThreadLocal使用示例

  3. 真实案例剖析

  4. 深入ThreadLocal源码

  5. ThreadLocal为什么会内存泄漏

  6. 总结

什么是ThreadLocal?

ThreadLocal是Java里一种特殊的变量。有些伙伴喜欢把它和线程同步机制混为一谈,事实上ThreadLocal与线程同步无关。ThreadLocal虽然提供了一种解决多线程环境下成员变量的问题,但是它并不是解决多线程共享变量的问题。

ThreadLocal与线程同步机制不同,线程同步机制是多个线程共享同一个变量,而ThreadLocal是为每一个线程创建一个单独的变量副本,故而每个线程都可以独立地改变自己所拥有的变量副本,而不会影响其他线程所对应的副本。可以说ThreadLocal为多线程环境下变量问题提供了另外一种解决思路。

注意:ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key

ThreadLocal源码中有哪些方法?

在这些方法中其实我们主要关注这四个方法:

  1. get():返回此线程局部变量的当前线程副本中的值。

  2. initialValue():返回此线程局部变量的当前线程的“初始值”。

  3. remove():移除此线程局部变量当前线程的值。

  4. set(T value):将此线程局变量的当前线程副本中的值设置为指定值。

除了这四个方法,ThreadLocal内部还有一个静态内部类

  1. ThreadLocalMap:该内部类才是实现线程隔离机制的关键,get()、set()、remove()都是基于该内部类操作。

ThreadLocalMap提供了一种用键值对方式存储每一个线程的变量副本的方法,key为当前ThreadLocal对象,value则是对应线程的变量副本。

Thread、ThreadLocal、ThreadLocalMap的关系如下;
Thread
	--ThreadLocal(属于Thread的成员变量,而不是Thread包含在ThreadLocal中,不要搞错了哦)
	   --ThreadLocalMap(属于ThreadLocal的静态内部类)

ThreadLocal使用示例

public class ThreadLocalTest {
    private static ThreadLocal<Integer> total  = new ThreadLocal<Integer>(){
        // 实现initialValue()
        @Override
        public Integer initialValue() {
            return 0;
        }
    };

    public static int addOne(){
        total.set(total.get()+1);
        return total.get();
    }

    public static void main(String[] args) {
        Runnable task = () -> {
            for(int i=0;i<5;i++){
                System.out.println(Thread.currentThread().getName()+"  :ThreadLocalTest"+ThreadLocalTest.addOne());
            }
        };
        Thread thread1 = new Thread(task);
        Thread thread2 = new Thread(task);
        Thread thread3 = new Thread(task);
        thread1.start();
        thread2.start();
        thread3.start();
    }
}

运行结果:从运行结果可以看出,ThreadLocal确实是可以达到线程隔离机制,确保变量的安全性。

看了这个例子可能很多小伙伴还不知道在实际场景是怎么用的,对吧?那我这里再讲一个实际工作开发中的遇到的一个真实案例;

真实案例剖析:

一天,张大胖上午遇到了一个棘手的问题,他在一个AccountService中写了一段类似这样的代码:

Context ctx = new Context();
ctx.setTrackerID(.....)

然后这个AccountService 调用了其他Java类,不知道经过了多少层调用以后,最终来到了一个叫做AccountUtil的地方,在这个类中需要使用Context中的trackerID来做点儿事情:

很明显,这个AccountUtil没有办法拿到Context对象, 怎么办?

张大胖想到,要不把Context对象一层层地传递下去,这样AccountUtil不就可以得到了吗?可是这么做改动量太大!涉及到的每一层函数调用都得改动,有很多类都不属于自己的小组管理,还得和别人协调。

更要命的是有些类根本就没有源码,想改都改不了。

这也难不住我,张大胖想:可以把那个set/get TrackerID的方法改成静态(static)的,这样不管跨多少层调用都没有问题!

public class Context{
    public static String getTrackerID(){
        ......
    }
    public static void setTrackerID(String id){
        ......
    }
}

这样就不用一层层地传递了,Perfect!

张大胖得意洋洋地把代码提交给Bill做Review。

Bill看了一眼就指出了致命的问题:多线程并发的时候出错!

张大胖恨不得找个地缝钻进去:又栽在多线程上面了,这次犯的还是低级错误!

线程1调用了Context.setTrackerID(), 线程2 也调用了Context.setTrackerID(),数据互相覆盖,不出乱子才怪。

张大胖感慨地说:“像我这样中情况,需要在某处设置一个值,然后经过重重方法调用,到了另外一处把这个值取出来,又要线程安全,实在是不好办啊, 对了,我能不能把这个值就放到线程中?让线程携带着这个值到处跑,这样我无论在任何地方都可以轻松获得了!”

Bill说:“有啊,每个线程都有一个私家领地!在Thread这个类中有个专门的数据结构,你可以放入你的TrackerID,然后到任何地方都可以把这个TrackerID给取出来。”

“这么好?”

张大胖打开JDK中的Thread类,仔细查看,果然在其中有个叫做threadLocals的变量,还是个Map类型 , 但是在Thread类中却没有对这个变量操作的方法。

看到张大胖的疑惑,Bill说:“也许你注意到了,这个变量不是通过Thread的访问的,对他的访问委托给了ThreadLocal这个类。”

“那我怎么使用它?”

“非常简单, 你可以轻松创建一个ThreadLocal类的实例:

ThreadLocal<String> threadLocalA= new ThreadLocal<String>();

线程1:threadLocalA.set("1234");
线程2:threadLocalA.set("5678");

像‘1234’, ‘5678’这些值都会放到自己所属的线程对象中。”

“等你使用的时候,可以这么办:” 线程1:threadLocalA.get()  --> "1234" 线程2:threadLocalA.get() --> "5678"

“明白了,相当于把各自的数据放入到了各自Thread这个对象中去了,每个线程的值自然就区分开了。可是我不明白的是为什么那个数据结构是个map 呢?”

“你想想,假设你创建了另外一个threadLocalB:”

ThreadLocal<Integer> threadLocalB = new ThreadLocal<Integer>();

线程1:threadLocalB.set(30);
线程2:threadLocalB.set(40);

那线程对象的Map就起到作用了:

“明白了,这个私家领地还真是好用,我现在就把我那个Context给改了,让它使用ThreadLocal:”

public class Context {
    private static final ThreadLocal<String> mThreadLocal
        = new ThreadLocal<String>();

    public static void setTrackerID(String id) {
        mThreadLocal.set(id);
    }
    public static String getTrackerID() {
        return mThreadLocal.get();
    }
}

深入ThreadLocal源码:

ThreadLocalMap是ThreadLocal的匿名内部类,是实现ThreadLocal的关键;

ThreadLocalMap其内部利用Entry来实现key-value的存储,如下:从上面可以看出:

  • ThreadLocalMap是属于ThreadLocald匿名内部类

  • Entry是ThreadLocalMap的匿名内部类

  • Entry的key就是ThreadLocal(同時這裏说明了ThreadLocal实例本身是不存储值,它只是提供了一个在当前线程中找到副本值得key),而value就是具体的值

  • Entry也继承WeakReference,所以说Entry所对应key(ThreadLocal实例)的引用为一个弱引用(关于弱引用后续会出一篇文章讲解一下,请持续关注)

ThreadLocalMap的源码比较多,我们就看两个最核心的方法吧!
  • set(ThreadLocal key, Object value)

  • getEntry()

set(ThreadLocal key, Object value)
      /**
         * Set the value associated with key.
         *
         * @param key the thread local object
         * @param value the value to be set
         */
        private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            // 根据 ThreadLocal 的散列值,查找对应元素在数组中的位置
            int i = key.threadLocalHashCode & (len-1);

			// 采用“线性探测法”,寻找合适位置(解决hash冲突的一种方法)
            for (Entry e = tab[i];
                 e != null;
                 //查找相邻的曹
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

				//判断:key 存在,直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }
              // key == null,但是存在值(因为此处的e != null)
              //说明之前的ThreadLocal对象已经被回收了
                if (k == null) {
                   // 用新元素替换陈旧的元素
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// ThreadLocal对应的key实例不存在也没有陈旧元素,new 一个
            tab[i] = new Entry(key, value);
            int sz = ++size;
                   
			// cleanSomeSlots 清除老旧的Entry(key == null)
			// 如果没有清理陈旧的 Entry 并且数组中的元素大于了阈值,则进行 rehash
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

这里的set()操作和我们在集合类的put()方式不一样的,虽然他们都是key-value结构,不同在于他们解决散列冲突的方式不同。

  • 在集合类中Map的put()采用的是拉链法

  • ThreadLocalMap的set()则是采用开放定址法

set()操作除了存储元素外,还有一个很重要的作用,就是replaceStaleEntry()和cleanSomeSlots(),这两个方法可以清除掉key == null 的实例,防止内存泄漏。

set()方法中还有一个很重要变量:threadLocalHashCode,定义如下:

private final int threadLocalHashCode = nextHashCode();

从名字上面我们可以看出threadLocalHashCode应该是ThreadLocal的散列值,定义为final,表示ThreadLocal一旦创建其散列值就已经确定了,生成过程则是调用nextHashCode():

    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     * The difference between successively generated hash codes - turns
     * implicit sequential thread-local IDs into near-optimally spread
     * multiplicative hash values for power-of-two-sized tables.
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     * Returns the next hash code.
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

nextHashCode表示分配下一个ThreadLocal实例的threadLocalHashCode的值,HASH_INCREMENT则表示分配两个ThradLocal实例的threadLocalHashCode的增量,从nextHashCode就可以看出他们的定义。

getEntry()
/**
         * Get the entry associated with key.  This method
         * itself handles only the fast path: a direct hit of existing
         * key. It otherwise relays to getEntryAfterMiss.  This is
         * designed to maximize performance for direct hits, in part
         * by making this method readily inlinable.
         *
         * @param  key the thread local object
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

由于采用了开放定址法,所以当前key的散列值和元素在数组的索引并不是完全对应的,首先取一个探测数(key的散列值),如果所对应的key就是我们所要找的元素,则返回,否则调用getEntryAfterMiss(),如下:

        /**
         * Version of getEntry method for use when key is not found in
         * its direct hash slot.
         *
         * @param  key the thread local object
         * @param  i the table index for key's hash code
         * @param  e the entry at table[i]
         * @return the entry associated with key, or null if no such
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            while (e != null) {
                ThreadLocal<?> k = e.get();
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

这里有一个重要的地方,当key == null时,调用了expungeStaleEntry()方法,该方法用于处理key == null,有利于GC回收,能够有效地避免内存泄漏。

看完了ThreadLocal的ThreadLocalMap,我们再来看看ThreadLocal的set()和get(), initialValue()方法;

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() {
        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();
    }

首先通过当前线程获取所对应的成员变量ThreadLocalMap,然后通过ThreadLocalMap获取当前ThreadLocal的Entry,最后通过所获取的Entry获取目标值result。

getMap()方法可以获取当前线程所对应的ThreadLocalMap,如下:

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

set(T value)

设置当前线程的线程局部变量的值。

 /**
     * Sets the current thread's copy of this thread-local variable
     * to the specified value.  Most subclasses will have no need to
     * override this method, relying solely on the {@link #initialValue}
     * method to set the values of thread-locals.
     *
     * @param value the value to be stored in the current thread's copy of
     *        this thread-local.
     */
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

获取当前线程所对应的ThreadLocalMap,如果不为空,则调用ThreadLocalMap的set()方法,key就是当前ThreadLocal,如果不存在,则调用createMap()方法新建一个,如下:

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

initialValue()

返回该线程局部变量的初始值。

    protected T initialValue() {
        return null;
    }
  • 该方法定义为protected级别且返回为null,很明显是要子类实现它的(子类的修饰范围要大于父类的修饰符),所以我们在使用ThreadLocal的时候一般都应该覆盖该方法。

  • 该方法不能显示调用,只有在第一次调用get()或者set()方法时才会被执行,并且仅执行1次。

remove()

将当前线程局部变量的值删除。
    //该方法的目的是减少内存的占用。当然,我们不需要显示调用该方法
    //因为一个线程结束后,它所对应的局部变量就会被垃圾回收。
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

ThreadLocal为什么会内存泄漏

前面提到每个Thread都有一个ThreadLocal.ThreadLocalMap的map,该map的key为ThreadLocal实例,它为一个弱引用,我们知道弱引用有利于GC回收。当ThreadLocal的key == null时,GC就会回收这部分空间,但是value却不一定能够被回收,因为他还与Current Thread存在一个强引用关系,如下(图片来自http://www.jianshu.com/p/ee8c9dccc953):因为map中存在这个强引用关系,这就会会导致这个value无法被回收。如果这个线程对象不被销毁,那么这个强引用关系会一直存在,然后就会出现内存泄漏情况了。所以说只要这个线程对象能够及时被GC回收,就不会出现内存泄漏。如果碰到线程池,那问题就更大了。

那这样要怎么避免这个问题呢?

在前面提过,在ThreadLocalMap中的setEntry()、getEntry(),如果遇到key == null的情况,会对value设置为null。当然我们也可以显示调用ThreadLocal的remove()方法进行处理。

小结

下面再对ThreadLocal进行简单的总结:

  • ThreadLocal 不是用于解决共享变量的问题的,也不是为了协调线程同步而存在,而是为了方便每个线程处理自己的状态而引入的一个机制。这点至关重要。

  • 每个Thread内部都有一个ThreadLocal.ThreadLocalMap类型的成员变量,该成员变量用来存储实际的ThreadLocal变量副本。

  • ThreadLocal并不是为线程保存对象的副本,它仅仅只起到一个索引的作用。它的主要木得视为每一个线程隔离一个类的实例,这个实例的作用范围仅限于线程内部。

参考来源:https://mp.weixin.qq.com/s/L_5WPcX5pzM8GmTpQbytzw

                       https://mp.weixin.qq.com/s/k4cMqePHagb15-jlYh4PkA            

关注微信公众号【悟能之能】了解更多编程技巧。


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Java面试大全

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值