Android-ThreadLocal并发安全与内存泄漏原理详解

你生来一无所有,何惧从头再来 ---勉励自己

  • ThreadLocal是如何实现线程隔离的?
  • 为什么ThreadLocal会造成内存泄露? 如何解决?

本篇文章主要是针对这两个问题进行剖析,确保每个小伙伴都能读懂,深刻理解,篇幅较长,请耐心阅读。大家如果还有什么难点,欢迎在评论区留言,小编将和大家一起学习。

  1. 定义:ThreadLocal提供线程局部变量,通过为每个线程提供不同的局部变量副本,实现线程之间的数据隔离。每个线程都可以访问自己的变量副本。用于解决多线程并发问题。
  2. 原理图

  1. 基本使用
/**
 * ThreadLocal基本使用
 */

//定义一个ThreadLocal局部变量
 val threadLocal = ThreadLocal<Int>()

    for (i in 0 until 3){
         thread {
             threadLocal.set(i + 1)
             Log.i(TAG, "输出: "+ (Thread.currentThread().name+":"+threadLocal.get()))
          }
     }

//输出
2022-09-05 10:47:30.137 9709-14942/? I/FLOW_DEMO: 输出: Thread-5:1
2022-09-05 10:47:30.137 9709-14944/? I/FLOW_DEMO: 输出: Thread-7:3
2022-09-05 10:47:30.138 9709-14943/? I/FLOW_DEMO: 输出: Thread-6:2

我们通过使用三个线程来给ThreadLocal赋值,进行累加操作,不同的线程输出的值都是累加后的。从输出结果可以看出来,不同线程之间的操作不会彼此影响结果值。从而保证了线程并发访问的正确性。

  1. TheadLocal原理

我们从源码进行分析,看一下ThreadLocal是如何进行线程之间的数据隔离的?

首先我们来看一下set方法:

    /**
     * 将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写此方法,
     * 仅依靠initialValue方法来设置线程局部变量的值。
     * 参数:
     * value – 要存储在此线程本地的当前线程副本中的值
     */
    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //通过当前线程获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //判断当前ThreadLocal是否为null
        if (map != null)
            map.set(this, value); //将当前ThreadLocal作为key,局部变量值value,set给map
        else
            createMap(t, value); //若map为null,则创建新的ThreadLocalMap并赋值
    }

我们来看一下getMap这个方法。

    /**
     * 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;  
    }
    // Thread中的ThreadLocal.ThreadLocalMap threadLocals = null;

从上述源码中,可以看出ThreadLocal先获取当前线程,通过当前线程的getMap拿到线程的成员变量threadLocals,threadLocals的类型是ThreadLocalMap,然后以当前ThreadLocal作为key,局部变量value将数据保存到ThreadLocalMap中。那么ThreadLocalMap是如果保存数据的呢,接下来我们一起往下看。

static class ThreadLocalMap {

    
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
               /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        ........
}

从上述源码中可以看出,ThreaLocalMap类中有个静态内部类Entry,Entry继承WeakReference弱引用,并且将ThreadLocal作为key,Object作为value。,除此之外,ThreadLocalMap中还有一个成员变量table,它的类似想Entry数组,那这个table的作用是干啥的呢,我们接着往下看。

我们看过ThreadLocal的set方法,知道了线程是如何将数据存储到ThreadLocal中的,接下来我们来看一下线程是如何从ThreadLocal中取出数据的。

/**
 *返回此线程局部变量的当前线程副本中的值。
 *如果变量没有当前线程的值,则首先将其初始化为调用initialValue方法返回的值。
 *返回:此线程本地的当前线程的值
*/
public T get() {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取线程的局部变量ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //取出entry
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //如果map为null,则初始化结果
        return setInitialValue();
    }

  private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

从上述源码中我们可以看出,调用了ThreadLocalMap的getEntry方法,将ThreadLocal作为参数传进去,获取Entry,然后返回Entry的value。我们继续往下看。

    private Entry getEntry(ThreadLocal<?> key) {
           // 位运算获取下标
            int i = key.threadLocalHashCode & (table.length - 1);
            //通过下标获取对应的Entry
            Entry e = table[i];
            // Android-changed: Use refersTo()
            if (e != null && e.refersTo(key)) //Entry不为bull且key未被回收
                return e; //返回Entry
            else
                return getEntryAfterMiss(key, i, e); //返回null
        }

我们再介绍Entry这个静态内部类的时候提到过Entry[ ] table这个成员变量。用来存放在ThreadLocal插入的变量。我们来看一下ThreadLocal与Entry的关系。

目前为止我们可以得出以下几点。

  1. 每个线程中都有一个ThreadLocal.ThreadLocalMap 成员变量。
  2. ThreadLocalMap中通过数组保存Entry,每一个Entry中都有一个ThreadLocal作为Key,Object为Value用来保存数据变量。
  3. 不同线程之间获取的ThreadLocalMap不同,从而访问的数据变量副本不同。实现了线程之间的数据隔离。

我们接着往下看ThreadLocal是如果解决内存泄漏的。

 private void set(ThreadLocal<?> key, Object value) {
            Entry[] tab = table; //保存entry的数组
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1); //hash算法

            for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get(); //遍历数组
                if (k == key) { //如果key相同 直接覆盖value
                    e.value = value;
                    return;
                }

                if (k == null) { //如果key为null ,释放value
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }
 private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            int slotToExpunge = staleSlot;
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                    slotToExpunge = i;
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    // Start expunge at preceding stale entry if it exists
                    if (slotToExpunge == staleSlot)
                        slotToExpunge = i;
                    cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
                    return;
                }

                // If we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                if (k == null && slotToExpunge == staleSlot)
                    slotToExpunge = i;
            }

            // 如果key为bull  则将value赋值为null
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // If there are any other stale entries in run, expunge them
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

当我们调用threadLocal的set,get方法时,会判断当前的key是否为null,将Entry中的value赋值为null,但是这个释放value还有其他条件限制,并不是一定会发生,当系统内存不足时,由于Entry中的key继承软引用,回被垃圾回收器回收调,这时,Entry中的key为null,无法被线程访问,但是value仍然占用一定的内存空间,虽然在调用set,get方法时有可能进行系统回收,仍然无法回收无用所有内存。无法被访问的vlaue就会导致内存泄漏,怎么解决内存泄漏呢,最好的方法就是当我们使用完变量副本后及时调用remove方法,手动进行垃圾回收。

  public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
   /**
    * Remove the entry for key.
    */
 private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();//清除value
                    expungeStaleEntry(i);
                    return;
                }
            }
    }

当线程发生内存泄漏时,线程与内部的ThreadLocalMap之间存在着强引用,导致ThreadLocalMap无法被释放,这时由于ThreadLocalMap中的Entry的key为弱引用,ThreadLocal容易被回收,导致key为null,当调用remove方法时,会清除key为null对应的value。所以为了避免内存泄漏的出现,我们在使用完ThreadLocal的set方法后,及时调用remove方法进行内存释放。避免出现内存泄漏。

  • 18
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值