深入浅出 ThreadLocal 的实现原理

ThreadLocal的简介

ThreadLocal称为线程局部变量。在每个线程中都有自己独立的ThreadLocal变量。
每个线程中可有多个threadLocal变量。

ThreadLocal的用法

public class ThreadLocalDemo {

    private static ThreadLocal<Integer> threadLocal1 = new ThreadLocal<Integer>();
    private static ThreadLocal<String> threadLocal2 = new ThreadLocal<String>(){
        @Override
        protected String initialValue() {
            return "默认值";
        }
    };

    public static void main(String[] args) {

        int temp = 100;
        new Thread(() ->{
            threadLocal1.set(temp + 1);
            threadLocal2.set("线程A");
            System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal1 的值为: " +  threadLocal1.get());
            System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal2 的值为: " +  threadLocal2.get());
        }).start();

        new Thread(() ->{
            threadLocal1.set(temp - 1);
            System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal1 的值为: " +  threadLocal1.get());
            System.out.println("线程:" +Thread.currentThread().getName() + "中 threadLocal2 的值为: " + threadLocal2.get());
        }).start();

    }
}

输出:

线程:Thread-0中 threadLocal1 的值为: 101
线程:Thread-0中 threadLocal2 的值为: 线程A
线程:Thread-1中 threadLocal1 的值为: 99
线程:Thread-1中 threadLocal2 的值为: 默认值

可以看到每个线程都保存着自己独立的变量threadLocal1、threadLocal2,互不影响。

ThreadLocal实例通常定义成 static 变量,因此每个线程都能访问到该实例。

抛出问题:为什么每个线程通过ThreadLocal实例时可以拿到自己设置的值?是如何实现的?

ThreadLocal 的实现原理

ThreadLocal类主要有四个方法set()、get()、remove()、setInitialValue(),要想了解它的实现原理,那么就来看看这几个主要方法是如何实现的。

set() 方法
public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
getMap(): 获取ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

threadLocals变量在线程Thread类中定义:

ThreadLocal.ThreadLocalMap threadLocals = null;

每个线程都有一个threadLocals变量即ThreadLocalMap对象,ThreadLocalMap类是ThreadLocal类的静态内部类,用来存储相应的值,总之就是一个Map,后面会详细讲到。

createMap(): new 一个ThreadLocalMap
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

set()方法的过程如下:

1. 当前线程调用ThreadLocal.set()方法时,首先获取当前线程对象t;

2. 通过当前线程对象t获取到t线程中的threadLocals,即ThreadLocalMap

  • 如果ThreadLocalMap存在,那么更新value,key为当前ThreadLocal对象;
  • 如果ThreadLocalMap不存在,那么就根据当前线程对象t创建一个ThreadLocalMap,并将value存入。

到这里,基本就能回答开头所提出的的问题,大体上知道了ThreadLocal的实现原理。再来看get()方法的实现。

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

从上面可以看到,当调用get() 方法时,实际上就是先从当前线程中获取ThreadLocalMap,然后再根据当前this对象即ThreadLocal对象来获取对应的值。如果map不存在或者this对象的key不存在,那么就返回设置的初始值。

到这里就可以总结一下ThreadLocal的实现原理:每个线程都有一个自己的ThreadLocalMap对象,用来存储以ThreadLocal为key、Object为值的键值对,线程与线程之间互不影响。

原理图

image

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

当ThreadLocal对象调用该方法时,获取到线程的ThreadLocalMap,移除以this对象(该ThreadLocal对象)为key的键值对。

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;
    }
protected T initialValue() {
        return null;
    }

该方法主要作用是用来返回初始值,即initialValue()中的值,默认为null,在新建ThreadLocal时可以重写该方法,设置一个初始值。

ThreadLocalMap

前面提到,ThreadLocalMap就是存储相应变量的地方。
ThreadLocalMap和大多数容器一样,维护了一个内部数组,即Entry[]数组,Entey节点如下:

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

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

Entey是一个以ThreadLocal为key,Object为value的键值对,另外Entey继承了WeakReference(弱引用),在Entry的构造方法中,调用了super(k)方法将ThreadLocal实例包装成一个WeakReference。

为什么使用弱引用(ThreadLocal)作为key?

如果是强引用的话,ThreadLocalMap中一直会持有ThreadLocal的强引用,如果没有手动删除,那么ThreadLocal对象就无法回收,导致内存泄漏。

我们知道弱引用无论内存是否足够都会被GC回收。这样当没有强引用指向ThreadLocal对象时就可以被回收,因此也就不会出现ThreadLocal对象的内存泄漏。但还是会出现另一种内存泄漏问题,见下面问题。

为什么会引起内存泄漏?什么时候发生内存泄漏?如何防止内存泄漏?

在线程的生命周期内发生内存泄漏。

我们知道ThreadLocalMap中存储的是key为ThreadLocal的引用,当这个引用失效时即为null时,那么线程中就存在ThreadLocalMap<null, Value>的键值对,此时无法获得对应的Value,于是就存在一条Thread Ref -> Thread -> ThreaLocalMap -> Entry -> Value 强引用链,无法访问到Value,因此就出现了内存泄漏的问题。

防止内存泄漏:
1. 在ThreadLocalMap的set()、get()、remove()方法中,有一个将key为null的Entry擦除的过程,这样Entry内的value也就没有强引用链,自然会被回收。(不能保证一定擦除)

2. 当使用完毕后,显示调用remove()方法,直接清除ThreadLocalMap中以ThreadLocal对象为key的键值对;

扩展

ThreadLocalMap使用开放地址法来处理Hash冲突,而不是拉链法(HashMap、concurrentHashMap)。
主要原因是:在ThreadLocalMap中的散列值分散的十分均匀,很少会出现冲突。并且ThreadLocalMap经常要清除无用的对象,使用纯数组更加方便。

开放地址法:当发现有Hash冲突的时候,不会创建链表,而是继续在数组中寻找空的单元。探测数组中空单元的方式有很多,如线性探测法:从冲突的数组单元开始,依次往后搜索空单元,如果到了尾部还未找到就再从头开始查找,直到找到为止。

ThreadLocalMap <ThreadLocal,Object>>键值对数量为ThreadLocal的数量,一般来说ThreadLocal数量很少,相比在ThreadLocal中用Map<Thread, Object>键值对存储线程变量(Thread数量一般来说比ThreadLocal数量多),性能提高很多。还有一个原因,如果是使用Map<Thread, Object>的方式存储线程变量,还要考虑到增加减少线程时的并发问题。

小结
  • ThreadLocal由于在每个线程中都创建了副本,因此threadLocal会占用一定的内存;是一种空间换时间的思想;
  • threadLocal只是一个工具,封装了ThreadLocalMap对象方法的入口;
  • threadLocal可以用来解决数据库连接、Session管理等问题,在spring也有大量使用,比如HttpServletRequest也是基于ThreadLocal来实现的。
  • ThreadLocal适用于每个线程需要有自己单独的实例,并且该实例需要在多个方法中共享,但不希望被多线程共享的场景。
  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值