启动线程报空指针异常_ThreadLocal类如何维护线程封闭性?

本文解析了ThreadLocal如何实现线程封闭,阐述了其工作原理,通过实例说明如何在多线程环境中利用ThreadLocal保证数据安全,以及主要方法get、set、remove的使用。重点在于展示了如何利用ThreadLocal避免同步机制,确保单线程环境下数据一致性。
摘要由CSDN通过智能技术生成

ThreadLocal声明

什么是线程封闭?在多线程中访问共享可变数据时通常需要同步机制来保证线程安全。线程封闭是一种不使用同步来实现线程安全的方式----如果我们仅在单线程中使用该数据,当然就不用使用同步机制了。也就是说,将数据封闭在单线程中来自动实现线程安全。

ThreadLocal可以实现线程封闭,因为我们都知道ThreadLocal类为变量在每个线程中创建了一个副本,在每个线程中操作的其实是该线程独占的变量副本,不需要同步机制即可实现线程安全。

首先,我们来看一下ThreadLocal上的声明:

该类提供线程局部变量。这些变量和它们相对应的正常变量不同,因为每一个线程访问的都是自己独占的、独立初始化的变量副本。所谓线程局部变量,我们可以对比局部变量的定义,该变量的作用域只在本线程中,线程消失,线程局部变量消失(除非其他地方仍在存在引用)。例如下面的类为每个线程生成本地唯一标识符(线程的ID是在第一次调用 ThreadId.get()时分配的,并且在以后的调用中保持不变):

05b64da628a710c910e290ad519a17d1.png

示例

每个线程都会对其线程局部变量的副本保留隐式引用,前提是线程是活动的并且该ThreadLocal可以访问。线程消失后,其所有ThreadLocal实例副本都将进行垃圾回收(除非存在对这些副本的其他引用)

上面的示例代码可能有些人看不懂,这里简单解释一下(看不懂没关系,后面会详细讲解):

示例中用ThreadLocal类对nextId实现了了线程封闭,每个线程来调用ThreadId类的get方法,其实是在对nextId的副本进行操作,我们写一个main方法来测试一下:

415b6784d2b8df8c89c344490b5d77ce.png

测试方法

输出如下:

14f5db178a05f39d499709edfad6f599.png

输出结果

可以看到,thread2并没有对thread1产生影响,thread1在thread2执行过之后,还是会输出原来的1。好了,这里已经证明了ThreadLocal可以实现线程封闭。

下面来熟悉一下ThreadLocal的方法和实现逻辑(然后就能弄清楚为什么上面的示例可以实现一个线程多次调用get方法返回相同的值)。

ThreadLocal剖析

ThreadLocal主要对外提供4个方法:

public T get() { ... } //获取变量副本public void set(T value) { ... } //设置变量副本public void remove() { ... } //移除变量副本protected T initialValue() { ... } //初始化副本。需要用户重写该方法(默认实现直接返回null)

get方法和initialValue方法

340ce2fe073671faed0e8833c741ebd1.png

get方法

get()方法返回此线程局部变量的当前线程副本的值。 如果该变量没有当前线程的副本的值,则首先将其初始化为调用initialValue()方法返回的值。

43398c7345a835b09899c747235981d6.png

setInitialValue方法会调用initialValue初始化

79f59f086966520307f4a9544eb1872b.png

默认intialValue方法,所以需要我们重写

那么该方法是如何获取当前线程副本的值的呢?看代码不难猜出,每个线程Thread都有一个ThreadLocalMap类型,其中保存有该线程中用到的变量副本。键为当前ThreadLocal变量(代码中查询map传入的是this,即当前的ThreadLocal),value为变量副本(即T类型的变量)。

简单来讲,当前线程里面如果要使用副本变量,就是通过get方法在threadLocals里面查找。反过来看上面的例子,线程thread1的ThreadLocalMap中就保存有这个副本(当然第一次调用get方法时ThreadLocalMap是空的,这时候就会调用我们重写的initialValue()方法初始化并放入ThreadLocalMap中,以后调用get直接去Map中查找即可,所以同一个线程里每次调用返回的值都一样),如果线程还用到了另一个变量副本比如nextTime,可能ThreadLocalMap中会增加一条数据:。

set方法

5442e1cacf401c7aaaa4a80d942fa7cb.png

set方法

可以看到,set方法和setInitialValue方法的前半部分一模一样。仔细分析代码可以发现,如果我们没有实现initialValue方法,那么在get之前,必须手动调用set方法先保存线程局部变量到ThreadLocalMap,否则会报空指针异常。

remove方法

remove方法很简单,只是从ThreadLocalMap中删除该线程局部变量而已。

67e76dd7ca513a6ff5f7b082962f1d5b.png

remove方法

总结

  1. 每一个线程局部变量ThreadLocal是保存在线程Thread的ThreadLocalMap中的,一个线程有多少个线程局部变量,ThreadLocalMap中就有多少个元素。
  2. 因为线程局部变量ThreadLocal是保存在线程中的,只有该线程可以访问,所以是线程封闭的,可以保证多线程安全。

上一篇文章

帮你理清楚Java反射应该如何写

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值