Java ThreadLocal | 实现线程之间数据隔离

前言

ThreadLocal的作用主要是做线程之间的数据隔离,原理 👉 在于每个线程保留一份自己的数据,所以数据对别的线程而言是相对隔离的,在多线程环境下,能够防止自己的变量被其它线程篡改。

简单使用一下
public class Test {
    public static void main(String[] args) {
        // 声明ThreadLocal
        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        // 给ThreadLocal赋值
        threadLocal.set("Hello SunnyBoy");
        // 取ThreadLocal中的值
        String resultGet = threadLocal.get();
        // 打印
        System.out.println(resultGet);
        // 清除ThreadLocal中的值
        threadLocal.remove();
    }
}

可以看到,日常使用主要是get()方法、set()方法、remove()方法

为什么能实现线程之间的数据隔离?

其实每个线程Thread都维护了自己的ThreadLocal变量,所以在每个线程创建ThreadLocal的时候,实际上数据是存在自己线程Thread的threadLocals变量里面的,别人没办法拿到,从而实现了隔离。

可以在Thread类找到ThreadLocalMap属性

ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap中定义了静态类Entry,并且Entry中的key是继承弱引用的,在后面我们会讲这个Entry带来的问题。

ThreadLocalMap中也定义了Entry数组table,实际上table就是具体保存数据的数组

static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
  Object value;
  Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
  }
 private Entry[] table;

经过上面的分析,可以知道几者之间的关系如下图所示

1609602057810

ThreadLocal使用场景

在常见的Java EE三层架构里面,如果使用事务,要在service层里面进行开启事务(因为处理逻辑业务都是service层),如果我们要使用事务,那么就必须保证执行sql语句的connection连接和开启事务的connection连接都要保持是同一个对象,所以我们要确保在service层和dao层的两个connection连接都是同一个,

但是怎么保证connection连接对象都是同一个呢?

  • 一是通过方法传参的方式进行数据的一层一层的传递,但是这样不好,因为你把架构分成三层的目的就是为了数据的处理和逻辑业务的处理分离开来(就是dao层处理数据,service层处理业务),connection连接对象我们应该是在service层出现的,但是你却放到了dao层,这样数据的处理和逻辑业务的处理没有分离开来,javaee的三层开发就没有他的效果了,所以这一种方式的解决方法不好。
  • 二是通过ThreadLocal的方式来存储这个connection对象,这样就能够保证在service层和dao层的数据保证一致了,并且不会出现耦合,事实上spring事务就是基于ThreadLocal实现的。
源码解析
get()方法

前面我们说过,数据是保存在线程本身的ThreadLocalMap中的,所以get()主要也是去ThreadLocalMap取值,而想获取到ThreadLocalMap,首先就是通过当前线程去获取。

 public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取一个ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 如果取到的map不为null,从ThreadLocalMap中取值
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 取到值就返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果拿到的ThreadLocalMap为空,交由setInitialValue()处理
        return setInitialValue();
    }

getMap(Thread t) 逻辑很简单,获取一个ThreadLocalMap,返回线程的本地变量即可。

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

同时,在拿到保存数据的ThreadLocalMap后,上面代码注释提到,如果ThreadLocalMap不为null,他会进一步通过key映射出应该取table中的哪个数据,具体实现是通过getEntry()实现的

private Entry getEntry(ThreadLocal<?> key) {
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e;
            else
                return getEntryAfterMiss(key, i, e);
        }

在ThreadLocal的get()方法最后,前面介绍,如果拿到的ThreadLocalMap为空,会调用setInitialValue(),

private T setInitialValue() {
    // initialValue()实际上源码中只会返回null,但可重写,使其返回预期的值。
    T value = initialValue();
    // 获取当前线程
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    // 如果获取到ThreadLocalMap,往map里放键值对,这里,key就是当前的ThreadLocal,value就是null
    // (——如果不重写initialValue()的话)
    if (map != null)
        map.set(this, value);
    // 否则创建一个ThreadLocalMap
    else
        createMap(t, value);
    // 返回null
    return value;
}

补充initialValue()如下,默认是返回null

protected T initialValue() {
    return null;
}

前面提到,我们也可以重写initialValue()方法,让他返回预期的初始值,例如:

 //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM 
        = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

ThreadLocalMap取不到时候,需要创建,调用createMap()如下所示

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
set()方法
public void set(T value) {
        // 与get方法一样,获取当前线程
        Thread t = Thread.currentThread();
        // 同样去获取当前线程保留的ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // 不为空就set
        if (map != null)
            map.set(this, value);
        else
            // 创建map并传入值
            createMap(t, value);
    }

在上面调用了ThreadLocalMap的set()方法

private void set(ThreadLocal<?> key, Object value) {
        // 往Entry数组中放数据
        Entry[] tab = table;
        int len = tab.length;
        int i = key.threadLocalHashCode & (len-1);
 
        for (Entry e = tab[i]; e != null;e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
             // 如果key已经存在,赋值即可
            if (k == key) {
                e.value = value;
                return;
            }
            // key不存在,
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        // e==null,那么就在e这个位置创建新Entry
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
remove()

同样,既然ThreadLocalMap是存在线程中的,故也应该先获取当前线程。

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

移除操作如下所示

private void remove(ThreadLocal<?> key) {
            Entry[] tab = table;
            int len = tab.length;
            int i = key.threadLocalHashCode & (len-1);
            // 遍历找到,删除
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }
使用InheritableThreadLocal实现线程间数据传递

ThreadLocal使得线程之间数据是隔离的,那么如果要实现线程间数据传递怎么办

可以使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

public static void main(String[] args) {
        ThreadLocal threadLocal1 = new InheritableThreadLocal();
        threadLocal1.set("Hello SunnyBoy");
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName()+":"+threadLocal1.get());
            }
        }).start();
    }
// 打印结果为Thread-0:Hello SunnyBoy
ThreadLocal可能导致的内存泄漏问题

前面我们讲过ThreadLocalMap中的Entry的key,也就是ThreadLocal,它是继承弱引用的,而value是强引用,弱引用是当每次GC时都会释放资源,那么就很有可能出现key被释放,value依旧存在的情况,这就会产生内存泄漏问题。

怎么办?

  • ThreadLocalMap实现中已经考虑了这种情况,在调用 set()get()remove() 方法的时候,会清理掉 key 为 null 的记录。
  • 使用完 ThreadLocal方法后 最好手动调用remove()方法
static class Entry extends WeakReference<ThreadLocal<?>>
为什么要将key设计成ThreadLocal的弱引用?

如果key是强引用,同样会发生内存泄漏的

引用ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。

如果是弱引用的话,引用ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收,此时的key为null,但在下一次调用ThreadLocalMap的set()、get()、remove()方法时,会清除 key 为 null 的 value 值,避免内存泄漏。

因此,ThreadLocal内存泄漏的根本原因是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏,而不是因为弱引用。

所以两种方案比较下来,还是ThreadLoacl的key为弱引用好一些。

总结

本篇博文是笔者的一个知识总结,总台来说较为啰嗦,但是有些地方是故意重复的,毕竟重复的是重点,重点是考点~

参考

本文部分文字和图片参考以下文章~

https://www.cnblogs.com/aobing/p/13382184.html
https://www.itqiankun.com/article/1564891332/1000
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值