线程安全的set_「Java」 - 多线程四 & ThreadLocal

一、ThreadLocal简单使用

多线程访问同一个共享变量容易出现并发问题,特别是多个线程对一个共享变量进行写入时,为了保证线程安全,一般需要在访问共享变量时进行的同步,增加了使用者的负担。在JDK 1.2开始引入ThreadLocal,提供了线程本地变量。

如果创建一个ThreadLocal变量,访问这个变量的每个线程都会有这个变量的一个本地拷贝,多个线程操作这个变量的时候,实际是操作的自己本地内存里面的变量,从而避免了线程安全问题。

a79b538e0b3c47ab845d031d0ec07ebb.png

A、不使用static修饰ThreadLocal

public 

使用包含ThreadLocal的对象,结果每个线程都能访问自己的线程本地变量。

threadOne A : null
threadTwo A : null
threadOne B : threadOne local variable
threadTwo B : threadTwo local variable

B、使用static修饰ThreadLocal

如果不使用static修饰,会导致建立一个对象,内部都会创建一个ThreadLocal实例,如果使用修饰为static,则所有的实例共享一个ThreadLocal实例。建议使用final static修饰ThreadLocal变量。

二、ThreadLocal原理

874062c277fa78a83be896e3377a2dfc.png

在Thread类中有个成员变量threadLocals,其默认值为null。

ThreadLocal.ThreadLocalMap threadLocals = null;

threadLocals是ThreadLocalMap类型的变量,而ThreadLocalMap是一个定制化的Hashmap,默认每个线程中这个变量都为null,只有当前线程第一次调用了ThreadLocal的set或者get方法时候才会创建threadLocals变量。

其实每个线程的本地变量不是存放到ThreadLocal实例里面的,而是存放到调用线程的threadLocals变量里面,也就是说,ThreadLocal类型的本地变量是存放到具体的线程内存空间的。

ThreadLocal就是一个工具壳,它通过set方法把value值放入调用线程的threadLocals里面存放起来,当调用线程调用它的get方法时再从当前线程的threadLocals变量里面拿出来使用。

final 

使用反射获取每个线程的ThreadLocalMap,结果每个线程的ThreadLocalMap是不同的。

threadOne A : null
threadTwo A : null
threadOne B : threadOne local variable
threadTwo B : threadTwo local variable
threadOne C : java.lang.ThreadLocal$ThreadLocalMap@214cda35
threadTwo C : java.lang.ThreadLocal$ThreadLocalMap@eb514c1

每个线程内部都有一个名字为threadLocals的成员变量,该变量类型为HashMap,其中key为定义的ThreadLocal变量的this引用,value则为set时候的值,每个线程的本地变量是存到线程自己的内存变量threadLocals里面的,如果当前线程一直不消失那么这些本地变量会一直存在,从而可能会造成内存溢出,因此使用完毕后需要调用ThreadLocal的remove方法删除对应线程的threadLocals中的本地变量。

ThreadLocalMap 

三、ThreadLocal导致的内存泄漏

ThreadLocal只是一个工具类,具体存放变量的是在线程的threadLocals变量里,threadLocals是一个ThreadLocalMap类型。ThreadLocalMap内部是一个Entry数组,Entry继承自WeakReference,Entry内部的value用来存放通过ThreadLocal的set方法传递的值。

A、ThreadLocal内存泄漏的原因

Entry中key使用的是对ThreadLocal对象的弱引用,这为避免内存泄露是一个进步,如果是强引用,即使其他地方没有对ThreadLocal 对象的引用,ThreadLocalMap中的ThreadLocal对象还是不会被回收,而如果是弱引用则这时候ThreadLocal引用是会被回收掉的。

线程的 ThreadLocalMap里面的key是弱依赖,则当前线程的ThreadLocalMap里面的ThreadLocal变量的弱引用会被在gc的时候回收,但是对应value还是会造成内存泄露,这时候ThreadLocalMap里面就会存在key为null但是value不为null的entry项。

在使用线程本地变量完毕后,还是手动调用remove方法解决内存泄露。

B、线程池中ThreadLocal的内存泄漏

static 

由于没有调用线程池的shutdown或者shutdownNow方法所以线程池里面的用户线程不会退出,JVM 进程也不会退出。

