ThreadLocal的工作原理

概述

是一个线程内部的数据存储类(泛型类)public class ThreadLocal ,可以在指定的线程中存取数据,Looper、ActivityThread以及AMS中都用到了ThreadLocal。当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑采用ThreadLocal。
ThreadLocal封装了对当前线程的ThreadLocalMap存取,存取数据的实质就是向当前线程的一个数组(ThreadLocalMap中的table)中存取值。

public class Thread implements Runnable {
    // ......
    // 每个线程对象都会持有一个ThreadLocalMap,存储数据的容器
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ......
}

ThreadLocal.ThreadLocalMap

它实际上封装了一个数组,通过ThreadLocal对象的哈希去索引数组下标来存取数值

static class ThreadLocalMap {
    // ......
    // map中的数据数组
    // 散列表,表的长度必须为2的幂
    private Entry[] table;
    // ......
}

ThreadLocalMap.Entry

是一个弱引用ThreadLocal对象的弱引用类,内部的Object就是我们存储的数据。既然是弱引用,那么ThreadLocal对象就有可能会被回收,就可能在Entry数组中出现某个Entry对象指向空的情况。因为无法再次通过ThreadLocal去访问到,便要将其从Entry数组中清除。

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

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

image

ThreadLocal # set()

ThreadLocal可以获取当前执行的Thread对象,Thread对象可以获取到其ThreadLocalMap,通过ThreadLocal对象计算Entry在数组中的位置,然后获取Entry的value成员。

    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

ThreadLocal # getMap()

返回的是线程的全局变量ThreadLocalMap

    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

ThreadLocal # createMap()

创建ThreadLocalMap

    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

ThreadLocalMap # ThreadLocalMap()

    private static final int INITIAL_CAPACITY = 16;
    ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            // 创建了一个Entry数组,默认大小为16
            table = new Entry[INITIAL_CAPACITY];
            // 用ThreadLocal对象的hash值获取一个索引值
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 创建这个ThreadLocal对应的Entry对象并加入Entry数组
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            // 对数组进行扩充
            // 负载因子,默认情况下为当前数组长度的2/3
            setThreshold(INITIAL_CAPACITY);
    }

ThreadLocalMap # set()

有三种情况,第二种情况key出现null的原因由于Entry的key是继承了弱引用,在下一次GC是不管它有没有被引用都会被回收,当出现null时,会调用replaceStaleEntry()方法循环寻找相同的key,**如果存在,直接替换旧值。如果不存在,则在当前位置上重新创新的Entry。**第三种情况如果数组中找不到相同的key,就找空位添加?(虽然计算出来的索引值相同)

    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对象的ThreadLocalHashCode是唯一确定的
            int i = key.threadLocalHashCode & (len-1);
        
            // 遍历tab entry不为null
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();

                // 如果已经存在则直接覆盖
                if (k == key) {
                    e.value = value;
                    return;
                }

                // 如果当前Entry对象对应Key值为null,则清空所有Key为null的数据(即把entry对象置为null),把当前Entry对象换成这个新的对象(如果后面没有key值相同的),分多钟情况?
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            // 如果上面没有遍历成功就创建新值
            tab[i] = new Entry(key, value);
            int sz = ++size;
            // 满足条件数组扩容×2,threshold:负载因子 用于数组扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                // 会清除所有key==null的数据
                // 如果需要扩容,就扩容并且重新计算数据中的位置
                rehash();
    }
    
    
    private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
    }
    
    private void resize() {
            Entry[] oldTab = table;
            int oldLen = oldTab.length;
            int newLen = oldLen * 2;
            Entry[] newTab = new Entry[newLen];
            int count = 0;

            for (int j = 0; j < oldLen; ++j) {
                Entry e = oldTab[j];
                if (e != null) {
                    ThreadLocal<?> k = e.get();
                    if (k == null) {
                        e.value = null; // Help the GC
                    } else {
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
    }

ThreadLocalMap 解决冲突的方法是线性探测法(不断加 1),而不是HashMap的链地址法。

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

ThreadLocal # get()

    public T get() {
        Thread t = Thread.currentThread();
        // 拿到线程中的map
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            // 根据key值(ThreadLocal对象)获取相应的数据
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 得到value值
                T result = (T)e.value;
                return result;
            }
        }
        // 如果ThreadLocalMap为空,创建新的ThreadLocalMap
        return setInitialValue();
    }

ThreadLocalMap # getEntry()

    private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            // 哈希码计算出来的位置去找到相应的Entry对象
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            // key对应的值为null 或者 
            // 由于位置冲突,key对应的值存储的位置并不在i位置上
            else
                return getEntryAfterMiss(key, i, e);
    }

