美文网首页Android开发Android开发Android开发经验谈
Java ThreadLocal你之前了解的可能有误

Java ThreadLocal你之前了解的可能有误

作者: 小鱼人爱编程 | 来源:发表于2021-10-15 23:54 被阅读0次

    前言

    线程并发系列文章:

    Java 线程基础
    Java 线程状态
    Java “优雅”地中断线程-实践篇
    Java “优雅”地中断线程-原理篇
    真正理解Java Volatile的妙用
    Java ThreadLocal你之前了解的可能有误
    Java Unsafe/CAS/LockSupport 应用与原理
    Java 并发"锁"的本质(一步步实现锁)
    Java Synchronized实现互斥之应用与源码初探
    Java 对象头分析与使用(Synchronized相关)
    Java Synchronized 偏向锁/轻量级锁/重量级锁的演变过程
    Java Synchronized 重量级锁原理深入剖析上(互斥篇)
    Java Synchronized 重量级锁原理深入剖析下(同步篇)
    Java并发之 AQS 深入解析(上)
    Java并发之 AQS 深入解析(下)
    Java Thread.sleep/Thread.join/Thread.yield/Object.wait/Condition.await 详解
    Java 并发之 ReentrantLock 深入分析(与Synchronized区别)
    Java 并发之 ReentrantReadWriteLock 深入分析
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(原理篇)
    Java Semaphore/CountDownLatch/CyclicBarrier 深入解析(应用篇)
    最详细的图文解析Java各种锁(终极篇)
    线程池必懂系列

    Android事件驱动Handler-Message-Looper解析中提到过ThreadLocal,由于篇幅限制没有展开来说,这次来一探究竟。
    通过这篇文章你将知道:

    1、为什么需要ThreadLocal
    2、ThreadLocal应用场景
    3、ThreadLocal和线程安全关系
    4、ThreadLocal原理解析
    5、ThreadLocal在Android源码里的运用
    6、ThreadLocal会内存泄漏吗

    线程本地变量

    先从一个简单的例子开始:

        class PersonHeight {
            float height;
    
            public PersonHeight(float height) {
                this.height = height;
            }
    
            public float getHeight() {
                return height;
            }
    
            public void setHeight(float height) {
                this.height = height;
            }
        }
        
        PersonHeight personHeight = new PersonHeight(0);
        Thread t1 = new Thread(() -> {
            personHeight.setHeight(11);
            System.out.println(personHeight.getHeight());
        });
    
        Thread t2 = new Thread(() -> {
            personHeight.setHeight(22);
            System.out.println(personHeight.getHeight());
        });
    

    t1、t2共享成员变量personHeight并修改,显而易见会引发线程并发安全(有关线程安全问题请参考:真正理解Java Volatile的妙用,为此需要使用ThreadLocal,改造如下:

        class PersonHeight {
            float height;
    
            public PersonHeight(float height) {
                this.height = height;
            }
    
            public float getHeight() {
                return height;
            }
    
            public void setHeight(float height) {
                this.height = height;
            }
        }
    
        ThreadLocal<PersonHeight> threadLocal = new ThreadLocal<>();
    
        Thread t1 = new Thread(() -> {
            threadLocal.set(new PersonHeight(11));
            System.out.println(threadLocal.get().getHeight());
        });
    
        Thread t2 = new Thread(() -> {
            threadLocal.set(new PersonHeight(22));
            System.out.println(threadLocal.get().getHeight());
        });
    

    这样就没有线程安全问题了。
    以上思想是网上一些帖子对ThreadLocal 使用场景的阐述,认为ThreadLocal的作用是为了避免线程安全问题。个人认为这例子用来描述使用ThreadLocal的理由并不太恰当。
    两点理由:

    1、可以看出使用ThreadLocal时,实际上是重新生成了新的PersonHeight对象。既然两个线程访问的不是同一Person对象,那么就没有线程安全问题,没有线程安全问题,何来的避免线程安全问题呢?(多个线程各自访问不同的变量,这不叫避免了线程安全问题)。
    2、我们现在的设备,更多的瓶颈是在内存而非CPU,因此上面例子里解决并发问题我们可以上锁来解决。PersonHeight对象所占空间很小,复制一份对象没问题,如果对象很大呢,为了线程安全问题,每个线程都新建一份岂不是很浪费内存。

    综上所述,ThreadLocal并不是为了解决线程安全问题而设计的。
    问题来了,ThreadLocal在什么场景使用?还是从例子开始:
    1、变量在线程之间无需可见共享

        class DataPool {
            int data[];
    
            public DataPool(int[] data) {
                this.data = data;
            }
    
            public int[] getData() {
                return data;
            }
    
            public void setData(int[] data) {
                this.data = data;
            }
        }
    
        private void writeDataPool() {
            dataPool.getData()[0] = 3;
        }
    
        private int readDataPool(int pos) {
            if (dataPool.getData().length <= pos) {
                return -1;
            }
    
            return dataPool.getData()[pos];
        }
    
        DataPool dataPool = new DataPool(new int[5]);
        Thread t1 = new Thread(() -> {
            writeDataPool();
            readDataPool(1);
        });
    
        Thread t2 = new Thread(() -> {
            writeDataPool();
            readDataPool(1);
        });
    

    我们本来要设计线程访问各自私有的数据池,它们之间的数据池毫无关联也不需要关联,只和线程本身有关。如上写法明显不符合设计初衷,因此我们无需定义为成员变量,改造如下:
    2、避免过多无用参数传递

        private void writeDataPool(DataPool dataPool, int pos) {
            if (dataPool == null || dataPool.getData() == null || dataPool.getData().length <= pos)
                return;
            dataPool.getData()[0] = 3;
        }
    
        private int readDataPool(int pos) {
            //读取dataPool pos处的值
            return 0;
        }
    
        private void performA() {
            performB();
        }
    
        private void performB() {
            performC();
        }
    
        private void performC() {
            int value = readDataPool(0);
        }
    
    
        Thread t1 = new Thread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            writeDataPool(dataPool, 0);
            performA();
        });
    
        Thread t2 = new Thread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            writeDataPool(dataPool, 0);
            performA();
        });
    

    t1、t2已经拥有各自的dataPool,各自处理互不影响。线程先往dataPool里写入数据,而后经过performA()->performB()->performC()->readDataPool(int pos)层层调用,最后想读取dataPool的值。但是问题来了,readDataPool(int pos) 能读取到dataPool变量的值吗?显而易见,readDataPool(int pos)没有任何引用能找到dataPool对象,因此无法读取dataPool的值。这时候你可能想到了,没有dataPool对象,我传进去啊不行吗?没错,是可以传,但是回溯整个调用栈就需要每个调用的地方都需要传dataPool对象,此方法可行吗?可行!可取吗?不可取!试想,先不说有多少层的调用就要写多少传参,关键是中间调用比如performA()、performB()、performC()根本不关心dataPool,为啥要扣个参数在它们头上呢?
    我们再想想,怎样才能让线程里调用的任何方法都可以访问到dataPool呢?聪明的你可能想到了成员变量,既然线程里执行,那么可以在Thread类里增加成员变量来存储dataPool。
    成员变量存储dataPool:

        private int readDataPool(int pos) {
            //读取dataPool pos处的值
            DataPool dataPool = ((NewThread) Thread.currentThread()).getDataPool();
            if (dataPool != null && dataPool.getData().length > pos) {
                return dataPool.getData()[pos];
            }
            return 0;
        }
        
        class NewThread extends Thread {
    
            public NewThread(@Nullable Runnable target) {
                super(target);
            }
    
            private DataPool dataPool;
    
            public DataPool getDataPool() {
                return dataPool;
            }
    
            public void setDataPool(DataPool dataPool) {
                this.dataPool = dataPool;
            }
        }
    
        NewThread t1 = new NewThread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            ((NewThread) Thread.currentThread()).setDataPool(dataPool);
            writeDataPool(dataPool, 0);
            performA();
        });
    
        NewThread t2 = new NewThread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            ((NewThread) Thread.currentThread()).setDataPool(dataPool);
            writeDataPool(dataPool, 0);
            performA();
        });
    

    如上,NewThread继承自Thread,并新增DataPool字段,一开始将DataPool对象存储至dataPool字段,而后在readDataPool(int pos)里获取,这样子就无需层层传递参数了。
    既然要做成通用的字段,就不能直接使用DataPool类型了,将之改为泛型:

        class NewThread<T> extends Thread {
    
            public NewThread(@Nullable Runnable target) {
                super(target);
            }
    
            private T data;
    
            public T getData() {
                return data;
            }
    
            public void setData(T data) {
                this.data = data;
            }
        }
    

    大家注意到了我们t1、t2分别调用了setData getData

    ((NewThread) Thread.currentThread()).setData(dataPool);
    ((NewThread) Thread.currentThread()).getDataPool();
    

    实际上调用方式一模一样的,想想是不是可以统一管理一下。

        static NewThreadLocal<DataPool> newThreadLocal = new NewThreadLocal<>();
        NewThread t1 = new NewThread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            newThreadLocal.set(dataPool);
            writeDataPool(dataPool, 0);
            performA();
        });
    
        NewThread t2 = new NewThread(() -> {
            DataPool dataPool = new DataPool(new int[5]);
            newThreadLocal.set(dataPool);
            writeDataPool(dataPool, 0);
            performA();
        });
    
        static class NewThreadLocal<T> {
            public void set(T data) {
                ((NewThread) Thread.currentThread()).setData(data);
            }
    
            public T getData() {
                return (T) ((NewThread) Thread.currentThread()).getData();
            }
        }
    

    为了线程方便调用newThreadLocal变量,将之定义为static类型。
    之前我们处理的都是单个数据类型如DataPool,现在将DataPool分为免费和收费

        static NewThreadLocal<FreeDataPool> newThreadLocalFree = new NewThreadLocal<>();
        static NewThreadLocal<ChargeDataPool> newThreadLocalCharge = new NewThreadLocal<>();
        NewThread t1 = new NewThread(() -> {
            FreeDataPool freeDataPool = new FreeDataPool(new int[5]);
            ChargeDataPool chargeDataPool = new ChargeDataPool(new int[5]);
            newThreadLocalFree.set(freeDataPool);
            newThreadLocalCharge.set(chargeDataPool);
            writeDataPool(freeDataPool, 0);
            writeDataPool(chargeDataPool, 0);
            performA();
        });
    
        class FreeDataPool extends DataPool {
            public FreeDataPool(int[] data) {
                super(data);
            }
        }
    
        class ChargeDataPool extends DataPool {
            public ChargeDataPool(int[] data) {
                super(data);
            }
        }
    

    可以看到,freeDataPool变量会覆盖chargeDataPool,因为NewThread只有一个data字段。为了一个线程内能够存储多个不同类型的变量,考虑将data字段升级为Map存储。当然freeDataPool和chargeDataPool当作Map的value字段,那么key该选什么呢?key分别选择newThreadLocalFree和newThreadLocalCharge,一般我们会用对象的hashcode方法。可在NewThreadLocal里添加生成key的方法,最终改造如下:

        class NewThread<T> extends Thread {
    
            public NewThread(@Nullable Runnable target) {
                super(target);
            }
    
            private Map<String, T> data;
    
            public Map getData() {
                return data;
            }
    
            public void setData(Map data) {
                this.data = data;
            }
        }
    
        class NewThreadLocal<T> {
            public void set(T data) {
                Map<String, T> map = ((NewThread) Thread.currentThread()).getData();
                map.put(this.hashCode() + "", data);
            }
    
            public T getData() {
                Map<String, T> map = ((NewThread) Thread.currentThread()).getData();
                return map.get(this.hashCode() + "");
            }
        }
    
        static NewThreadLocal<FreeDataPool> newThreadLocalFree = new NewThreadLocal<>();
        static NewThreadLocal<ChargeDataPool> newThreadLocalCharge = new NewThreadLocal<>();
        NewThread t1 = new NewThread(() -> {
            FreeDataPool freeDataPool = new FreeDataPool(new int[5]);
            ChargeDataPool chargeDataPool = new ChargeDataPool(new int[5]);
            newThreadLocalFree.set(freeDataPool);
            newThreadLocalCharge.set(chargeDataPool);
            writeDataPool(freeDataPool, 0);
            writeDataPool(chargeDataPool, 0);
            performA();
        });
    

    NewThread使用Map存储newThreadLocalFree和newThreadLocalCharge变量,这样不管NewThread存储多少个变量都不会覆盖。老规矩,还是用图表示一下:


    image.png

    至此,我们从探究ThreadLocal使用的场景,一步步摸索到自定义NewThreadLocal解决遇到的问题。幸运的是,Java JDK工程师已经考虑上述所遇到的问题,就是本篇的主角ThreadLocal。NewThreadLocal基本上和ThreadLocal对应,理解了NewThreadLocal,相信很快看懂ThreadLocal,如果直接分析ThreadLocal可能不太好理解为啥这么设计。新模型/方法的设计一定为了解决某些问题的,只有知其然也知所以然才能指导我们更好理解问题直至解决问题。NewThreadLocal和Java ThreadLocal有些差异,为方便理解,对应关系如下:

    • NewThreadLocal->ThreadLocal
    • NewThread->Thread
    • NewThread.data->Thread.threadLocals
    • Map->ThreadLocal.ThreadLocalMap

    NewThreadLocal和ThreadLocal差异之处:
    在于存储结构的不同,NewThreadLocal使用的是HashMap,而ThreadLocal使用的是自定义的ThreadLocalMap,简单比较一下两者(HashMap和ThreadLocalMap异同点):
    1、HashMap使用key本身的hashcode再进行运算得出,而ThreadLocalMap使用ThreadLocal对象作为key,因此有可能引入内存泄漏问题,为此该key使用WeakReference变量修饰。
    2、两者都使用Hash算法:除留余数法
    ThreadLocalMap生成 hash的方法:

        private static final int HASH_INCREMENT = 0x61c88647;
        private static int nextHashCode() {
            return nextHashCode.getAndAdd(HASH_INCREMENT);
        }
    

    使用static变量nextHashCode,其类型为AtomicInteger,每次使用后都会加上固定值HASH_INCREMENT(斐波那契数列相关,目的使hash值分布更均匀),最后将结果更新赋值给nextHashCode。而HashMap使用key的hashcode进行再运算得出。
    3、hash冲突解决方式:HashMap使用链地址法,ThreadLocalMap使用开放地址法-线性探测法。

    总结:

    TheadLocal应用场景:

    变量在线程之间无需可见共享,为线程独有
    变量创建和使用在不同的方法里且不想过度重复书写形参

    ThreadLocal在Android 源码里的运用

    大家还记得上篇文章解析过Handler-Looper,Handler和Looper共享MessageQueue是通过Looper对象作为桥梁的,而Looper对象是以ThreadLocal方式存放在线程里的。

        // sThreadLocal.get() will return null unless you've called prepare().
        @UnsupportedAppUsage
        static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
        @UnsupportedAppUsage
        private static Looper sMainLooper;  // guarded by Looper.class
    

    分三个步骤说一下:
    1、构造并设置Looper对象
    开始的时候,先构造Looper对象,并设置到ThreadLocal里(实际设置到线程变量里),如下:

        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            //ThreadLocal设置looper对象
            sThreadLocal.set(new Looper(quitAllowed));
        }
    

    2、构造Handler并关联MessageQueue

        Looper.java
        public static @Nullable Looper myLooper() {
            return sThreadLocal.get();
        }
    
        public Handler(@Nullable Callback callback, boolean async) {
            if (FIND_POTENTIAL_LEAKS) {
                final Class<? extends Handler> klass = getClass();
                if ((klass.isAnonymousClass() || klass.isMemberClass() || klass.isLocalClass()) &&
                        (klass.getModifiers() & Modifier.STATIC) == 0) {
                    Log.w(TAG, "The following Handler class should be static or leaks might occur: " +
                        klass.getCanonicalName());
                }
            }
    
            //获取looper对象
            mLooper = Looper.myLooper();
            if (mLooper == null) {
                throw new RuntimeException(
                    "Can't create handler inside thread " + Thread.currentThread()
                            + " that has not called Looper.prepare()");
            }
            //关联MessageQueue
            mQueue = mLooper.mQueue;
            mCallback = callback;
            mAsynchronous = async;
        }
    

    Looper.myLooper()方法获取存放在当前线程里的looper对象,如果在第一步中当前线程里调用过prepare设置looper对象,那么此刻myLooper()就能返回之前设置的looper对象。
    3、Looper.java loop()遍历MessageQueue

        public static void loop() {
            //获取当前线程looper对象
            final Looper me = myLooper();
            if (me == null) {
                throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
            }
            //获取MessageQueue
            final MessageQueue queue = me.mQueue;
            //遍历MessageQueue获取message
    }
    

    通过以上三个步骤就实现了Android里的消息队列循环。可以看出,Looper对象是和线程有关,并且不是线程间共享的,每个线程拥有自己独立的Looper对象,并且其一次创建,多个不同的地方使用,因此使用ThreadLocal来持有Looper对象是比较合适的。
    在创建主线程的Looper对象时,将之赋予static变量sMainLooper,而子线程的looper对象存放在ThreadLocal。我们判断是否在主线程里的方法之一:

                    if (Looper.myLooper() == Looper.getMainLooper()) {
                        //主线程
                    }
    

    当然,当前线程可能没有looper,说明一定是主线程。

    ThreadLocal 会有内存泄漏吗

    内存泄漏产生的原因:长生命周期对象持有短生命周期的对象导致短生命周期对象无法被释放。典型的例子就是handler泄漏,因为handler作为内部类持有activity引用,而handler被MessageQueue持有,MessageQueue一直都存在(除非handler的发送的消息都执行完毕或者手动移除handler发送的消息)。
    我们知道ThreadLocal仅仅只是个管理类而已,真正的对象存储在Thread里。ThreadLocal会被当作ThreadLocalMap的key,而Thread持有ThreadLocalMap,进而间接持有ThreadLocal,正常情况下这就可能有内存泄漏的风险(Thread长周期 ThreadLocal短周期),对此ThreadLocalMap对此做了预防:

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

    Entry的key使用了弱引用。
    再者,我们正常使用的时候,一般会设置ThreadLocal为static,而static又是全局的,就不会存在ThreadLocal泄漏的说法了。
    但是:Entry的key使用了弱引用,而其value(我们存储的对象)并没有使用弱引用。当key没有强引用的时候,会被回收,此时key=null,而value并没有被回收。
    综上两点,ThreadLocal变量本身不存在内存泄漏问题,但是value有内存泄漏风险。
    解决方法:使用完毕记得调用ThreadLocal.remove()移除key和value。
    为什么Entry value不设置为弱引用呢?
    还是以Looper.java为例

        private static void prepare(boolean quitAllowed) {
            if (sThreadLocal.get() != null) {
                throw new RuntimeException("Only one Looper may be created per thread");
            }
            //new Looper(quitAllowed)->Looper looper = new Looper(quitAllowed)
            //这里直接new一个Looper对象,最终持有这个对象的引用是ThreadLocalMap里的Entry value
            //如果此时value设置为弱引用,当GC发生时,由于该Looper对象没有强引用,因此会被回收。
            //而此时Entry key还存在,通过key索引value找不到当初设置的Looper对象。
            sThreadLocal.set(new Looper(quitAllowed));
        }
    

    对线程状态有疑问的同学请移步:Java 线程状态

    相关文章

      网友评论

        本文标题:Java ThreadLocal你之前了解的可能有误

        本文链接:https://www.haomeiwen.com/subject/uupvoltx.html