ThreadLocal应用和那些“坑”

ThreadLocal概述和API

 

三、典型应用

 

3.1:下面的类为每个线程生成不同的ID,当某个线程第一次调用Thread.get()时,会为该线程赋予一个ID,并且在后续的调用中不再改变。

import java.util.concurrent.atomic.AtomicInteger;
 public class ThreadId {
     // Atomic integer containing the next thread ID to be assigned
     private static final AtomicInteger nextId = new AtomicInteger(0);
     // Thread local variable containing each thread's ID
     private static final ThreadLocal<Integer> threadId =
         new ThreadLocal<Integer>() {
             @Override protected Integer initialValue() {
                 return nextId.getAndIncrement();
         }
     };
     // Returns the current thread's unique ID, assigning it if necessary
     public static int get() {
         return threadId.get();
     }
 }

 3.2:最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等

private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {  
    public Connection initialValue() {  
        return DriverManager.getConnection(DB_URL);  
    }  
};  
  
public static Connection getConnection() {  
    return connectionHolder.get();  
}  

 3.3:Hiberante的Session 工具类HibernateUtil

 

  • 这个类是Hibernate官方文档中HibernateUtil类,用于session管理。
     
  • 在这个类中,由于没有重写ThreadLocal的initialValue()方法,则首次创建线程局部变量session其初始值为null,第一次调用currentSession()的时候,线程局部变量的get()方法也为null。
  • 因此,对session做了判断,如果为null,则新开一个Session,并保存到线程局部变量session中,这一步非常的关键,这也是“public static final ThreadLocal session = new ThreadLocal()”所创建对象session能强制转换为Hibernate Session对象的原因。

 

 3.4:创建一个Bean,通过不同的线程对象设置Bean属性,保证各个线程Bean对象的独立性。

/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:45:02
 * 学生
 */
public class Student {
    private int age = 0;   //年龄
 
    public int getAge() {
        return this.age;
    }
 
    public void setAge(int age) {
        this.age = age;
    }
}
/**
 * Created by IntelliJ IDEA.
 * User: leizhimin
 * Date: 2007-11-23
 * Time: 10:53:33
 * 多线程下测试程序
 */
public class ThreadLocalDemo implements Runnable {
    //创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
    private final static ThreadLocal studentLocal = new ThreadLocal();
 
    public static void main(String[] agrs) {
        ThreadLocalDemo td = new ThreadLocalDemo();
        Thread t1 = new Thread(td, "a");
        Thread t2 = new Thread(td, "b");
        t1.start();
        t2.start();
    }
 
    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 student = getStudent();
        student.setAge(age);
        System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
        try {
            Thread.sleep(500);
        }
        catch (InterruptedException ex) {
            ex.printStackTrace();
        }
        System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
    }
 
    protected Student getStudent() {
        //获取本地线程变量并强制转换为Student类型
        Student student = (Student) studentLocal.get();
        //线程首次执行此方法的时候,studentLocal.get()肯定为null
        if (student == null) {
            //创建一个Student对象,并保存到本地线程变量studentLocal中
            student = new Student();
            studentLocal.set(student);
        }
        return student;
    }
}

运行结果:

a is running! 
thread a set age to:76 
b is running! 
thread b set age to:27 
thread a first read age is:76 
thread b first read age is:27 
thread a second read age is:76 
thread b second read age is:27 
  • 可以看到a、b两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,又兼顾数据的安全性。

 3.5:最近项目中遇到如下的场景:在执行数据迁移时,需要按照用户粒度加锁,
