多线程篇之ThreadLocal详解

一、为什么需要ThreadLocal

public class ThreadLocalTest {
    private static Integer num = 0;
    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                num += 5;
                System.out.println(Thread.currentThread().getName() + "->" + num);
            }, "Thread-" + i);
            threads[i].start();
        }
    }
}

在这里插入图片描述
我们可以看到每个线程拿到的值都是不一样的,即它们共享了变量资源。

思考以下几种情况:
每个用户请求过来之后,它的数据是受保护的,隔离的。比如第一个用户的id是1,第二个用户的id是2。那么它把id改成了2,第一个用户拿到的id就是2了(我们希望拿到的是1),这样线程之间就会相互影响,是不允许存在的。

那我们想要每个线程都拿到各自自己的值该怎么做呢?

ThreadLocal可以解决这个问题

/**
 * ThreadLocal:每个线程有自身的存储本地、局部区域
 * 方法:get/set/initialValue
 */
public class ThreadLocalTest {
    private static Integer num = 0;
    public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;//初始值
        }
    };
    public static void main(String[] args) {
        Thread[] threads = new Thread[5];
        for (int i = 0; i < 5; i++) {
            threads[i] = new Thread(() -> {
                int num = local.get();
                num += 5;
                local.set(num);
                System.out.println(Thread.currentThread().getName() + "->" + num);
            }, "Thread-" + i);
            threads[i].start();
        }
    }
}

在这里插入图片描述
我们可以看到每个线程拿到的值都是一样的,线程之间没有相互影响。

二、ThreadLocal介绍

通常情况下,我们创建的变量是可以被任何⼀个线程访问并修改的。如果想实现每⼀个线程都有自己的专属本地变量该如何解决呢? JDK 中提供的 ThreadLocal 类正是为了解决这样的问题。

ThreadLocal 类主要解决的就是让每个线程绑定⾃⼰的值,可以将 ThreadLocal 类形象的⽐喻成存放数据的盒⼦,盒⼦中可以存储每个线程的私有数据。

如果你创建了⼀个 ThreadLocal 变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是 ThreadLocal 变量名的由来。可以使用 get 和 set 方法来获取默认值或将其值更改为当前线程所存的副本的值,从⽽避免了线程安全问题。

再举个简单的例子: 比如有两个⼈去宝屋收集宝物,这两个共用一个袋子的话肯定会产生争执,但是给他们两个人每个人分配⼀个袋子的话就不会出现这样的问题。如果把这两个人比作线程的话,那么 ThreadLocal 就是用来避免这两个线程竞争的。

三、ThreadLocal源码分析

1、先来看看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);//获取t线程的threadLocals 
        if (map != null) {//map不为空
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //map为空的话,就赋初试值
        return setInitialValue();
    }

再看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 ,其类型是ThreadLocalMap(ThreadLocal的静态内部类),就相当于每个线程都有一个集合去存储数据,那是不是就可以实现隔离了。

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

get()方法里的map为空时,就要赋一个初始值:

private T setInitialValue() {
        T value = initialValue();//value为初始值
        Thread t = Thread.currentThread();//得到当前线程t
        ThreadLocalMap map = getMap(t);//获取t线程的threadLocals 
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
protected T initialValue() {
        return null;
    }

我们可以重写initialValue()方法,那么其初始值就不是null了。

public static final ThreadLocal<Integer> local = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue() {
            return 0;//初始值
        }
    };

setInitialValue(),如果map为空,就调用createMap()方法

void createMap(Thread t, T firstValue) {
    t.threadLocals = new ThreadLocalMap(this, firstValue);//实例化一个ThreadLocalMap对象
}
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    table = new Entry[INITIAL_CAPACITY];//INITIAL_CAPACITY大小为16
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);//计算hash
    table[i] = new Entry(firstKey, firstValue);//把key和value放进去
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}
static class Entry extends WeakReference<ThreadLocal<?>> {
    /** The value associated with this ThreadLocal. */
    Object value;

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

ThreadLocalMap使用Entry[]数组来存储<key,value>键值对,key是ThreadLocal的一个对象(也就是当前线程),value是初始值initialValue。
在这里插入图片描述

2、再看set()方法

public void set(T value) {
    Thread t = Thread.currentThread();//获取当前线程t
    ThreadLocalMap map = getMap(t);//获取t线程的threadLocals 
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}

看ThreadLocalMap 里的 map.set()方法

private void set(ThreadLocal<?> key, Object value) {

            // We don't use a fast path as with get() because it is at
            // least as common to use set() to create new entries as
            // it is to replace existing ones, in which case, a fast
            // path would fail more often than not.

            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);//计算hash,用i找到对应的Entry数组元素

            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {//循环找
                ThreadLocal<?> k = e.get();//获取k

                if (k == key) {//找到了key,就覆盖value
                    e.value = value;
                    return;
                }
				//key 为 ThreadLocal 的弱引⽤,key为空就需要回收掉,不然会造成内存泄露
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			//数据超过16,就需要扩容
            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

3、总结

从Thread 类的源代码可以看出 Thread 类中有⼀个 threadLocals 和 ⼀个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为 ThreadLocal 类实现的定制化的 HashMap。

默认情况下这两个变量都是 null,只有当前线程调⽤ ThreadLocal 类的 set 或 get ⽅法时才创建它们,实际上调⽤这两个⽅法的时候,我们调⽤的是 ThreadLocalMap 类对应的 get() 、 set() ⽅法。

我们可以得出结论:
最终的变量是放在了当前线程的 ThreadLocalMap 中,并不是存在 ThreadLocal 上, ThreadLocal 可以理解为只是 ThreadLocalMap 的封装,传递了变量值。 ThrealLocal类中可以通过 Thread.currentThread() 获取到当前线程对象后,直接通过 getMap(Thread t) 可以访问到该线程的 ThreadLocalMap 对象。

我们在同⼀个线程中声明了两个 ThreadLocal 对象的话,都是使用那个 ThreadLocalMap 存放数据的,ThreadLocalMap 的 key 就是 ThreadLocal 对象,value 就是 ThreadLocal 对象调⽤ set 方法设置的值。

在这里插入图片描述
再了解下ThreadLocal的内存泄露问题:

ThreadLocalMap 中使⽤的 key 为 ThreadLocal 的弱引⽤,而value 是强引⽤。
所以,如果 ThreadLocal 没有被外部强引⽤的情况下,在垃圾回收的时候,key 会被清理掉,而value 不会被清理掉。

这样⼀来, ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远⽆法被 GC 回收,这个时候就可能会产⽣内存泄露

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set() 、 get() 、 remove() 方法的时候,会清理掉 key 为 null 的记录。使用完 ThreadLocal 方法后 最好手动调用 remove() 方法。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值