ThreadLocal的使用及解析

为什么要了解ThreadLocal

提起这个东西好多android开发人员可能会感觉比较陌生,因为在平时的工作中好像根本用不到。好像的确是这个样子,但是作为一个android开发人员却依然很有必要去了解一下它,因为它离我们
并不远。我们Android系统中由Handler、MessageQueue和Looper构成的消息循环机制有多重要就不用我多说了吧,而其中Looper和多线程就是通过
ThreadLocal来进行管理的,有兴趣的小伙伴可以查看相关源码,这里就不赘述了。

1.ThreadLocal简介

JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。使用这个工具类可以很简洁地编写出优美的多线程程序,ThreadLocal并不是一个Thread,而是Thread的局部变量。ThreadLocal一般称为线程本地变量,它是一种特殊的线程绑定机制,将变量与线程绑定在一起,为每一个线程维护一个独立的变量副本。通过ThreadLocal可以将对象的可见范围限制在同一个线程内。

2.ThreadLocal的使用

上面我们简单介绍了什么是ThreadLocal,那么我们首先来看一下它的用法

public class ThreadLocalTest {
    // 新建一个 ThreadLocal 对象
    static ThreadLocal<Integer> value = new ThreadLocal<Integer>();

    public static void startTest() {
        // 新建 5 个子线程,run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    while (value.get() <= 5) {
                        System.out.println(Thread.currentThread().getName() + " 的 value 值:" + value.get());
                        // 当前线程的 value 自增一
                        value.set(value.get() + 1);
                    }
                }
            }, "线程 " + (i+1)).start();
        }
    }

    public static void main(String[] args) {
        ThreadLocalTest.startTest();
    }
}

怎么样代码是不是很简单,可是你运行上面代码你会发现报错了,还是我们常见的空指针异常。报错发生在10行,不难分析是value.get()为null造成的
。如果你对ThreadLocal的用法比较了解的话,肯定一眼就能看出来,我在创建ThreadLocal对象的时候没有重写它的initialValue()方法。许多文章
只是在介绍用法的时候提醒一定要重写这个方法或者先调用set()进行初始化,并没有详细解释,下面我们根据源码去看看原因。

首先我们先找到ThreadLocal的get()方法

/**
     * Returns the value in the current thread's copy of this
     * thread-local variable.  If the variable has no value for the
     * current thread, it is first initialized to the value returned
     * by an invocation of the {@link #initialValue} method.
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }

看代码我们可以知道,首先是获取到了当前进线程,然后根据当前线程获取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对象的threadLocals属性,这时还没有对它进行赋值,获取到的是它的初始值null,那么get()方法中的if判断就会跳过,那么我们只需要看一下
setInitialValue()方法中做了什么就可以了。

/**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    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;
    }

     protected T initialValue() {
        return null;
    }

我们可以看到首先对调用了initialValue()方法对value进行了赋值,而我们在没重写这个方法的时候返回的是null,后面走完判断的代码直接将null返回了
所以到这我们就找到了报空指针的原因了。那么正确的使用方法如下:

public class ThreadLocalTest {
    // 新建一个 ThreadLocal 对象
    static ThreadLocal<Integer> value = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            //进行初始化 进行初始化 进行初始化
            return 0;
        }
    };

    public static void startTest() {
        // 新建 5 个子线程,run 方法中调用新建的 ThreadLocal 对象 value 的 get/set 方法来获取/设置对应值
        for (int i = 0; i < 5; i++) {
            new Thread(new Runnable() {
                public void run() {
                    while (value.get() <= 5) {
                        System.out.println(Thread.currentThread().getName() + " 的 value 值:" + value.get());
                        // 当前线程的 value 自增一
                        value.set(value.get() + 1);
                    }
                }
            }, "线程 " + (i+1)).start();
        }
    }

    public static void main(String[] args) {
        ThreadLocalTest.startTest();
    }
}

下面是运行结果,我们可以看到线程之间是不受影响的

运行结果

3.进一步分析

到这并没有结束,上面我们还留着一个疑问,ThreadLocalMap是干什么的呢,其实它才是关键,这个类是ThreadLocal的静态内部类,有点类似于HashMap,
通过一个内部类Entry来保存对应的键值对信息,并且键必须是一个ThreadLocal对象。由于这个类比较占篇幅,就不贴代码了,感兴趣的同学可以自己去查看源码,那么它是如何创建的呢,仔细的同学可能会发现上面的代码中createMap()这个方法,
那么我们看一下这个方法中做了什么

 /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);//this就是代表的当前ThreadLocal对象
    }

很明显,这个方法是为当前线程创建了一个ThreadLocalMap对象,而它的Entry对象的value其实就相当于一个副本,至此,大家应该已经明白了ThreadLocal是如何为每个线程创建变量的副本的了吧。

总结

到这里我们就把ThreadLocal的用法和原理大致走了一遍,那我们简单总结一下。 ThreadLocal在某些情况可以代替线程之间的同步机制解决线程之间的同步问题,但是也要根据具体情况来使用。因
为 ThreadLocal 采用的机制是每个线程内部都保存了相应的副本,而不同的线程的值是独立的。而比较常见的卖票问题就无法用它来完成,比较稳妥的方法还是通过java的锁机制去解决。如果在内存足够的情况下,
ThreadLocal是比较高效的,因为它毕竟就是用空间换时间的一种做法。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值