ThreadLocal使用初探

[消息]
 
好文推荐到我的圈子

分类:Java核心技术

标签: (可以给文章补个标签哟,最多可添加3个标签)

未引用的圈子:

 

 

本文分为三部分,第一部分通过例子简单列举同步场景,第二部分则对ThreadLocal进行介绍,最后一部分通过例子展现在web场景(sofa)下使用的一些问题,并附上例子程序

java同步的基本知识

Java良好的支持了多线程,就java的基础工具来说,java提供了synchronized关键字最为我们熟悉,但使用不当会导致大量的等待,最后耗光线程池,synchronized和ThreadLocal都是用来解决多线程并发访问的问题。大家可能对synchronized较为熟悉,而对ThreadLocal就要陌生得多了。当一个对象被两个线程同时访问时,由于cpu在分配时间片的时候并不按照线程排队执行,则可能有一个线程会得到不可预期的结果。

如下一个简单例子,小猫钓鱼,小猫有一个变量叫鱼,用以记录它钓鱼的数目


小猫钓鱼类,该类是一个多线程类,在这个类中,我们定义一个叫桶的变量,小猫钓鱼后将鱼儿装到桶中,钓鱼这个行为用随机数来模拟(也就是run方法中用一个随机数来模拟钓鱼的情况),在他钓鱼前,我们将它钓鱼的数量打印出来,然后线程等一会儿继续执行,查看小猫钓鱼的数量,在执行的过程中,我们打印出它钓鱼执行的时间,这个时间包括两个,一个是小猫从出门到钓鱼结束的时间,另外一个是小猫钓鱼的时间

系统让两个小猫开始钓鱼:


输出结果:


很明显,小猫在钓鱼的过程中,两只小猫钓鱼是是是数量发生了干扰,小猫小猫2钓到了3结果变成了4,两个钓鱼基本上在同一个时间内执行完毕,程序的执行的时候遇到的问题就是小猫1和小猫2在执行钓鱼的时候对桶进行了争抢,小猫1在中途倒掉了小猫2的鱼数目装上了自己的鱼,记过小猫1和小猫2都觉得自己钓到了4条。

很明显,这样的情况是一个很大的风险,竞争导致了对资源的集中占用,如果不加控制就变会导致A线程的量到B线程从而引起问题。

在这个问题中,Java中最简单的解决办法就是使用synchronized关键字,如下,在在小猫钓鱼的方法中加入该关键字即可产生小猫1用完桶。然后小猫2在继续的效果。


输出结果为:


由此可见synchronized是实现java的同步机制。同步机制是为了实现同步多线程对相同资源的并发访问控制。保证多线程之间的通信。  可见,同步的主要目的是保证多线程间的数据共享。同步会带来巨大的性能开销,所以同步操作应该是细粒度的。如果同步使用得当,带来的性能开销是微不足道的。使用同步真正的风险是复杂性和可能破坏资源安全,而不是性能。

如上,小猫1和小猫2一起去钓鱼,一起准备,然后小猫1钓鱼的时候小猫2等待,小猫1钓完,小猫2才开始钓鱼,比刚才的等待少了2000ms。小猫1和小猫2归结起来就是对资源的竞争,小猫1和小猫2为竞争一个装鱼的桶相互等待,即是时间换空间,这个时候小猫2含着眼泪想要是自己能有一个桶那该多好啊,在程序中使用同步是非常复杂的。并且同步会带来性能的降低。好在Java提供了另外的思路,即通过ThreadLocal空间换时间

ThreadLocal在本例中的使用

从字面上理解,很容易会把ThreadLocal误解为一个线程的本地变量。其实ThreadLocal并不是代表当前线程,ThreadLocal其实是采用哈希表的方式来为每个线程都提供一个变量的副本。从而保证各个线程间数据安全。每个线程的数据不会被另外线程访问和破坏,我们可以简单理解为它就是一个隐式的包含在你启动的线程中的变量,在这个线程执行的生命周期中它如影随形。

例如我们在对上面的小猫钓鱼中,我们使用ThreadLocal就相当于小猫在一出门的时候,就自己准备了一个桶用于装鱼。



在这个类中,每个线程维护自己的ThreadLocal,由于这个ThreadLocal是和线程一一对应,所以对于小猫1和小猫2来说,不存在对该资源的竞争,所以就不会出现等待。


如上,小猫由于有了各自的桶,所以很方便的钓到了鱼。

ThreadLocal类接口很简单,只有4个方法,并在jdk5以后支持泛型,方法列举如下:

void set(Object value)

void set(T value)

设置当前线程的线程局部变量的值

public Object get()

该方法返回当前线程所对应的线程局部变量

public void remove()

将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。

protected Object initialValue()

T initialValue()

返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

ThreadLocal是如何做到为每一个线程维护变量的副本的呢?其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为线程对象,而值对应线程的变量副本。我们自己就可以提供一个简单的实现版本:


一些注意事项:

l  线程和子线程的ThreadLocal

一种很常见的现象就是一个线程派生一个子线程,在结构上,线程和子线程是父子关系,我们在web请求中一种最常见的情况是如下:


在这个请求过程中,对于一些参数,需要从头传递到尾,如果这样的参数过多而彼此之间并没有一个很强的逻辑关系,则不得不在方法体中声明一大堆入参用于显示的传递参数,这样一方面导致代码不够优雅,此外,如果多个访问实例使用该资源还可能导致竞争从而导致程序出现不必要的等待,所以ThreadLocal为这个场景提供了一种新的思路,程序可以在请求的时候将变量放入到ThreadLocal中,在需要的时候取出,这个功能很像session的处理方式。

值得注意的是如果在线程中派生出了子线程,则子线程是无法直接获取到父线程的ThreadLocal的


如下,我们用一个例子加以说明:


输出为:


由此可见子线程不能获取到父线程的变量,但java提供了一个ThreadLocal的子类,该类能够获取到父线程的变量值。


如下输出为:


l  Web服务器中线程池的状态问题

Web服务在创建线程的过程中,频繁的创建线程对系能的影响巨大,故很多服务器都采用了线程池的方式来解决线程不断创建锁导致的问题,所以在使用线程

池的过程中,由于线程是不断的回收和利用故ThreadLocal在服务器中也是被反复利用的,在使用中如果不进行清查操作,很容易导致变量污染。尽管对于很多服务器来说,ThreadLocal是的确是相对于每个线程,每个线程会有自己的ThreadLocal。但考虑到服务器都会维护一套线程池。因此,不同用户访问,可能会接受到同样的线程。因此,在做基于TheadLocal时,需要谨慎,避免出现ThreadLocal变量的缓存,导致其他线程访问到本线程变量,如果运用不当,会导致系统效率低下,举个例子,假设我们得系统在访问的时候在ThreadLocal中加入变量不予以更新和删除,则这个保存的对象就变成一个增量的容器对象,如果访问量巨大,将导致jvm内存不足而频繁触发gc,gc在工作的时候会进行数据复制,频繁的触发gc对系统的性能会带来不利影响,同时还有可能导致内存溢出。

 
评论      

 

提示信息
您确定要删除吗?

温馨提示
您是游客无法进行此操作,请先去我的ATA补充资料!

<a href="http://www.linezing.com"><img src="http://img.tongji.linezing.com/2951307/tongji.gif"/></a>
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值