ThreadLocalMap # getEntryAfterMiss()

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
            
            // 遍历table数组
            while (e != null) {
                ThreadLocal<?> k = e.get();
                // 如果key值相同返回
                if (k == key)
                    return e;
                // 如果key==null,清除当前位置下所有key==null的数据
                if (k == null)
                    // expungeStaleEntry()方法主要干了三件事
                    // 第一件,将staleSlot的位置对应的数据置为null
                    // 第二件,删除此位置并删除此位置后对应相关联位置key = null的数据。
                    // 第三件,如果key不为null,但是该key对应的threadLocalHashCode发生变化,计算变化后的位置,并将元素放入新位置中
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
    }

ThreadLocal内存泄漏

Entry中的key使用ThreadLocal的弱引用(如果key使用强引用,那么当引用ThreadLocal的对象被回收了,但ThreadLocalMap中还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致内存泄漏)
如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收。但是如果key==null时,我们的value却不能回收,造成内存泄漏。因为存在一条从current thread连接过来的强引用(Thread Ref(当前线程引用) -> Thread -> ThreadLocalMap -> Entry -> value) 只有当前thread结束以后, current thread就不会存在栈中,强引用断开, Current Thread, Map, value将全部被GC回收。

所以,不管是调用ThreadLocal的set()还是get()方法,都会去清除线程ThreadLocalMap里所有key==null的数据(调用expungeStaleEntry())。但是这样仍然不是很保险,因为如果不再调用这些操作,就无法清除无用对象了,除非线程结束。所以保险起见还是应该去手动调用ThreadLocal的remove()清除该无用Entry。

ThreadLocal使用注意事项

虽然ThreadLocal帮我们考虑了内存泄漏,为我们加上了防护措施。但是我们需要注意避免以下两种情况,它们仍然有可能会导致内存泄漏。

Java源码中的描述: ThreadLocal类用来提供线程内部的局部变量,这种变量在多线程环境下访问(通过get或set方法访问)时能保证各个线程里的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static 类型,用于关联线程。那是否要使用static的ThreadLocal?

使用static的ThreadLocal

它延长了ThreadLocal的生命周期,可能导致内存泄漏。Java虚拟机在加载类的过程中为静态变量分配内存,static变量的生命周期取决于类的生命周期,也就是说类被卸载时,静态变量才会被销毁并释放内存空间。而类的生命周期结束需满足下面三个条件。

  • 该类所有的实例都已经被回收(Java堆中不存在该类的任何实例)
  • 加载该类的ClassLoader已经被回收
  • 该类对应的java.lang.Class对象没有被任何地方引用,没有在任何地方通过反射访问该类的方法

分配使用了ThreadLocal又不再调用set(),get(),remove()方法

第一次调用了ThreadLocal设置数据后,就不再调用set(),get(),remove()方法。现在ThreadLocalMap中只有一条数据,如果调用ThreadLocal的线程一直不结束,即使ThreadLocal被GC回收,也一直存在一条强引用链,导致数据无法回收,造成内存泄漏。

ThreadLocal特性

ThreadLocal和Synchronized都是为了解决多线程中相同变量访问冲突问题,不同的是:

  • Synchronized是通过线程等待,牺牲时间来解决访问冲突
  • ThreadLocal是通过每个线程单独一份存储空间,牺牲空间来解决冲突。相比于Synchronized,ThreadLocal具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问到想要的值

Handler使用时内存泄漏

一般如果我们像下面这么写,handler在activity中使用的时候可能会造成内存泄漏

  • 在Java中,非静态内部类/匿名类都默认会隐式持有外部类的引用
  • 主线程的Looper对象的生命周期 = 该应用程序的生命周期

