您的位置 首页 java

Java 多线程为啥要有ThreadLocal,怎么用,这篇讲全了

前面我们学习的 线程 并发时的同步控制,是为了保证多个线程对共享数据争用时的正确性的。那如果一个操作本身不涉及对共享数据的使用,相反,只是希望变量只能由创建它的线程使用(即线程隔离)就需要到线程本地存储了。

Java 通过 ThreadLocal 提供了程序对线程本地存储的使用。

通过创建 threadLocal 类的实例,让我们能够创建只能由同一线程读取和写入的变量。因此,即使两个线程正在执行相同的代码,并且代码引用了相同名称的 ThreadLocal 变量,这两个线程也无法看到彼此的存储在 ThreadLocal 里的值。否则也就不能叫线程本地存储了。

本文大纲如下:

ThreadLocal

ThreadLocal 是 Java 内置的类,全称 java.lang .ThreadLoal, java.lang 包里定义的类和接口在程序里都是可以直接使用,不需要导入的。

ThreadLocal 的类定义如下:

 public class ThreadLocal<T> {
    public T get() {
         Thread  t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //......
        return setInitialValue();
    }
    
    public  void  set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map !=  Null ) {
            map.set(this, value);
        } else {
            createMap(t, value);
        }
    }
    
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null) {
             m.remove(this);
         }
     }

    protected T initialValue() {
        return null;
    }
    
    public  static  <S> ThreadLocal<S> withInitial(Supplier<?  extends  S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    
    // ...
}
复制代码  

上面只是列出了 ThreadLocal类里我们经常会用到的方法,这几个方法他们的说明如下。

  • T get()- 用于获取 ThreadLocal 在当前线程中保存的变量副本。
  • void set(T value) – 用于向ThreadLocal中设置当前线程中变量的副本。
  • void remove() – 用于删除当前线程保存在ThreadLocal中的变量副本。
  • initialValue() – 为 ThreadLocal 设置默认的 get方法获取到的始值,默认是 null ,想修改的话需要用子类重写 initialValue 方法,或者是用TheadLocal提供的withInitial方法 。

下面我们详细看一下 ThreadLocal 的使用。

创建和读写 ThreadLocal

通过上面 ThreadLocal 类的定义我们能看出来, ThreadLocal 是支持泛型的,所以在创建 ThreadLocal 时没有什么特殊需求的情况下,我们都会为其提供类型参数,这样在读取使用 ThreadLocal 变量时就能免去类型转换的操作。

  private  ThreadLocal threadLocal = new ThreadLocal();
threadLocal.set("A thread local value");
// 创建时没有使用 泛型 指定类型,默认是 Object
// 使用时要先做类型转换
String threadLocalValue = (String) threadLocal.get();
复制代码  

上面这个例子,在创建 ThreadLocal 时没有使用泛型指定类型,所以存储在其中的值默认是 Object 类型,这样就需要在使用时先做类型转换才行。

下面再看一个使用泛型的版本

 private ThreadLocal<String> myThreadLocal = new ThreadLocal<String>();

myThreadLocal.set("Hello ThreadLocal");
 String  threadLocalValue = myThreadLocal.get();
复制代码  

现在我们只能把 String 类型的值存到 ThreadLocal 中,并且从 ThreadLocal 读取出值后也不再需要进行类型转换。

想要删除一个 ThreadLocal 实例里存储的值,只需要调用ThreadLocal实例中的 remove 方法即可。

 myThreadLocal.remove();
复制代码  

当然,这个删除操作只是删除的变量在本地线程中的副本,其他线程不会受到本线程中删除操作的影响。下面我们把 ThreadLocal 的创建、读写和删除攒一个简单的例子,做下演示。

 // 源码: https:// GitHub .com/kevinyan815/JavaXPlay/blob/main/src/com/threadlocal/ThreadLocalExample.java
package com.threadlocal;

public class ThreadLocalExample {

    private  ThreadLocal<Integer> threadLocal = new ThreadLocal<>();

    private void setAndPrintThreadLocal() {
        threadLocal.set((int) (Math.random() * 100D) );
        try {
            Thread.sleep(2000);
        } catch (Interrupted Exception  e) {
            e.printStackTrace();
        }
        System.out.println( Thread.currentThread().getName() + ": " + threadLocal.get() );

        if ( threadLocal.get() % 2 == 0) {
            // 测试删除 ThreadLocal
            System.out.println(Thread.currentThread().getName() + ": 删除ThreadLocal");
            threadLocal.remove();
        }
    }

    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample tlExample = new ThreadLocalExample();
        Thread thread1 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "线程1");
        Thread thread2 = new Thread(() -> tlExample.setAndPrintThreadLocal(), "线程2");

        thread1.start();
        thread2.start();

        thread1.join();
        thread2.join();
    }
}
复制代码  

