手撕Java类 ThreadLocal

1. ThreadLocal 简单使用

ThreadLocal 提供线程的局部变量。这样的变量与它正常的对应变量不同之处在于,每个线程访问(通过ThreadLocal的 get 或 set 方法)都具有自己的,独立初始化的变量副本。


我们很容易写出如下的用法:

ThreadLocal<Integer> count = new ThreadLocal<>();
        System.out.println("初始值" +  count.get());
        count.set(1);
        for (int i = 0; i < 3; i++) {
            // 糟糕的线程创建方式
            new Thread(){
                @Override
                public void run() {
                    Integer countV = count.get();
                    countV++;
                    System.out.println(Thread.currentThread().getName() + "对应的cout值为"+count.get());
                }
            }.start();
        }
        System.out.println("结束值" + count.get());
// 抛出空指针异常

以上的代码运行结果会抛出 java.lang.NullPointerException, 原因在于使用 countV++ 时,并没有获取到之前设置的1。原因是因为当前的代码也是运行在线程中的(可能是你的main方法),ThreadLocal 针对每个线程都会有一个副本,所以 count.set(1)并不能对其它线程生效。简单修改代码为如下:

ThreadLocal<Integer> count = new ThreadLocal(){
            @Override
            protected Object initialValue() {
                return 1;
            }
        };
        System.out.println("初始值" +  count.get());
        for (int i = 0; i < 3; i++) {
            // 糟糕的线程创建方式
            new Thread(){
                @Override
                public void run() {
                    Integer countV = count.get();
                    countV++;
                    System.out.println(Thread.currentThread().getName() + "对应的cout值为"+count.get());
                }
            }.start();
        }
        System.out.println("结束值" + count.get());

这样就能保证每个线程在初次使用时都能获取初始值。

2. ThreadLocal 源码简单解析

ThreadLocal的实现原理是:在每个Thread对象中具有一个threadLocals成员,该成员的类型是 ThreadLocal.ThreadLocalMap,初始值为 null ;当线程调用ThreadLocal的 get 或 set 方法时,会为该成员赋值。

// Thread 中的 threadLocals
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

// ThreadLocal 中该成员的创建
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

当通过 ThreadLocal 的 get 方法调用 createMap 方法时(当前线程的 threadLocals 尚未赋值),会首先调用 ThreadLocal 的 initialValue 方法,也就是在上面修改了以后的代码使用的方法;方法的返回值会作为 createMap 的 firstValue 参数传入。

当通过 ThreadLocal 的 set 方法调用 createMap 方法时(当前线程的 threadLocals 尚未赋值), set 方法的参数就直接作为 createMap的参数了。


这里没有去细细的研究 ThreadLocal.ThreadLocalMap 这个类,但以 Map 结尾,无疑是一个映射关系了;值得注意的是映射的 key 是当前的 ThreadLocal 对应的哈希值与一个固定值作与运算的结果,也就是说当在一个线程中调用多个 ThreadLocal 时,多个 ThreadLocal 组成了这个 Map ,值即为 ThreadLocal 所代表的值。

哈希值来源于 ThreadLocal 内部维护的一个静态 AtomicInteger ,所以在无需担心 key 值重复的问题

3. ThreadLocal 为什么不会有线程安全问题

ThreadLocal 实例化对象被各个线程使用,但其代表的值却存储在当前线程自己的成员,在我看来,对各个线程来说,ThreadLocal 相当于一个 key ,每次取值时,各线程通过这个 key 去 成员map 中取出值,所以也就不会有什么线程安全问题了。值得注意是 ThreadLocal 中需要用到当前线程的方法, Thread t = Thread.currentThread();都放在了方法的第一句去执行;

4. ThreadLocal 父子线程中的使用

在父子线程中使用时,如果需要当前线程的 ThreadLocal 设置的值对于子类任然适用的话,则需要使用 InheritableThreadLocal ,该类继承自 ThreadLocal,Thread 类中维护了另一个成员 inheritableThreadLocals, 该成员类型任然是 ThreadLocal.ThreadLocalMap, 不过只有是 InheritableThreadLocal 实例化的对象才会存储到这里。也就是说对于可继承和不可继承的 ThreadLocal ,Thread 是分开维护的。

至于子线程为何能够继承到父线程的 ThreadLocal ,是因为在线程的构造函数中,会进行检测:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

使用例子:

ThreadLocal<Integer> count = new InheritableThreadLocal() {
            @Override
            protected Object initialValue() {
                return 1;
            }
        };
        System.out.println("初始值" + count.get());
        count.set(2);
        // 糟糕的线程创建方式
        new Thread() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "对应的cout值为" + count.get());
                count.set(3);
                new Thread() {
                    @Override
                    public void run() {
                        System.out.println(Thread.currentThread().getName() + "对应的cout值为" + count.get());
                    }
                }.start();
            }
        }.start();
        System.out.println("结束值" + count.get());

// 运行结果
初始值1
结束值2
Thread-0对应的cout值为2
Thread-1对应的cout值为3
5. ThreadLocal 新增的方法(Java8)

文章最初的代码(创建匿名类)就介绍了如何去继承这个类,重写初始化方法来达到线程取值时的初始化;在 Java 8 中则有更优雅的方法(我之所以认为它是Java 8新增的,是因为其使用了 Supplier):

ThreadLocal<Integer> count = ThreadLocal.withInitial(() -> 1);

这种方法实现的原理也很好理解:

// withInitial方法
 public static <S> ThreadLocal<S> withInitial(Supplier<? extends S> supplier) {
        return new SuppliedThreadLocal<>(supplier);
    }
    
// SuppliedThreadLocal 类
static final class SuppliedThreadLocal<T> extends ThreadLocal<T> {

        private final Supplier<? extends T> supplier;

        SuppliedThreadLocal(Supplier<? extends T> supplier) {
            this.supplier = Objects.requireNonNull(supplier);
        }

        @Override
        protected T initialValue() {
            return supplier.get();
        }
    }
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值