ThreadLocal应用和那些“坑”

来源:互联网 发布:淘宝上买狗狗怎么快递 编辑:程序博客网 时间:2024/04/30 08:24
ThreadLocal概述和API

三、典型应用

3.1:下面的类为每个线程生成不同的ID,当某个线程第一次调用Thread.get()时,会为该线程赋予一个ID,并且在后续的调用中不再改变。
  1. import java.util.concurrent.atomic.AtomicInteger;
  2. public class ThreadId {
  3. // Atomic integer containing the next thread ID to be assigned
  4. private static final AtomicInteger nextId = new AtomicInteger(0);
  5. // Thread local variable containing each thread's ID
  6. private static final ThreadLocal<Integer> threadId =
  7. new ThreadLocal<Integer>() {
  8. @Override protected Integer initialValue() {
  9. return nextId.getAndIncrement();
  10. }
  11. };
  12. // Returns the current thread's unique ID, assigning it if necessary
  13. public static int get() {
  14. return threadId.get();
  15. }
  16. }

 3.2:最常见的ThreadLocal使用场景为 用来解决数据库连接、Session管理等
  1. private static ThreadLocal<Connection> connectionHolder = new ThreadLocal<Connection>() {
  2. public Connection initialValue() {
  3. return DriverManager.getConnection(DB_URL);
  4. }
  5. };
  6. public static Connection getConnection() {
  7. return connectionHolder.get();
  8. }

 3.3:Hiberante的Session 工具类HibernateUtil

  • 这个类是Hibernate官方文档中HibernateUtil类,用于session管理。
    1. public class HibernateUtil {
    2. private static Log log = LogFactory.getLog(HibernateUtil.class);
    3. private static final SessionFactory sessionFactory; //定义SessionFactory
    4. static {
    5. try {
    6. // 通过默认配置文件hibernate.cfg.xml创建SessionFactory
    7. sessionFactory = new Configuration().configure().buildSessionFactory();
    8. } catch (Throwable ex) {
    9. log.error("初始化SessionFactory失败!", ex);
    10. throw new ExceptionInInitializerError(ex);
    11. }
    12. }
    13. //创建线程局部变量session,用来保存Hibernate的Session
    14. public static final ThreadLocal session = new ThreadLocal();
    15. /**
    16. * 获取当前线程中的Session
    17. * @return Session
    18. * @throws HibernateException
    19. */
    20. public static Session currentSession() throws HibernateException {
    21. Session s = (Session) session.get();
    22. // 如果Session还没有打开,则新开一个Session
    23. if (s == null) {
    24. s = sessionFactory.openSession();
    25. session.set(s); //将新开的Session保存到线程局部变量中
    26. }
    27. return s;
    28. }
    29. public static void closeSession() throws HibernateException {
    30. //获取线程局部变量,并强制转换为Session类型
    31. Session s = (Session) session.get();
    32. session.set(null);
    33. if (s != null)
    34. s.close();
    35. }
    36. }

  • 在这个类中,由于没有重写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对象的独立性。
 
  1. /**
  2. * Created by IntelliJ IDEA.
  3. * User: leizhimin
  4. * Date: 2007-11-23
  5. * Time: 10:45:02
  6. * 学生
  7. */
  8. public class Student {
  9. private int age = 0; //年龄
  10. public int getAge() {
  11. return this.age;
  12. }
  13. public void setAge(int age) {
  14. this.age = age;
  15. }
  16. }

  1. /**
  2. * Created by IntelliJ IDEA.
  3. * User: leizhimin
  4. * Date: 2007-11-23
  5. * Time: 10:53:33
  6. * 多线程下测试程序
  7. */
  8. public class ThreadLocalDemo implements Runnable {
  9. //创建线程局部变量studentLocal,在后面你会发现用来保存Student对象
  10. private final static ThreadLocal studentLocal = new ThreadLocal();
  11. public static void main(String[] agrs) {
  12. ThreadLocalDemo td = new ThreadLocalDemo();
  13. Thread t1 = new Thread(td, "a");
  14. Thread t2 = new Thread(td, "b");
  15. t1.start();
  16. t2.start();
  17. }
  18. public void run() {
  19. accessStudent();
  20. }
  21. /**
  22. * 示例业务方法,用来测试
  23. */
  24. public void accessStudent() {
  25. //获取当前线程的名字
  26. String currentThreadName = Thread.currentThread().getName();
  27. System.out.println(currentThreadName + " is running!");
  28. //产生一个随机数并打印
  29. Random random = new Random();
  30. int age = random.nextInt(100);
  31. System.out.println("thread " + currentThreadName + " set age to:" + age);
  32. //获取一个Student对象,并将随机数年龄插入到对象属性中
  33. Student student = getStudent();
  34. student.setAge(age);
  35. System.out.println("thread " + currentThreadName + " first read age is:" + student.getAge());
  36. try {
  37. Thread.sleep(500);
  38. }
  39. catch (InterruptedException ex) {
  40. ex.printStackTrace();
  41. }
  42. System.out.println("thread " + currentThreadName + " second read age is:" + student.getAge());
  43. }
  44. protected Student getStudent() {
  45. //获取本地线程变量并强制转换为Student类型
  46. Student student = (Student) studentLocal.get();
  47. //线程首次执行此方法的时候,studentLocal.get()肯定为null
  48. if (student == null) {
  49. //创建一个Student对象,并保存到本地线程变量studentLocal中
  50. student = new Student();
  51. studentLocal.set(student);
  52. }
  53. return student;
  54. }
  55. }

运行结果:
  1. a is running!
  2. thread a set age to:76
  3. b is running!
  4. thread b set age to:27
  5. thread a first read age is:76
  6. thread b first read age is:27
  7. thread a second read age is:76
  8. thread b second read age is:27

  • 可以看到ab两个线程age在不同时刻打印的值是完全相同的。这个程序通过妙用ThreadLocal,既实现多线程并发,又兼顾数据的安全性。

 3.5:最近项目中遇到如下的场景:在执行数据迁移时,需要按照用户粒度加锁,因此考虑使用排他锁,迁移工具和业务服务属于两个服务,因此需要使用分布式锁。
  • 我们使用缓存(Tair或者Redis)实现分布式锁,具体代码如下:
    1. @Service
    2. public class Locker {
    3. @Resource(name = "tairClientUtil")
    4. private TairClientUtil tairClientUtil;
    5. private ThreadLocal<Long> lockerBeanThreadLocal = new ThreadLocal<>();
    6. public void init(long userid) {
    7. lockerBeanThreadLocal.remove();
    8. lockerBeanThreadLocal.set(userid);
    9. }
    10. public void updateLock() {
    11. String lockKey = Constants.MIGRATION_PREFIX + lockerBeanThreadLocal.get();
    12. tairClientUtil.incr(lockKey, Constants.COUNT_EXPIRE);
    13. }
    14. public void invalidLock() {
    15. String lockKey = Constants.MIGRATION_PREFIX + lockerBeanThreadLocal.get();
    16. tairClientUtil.invalid(lockKey);
    17. }
    18. }

  • 因为每个线程可能携带不同的userid发起请求,因此在这里使用ThreadLocal变量存放userid,使得每个线程都有一份自己的副本。
 3.6:作为一种用于“方便传参”的工具

  • 每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。
    1. public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
  • RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发訪问这个变量,对它进行写入、读取操作,都是线程安全的。
  • 比方一个线程通过RESOURCE.set(“aaaa”);将数据写入ThreadLocal中,在不论什么一个地方,都能够通过RESOURCE.get();将值获取出来。

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

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

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

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