您的当前位置:首页Android 多线程系列----ThreadLocal的使用和

Android 多线程系列----ThreadLocal的使用和

2024-12-14 来源:哗拓教育

一、ThreadLocal的简介和使用

ThreadLocal是一个线程内部的数据存储类。顾名思义,就是用来存储线程的私有数据用的,或者说存放线程自己的变量。举个例子,有三个线程下载文件,每个线程都要记录自己的下载进度的百分比。那我们将如何来实现这个功能呢?
可能有些朋友想了,用一个map来存储三个线程的进度值就可以了嘛,用thread的线程名作为key。实现代码如下:

     private Map<String, Integer> progressMap = new HashMap<>();
    private void test() {
        new Thread(mDlFileRunnable, "thread-1").start();
        new Thread(mDlFileRunnable, "thread-2").start();
        new Thread(mDlFileRunnable, "thread-3").start();
    }
    private Runnable mDlFileRunnable = new Runnable() {
        @Override
        public void run() {
            // do something download file
            int progress = 30;// 模拟计算进度值
            synchronized(progressMap) {
                progressMap.put(Thread.currentThread().getName(), progress); // 设置下载进度
            }
            Log.d(TAG, Thread.currentThread().getName() 
                      + " progress : " + progressMap.get(Thread.currentThread().getName()));

        }
    };

这个想法很直观,其他人也很容易理解,但是有一些问题:
1、增加了一个全局Map变量,需要手动维护,增加了维护的成本。
2、每次记录下载进度,都需要加锁,这种操作比较耗时,影响性能。

下面我们来看一下,怎么使用ThreadLocal实现上述的功能,废话不多说,直接上代码:

   ThreadLocal<Integer> mProgressThreadLocal = new ThreadLocal<>();
    private void testThreadLocal() {
        new Thread(mDlFileRunnableNew, "thread-1").start();
        new Thread(mDlFileRunnableNew, "thread-2").start();
        new Thread(mDlFileRunnableNew, "thread-3").start();
    }
    private Runnable mDlFileRunnableNew = new Runnable() {
        @Override
        public void run() {
            // do something download file
            int progress = 30;// 模拟计算进度值
            mProgressThreadLocal.set(progress);// 设置下载进度
            Log.d(TAG, Thread.currentThread().getName() + " progress : " + mProgressThreadLocal.get());
        }
    };

可以看到这里ThreadLocal 可以很方便的保存每个线程自己的进度,并且不需要维护一个map,也不用考虑这方面的效率问题。

二、ThreadLocal的源码分析

ThreadLocal在很多方面可以实现一些很复杂的功能,例如Android的Looper、ActivityThread、AMS等等。关于Looper的功能,我们后面会专门有文章分析。
现在我们就简单的分析一下ThreadLocal的源码,看看它的实现原理。
ThreadLocal主要使用就是set和get方法。
下面我们就以这两个方法为入口,跟踪源码来分析一下ThreadLocal的原理。

    (注:JDK和Android SDK中的源码有所区别,本文以Android SDK为基准分析的,JDK的实现思路是一致的)

我们先来看ThreadLocal的set方法:


    /**
     * Sets the value of this variable for the current thread. If set to
     * {@code null}, the value will be set to null and the underlying entry will
     * still be present.
     *
     * @param value the new value of the variable for the caller thread.
     */
    public void set(T value) {
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values == null) {
            values = initializeValues(currentThread);
        }
        values.put(this, value);
    }

    /**
     * Gets Values instance for this thread and variable type.
     */
    Values values(Thread current) {
        return current.localValues;
    }
  /**
     * Creates Values instance for this thread and variable type.
     */
    Values initializeValues(Thread current) {
        return current.localValues = new Values();
    }

 /**
         * Sets entry for given ThreadLocal to given value, creating an
         * entry if necessary.
         */
        void put(ThreadLocal<?> key, Object value) {
            cleanUp();

            // Keep track of first tombstone. That's where we want to go back
            // and add an entry if necessary.
            int firstTombstone = -1;

            for (int index = key.hash & mask;; index = next(index)) {
                Object k = table[index];

                if (k == key.reference) {
                    // Replace existing entry.
                    table[index + 1] = value;
                    return;
                }

                if (k == null) {
                    if (firstTombstone == -1) {
                        // Fill in null slot.
                        table[index] = key.reference;
                        table[index + 1] = value;
                        size++;
                        return;
                    }

                    // Go back and replace first tombstone.
                    table[firstTombstone] = key.reference;
                    table[firstTombstone + 1] = value;
                    tombstones--;
                    size++;
                    return;
                }

                // Remember first tombstone.
                if (firstTombstone == -1 && k == TOMBSTONE) {
                    firstTombstone = index;
                }
            }
        }

流程解读:

  • 1、获取当前线程的Thread对象
  • 2、获取当前线程Thread的数据存储区域,即ThreadLocal.Values localValues;
    ThreadLocal.Values是ThreadLocal的一个内部类,功能就是存放数据的键值对。在JDK的源码中,localValues是一个Map,但是在Android SDK中,却是自己用数组实现的HashMap。为什么Google要用数组,自己实现这个hash算法,我们不得而知,可能是出于对自己技术的自信吧。(.)
  • 3、如果localValues 为空,则初始化之
  • 4、调用ThreadLocal.Values的put方法,以ThreadLocal的reference为键,插入数据到数组类型的hash Map中去。

接下来,我们看ThreadLocal的get方法:

  /**
     * Returns the value of this variable for the current thread. If an entry
     * doesn't yet exist for this variable on this thread, this method will
     * create an entry, populating the value with the result of
     * {@link #initialValue()}.
     *
     * @return the current value of the variable for the calling thread.
     */
    @SuppressWarnings("unchecked")
    public T get() {
        // Optimized for the fast path.
        Thread currentThread = Thread.currentThread();
        Values values = values(currentThread);
        if (values != null) {
            Object[] table = values.table;
            // 直接通过hash算法,获取到可能保存的位置
            int index = hash & values.mask;
          // 如果保存位置的reference和ThreadLocal的reference相同,那就是之前保存的
            if (this.reference == table[index]) {
                return (T) table[index + 1];
            }
        } else {
            values = initializeValues(currentThread);
        }

        // 否则进行哈希冲突的处理,即通过哈希算出位置后,依次向后遍历
        return (T) values.getAfterMiss(this);
    }

具体的关系见下图:


ThreadLocal和Thread的关系图.png

通过源码,我们可以得出以下结论:

  • 1、一个ThreadLocal对象,只能为一个Thread保存一个值,因为在此设置会覆盖
  • 2、ThreadLocal保存的线程私有数据,实际上,是保存到了Thread对象自身里面,通过一个哈希map的形式保存
  • 3、通过在Thread自身保存哈希Map的方式,而不是在ThreadLocal中以Manager的方式统一管理,应该主要是从两方面考虑,
    其一降低了代码之间的耦合度,比如将ThreadLocal.Values 拆出来,提供查找和插入操作,逻辑上独立,降低代码耦合
    其二,不同Threa各自管理自己的私有数据Map,存储和查找都很方便,因为Map都相对较小,提高了效率,同时节约了内存。

参考文献:

显示全文