ThreadLocal介绍

    科技2022-07-12  155

    ThreadLocal详细介绍

    ThreadLocal的使用ThreadLcoal实现原理ThreadLocal的内存泄露错误使用ThreadLocal出现 线程不安全的情况ThreadLocal在Android中的使用场景 ThreadLocal和Synchonized都用于解决多线程并发访问,但实现机制却有着本质区别。Synchronized是利用加锁的方式,允许某个代码块只能被一个线程所访问。而ThreadLocal则是通过为每个线程都提供变量的副本,这样每个线程在访问时,实际上访问的是属于自身的变量,而不是同一个变量,这样就隔离了多个线程对数据的共享。

    ThreadLocal的使用

    ThreadLocal类常用的方法有4个,分别是:

    void set (T var1) // 设置当前线程的线程局部变量的值。void T get () // 返回当前线程所对应的线程局部变量。void remove () // 将当前线程的局部变量的值给删除,目的是为了减少内存的使用,需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显示调用该方法去清除线程的局部变量不是必须的操作,但它可以加快内存回收的速度。void initialValue() // 返回该线程局部变量的初始值,该方法是一个protected方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用的方法,在线程第一次set(T var1) 或者get() 才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    ThreadLcoal实现原理

    每个Thread持有一个ThreadLocalMap对象,ThreadLocalMap中存在Entry数据结构,Entry类似HashMap,Entry中的key是ThreadLocal,value则是Object。

    static class Entry extends WeakReference<ThreadLocal<?>> { /** The value associated with this ThreadLocal. */ Object value; Entry(ThreadLocal<?> k, Object v) { super(k); value = v; } }

    这上图,我们可以知道,每个Thread都有自己的ThreadLocalMap来访问自己的变量,也就不存在访问冲突的问题了。在ThreadLocal的get() 或者是set(T var1) 方法中,我们首先是通过Thread.currentThread() 来获得当前线程持有的ThreadLocalMap对象,然后再进行赋值或者是取值操作。

    ThreadLocal的内存泄露

    ThreadLocal仅仅是一个管理类而已,真正的对象存储再Thread里。ThreadLocal会被当做ThreadLocalMap中Entry的key,且Thread持有ThreadLocalMap,进而简介持有ThreadLocal, 正常情况下这就有可能出现内存泄漏的风险(Thread长周期, ThreadLocal短周期),对此ThreadLocalMap中的Entry中的采用了上图所示的预防。 Entry中的key使用了弱引用, 再者,我们正常使用ThreadLocal的时候,一般将其设置为static, 而static又是全局的,就不会存在ThreadLocal泄漏的说法了。 但是:Entry中的key使用了弱引用,而其value(我们存储的对象)并没有使用弱引用。当JVM进行垃圾回收时,就会把key给回收掉,此时key = null, 而value并没有被回收。 结论:ThreadLocal变量本身不存在内存泄漏问题,但是value有内存泄露的风险。 解决办法 使用完毕记得调用ThreadLocal.remove() 移除key和value。(remove()方法的核心在于执行了expungeStaleEntry(int staleSlot) 方法,它会清除在key = null 的情况下的value值,在set(T var1)或者 get() 方法时也会执行expungeStaleEntry(int staleSlot) 方法,但不是每次都会执行)

    错误使用ThreadLocal出现 线程不安全的情况

    首先运行如下代码:

    public class ThreadLocalUnsafe implements Runnable { public static Number number = new Number(0); public void run() { //每个线程计数加一 number.setNum(number.getNum()+1); //将其存储到ThreadLocal中 value.set(number); SleepTools.ms(2); //输出num值 System.out.println(Thread.currentThread().getName()+"="+value.get().getNum()); } public static ThreadLocal<Number> value = new ThreadLocal<Number>() { }; public static void main(String[] args) { for (int i = 0; i < 5; i++) { new Thread(new ThreadLocalUnsafe()).start(); } } private static class Number { public Number(int num) { this.num = num; } private int num; public int getNum() { return num; } public void setNum(int num) { this.num = num; } @Override public String toString() { return "Number [num=" + num + "]"; } } }

    出现如下结果: 这里每个线程都输出5的原因在于ThreadLocalMap中保存的其实一个对象的引用,这样的话,当有其它线程对这个引用指向的对象实例做修改时,其实也影响了所有的线程持有的对象引用所指向的同一个对象实例。这也就解释了上面5个线程出现同样的结果:5个线程保存的是同一个Number对象的引用,在线程休眠的时候,其它线程将num变量做了修改,而修改的对象Number的实例是同一份,因此它们最终的输出结果是一样的。 解决办法: 每个线程中的ThreadLocal都应该持有一个新的Number对象,可以将去掉修饰Number的static 关键字,或者重写给ThreadLocal的initialValue()方法,给定一个初始值。

    ThreadLocal在Android中的使用场景

    Handler创建的时候会采用当前线程的Looper来构造消息循环系统,那么Handler是如何获取得到当前线程的Looper呢?这就要使用ThreadLocal了, ThreadLocal可以在不同的线程之中互不干扰地存储并提供数据,通过ThreadLocal可以轻松获取每个线程地Looper。需要注意的是,线程默认是没有Looper的,使用Handler就必须为线程创建Looper,大家经常提到的主线程(UI线程),它就是ActivityThread, ActivityThread被创建的时候就会初始化Looper,这也就是主线程默认可以使用Handler的原因。 一般来说,当某些数据以线程为作用域并且不同线程具有不同的数据副本的时候,就可以考虑使用ThreadLocal,比如Handler,它需要获取当前线程的Looper,显然Looper的作用域就是线程并且不同的线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程的存取。

    Processed: 0.010, SQL: 8