zoukankan      html  css  js  c++  java
  • 并发编程之:ThreadLocal

    大家好,我是小黑,一个在互联网苟且偷生的农民工。

    从前上一期【并发编程之:synchronized】 我们学到要保证在并发情况下对于共享资源的安全访问,就需要用到锁。

    但是,加锁通常情况下会让运行效率降低,那有什么办法可以彻底避免对共享资源的竞争,同时又可以不影响效率呢?答案就是小黑今天要和大家讲的ThreadLocal。

    ThreadLocal是什么?

    该类提供了线程局部 (thread-local) 变量。这些变量不同于它们的普通对应物,因为访问某个变量(通过其 get 或 set 方法)的每个线程都有自己的局部变量,它独立于变量的初始化副本。ThreadLocal 实例通常是类中的 private static 字段,它们希望将状态与某一个线程(例如,用户 ID 或事务 ID)相关联。

    以上来源官方API。大概可以总结为两点:

    1. ThreadLocal提供get/set方法,可以访问属于当前线程的变量,也就是可以保证每个线程的变量不一样。
    2. ThreadLocal使用时通常定义为private static的。

    从字面意思理解,可能会将ThreadLocal认为是本地线程,其实ThreadLocal并不是线程,而是线程Thread的局部变量。

    如何使用

    首先,定义一个private static的ThreadLocal对象。

    private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
    

    然后每个线程可以将当前线程需要存放在局部变量中,并且可以从中获取。

    public void setAndGet(String name) {
        threadLocal.set(name);
        String s = threadLocal.get();
    }
    

    最后在使用完之后,需要将ThreadLocal中的值移除。

    public void remove() {
        threadLocal.remove();
    }
    

    原理

    那么ThreadLocal是如何做到保证每个线程get出来的数据不一样的呢?我们通过源码来看一下。

    public void set(T value) {
        Thread t = Thread.currentThread();
        // 通过当前线程获取出来一个ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    

    我们发现在set方法中,会创建一个ThreadLocalMap,然后将要设置的值放在这个Map中,而当前这个ThreadLocal对象作为key;

    然后再将这个ThreadLocalMap赋值给Thread的threadLocals里。如果去看Thread类的代码会发现,在Thread类中存在两个变量threadLocals和inheritableThreadLocals,它们的类型就是ThreadLocal.ThreadLocalMap。通过下图可以看出Thread,ThreadLocal,ThreadLocalMap三者的关系。

    image

    这时候我们再看看get()和remove()方法的代码。

    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(Thread t) {
        return t.threadLocals;
    }
    
    public void remove() {
        ThreadLocalMap m = getMap(Thread.currentThread());
        if (m != null)
            m.remove(this);
    }
    

    可以看出来,和我们上面图中的结构一样,get()方法就是从Thread中取出来ThreadLocalMap,然后通过ThreadLocal对象作为Key取出值;remove()方法则是取出ThreadLocalMap将ThreadLocal对应的数据移除。

    那么ThreadLocalMap到底是什么呢?是java.util.Map接口的子类吗?我们来看源码。

    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;
            }
        }
        private static final int INITIAL_CAPACITY = 16;
        private Entry[] table;
        private int size = 0;
        private int threshold; // Default to 0
        // 省略其他代码
    }
    

    通过源码发现,ThreadLocalMap并没有实现Map接口,也没有集成其他任何的Map类。是定义在ThreadLocal类中的一个静态内部类。而它的结构和HashMap结构极其相似。有一个Entry[]数组存放数据。而这个Entry类是继承自WeakReference类的子类,这一点和HashMap有所不同。

    ThreadLocalMap和HashMap结构有很多不同,但是还有一点和HashMap不一样,我们来看一下ThreadLocalMap的set方法。

    private void set(ThreadLocal<?> key, Object value) {
        Entry[] tab = table;
        int len = tab.length;
        // 计算key对应放在tab中的下标
        int i = key.threadLocalHashCode & (len-1);
    
        //循环遍历tab,首先获取到下标i对应的对象,如果不为空,则执行循环体
        // 如果不是相同的threadLocal或者位置失效,则要寻找下一个位置。
        for (Entry e = tab[i];
             e != null;
             e = tab[i = nextIndex(i, len)]) {
            ThreadLocal<?> k = e.get();
    	    // 判断与当前ThreadLocal是否是同一个对象,如果是则进行值替换,结束
            if (k == key) {
                e.value = value;
                return;
            }
    		// 如果k==null代表threadLocal的key失效了,将失效的key进行替换
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
    
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }
    

    代码中循环遍历tab,首先获取到下标i对应的对象,如果不为空,则执行循环体,如果在循环体中的两个条件都不满足,则会执行nextIndex()方法,这个方法的代码如下:

    private static int nextIndex(int i, int len) {
        return ((i + 1 < len) ? i + 1 : 0);
    }
    

    这个方法的逻辑就是开放寻址法。而HashMap则是通过拉链法rehash来做的。

    哪些场景使用

    通过上面的内容基本可以掌握ThreadLocal的基本用法,那么ThreadLocal主要在什么场景中使用呢。

    ThreadLocal的作用通过以上了解我们知道主要是用来做线程间数据隔离。那么在什么场景下能用到线程隔离呢?

    首先想到的就是SimpleDateFormat这个工具类,它不是线程安全的,可以通过ThreadLocal在每个线程中放一份,保证线程安全。

    还有比如说用户登录的session,或者token数据,只数据当前会话线程,也可以通过ThreadLocal存储。

    再比如在某些场景下,上下文数据在不同方法之间调用,传递起来非常麻烦,可以通过ThreadLocal存放,只需要在需要用到的地方获取就可以。

    除了这些场景,在某些框架源码中也会使用到,比如Spring中的事务也主要是通过ThreadLocal和面向切面编程AOP实现的,感兴趣的同学可以查看源码了解。

    避免踩坑

    内存泄漏

    ThreadLocalMap中的Entry的Key是一个弱引用,因此如果在使用后不调用remove方法清除掉会导致对应的value内存泄漏。所以在使用完以后一定要记得调用remove方法清除数据。

    image


    好的,今天的内容就到这里,我们下期再见。
    关注我的公众号【小黑说Java】干货内容第一时间送达。
    image

  • 相关阅读:
    Hibernate save, saveOrUpdate, persist, merge, update 区别
    Eclipse下maven使用嵌入式(Embedded)Neo4j创建Hello World项目
    Neo4j批量插入(Batch Insertion)
    嵌入式(Embedded)Neo4j数据库访问方法
    Neo4j 查询已经创建的索引与约束
    Neo4j 两种索引Legacy Index与Schema Index区别
    spring data jpa hibernate jpa 三者之间的关系
    maven web project打包为war包,目录结构的变化
    创建一个maven web project
    Linux下部署solrCloud
  • 原文地址:https://www.cnblogs.com/heiz123/p/15209905.html
Copyright © 2011-2022 走看看