【并发】对ThreadLocal的理解和使用

12 篇文章 1 订阅
4 篇文章 0 订阅

参考:

  1. Java并发编程:深入剖析ThreadLocal

  2. 面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)
     

一、介绍:

ThreadLocal,叫做线程本地变量,也可以叫做线程本地存储,ThreadLocal为变量在每一个线程中都创建出自己的副本值,所以多个线程之间相互不影响。

Thread类的源码中,有一个 ThreadLocal.ThreadLocalMap 类型的成员变量 threadlocals ,用来存储与当前线程相关的所有ThreadLocal值。

ThreadLocalMap是ThreadLocal的内部类,使用 Entry 结构来存储值,Entry的key是当前ThreadLocal变量,value是变量副本。

结构如下所示

Thread类中的threadLocals变量:


	/* ThreadLocal values pertaining to this thread. This map is maintained
 	* by the ThreadLocal class. */
	ThreadLocal.ThreadLocalMap threadLocals = null;

 

ThreadLocal类的内部类ThreadLocalMap:

static class ThreadLocalMap {

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

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

    /**
     * The initial capacity -- MUST be a power of two.
     */
    private static final int INITIAL_CAPACITY = 16;

    /**
     * The table, resized as necessary.
     * table.length MUST always be a power of two.
     */
    private Entry[] table;
    ......
    ......
}

 

二、ThreadLocal常用方法源码

1. get

获取当前线程在这个threadLocal变量中的副本值。

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();
}

2. set

设置当前线程指定的副本值存入threadLocal变量。

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

3. remove

public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
        m.remove(this);
}

4. setInitialValue

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;
}

5. initialValue

protected T initialValue() {
        return null;
}

6. getMap

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

三、创建副本流程

首先,在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,它就是用来存储实际的变量副本的,键为当前ThreadLocal变量,value为变量副本(即T类型的变量)。

初始时,在Thread类里面,threadLocalsnull,当通过ThreadLocal变量调用get()方法或者set()方法时,就会threadLocals进行初始化,并且以当前ThreadLocal变量为键,以ThreadLocal要保存的变量副本为value,存到threadLocals

然后在当前线程里面,如果要使用副本变量,就可以通过get()方法在threadLocals里面查找。
  
下面通过一个例子来证明通过ThreadLocal能达到在每个线程中创建变量副本的效果:

public class TestThreadLocal2 {

    //long类型线程本地变量
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();

    //string类型线程本地变量
    ThreadLocal<String> stringThreadLocal = new ThreadLocal<>();

    public void set() {
        longThreadLocal.set(Thread.currentThread().getId());
        stringThreadLocal.set(Thread.currentThread().getName());
    }

    public Long getLong() {
        return longThreadLocal.get();
    }

    public String getString() {
        return stringThreadLocal.get();
    }

    public static void main(String[] args) throws InterruptedException {
        TestThreadLocal2 test = new TestThreadLocal2();

        test.set();
        System.out.println("threadId: " + test.getLong());
        System.out.println("threadName: " + test.getString());

        Thread thread = new Thread(new Runnable() {
            @Override
            public void run() {
                test.set();
                System.out.println("threadId: " + test.getLong());
                System.out.println("threadName: " + test.getString());
            }
        });

        thread.start();
        thread.join();

        System.out.println("threadId: " + test.getLong());
        System.out.println("threadName: " + test.getString());
    }
}

运行结果:

threadId: 1
threadName: main
threadId: 13
threadName: Thread-0
threadId: 1
threadName: main

从这段代码的输出结果可以看出,在main线程中和Thread-0线程中,longThreadLocal保存的副本值和stringThreadLocal保存的副本值都不一样。
最后一次在main线程再次打印副本值是为了证明在main线程中和thread线程中的副本值确实是不同的。

如果把上边main方法中最开始的test.set()代码去掉会怎么样呢,这样代码会出现空指针异常。
因为在上面的源码展示部分中,我们发现如果没有先set的话,即在map中查找不到对应的存储,那么调用get()方法时则会通过调用setInitialValue()方法返回,而在setInitialValue()方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。这就是出现空指针异常的原因。

在这里插入图片描述

在这里插入图片描述
要解决也很简单,只需要重写initialValue()方法:

    //long类型线程本地变量
    ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>() {
        @Override
        protected Long initialValue() {
            return Thread.currentThread().getId();
        }
    };

    //string类型线程本地变量
    ThreadLocal<String> stringThreadLocal = new ThreadLocal<String>() {
        @Override
        protected String initialValue() {
            return Thread.currentThread().getName();
        }
    };

这样的话不调用set()直接调用get()

四、总结

1)实际上,通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;

2)为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象?那是因为每个线程中可以有多个threadLocal变量,就像上面代码中的longThreadLocalstringThreadLocal

3)在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值