ThreadLocal

ThreadLocal


一、官方介绍

ThreadLocal简介:ThreadLocal 类用来提供线程内部的局部变量。这种变量在多线程环境下访问(提供 get 和 set 方法访问)时,能保证各个线程的变量独立于其他线程内的变量。ThreadLocal 实例通常来说都是private static类型的,用于关联线程和线程上下文的。

总结就是,ThreadLocal 的作用:提供线程内的局部变量,不同的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度。

所用场景:

  • 线程并发:在多线程并发的环境下(单线程用不到)。
  • 传递数据:可以通过 ThreadLocal 在同一线程,不同组件中传递公共变量。
  • 线程隔离:每个线程的变量都是独立的,不会相互影响。

二、基本使用

1 常用方法
方法声明描述
ThreadLocal()创建ThreadLocal对象
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量
2 使用案例

需求:线程隔离
在多线程并发的场景下,每个线程中的变量都是相互独立。
线程A:设置变量a,获取变量a
线程B:设置变量b,获取变量b

ThreadLocal:

  • set():将变量绑定到当前线程中。
  • get():获取当前线程绑定的变量。
public class TestMain {

    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        TestMain tm = new TestMain();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    tm.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("===================================");
                    System.out.println(Thread.currentThread().getName() + "===>" + tm.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

运行效果:
在这里插入图片描述

发现无论运行多少次,各个线程和获取到的数据都是乱的,也就是各个线程之间没有线程隔离。
当进行如下修改过后,实现了线程隔离:

public class TestMain {

    ThreadLocal<String> t1 = new ThreadLocal<>();

    private String content;

    private String getContent() {
        // return content;
        // 获取当前线程绑定的变量
        return t1.get();
    }

    private void setContent(String content) {
        // this.content = content;
        // 将变量绑定到当前线程中去
        t1.set(content);
    }

    public static void main(String[] args) {
        TestMain tm = new TestMain();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    tm.setContent(Thread.currentThread().getName() + "的数据");
                    System.out.println("===================================");
                    System.out.println(Thread.currentThread().getName() + "===>" + tm.getContent());
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

效果图:各个线程之间数据一一对应:

在这里插入图片描述

3 ThreadLocal 和 synchronized 同步方式

在上述例子中我们也可以通过加锁的方式来实现,用 synchronized 加锁实现的效果如下:

public class TestMain2 {

    private String content;

    private String getContent() {
         return content;
    }

    private void setContent(String content) {
         this.content = content;
    }

    public static void main(String[] args) {
        TestMain2 tm = new TestMain2();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    synchronized (TestMain2.class) {
                        tm.setContent(Thread.currentThread().getName() + "的数据");
                        System.out.println("===================================");
                        System.out.println(Thread.currentThread().getName() + "===>" + tm.getContent());
                    }
                }
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

运行效果:
在这里插入图片描述

可以发现也实现了线程隔离,但是这样加锁的方式,使得线程之间只能排队访问,势必会使得性能降低,也使程序失去了并发性,显然本案例中使用 synchronized 是不合适的。

4 synchronized 和ThreadLocal的区别

虽然 ThreadLocal 锁和 synchronized 关键字都用于处理多线程并发访问变量的问题,不过两者处理问题的角度和思路不一样。

-synchronizedThreadLocal
原理同步机制采用 以时间换空间 的方式,只提供了一份变量,让不同的线程排队访问ThreadLocal采用 以空间换时间 的方式,为每一个线程都提供了一份变量的副本,从而实现同时访问而互相互不干扰
侧重点多个线程之间访问资源的同步多线程中每个线程之间的数据相互隔离

总结:在刚刚的案例中,索然使用 ThreadLocal 和 synchronized 都能实现线程之间数据的隔离,但是用 ThreadLocal 更为合适,因为这样可以使线程拥有更高的并发性。


三、运用场景

我们简单构建一个转账场景:有一个 account 表,里面有两个用户张三和李四,用户张三给李四转账。

在这里插入图片描述
由于代码比较简单,具体的java代码此处就不再贴出,而是直接思考问题。
案例中的转账涉及到两个 DML 操作:一个转出、一个转入。这些操作是需要具备原子性的,不可分割,不然就会出现数据修改的异常问题。

比如 service 层的代码:

public class AccountService {
    
    // 转帐
    public Boolean transfer(String outUser, String inUser, int money) {
        AccountDao ad = new AccountDao();
        try {
            // 转出
            ad.out(outUser, money);
            // 异常(会导致转出成功,但转入失败)
            int a = 1/0;
            // 转入
            ad.in(inUser, money);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
        return true;
    }
}

常规处理:

对转帐这个操作进行事务处理,两个操作要么同时成功,要么同时失败。但是使用事务需要注意:service 层和 dao 层的连接对象保持一致,每个线程的 connection 对象必须前后一致,线程隔离。

ThreadLocal处理:

直接获取当前线程绑定的的连接对象,如果连接对象是空的,再去连接池中获取连接,将此连接对象跟当前线程进行绑定。

在一些特定场景下,使用 ThreadLocal 有以下两个突出的优势:

  • 传递数据:保证每个线程绑定的数据,在需要的地方可以直接获取,避免参数直接传递过来的代码耦合问题。
  • 线程隔离:各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失。

四、ThreadLocal 内部结构

如果我们不去看源代码的话,可能会猜测 ThreadLocal 是这样子设计的:每个ThreadLocal 都创建一个 Map,然后用线程作为 Map 的 key,要存储的局部变量作为 Map 的 value,这样就能达到各个线程的局部变量隔离的效果。这是最简单的设计方法,JDK 最早期的 ThreadLocal 确实是这样设计的,但现在已经不是了。

在这里插入图片描述

现在的设计:

在 jdk8 中,ThreadLocal 的设计是:每个 Thread 维护一个 ThreadLocalMap,这个 map 的 key 是 ThreadLocal 实例本身,value 才是真正要存储的值 object。

具体的过程如下:

  • 1、每个 Thread 线程内部都有一个 Map(ThreadLocalMap)。
  • 2、Map 里面存储 ThreadLocal 对象 (key) 和线程的变量副本 (value)。
  • 3、Thread 内部的 Map 是由 ThreadLocal 维护的,由 ThreadLocal 负责向 map 获取和设置线程的变量值。
  • 4、对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。

在这里插入图片描述
jdk8 这样设计的好处:

  • 1、每个 map 存储的Entry数量变少(因为实际开发中 ThreadLocal 变 Thread 数量少)。
  • 2、当 Thread 销毁的时候,ThreadLocalMap 也会随之销毁,减少内存的使用。

五、ThreadLocal 核心方法源码

基于 ThreadLocal 的内部结构,我们继续分析它的核心方法源码,更深入了解其操作原理。

除了构造方法之外,ThreadLocal 对外暴露的方法有以下4个:

方法声明描述
protected T initialValue()返回当前线程局部变量的初始值
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

以下是这4个方法的详细源码分析(为了保证思路清晰,ThreadLocalMap部分暂时不展开,下一个知识点详解)

1、set方法

set方法源码以及中文解释:

	// 设置当前线程的ThreadLocal值
    public void set(T value) {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null) {
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        } else {
            // 当前线程Thread 不存在ThreadlocalTMap对象
            // 则调用createMap进行ThreadlocalMap对象的初始化
            // 并将 t(当前规程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        }
    }
    
    /**
     * @param t 当前线程
     * @return the map 对应维护的ThreadLocalMap
     */
    ThreadLocal.ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * @param t 当前线程
     * @param firstValue 存放到map中的第一个entry值
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocal.ThreadLocalMap(this, firstValue);
    }

代码执行流程:

  • 1、首先获取当前线程,并根据当前线程获取一个 Map。
  • 2、如果获取的 Map 不为空,则将参数设置到 Map 中(当前 ThreadLocal 的引用作为key)。
  • 3、如果 Map 为空,则给该线程创建 Map,并设置初始值。
2、get方法

get方法源码以及中文解释:

    /**
     * 近回当前线程中保存ThreadLocal的值
     * 如果当前线程没有此ThreadLocal变量
     * 则它会通过调用{@link #initialvalue] 方法进行初始化值
     *
     * @return 近回当前线程中的ThreadLocal值
     */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadlocalMap对象
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的Threadlocal 为 key,调用getEntry获取对应的存储实体 e
            ThreadLocal.ThreadLocalMap.Entry e = map.getEntry(this);
            // 对 e进行判空
            if (e != null) {
                @SuppressWarnings("unchecked")
                // 获取存储实体 e 对应的 value值
                // 即为我们想要的当前线程对应此ThreadLocal的值
                T result = (T)e.value;
                return result;
            }
        }
        /**
         * 初始化 : 有两种情况有执行当前代码
         * 第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
         * 第二种情况: map存在,但是没有与当前ThreadLocal关联的entry
         */
        return setInitialValue();
    }
    
    /**
     * 初始化
     * @return 切始化后的值
     */
    private T setInitialValue() {
        // 调用initialvaue获取初始化的值
        // 此方法可以被子类重写,如果不重写默认返国null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadlocaTMap对象
        ThreadLocal.ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null) {
            // 存在则调用map.set设置此实体
            map.set(this, value);
        } else {
            // 1)当前线程Thread 不存在ThreadlocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前钱程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        }
        if (this instanceof TerminatingThreadLocal) {
            TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
        }
        return value;
    }

代码执行流程:

  • 1、首先获取当前线程,根据当前线程获取一个 Map
  • 2、如果获取的 Map 不为空,则在 Map 中以 ThreadLocal 的引用作为 key,在 Map 中获取对应的 Entry e,否则转
    到4
  • 3、如果e不为null,则返回e.value,否则转到4
  • 4、Map为空或者e为空,则通过 initialValue 函数获取初始值value,然后用 ThreadLocal 的引用和value作为 firstKey 和firstValue 创建一个新的 Map。
    总结: 先获取当前线程的 ThreadLocalMap 变量,如果存在则返回值,不存在则创建并返回初始值。

3、remove 方法

remove 方法源码以及中文解释:

    /**
     * 删除当前线程中保存的Threadlocal对应的实体entry
     */
    public void remove() {
        // 获取当前线程对象中维护的ThreadlocaMap对象
        ThreadLocal.ThreadLocalMap m = getMap(Thread.currentThread());.
        // 如果此map存在
        if (m != null) {
            // 存在则调用map.remove
            // 以当前ThreadLoca为key删除对应的实体entry
            m.remove(this);
        }
    }

代码执行流程:

  • 1、首先获取当前线程,并根据当前线程获取一个 Map。
  • 2、如果获取的 Map 不为空,则移除当前 ThreadLocal 对象对应的 entry。
4、initialValue 方法

initialValue 方法源码以及中文解释:

    /**
     * 近回当前线程对应的Threadlocal的初初始值
     * 此方法的第一次调用发生在当线程通过get方法访问此钱程的ThreadLocal值时,
     * 除非线程先调用了set方法,在这种情况下,initialvalue 才不会被这个线程调用,
     * 通常情况下,每个线程最多调用一次这个方法。
     * 这个方法仅仅简单的返回null {@code null};
     * 如果程序员想Threadlocal线程局部变量有一个除null以外的初始值
     * 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
     * 通常,可以遇过匿名内部类的方式实现
     * @return 当前Threadlocal的初始值
     */
    protected T initialValue() {
        return null;
    }

此方法的作用是,返回该线程局部变量的初始值。

  • 1、这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了 get 方法时才执行并且仅执行1次。
  • 2、这个方法缺省实现直接返回一个 null。
  • 3、如果想要一个除null之外的初始值,可以重写此方法。(备注:该方法是一个 protected 的方法,显然是为了让子类覆盖而设计的)

六、ThreadLocalMap源码分析

在分析 ThreadLocal 方法的时候,我们了解到 ThreadLocal 的操作实际上是围绕 ThreadLocalMap 展开的,ThreadLocalMap 的源码相对比较复杂,我们从以下三个方面进行讨论。

在这里插入图片描述
成员变量:

跟HashMap类似,INITIAL_CAPACITY 代表这个 Map 的初始容量;table 是一个 Entry 类型的数组,用于存诸数据;size 代表表中的存储数目;threshold 代表需要扩容时对应 size 的闽值。

       // 初始容量
       private static final int INITIAL_CAPACITY = 16;

       /**
        * 存储数据的table
        * table.length MUST always be a power of two.
        */
       private Entry[] table;

       /**
        * 数组里面entrys的个数,可以用于判断table当前使用量是否超过阈值
        */
       private int size = 0;

       /**
        * 进行扩容的阈值,表使用量大于它的时候进行扩容
        */
       private int threshold; // Default to 0
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值