当主线程里,实例化一个Handler对象后,它就会自动与主线程Looper的消息队列关联起来。所有发送到消息队列的消息Message都会拥有一个对Handler的引用,所以当Looper来处理消息时,会据此回调[Handler#handleMessage(Message)]

如果Handler消息队列还有未处理的消息/正在处理的消息时,消息队列中的Message持有Handler实例的引用,而Handler又持有外部类activity的引用。引用关系会一直保持直到Handler消息队列中的所有消息被处理完毕。(一旦消息被回收,它内部的各字段,包括目标 target 的引用都会被清空)此时如果外部类需要销毁(Handler的生命周期>外部类的生命周期),将使得外部类无法被GC回收。

public class MainActivity extends AppCompatActivity {
    // 通过匿名内部类实例化的Handler类对象
    private Handler mHandler = new Handler() {
        @Override
        public void handleMessage(Message msg) {
            if (msg.what == MSG_UPDATE_SEEK) {
                setProgress();
                if (!mIsSeeking) {
                    msg = obtainMessage(MSG_UPDATE_SEEK);
                    sendMessageDelayed(msg, 200);
                }
            }
        }
    };
    ...
}

image

解决方案1:静态内部类 + 弱引用

使用静态内部类/匿名类,静态内部类不默认持有外部类的引用。将Handler子类设置成静态内部类,如果需要在静态内部类中调用外部的activity,使用弱引用持有activity实例。

public class SampleActivity extends Activity {

  private static class MyHandler extends Handler {
    private final WeakReference<SampleActivity> mActivity;

    public MyHandler(SampleActivity activity) {
      mActivity = new WeakReference<SampleActivity>(activity);
      // 还有一个构造方法中的ReferenceQueue对象:在对象被回收后会把**弱引用对象**(WeakReference对象或者其子类)放入ReferenceQueue中
    }

    @Override
    public void handleMessage(Message msg) {
      SampleActivity activity = mActivity.get();
      if (activity != null) {
        // ...
      }
    }
  }

  private final MyHandler mHandler = new MyHandler(this);

  // 大部分博客都采用静态匿名内部类,为何不用静态内部类?
  private static final Runnable sRunnable = new Runnable() {
      @Override
      public void run() { /* ... */ }
  };

  @Override
  protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);

    // Post a message and delay its execution for 10 minutes.
    mHandler.postDelayed(sRunnable, 1000 * 60 * 10);
    // 被延迟的消息会在被处理之前存在于主线程消息队列中5分钟
    // Runnable也持有activity的引用
    // mHandler.postDelayed(new Runnable() {
    //     @Override
    //     public void run() {
    //
    //     }
    // }, 1000*60*10);

    // Go back to the previous Activity.
    finish();
  }
}

解决方案2:当外部类结束生命周期时,清空Handler内消息队列

当外部类结束生命周期时(调用onDestroy()),清除Handler消息队列里的所有消息,使得Handler的生命周期和外部类的生命周期是同步的。

@Override
    protected void onDestroy() {
        super.onDestroy();
        // 清空正在执行的callback和message
        mHandler.removeCallbacksAndMessages(null);
    }

如果要保证Handler中消息队列中的所有消息都能被执行,就使用解决方案1

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocal 是 Java 中的一个线程局部变量,它提供了一种在每个线程中存储数据的机制。每个线程都可以独立地访问自己的 ThreadLocal 变量,而不会影响其他线程的访问。 ThreadLocal工作原理是通过为每个线程创建一个独立的副本来实现的。当一个线程访问 ThreadLocal 变量时,它实际上是访问自己的副本。这样就避免了线程安全问题,每个线程都可以拥有自己独立的数据副本。 ThreadLocal 的应用场景包括: 1. 线程上下文信息的传递:在多个方法之间共享某些数据,但又不希望将这些数据作为参数传递。通过将数据存储在 ThreadLocal 中,可以在不传递参数的情况下,在不同方法之间共享数据。 2. 数据库连接和事务管理:在使用数据库连接池时,可以将每个线程的数据库连接存储在 ThreadLocal 中,确保每个线程使用自己的数据库连接,避免线程间的干扰。 3. 线程安全的日期格式化:日期格式化类通常不是线程安全的,使用 ThreadLocal 可以为每个线程创建一个独立的日期格式化对象,避免多线程并发访问时的线程安全问题。 4. 线程级别的缓存:在多线程环境下,可以使用 ThreadLocal 实现线程级别的缓存,每个线程都有自己独立的缓存,避免了线程间的数据竞争问题。 5. Web 应用中的用户身份管理:在 Web 应用中,可以使用 ThreadLocal 存储当前用户的信息,方便在不同层之间获取用户身份信息,如用户认证、权限控制等。 这些应用场景都是为了解决多线程环境下的线程安全问题,通过使用 ThreadLocal 可以在每个线程中存储独立的数据,避免了线程间的数据竞争和并发访问的问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值