​​​​​​     因此考虑使用排他锁,迁移工具和业务服务属于两个服务,因此需要使用分布式锁。

  • 我们使用缓存(Tair或者Redis)实现分布式锁,具体代码如下:
    @Service
    public class Locker {
        @Resource(name = "tairClientUtil")
        private TairClientUtil tairClientUtil;
        private ThreadLocal<Long> lockerBeanThreadLocal = new ThreadLocal<>();
        public void init(long userid) {
            lockerBeanThreadLocal.remove();
            lockerBeanThreadLocal.set(userid);
        }
        public void updateLock() {
            String lockKey = Constants.MIGRATION_PREFIX + lockerBeanThreadLocal.get();
            tairClientUtil.incr(lockKey, Constants.COUNT_EXPIRE);
        }
        public void invalidLock() {
            String lockKey = Constants.MIGRATION_PREFIX + lockerBeanThreadLocal.get();
            tairClientUtil.invalid(lockKey);
        }
    }
    1. 因为每个线程可能携带不同的userid发起请求,因此在这里使用ThreadLocal变量存放userid,使得每个线程都有一份自己的副本。

 3.6:作为一种用于“方便传参”的工具

  • 每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。

public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();

  • RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发訪问这个变量,对它进行写入、读取操作,都是线程安全的。
  • 比方一个线程通过RESOURCE.set(“aaaa”);将数据写入ThreadLocal中,在不论什么一个地方,都能够通过RESOURCE.get();将值获取出来。
  • 可是它也并不完美,有很多缺陷,就像大家依赖于它来做參数传递一样。接下来我们就来分析它的一些不好的地方。
    1. 为什么有些时候会将ThreadLocal作为方便传递參数的方式呢?比如当很多方法相互调用时,最初的设计可能没有想太多,有多少个參数就传递多少个变量,
    2. 那么整个參数传递的过程就是零散的。
    3.  
    4. 进一步思考:若A方法调用B方法传递了8个參数。
    5. B方法接下来调用C方法->D方法->E方法->F方法等仅仅须要5个參数,此时在设计API时就涉及5个參数的入口。
    6. 这些方法在业务发展的过程中被很多地方所复用。
    7. 某一天。我们发现F方法须要加一个參数,这个參数在A方法的入口參数中有,此时,假设要改中间方法牵涉面会非常大。并且不知道改动后会不会有Bug
    8. 作为程序猿的我们可能会随性一想,ThreadLocal反正是全局的,就放这里吧。确实好解决。
    9.  
    10. 可是此时你会发现系统中这样的方式有点像在贴补丁。越贴越多,我们必需要求调用相关的代码都使用ThreadLocal传递这个參数,有可能会搞得乱七八糟的。
    11. 换句话说,并非不让用。而是我们要明白它的入口和出口是可控的。
    12.  
    13. 诡异的ThreadLocal最难琢磨的是“作用域”,尤其是在代码设计之初非常乱的情况下,假设再添加很多ThreadLocal。
    14.  
    15. 系统就会逐渐变成神龙见首不见尾的情况。有了这样一个省事的东西。
    16. 可能很多小伙伴更加不在意设计,由于大家都觉得这些问题都能够通过变化的手段来解决。胖哥觉得这是一种恶性循环。

     

  • 解决:
    1. 对于这类业务场景。应当提前有所准备。须要粗粒度化业务模型。即使要用ThreadLocal,也不是加一个參数就加一个ThreadLocal变量。
    2. 比如,我们能够设计几种对象来封装入口參数,在接口设计时入口參数都以对象为基础。
    3. 或许一个类无法表达全部的參数意思,并且那样easy导致强耦合。
    4.  
    5. 通常我们依照业务模型分解为几大类型对象作为它们的參数包装,
    6. 而且将依照对象属性共享情况进行抽象,在继承关系的每个层次各自扩展对应的參数,
    7. 或者说加參数就在对象中加,共享參数就在父类中定义,这种參数就逐步规范化了。

