Java学习者论坛

 找回密码
 立即注册

QQ登录

只需一步,快速开始

手机号码,快捷登录

恭喜Java学习者论坛(https://www.javaxxz.com)已经为数万Java学习者服务超过8年了!积累会员资料超过10000G+
成为本站VIP会员,下载本站10000G+会员资源,购买链接:点击进入购买VIP会员
JAVA高级面试进阶视频教程Java架构师系统进阶VIP课程

分布式高可用全栈开发微服务教程

Go语言视频零基础入门到精通

Java架构师3期(课件+源码)

Java开发全终端实战租房项目视频教程

SpringBoot2.X入门到高级使用教程

大数据培训第六期全套视频教程

深度学习(CNN RNN GAN)算法原理

Java亿级流量电商系统视频教程

互联网架构师视频教程

年薪50万Spark2.0从入门到精通

年薪50万!人工智能学习路线教程

年薪50万!大数据从入门到精通学习路线年薪50万!机器学习入门到精通视频教程
仿小米商城类app和小程序视频教程深度学习数据分析基础到实战最新黑马javaEE2.1就业课程从 0到JVM实战高手教程 MySQL入门到精通教程
查看: 262|回复: 0

[Java线程学习]ThreadLocal如何解决并发安全性

[复制链接]
  • TA的每日心情
    开心
    2021-3-12 23:18
  • 签到天数: 2 天

    [LV.1]初来乍到

    发表于 2014-11-2 23:59:16 | 显示全部楼层 |阅读模式
    前面我们介绍了java当中多个线程抢占一个共享资源的问题。但不论是同步还是重入锁,都不能实实在在的解决资源紧缺的情况,这些方案只是靠制定规则来约束线程的行为,让它们不再拼命的争抢,而不是真正从实质上解决他们对资源的需求。

    在JDK 1.2当中,引入了java.lang.ThreadLocal。它为我们提供了一种全新的思路来解决线程并发的问题。但是他的名字难免让我们望文生义:本地线程?

    什么是本地线程?
    本地线程开玩笑的说:不要迷恋哥,哥只是个传说。

        其实ThreadLocal并非Thread at Local,而是LocalVariable in a Thread。

    根据WikiPedia上的介绍,ThreadLocal其实是源于一项多线程技术,叫做Thread Local Storage,即线程本地存储技术。不仅仅是Java,在C++、C#、.NET、python、Ruby、Perl等开发平台上,该技术都已经得以实现。  
      
       
       
         
       

         
       
      
        当使用ThreadLocal维护变量时,它会为每个使用该变量的线程提供独立的变量副本。也就是说,他从根本上解决的是资源数量的问题,从而使得每个线程持有相对独立的资源。这样,当多个线程进行工作的时候,它们不需要纠结于同步的问题,于是性能便大大提升。但资源的扩张带来的是更多的空间消耗,ThreadLocal就是这样一种利用空间来换取时间的解决方案。

    说了这么多,来看看如何正确使用ThreadLocal。

    通过研究JDK文档,我们知道,ThreadLocal中有几个重要的方法:get()、set()、remove()、initailValue(),对应的含义分别是:
    返回此线程局部变量的当前线程副本中的值、将此线程局部变量的当前线程副本中的值设置为指定值、移除此线程局部变量当前线程的值、返回此线程局部变量的当前线程的“初始值”。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    还记得我们在第三篇的上半节引出的那个例子么?几个线程修改同一个Student对象中的age属性。为了保证这几个线程能够工作正常,我们需要对Student的对象进行同步。
    下面我们对这个程序进行一点小小的改造,我们通过继承Thread来实现多线程:


      
    /**

      *
      *
    @author
      x-spirit
      
    */


    public
      
    class
      ThreadDemo3
    extends
      Thread{

         
    private
      ThreadLocal
    <
    Student
    >
      stuLocal
    =
      
    new
      ThreadLocal
    <
    Student
    >
    ();

         
    public
      ThreadDemo3(Student stu){
             stuLocal.set(stu);
         }

         
    public
      
    static
      
    void
      main(String[] args) {
             Student stu
    =
      
    new
      Student();
             ThreadDemo3 td31
    =
      
    new
      ThreadDemo3(stu);
             ThreadDemo3 td32
    =
      
    new
      ThreadDemo3(stu);
             ThreadDemo3 td33
    =
      
    new
      ThreadDemo3(stu);
             td31.start();
             td32.start();
             td33.start();
         }

         @Override
         
    public
      
    void
      run() {
             accessStudent();
         }

         
    public
      
    void
      accessStudent() {

             String currentThreadName
    =
      Thread.currentThread().getName();
             System.out.println(currentThreadName
    +
      
    "
      is running!
    "
    );
             Random random
    =
      
    new
      Random();
             
    int
      age
    =
      random.nextInt(
    100
    );
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      set age to:
    "
      
    +
      age);
             Student student
    =
      stuLocal.get();
             student.setAge(age);
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      first  read age is:
    "
      
    +
      student.getAge());
             
    try
      {
                 Thread.sleep(
    5000
    );
             }
    catch
      (InterruptedException ex) {
                 ex.printStackTrace();
             }
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      second read age is:
    "
      
    +
      student.getAge());

         }
    }
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/

    貌似这个程序没什么问题。但是运行结果却显示:这个程序中的3个线程会抛出3个空指针异常。读者一定感到很困惑。我明明在构造器当中把Student对象set进了ThreadLocal里面阿,为什么run起来之后居然在调用stuLocal.get()方法的时候得到的是NULL呢?
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    带着这个疑问,让我们深入到JDK的代码当中,去一看究竟。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    原来,在ThreadLocal中,有一个内部类叫做ThreadLocalMap。这个ThreadLocalMap并非java.util.Map的一个实现,而是利用java.lang.ref.WeakReference实现的一个键-值对应的数据结构其中,key是ThreadLocal类型,而value是Object类型,我们可以简单的视为HashMap<ThreadLocal,Object>。

    而在每一个Thread对象中,都有一个ThreadLocalMap的引用,即Thread.threadLocals。而ThreadLocal的set方法就是首先尝试从当前线程中取得ThreadLocalMap(以下简称Map)对象。如果取到的不为null,则以ThreadLocal对象自身为key,来取Map中的value。如果取不到Map对象,则首先为当前线程创建一个ThreadLocalMap,然后以ThreadLocal对象自身为key,将传入的value放入该Map中。



      
         ThreadLocalMap getMap(Thread t) {
             
    return
      t.threadLocals;
         }   

         
    public
      
    void
      set(T value) {
             Thread t
    =
      Thread.currentThread();
             ThreadLocalMap map
    =
      getMap(t);
             
    if
      (map
    !=
      
    null
    )
                 map.set(
    this
    , value);
             
    else

                 createMap(t, value);
         }



    而get方法则是首先得到当前线程的ThreadLocalMap对象,然后,根据ThreadLocal对象自身,取出相应的value。当然,如果在当前线程中取不到ThreadLocalMap对象,则尝试为当前线程创建ThreadLocalMap对象,并以ThreadLocal对象自身为key,把initialValue()方法产生的对象作为value放入新创建的ThreadLocalMap中。



      
         
    public
      T get() {
             Thread t
    =
      Thread.currentThread();
             ThreadLocalMap map
    =
      getMap(t);
             
    if
      (map
    !=
      
    null
    ) {
                 ThreadLocalMap.Entry e
    =
      map.getEntry(
    this
    );
                
    if
      (e
    !=
      
    null
    )
                     
    return
      (T)e.value;
             }
             
    return
      setInitialValue();
         }

         
    private
      T setInitialValue() {
             T value
    =
      initialValue();
             Thread t
    =
      Thread.currentThread();
             ThreadLocalMap map
    =
      getMap(t);
             
    if
      (map
    !=
      
    null
    )
                 map.set(
    this
    , value);
             
    else

                 createMap(t, value);
             
    return
      value;
         }
       
         
    protected
      T initialValue() {
             
    return
      
    null
    ;
         }


    这样,我们就明白上面的问题出在哪里:我们在main方法执行期间,试图在调用ThreadDemo3的构造器时向ThreadLocal置入Student对象,而此时,以ThreadLocal对象为key,Student对象为value的Map是被放入当前的活动线程内的。也就是Main线程。而当我们的3个ThreadDemo3线程运行起来以后,调用get()方法,都是试图从当前的活动线程中取得ThreadLocalMap对象,但当前的活动线程显然已经不是Main线程了,于是,程序最终执行了ThreadLocal原生的initialValue()方法,返回了null。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    讲到这里,我想不少朋友一定已经看出来了:ThreadLocal的initialValue()方法是需要被覆盖的。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    于是,ThreadLocal的正确使用方法是:将ThreadLocal以内部类的形式进行继承,并覆盖原来的initialValue()方法,在这里产生可供线程拥有的本地变量值。
    这样,我们就有了下面的正确例程:



      
    /**

      *
      *
    @author
      x-spirit
      
    */


    public
      
    class
      ThreadDemo3
    extends
      Thread{

         
    private
      ThreadLocal
    <
    Student
    >
      stuLocal
    =
      
    new
      ThreadLocal
    <
    Student
    >
    (){

             @Override
             
    protected
      Student initialValue() {
                
    return
      
    new
      Student();
             }

         };

         
    public
      ThreadDemo3(){
            
         }

         
    public
      
    static
      
    void
      main(String[] args) {
             ThreadDemo3 td31
    =
      
    new
      ThreadDemo3();
             ThreadDemo3 td32
    =
      
    new
      ThreadDemo3();
             ThreadDemo3 td33
    =
      
    new
      ThreadDemo3();
             td31.start();
             td32.start();
             td33.start();
         }

         @Override
         
    public
      
    void
      run() {
             accessStudent();
         }

         
    public
      
    void
      accessStudent() {

             String currentThreadName
    =
      Thread.currentThread().getName();
             System.out.println(currentThreadName
    +
      
    "
      is running!
    "
    );
             Random random
    =
      
    new
      Random();
             
    int
      age
    =
      random.nextInt(
    100
    );
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      set age to:
    "
      
    +
      age);
             Student student
    =
      stuLocal.get();
             student.setAge(age);
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      first  read age is:
    "
      
    +
      student.getAge());
             
    try
      {
                 Thread.sleep(
    5000
    );
             }
    catch
      (InterruptedException ex) {
                 ex.printStackTrace();
             }
             System.out.println(
    "
    thread
    "
      
    +
      currentThreadName
    +
      
    "
      second read age is:
    "
      
    +
      student.getAge());

         }
    }


    可见,要正确使用ThreadLocal,必须注意以下几点:
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    1. 总是对ThreadLocal中的initialValue()方法进行覆盖。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    2. 当使用set()或get()方法时牢记这两个方法是对当前活动线程中的ThreadLocalMap进行操作,一定要认清哪个是当前活动线程!
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    3. 适当的使用泛型,可以减少不必要的类型转换以及可能由此产生的问题。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    运行该程序,我们发现:程序的执行过程只需要5秒,而如果采用同步的方法,程序的执行结果相同,但执行时间需要15秒。以前是多个线程为了争取一个资源,不得不在同步规则的制约下互相谦让,浪费了一些时间。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/
    现在,采用ThreadLocal机制以后,可用的资源多了,你有我有全都有,所以,每个线程都可以毫无顾忌的工作,自然就提高了并发性,线程安全也得以保证。

    当今很多流行的开源框架也采用ThreadLocal机制来解决线程的并发问题。比如大名鼎鼎的 Struts 2.x 和 Spring 等。

    把ThreadLocal这样的话题放在我们的同步机制探讨中似乎显得不是很合适。但是ThreadLocal的确为我们解决多线程的并发问题带来了全新的思路。它为每个线程创建一个独立的资源副本,从而将多个线程中的数据隔离开来,避免了同步所产生的性能问题,是一种“以空间换时间”的解决方案。
    但这并不是说ThreadLocal就是包治百病的万能药了。如果实际的情况不允许我们为每个线程分配一个本地资源副本的话,同步还是非常有意义的。
    转载注明出处:http://x- spirit.javaeye.com/、http: //www.blogjava.net/zhangwei217245/

      
      
       
       

         
       

         
       
      
    复制代码

    源码下载:http://file.javaxxz.com/2014/11/2/235916046.zip
    回复

    使用道具 举报

    您需要登录后才可以回帖 登录 | 立即注册

    本版积分规则

    QQ|手机版|Java学习者论坛 ( 声明:本站资料整理自互联网,用于Java学习者交流学习使用,对资料版权不负任何法律责任,若有侵权请及时联系客服屏蔽删除 )

    GMT+8, 2025-2-25 16:12 , Processed in 0.298408 second(s), 36 queries .

    Powered by Discuz! X3.4

    © 2001-2017 Comsenz Inc.

    快速回复 返回顶部 返回列表