上面的例程会有如下输出,当然如果恰好两个线程里 ThreadLocal 变量里存储的都是偶数的话,就不会有第三行输出啦。

 线程2: 97
线程1: 64
线程1: 删除ThreadLocal
复制代码  

本例子的源码项目放在了GitHub上,需要的可自行取用进行参考:ThreadLocal变量操作示例–增删查

为 ThreadLocal 设置初始值

在程序里,声明ThreadLocal类型的变量时,我们可以同时为变量设置一个自定义的初始值,这样做的好处是,即使没有使用 set 方法给 ThreadLocal 变量设置值的情况下,调用ThreadLocal变量的 get() 时能返回一个对业务逻辑来说更有意义的初始值,而不是默认的 Null 值。

在 Java 中有两种方式可以指定 ThreadLocal 变量的自定义初始值:

  • 创建一个 ThreadLocal 的子类,覆盖 initialValue() 方法,程序中则使用ThreadLocal子类创建实例变量。
  • 使用 ThreadLocal 类提供的的 静态方法 withInitial(Supplier<? extends S> supplier) 来创建 ThreadLocal 实例变量,该方法接收一个函数式接口 Supplier 的实现作为参数,在 Supplier 实现中为 ThreadLocal 设置初始值。

关于函数式接口Supplier如果你还不太清楚的话,可以查看系列中 函数式编程 接口章节中的详细内容。下面我们看看分别用这两种方式怎么给 ThreadLocal 变量提供初始值。

使用子类覆盖 initialValue() 设置初始值

通过定义ThreadLocal 的子类,在子类中覆盖 initialValue() 方法的方式给 ThreadLocal 变量设置初始值的方式,可以使用匿名类,简化创建子类的步骤。

下面我们在程序里创建 ThreadLocal 实例时,直接使用匿名类来覆盖 initialValue() 方法的一个例子。

 public class ThreadLocalExample {

    private ThreadLocal threadLocal = new ThreadLocal<Integer>() {
        @Override protected Integer initialValue() {
            return (int) System.currentTimeMillis();
        }
    };
    
......   
}
复制代码  

有同学可能会问,这块能不能用 Lambda 而不是用匿名类,答案是不能,在这个专栏讲 Lambda 的文章中我们说过,Lambda 只能用于实现函数式接口(接口中有且只有一个抽象方法,所以这里只能使用匿名了简化创建子类的步骤,不过另外一种通过withInitial方法创建并自定义初始化ThreadLocal变量的时候,是可以使用Lambda 的,我们下面看看使用 withInital 静态方法设置 ThreadLocal 变量初始值的演示。

通过 withInital 静态方法设置初始值

为 ThreadLocal 实例变量指定初始值的第二种方式是使用 ThreadLocal 类提供的静态工厂方法 withInitial 。withInitial 方法接收一个函数式接口 Supplier 的实现作为参数,在 Supplier 的实现中我们可以为要创建的 ThreadLocal 变量设置初始值。

Supplier 接口是一个函数式接口,表示提供某种值的函数。 Supplier 接口也可以被认为是工厂接口。

@FunctionalInterface public interface Supplier { T get(); }

下面的程序里,我们用 ThreadLocal 的 withInitial 方法为 ThreadLocal 实例变量设置了初始值

 public class ThreadLocalExample {

    private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(new Supplier<Integer>() {
        @Override
        public String get() {
            return (int) System.currentTimeMillis();
        }
    });
    
......   
}
复制代码  

对于函数式接口,理所当然会想到用 Lambda 来实现。上面这个 withInitial 的例子用 Lambda 实现的话能进一步简化成:

 public class ThreadLocalExample {

private ThreadLocal<Integer> threadLocal = ThreadLocal.withInitial(() -> (int) System.currentTimeMillis());
......
}
复制代码  

关于 Lambda 和 函数式接口 Supplier 的详细内容,可以通过本系列中与这两个主题相关的文章进行学习。

Java Lambda 表达式的各种形态和使用场景,看这篇就够了 Java 中那些绕不开的内置接口 — 函数式编程和 Java 的内置函数式接口

ThreadLocal 在父子线程间的传递

ThreadLocal 提供的线程本地存储,给数据提供了线程隔离,但是有的时候用一个线程开启的子线程,往往是需要些相关性的,那么父线程的ThreadLocal中存储的数据能在子线程中使用吗?答案是不行……那怎么能让父子线程上下文能关联起来,Java 为这种情况专门提供了InheritableThreadLocal 给我们使用。

InheritableThreadLocal 是 ThreadLocal 的一个子类,其定义如下:

 public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    
    protected T childValue(T parentValue) {
        return parentValue;
    }

    /**
     * Get the map associated with a ThreadLocal.
     *
     * @param t the current thread
     */    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the table.
     */    void createMap(Thread t, T firstValue) {
        t.inheritableThreadLocals = new ThreadLocalMap(this, firstValue);
    }
}
复制代码  

