ThreadLocal的简单使用和原理

x33g5p2x  于2021-12-06 转载在 其他  
字(8.0k)|赞(0)|评价(0)|浏览(306)

ThreadLocal介绍

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

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

总结:

  • 线程并发:在多线程并发场景下使用
  • 传递数据:我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  • 线程隔离:每个线程的变量是独立的,不会相互影响(核心)

常用方法:

在使用之前,我们先来认识几个ThreadLocal的常用方法

方法介绍
ThreadLocal()创建ThreadLocal对象(构造方法)
public void set(T value)设置当前线程绑定的局部变量
public T get()获取当前线程绑定的局部变量
public void remove()移除当前线程绑定的局部变量

简单案例:

public class ThreadLocalDemo1 {

    private String demoVariable;    // 变量

    public String getDemoVariable() {
        return demoVariable;
    }

    public void setDemoVariable(String demoVariable) {
        this.demoVariable = demoVariable;
    }

    public static void main(String[] args) {
        ThreadLocalDemo1 demo = new ThreadLocalDemo1();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(() -> {
                demo.setDemoVariable(Thread.currentThread().getName()+"的数据");
                System.out.println("----------------------------------");
                System.out.println(Thread.currentThread().getName()+"------>"+demo.getDemoVariable());
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }

}

这段代码比较简单,首先一个类中声明一个变量(demoVariable),然后主方法中 创建五个线程同时操作同一个对象(demo)的setDemoVariable和getDemoVariable方法

代码输出结果:

----------------------------------
线程0------>线程0的数据
----------------------------------
----------------------------------
线程2------>线程2的数据
线程1------>线程2的数据
----------------------------------
----------------------------------
线程3------>线程3的数据
线程4------>线程3的数据

Process finished with exit code 0

看输出结果我们会发现数据比较混乱,有的线程获取的是其他线程的变量,例如:线程1------>线程2的数据
其实了解多线程的都知道,这段代码输出的结果是不确定的。多个线程操作同一对象的可变属性是线程不安全的。

那么怎样解决这个问题呢?
其实解决方案有多种,可以使用Jdk自带的synchronized锁、也可以使用基于AQS的锁、也可以利用CAS机制不加锁…这里这些都不是本篇文章的重点,接下来我们使用咱们的主角ThreadLocal来解决这个问题

上代码:

public class ThreadLocalDemo1 {
    private static ThreadLocal<String> t1 = new ThreadLocal<>();

    public String getDemoVariable() {
        return t1.get();    // 从t1(ThreadLocal对象)中获取值
    }

    public void setDemoVariable(String demoVariable) {
        t1.set(demoVariable);   // 将传过来的参数绑定到t1(ThreadLocal对象)中
    }

    public static void main(String[] args) {
        ThreadLocalDemo1 demo = new ThreadLocalDemo1();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(() -> {
                demo.setDemoVariable(Thread.currentThread().getName()+"的数据");
                System.out.println("----------------------------------");
                System.out.println(Thread.currentThread().getName()+"------>"+demo.getDemoVariable());
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

代码变化:

  1. 在类中增加了一个ThreadLocal对象——t1
  2. 删掉之前的demoVariable变量,因为我们不再操作它了
  3. 修改setDemoVariable方法:将传过来的参数绑定到t1(ThreadLocal对象)中
  4. 修改getDemoVariable方法:从t1(ThreadLocal对象)中获取值

代码输出结果:

----------------------------------
----------------------------------
线程1------>线程1的数据
线程0------>线程0的数据
----------------------------------
线程2------>线程2的数据
----------------------------------
线程3------>线程3的数据
----------------------------------
线程4------>线程4的数据

Process finished with exit code 0

此时我们会发现,每个线程输出的数据已经一一对应上了

ThreadLocal类与synchronized关键字区别

加锁解决方案的代码:

public class ThreadLocalDemo1 {

    private String demoVariable;    // 变量

    public String getDemoVariable() {
        return demoVariable;
    }

    public void setDemoVariable(String demoVariable) {
        this.demoVariable = demoVariable;
    }

    public static void main(String[] args) {
        ThreadLocalDemo1 demo = new ThreadLocalDemo1();

        for (int i=0; i<5; i++) {
            Thread thread = new Thread(() -> {
                synchronized (demo) {
                    demo.setDemoVariable(Thread.currentThread().getName()+"的数据");
                    System.out.println("----------------------------------");
                    System.out.println(Thread.currentThread().getName()+"------>"+demo.getDemoVariable());
                }
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

主要代码:

synchronized (demo) {
    demo.setDemoVariable(Thread.currentThread().getName()+"的数据");
    System.out.println("----------------------------------");
    System.out.println(Thread.currentThread().getName()+"------>"+demo.getDemoVariable());
}

其实主要代码就是这一块儿,每个线程在操作demoVariable变量的时候,都为demo这个对象加上锁

代码输出结果:

----------------------------------
线程0------>线程0的数据
----------------------------------
线程1------>线程1的数据
----------------------------------
线程2------>线程2的数据
----------------------------------
线程3------>线程3的数据
----------------------------------
线程4------>线程4的数据
Process finished with exit code 0

从结果可以发现,加锁确实可以解决这个问题,但是在这里我们强调的是线程数据隔离的问题,并不是线程共享数据的问题,在这个案例中使用synchronized关键字是不合适的

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

  • synchronized:

  • 原理:同步机制采用‘以时间换空间’的方式,只提供一份变量,让不同的线程排队访问

  • 侧重点:多线程之间访问资源的同步

  • ThreadLocal:

  • 原理:ThreadLocal采用‘以空间换时间’的方式,为每个线程都提供一份变量的副本,从而实现同时访问而不互相干扰

  • 侧重点:多线程中让每个线程之间的数据相互隔离

ThreadLocal的内部结构

JDK8中ThreadLocal的设计是:每个Thread维护一个ThreadLocalMap,这个Map的key是ThreadLocal示例本身,value才是真正要存储的值Object。

具体的过程是这样的:

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

ThreadLocal的核心方法源码

基于ThreadLocal的内部结构,我们继续分析它的核心方法源码,更深入地了解其操作原理。
除了构造方法之外,ThreadLocal对外暴露的方法有以下4个:

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

源码和对应的中文注释:

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

 /** * 获取当前线程Thread对应维护的ThreadLocalMap * * @param t the current thread 当前线程 * @return the map 对应维护的ThreadLocalMap */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
	/** *创建当前线程Thread对应维护的ThreadLocalMap * * @param t 当前线程 * @param firstValue 存放到map中第一个entry的值 */
	void createMap(Thread t, T firstValue) {
        //这里的this是调用此方法的threadLocal
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

代码执行流程:

​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则将参数设置到Map中(当前ThreadLocal的引用作为key)

​ C. 如果Map为空,则给该线程创建 Map,并设置初始值

源码和对应的中文注释:

/** * 返回当前线程中保存ThreadLocal的值 * 如果当前线程没有此ThreadLocal变量, * 则它会通过调用{@link #initialValue} 方法进行初始化值 * * @return 返回当前线程对应此ThreadLocal的值 */
    public T get() {
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 如果此map存在
        if (map != null) {
            // 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
            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 the initial value 初始化后的值 */
    private T setInitialValue() {
        // 调用initialValue获取初始化的值
        // 此方法可以被子类重写, 如果不重写默认返回null
        T value = initialValue();
        // 获取当前线程对象
        Thread t = Thread.currentThread();
        // 获取此线程对象中维护的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 判断map是否存在
        if (map != null)
            // 存在则调用map.set设置此实体entry
            map.set(this, value);
        else
            // 1)当前线程Thread 不存在ThreadLocalMap对象
            // 2)则调用createMap进行ThreadLocalMap对象的初始化
            // 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
            createMap(t, value);
        // 返回设置的值value
        return value;
    }

代码执行流程:

​ A. 首先获取当前线程, 根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,否则转到D

​ C. 如果e不为null,则返回e.value,否则转到D

​ D. Map为空或者e为空,则通过initialValue函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map

源码和对应的中文注释:

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

代码执行流程:
​ A. 首先获取当前线程,并根据当前线程获取一个Map

​ B. 如果获取的Map不为空,则移除当前ThreadLocal对象对应的entry

源码和对应的中文注释:

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

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

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

ThreadLocalMap源码分析

未完待续。。。

相关文章