四、ThreadLocal的坑

  1. 通过上面的分析。我们够认识到ThreadLocal事实上是与线程绑定的一个变量,
  2. 如此就会出现一个问题:假设没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。
  3. 因此,ThreadLocal的一个非常大的“坑”就是当使用不当时,导致使用者不知道它的作用域范围。
  4.  
  5. 大家可能觉得线程结束后ThreadLocal应该就回收了。假设线程真的注销了确实是这种,可是事实有可能并不是如此。
  6. 比如在线程池中对线程管理都是采用线程复用的方法(Web容器通常也会采用线程池)。
  7. 在线程池中线程非常难结束甚至于永远不会结束。这将意味着线程持续的时间将不可预測,甚至与JVM的生命周期一致。
  8. 那么对应的ThreadLocal变量的生命周期也将不可预測。
  9.  
  10. 或许系统中定义少量几个ThreadLocal变量也无所谓。
  11. 由于每次set数据时是用ThreadLocal本身作为Key的,同样的Key肯定会替换原来的数据。原来的数据就能够被释放了,理论上不会导致什么问题。
  12. 但世事无绝对,假设ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,
  13. 那么内部的集合类和复杂对象所占用的空间可能会開始膨胀。
  14. 抛开代码本身的问题。
  15.  
  16. 举一个极端的样例。
  17. 假设不想定义太多的ThreadLocal变量,就用一个HashMap来存放,这貌似没什么问题。
  18. 由于ThreadLocal在程序的不论什么一个地方都能够用得到,在某些设计不当的代码中非常难知道这个HashMap写入的源头,在代码中为了保险起见。
  19. 一般会先检查这个HashMap是否存在,若不存在,则创建一个HashMap写进去。若存在,通常也不会替换掉。
  20. 由于代码编写者一般会“害怕”由于这样的替换会丢掉一些来自“其它地方写入HashMap的数据”。从而导致很多不可预见的问题。
  21. 在这种情况下。HashMap第一次放入ThreadLocal中或许就一直不会被释放,而这个HashMap中可能開始存放很多Key-Value信息,
  22.  
  23. 假设业务上存放的Key值在不断变化(比如,将业务的ID作为Key),那么这个HashMap就開始不断变长,并且非常可能在每一个线程中都有一个这种HashMap,逐渐地形成了间接的内存泄漏。
  24. 以前有非常多人吃过这个亏,并且吃亏的时候发现这种代码可能不是在自己的业务系统中。而是出如今某些二方包、三方包中(开源并不保证没有问题)。
  25. 要处理这样的问题非常复杂,只是首先要保证自己编写的代码是没问题的。要保证没问题不是说我们不去用ThreadLocal。甚至不去学习它。
  26. 由于它肯定有其应用价值。
  27.  
  28. 在使用时要明确ThreadLocal最难以捉摸的是“不知道哪里是源头”(一般是代码设计不当导致的),仅仅有知道了源头才干控制结束的部分。
  29. 或者说我们从设计的角度要让ThreadLocalsetremove有始有终,通常在外部调用的代码中使用finallyremove数据,
  30. 仅仅要我们细致思考和抽象是能够达到这个目的的。
  31.  
  32. 有些是二方包、三方包的问题,对于这些问题我们须要学会的是找到问题的根源后解决,关于二方包、三方包的执行跟踪,
  33.  
  34. 补充:在不论什么异步程序中(包含异步I/O、非堵塞I/O),ThreadLocal的參数传递是不靠谱的,由于线程将请求发送后。
  35. 就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是还有一个。

 

五、总结

  1. ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。
  2. ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,
  3. 这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
  4.  
  5. ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。
  6.  
  7. ThreadLocalSynchonized都用于解决多线程并发访问。但是ThreadLocalsynchronized有本质的区别。
  8.  
  9. synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
  10. ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
  11. Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
  12.  
  13. Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
  • 当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。 

 

六、ThreadLocal使用的一般步骤

  • 1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
  • 2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),
  • 在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
  • 3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。

 

参考来源:http://blog.51cto.com/lavasoft/51926

参考来源: https://www.cnblogs.com/yxysuanfa/p/7125761.html

评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值