与 ThreadLocal 让线程拥有变量在本地存储的副本这个形式不同的是,InheritableThreadLocal 允许让创建它的线程和其子线程都能访问到在它里面存储的值。

下面是一个 InheritableThreadLocal 的使用示例

 // 源码: 
package com.threadlocal;

public class InheritableThreadLocalExample {

    public static void main(String[] args) {

        ThreadLocal<String> threadLocal = new ThreadLocal<>();
        InheritableThreadLocal<String> inheritableThreadLocal =
                new InheritableThreadLocal<>();

        Thread thread1 = new Thread(() -> {
            System.out.println("===== Thread 1 =====");
            threadLocal.set("Thread 1 - ThreadLocal");
            inheritableThreadLocal.set("Thread 1 - InheritableThreadLocal");

            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());

            Thread childThread = new Thread( () -> {
                System.out.println("===== ChildThread =====");
                System.out.println(threadLocal.get());
                System.out.println(inheritableThreadLocal.get());
            });
            childThread.start();
        });

        thread1.start();

        Thread thread2 = new Thread(() -> {
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println("===== Thread2 =====");
            System.out.println(threadLocal.get());
            System.out.println(inheritableThreadLocal.get());
        });
        thread2.start();
    }
}
复制代码  

运行程序后,会有如下输出

 ===== Thread 1 =====
Thread 1 - ThreadLocal
Thread 1 - InheritableThreadLocal
===== ChildThread =====
null
Thread 1 - InheritableThreadLocal
===== Thread2 =====
null
null
复制代码  

这个例程中创建了分别创建了 ThreadLocal 和 InheritableThreadLocal的 实例,然后例程中创建的线程Thread1, 在线程 Thread1中向 ThreadLocal 和 InheritableThreadLocal 实例中都存储了数据,并尝试在开启了的子线程 ChildThread 中访问这两个数据。按照上面的解释,ChildThread 应该只能访问到父线程存储在 InheritableThreadLocal 实例中的数据。

在例程的最后,程序又创建了一个与 Thread1 不相干的线程 Thread2, 它在访问 ThreadLocal 和 InheritableThreadLocal 实例中存储的数据时,因为它自己没有设置过,所以最后得到的结果都是 null。

ThreadLocal 的实现原理

梳理完 ThreadLocal 相关的常用功能都怎么使用后,我们再来简单过一下 ThreadLocal 在 Java 中的实现原理。

在 Thread 类中维护着一个 ThreadLocal.ThreadLocalMap 类型的成员变量threadLocals。这个成员变量就是用来存储当前线程独占的变量副本的。

 public class Thread implements Runnable {
    // ...
    ThreadLocal.ThreadLocalMap threadLocals = null;
    // ...
}
复制代码  

ThreadLocalMap类 是 ThreadLocal 中的静态内部类,其定义如下。

 package java.lang;

public class ThreadLocal<T> {
    // ...
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;
        }
    }
    // ...
}
}
复制代码  

它维护着一个 Entry 数组,Entry 继承了 WeakReference ,所以是弱引用。 Entry 用于保存键值对,其中:

  • key 是 ThreadLocal 对象;
  • value 是传递进来的对象(变量副本)。

ThreadLocalMap 虽然是类似 HashMap 结构的数据结构,但它解决哈希碰撞的时候,使用的方案并非像 HashMap 那样使用拉链法(用链表保存冲突的元素)。

实际上,ThreadLocalMap 采用了线性探测的方式来解决哈希碰撞冲突。所谓线性探测,就是根据初始 key 的 hashcode 值确定元素在哈希表数组中的位置,如果发现这个位置上已经被其他的 key 值占用,则利用固定的算法寻找一定步长的下个位置,依次判断,直至找到能够存放的位置。

总结

关于 ThreadLocal 的内容就介绍到这了,这块内容在一些基础的面试中还是挺常被问到的,与它一起经常被问到的还有一个 volatile 关键字,这部分内容我们放到下一篇再讲,喜欢本文的内容还请给点个赞,点个关注,这样就能及时跟上后面的更新啦。

引用链接

  • Java并发编程–多线程间的同步控制和通信
  • 看了这篇Java 泛型通关指南,再也不怵满屏尖括号了
  • Java Lambda 表达式的各种形态和使用场景,看这篇就够了
  • Java 中那些绕不开的内置接口 — 函数式编程和 Java 的内置函数式接口
  • ThreadLocal变量操作示例–增删查源代码

原文链接:
来源:稀土掘金

文章来源:智云一二三科技

文章标题:Java 多线程为啥要有ThreadLocal,怎么用,这篇讲全了

文章地址:https://www.zhihuclub.com/192305.shtml

关于作者: 智云科技

热门文章

网站地图