使用Idea下的VisualVM Launcher插件配置jvisualvm查看进程内存占用。

14b1dcf8fd1ae4de5de16109da11e12d.png

fea9d8a4354111cfef1361c89efad2f5.png

9dca1d092086e595fc8c67287cd974b3.png

当主线程处于休眠时候进程占用了大概75+M内存,加入remove代码运行结果则占用了大概25M内存,可知不增加remove时发生了内存泄露。

四、InheritableThreadLocal

A、子线程无法获取父线程中设置的ThreadLocal变量的值

// 创建线程变量

同一个ThreadLocal变量在父线程中设置值后,子线程中是获取不到的。

因为子线程调用get方法时候当前线程为子线程,而调用set方法设置线程变量是main线程,两者是不同的线程,子线程访问时候返回null。

main:hello world
thread:null

B、通过InheritableThreadLocal获取父线程本地变量

为了解决子线程获取父线程本地变量问题,JDK提供InheritableThreadLocal解决。

InheritableThreadLocal继承了ThreadLocal,重写了createMap方法,当第一次调用set方法时候创建的是当前线程的inheritableThreadLocals变量的实例而不再是threadLocals。

调用get方法获取当前线程的内部map变量时候,获取的是inheritableThreadLocals而不再是threadLocals。

public 

结果可以获取父线程本地变量。

main:hello world
thread:hello world

五、ThreadLocalRandom

java.util.Random是应用广泛的随机数生成工具类。根据JDK介绍,是线程安全的。

Instances of {@code java.util.Random} are threadsafe.

A、nextInt

public 

在单线程情况下每次调用nextInt都是根据老的种子计算出来新的种子,可以保证随机数产生的随机性。

但是在多线程下,多个线程可能都拿同一个老的种子去计算新的种子,会导致多个线程产生的新种子是一样的,由于计算随机数算法是固定的,会导致多个线程产生相同的随机值。

多个线程在根据同一个老种子计算新种子的时候要保证原子性,第一个线程的新种子计算出来后,第二个线程要丢弃自己老的种子,要使用第一个线程的新种子来计算自己的新种子,依次类推,才能保证多线程下产生的随机数是随机的。

B、next

Random函数使用一个原子变量达到了这个效果,在创建Random对象时初始化的种子就保存到了种子原子变量里面。

protected 

使用CAS操作,用新的种子去更新老的种子,多线程下可能多个线程都同时执行到了代码oldseed,那么可能多个线程拿到的当前种子的值是同一个,然后执行步骤nextseed计算的新种子也都是一样的,但是while循环中的CAS操作会保证只有一个线程可以更新老的种子为新的,失败的线程会通过循环重新获取更新后的种子作为当前种子去计算老的种子,保证了随机数的随机性。return处则使用固定算法根据新的种子计算随机数。

每个Random实例里面有一个原子性的种子变量用来记录当前种子的值,当要生成新的随机数时要根据当前种子计算新的种子并更新回原子变量。多线程下使用单个Random实例生成随机数,多个线程同时计算随机数计算新种子的时候,它们会竞争同一个原子变量的更新操作,因为原子变量的更新是CAS操作,同时只有一个线程会成功,所以会造成大量线程进行自旋重试,这是会降低并发性能,于是ThreadLocalRandom应运而生。

C、ThreadLocalRandom

为了解决多线程高并发下Random的缺陷,JUC包下新增了ThreadLocalRandom类。

public 

调用ThreadLocalRandom.current()来获取当前线程的随机数生成器。

ThreadLocal通过让每一个线程拷贝一份变量,每个线程对变量进行操作时实际是操作自己本地内存里面的拷贝,从而避免了对共享变量进行同步。

ThreadLocalRandom的实现也是这个原理,Random的缺点是多个线程会使用同一个原子性种子变量,会导致对原子变量更新的竞争。

445f99dd4da5135f61d88fea5ba4d228.png

每个线程维护一个自己的种子变量,每个线程生成随机数时根据自己老的种子计算新的种子,并使用新种子更新老的种子,然后根据新种子计算随机数,就不会存在竞争问题,这会大大提高并发性能。

六、Spring Request Scope作用域

Spring在XML里面配置Bean时可以指定scope属性来配置Bean的作用域为singleton、prototype、request、session等,其中作用域为request的实现原理就是使用ThreadLocal实现。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值