ThreadLocal原理(简单易懂)

最近开发过程中有一个新功能,让一个web应用供登录用户通过浏览器访问,后台应用会获取用户的登录信息(如用户名),并对每个用户的访问做记录. 这是一个并发场景,每次请求都分配一个线程去处理这个请求,web容器一般都会有一个线程池,每次请求都会分配其中的一个空闲线程去处理用户的这次请求, 处理完毕后,线程归还线程池等待后续访问的线程分配.

当然,用户登录信息可以从当前请求request中获取,但是后台应用的多个地方可能都会需要用户登录信息, 一个解决办法是向这些所有用到的地方传递request参数,显然是麻烦的。另外一个办法就是利用ThreadLocal, 获取登录信息后把它放到当前线程中的ThradLocal变量中,任何需要的时候从当前线程中取就可以了,是不是很方便呢?

ThreadLocal介绍

ThreadLocal是线程局部变量,和普通变量的不同在于:每个线程持有这个变量的一个副本,这个变量于当前线程所独有,并且线程之间不会发生冲突。

类中定义的ThreadLocal实例一般会被private static修饰,这样可以让ThreadLocal实例的状态和Thread绑定在一起,业务上,一般用ThreadLocal包装一些业务ID(user ID或事务ID)——不同的线程使用的ID是不相同的。

下面我们从一个例子开始,循着源码,走入ThreadLocal的原理世界吧!(作者目前使用的JDK版本是1.8.0_144版)

使用示例

public class ThreadLocalTest {
    public static void main(String[] args) {
        Bank bank = new Bank();
        Transfor transfor = new Transfor(bank);
        Thread t1 = new Thread(transfor);
        Thread t2 = new Thread(transfor);
        t1.start();
        t2.start();
        System.out.println(bank.get());
    }
}

class Bank {
    // 2 ways to override the initialValue() method
    private ThreadLocal<Integer> money = ThreadLocal.withInitial(() -> 100);
    /* 
    ThreadLocal<Integer> money = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 100;
        }
    };
    */
    
    public int get() {
        return money.get();
    }

    public void set() {
        money.set(money.get() + 10);
    }
}

class Transfor implements Runnable {

    private Bank b;

    public Transfor(Bank bank) {
        b = bank;
    }

    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            b.set();
            System.out.println(Thread.currentThread() + " ---- " + b.get());
        }
    }
}

从以上代码,我们可以看到:我创建了一个Bank类,并重载initialValue()方法使money=100;Transfor类实现Runnable接口,并由main方法生成两个子线程进行调用。

猜猜打印出来的结果是什么?

Thread[Thread-0,5,main] ---- 110
Thread[Thread-0,5,main] ---- 120
Thread[Thread-0,5,main] ---- 130
Thread[Thread-0,5,main] ---- 140
Thread[Thread-0,5,main] ---- 150
Thread[Thread-0,5,main] ---- 160
100
Thread[Thread-1,5,main] ---- 110
Thread[Thread-0,5,main] ---- 170
Thread[Thread-1,5,main] ---- 120
Thread[Thread-0,5,main] ---- 180
Thread[Thread-1,5,main] ---- 130
Thread[Thread-0,5,main] ---- 190
Thread[Thread-0,5,main] ---- 200
Thread[Thread-1,5,main] ---- 140
Thread[Thread-1,5,main] ---- 150
Thread[Thread-1,5,main] ---- 160
Thread[Thread-1,5,main] ---- 170
Thread[Thread-1,5,main] ---- 180
Thread[Thread-1,5,main] ---- 190
Thread[Thread-1,5,main] ---- 200

机智的同学们,发现规律了么?ThreadLocal为money这个变量在每个线程中都生成了一个完全相互独立的值,在对这个值做任何操作的时候,线程之间不会相互影响。这样的特性能帮我们完成很多特殊的业务需求,同时让代码更加简洁且易于维护。想知道ThreadLocal怎样实现如此有趣的功能吗?请继续往下看:

源码分析

先上一张ThreadLocal内部结构图:
threadLocal内部结构图
发现了什么规律了么?ThreadLocalMap是这个class的存储核心!

  • 每个Thread线程内部都有一个Map。
  • Map里面存储线程本地对象(key)和线程的变量副本(value)。
  • Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。同一个Thread中多个ThreadLocal对象共用一个Map。

所以对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

那么首先我们来了解一下ThreadLocalMap相关知识吧!

ThreadLocalMap

此变量在Thread类中被定义:

public class Thread implements Runnable {
        /* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

而在ThreadLocal类中被初始化/调用

public class ThreadLocal<T> {
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

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

可以看到,Map中key存的是当前ThreadLocal对象,而value是我们需要保存的值。

ThreadLocal公有方法

ThreadLocal提供的公有方法如下:
在这里插入图片描述
这些方法都围绕着ThreadLocalMap进行操作

1.withInitial()
    public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }

Java 8以后用于初始化ThreadLocal的一种方法,在外部调用get()方法的时候,会通过Supplier确定变量的初始值;(相当于在new一个ThreadLocal对象时override initialValue()方法)

2. 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);
    }

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

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

步骤:
1.获取当前线程的成员变量map
2.map非空,则重新将ThreadLocal和新的value副本放入到map中。
3.map空,则对线程的成员变量ThreadLocalMap进行初始化创建,并将ThreadLocal和value副本放入map中。

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

步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.从map中获取线程存储的K-V Entry节点。
3.从Entry节点获取存储的Value副本值返回。
4.map为空的话返回初始值null,即线程变量副本为null,在使用时需要注意判断NullPointerException。

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

步骤:
1.获取当前线程的ThreadLocalMap对象threadLocals
2.根据ThreadLocal对象删除这个map中对应的Entity

至 此 , T h r e a d L o c a l 的 基 本 原 理 已 经 讲 完 了 。 如 果 你 还 想 了 解 更 多 , 请 继 续 向 下 看 . . . \color{#ea4335}{至此,ThreadLocal的基本原理已经讲完了。如果你还想了解更多,请继续向下看...} ThreadLocal...

Hash冲突

单独将Hash冲突提出来讲,是因为ThreadLocalMap解决冲突的方式与HashMap不同。众所周知,HashMap使用链地址法解决冲突,而ThreadLocalMap使用的是线性探测法。

线性探测:就是根据初始key的hashcode值确定元素在table数组中的位置,如果发现这个位置上已经有其他key值的元素被占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

为何出现

ThreadLocalMap为一个初始能够保存16个元素的map,每当已使用的空间占整个储存空间的2/3时(2/3为默认临界值),将会将整个Map的容量扩大一倍,并重新计算各个元素的Hash值,存入相应的地方。我们可以设想一下,存储位置都是由每个key的hashcode计算而来,并且希望存到2/3的时候才会扩容,那么一个Thread内的多个ThreadLocal对象之间就容易出现多个对象希望存入同一个空间的冲突,此时就需要解决此问题,让每个Map元素都有位置能存入。

 /**
         * 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;

        /**
         * The number of entries in the table.
         */
        private int size = 0;

        /**
         * The next size value at which to resize.
         */
        private int threshold; // Default to 0

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
如何解决

ThreadLocal引入了一个魔法HashCode: 0x61c88647来尽可能的减少hash冲突,nextHashCode()会使用到此HashCode。在每次生成一个新的ThreadLocal对象时,都会根据nextHashCode生成一个int值作为哈希值,然后根据这个哈希值&(数组长度-1),从而获取到哈希值的低N位(以len为16,16-1保证低四位都是1,从而获取哈希值本身的低四位值),从而获取到在数组中的索引位置。如果还是冲突了怎么办?请看以下ThreadLocalMap的set()源码。为了帮助理解,我加入了一些注释,能够看出使用的是线性探测法,且步长为1。

        private void set(ThreadLocal<?> key, Object value) {
            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();
                // 如果是同一个对象,则覆盖value值
                if (k == key) {
                    e.value = value;
                    return;
                }
                // 如果key为null,则替换它的位置
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
                // 否则继续向后线性探测,直到找到空位为止
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

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

是不是很奇怪为何要用这个魔法数字?因为这个魔法数字能将HashCode计算出来的int型hash值尽可能的均匀分布。
想知道这个魔法数字怎么来的吗?简单来说,是2的32次方乘以黄金比例(√5-1)÷2近似0.618。

关于魔法数字的更多细节,请参考博文 Why 0x61c88647?

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值