三、典型应用
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); } }
- 因为每个线程可能携带不同的userid发起请求,因此在这里使用ThreadLocal变量存放userid,使得每个线程都有一份自己的副本。
3.6:作为一种用于“方便传参”的工具
- 每一个ThreadLocal能够放一个线程级别的变量,可是它本身能够被多个线程共享使用,并且又能够达到线程安全的目的,且绝对线程安全。
public final static ThreadLocal<String> RESOURCE = new ThreadLocal<String>();
- RESOURCE代表一个能够存放String类型的ThreadLocal对象。此时不论什么一个线程能够并发訪问这个变量,对它进行写入、读取操作,都是线程安全的。
- 比方一个线程通过RESOURCE.set(“aaaa”);将数据写入ThreadLocal中,在不论什么一个地方,都能够通过RESOURCE.get();将值获取出来。
- 可是它也并不完美,有很多缺陷,就像大家依赖于它来做參数传递一样。接下来我们就来分析它的一些不好的地方。
为什么有些时候会将ThreadLocal作为方便传递參数的方式呢?比如当很多方法相互调用时,最初的设计可能没有想太多,有多少个參数就传递多少个变量,
那么整个參数传递的过程就是零散的。
进一步思考:若A方法调用B方法传递了8个參数。
B方法接下来调用C方法->D方法->E方法->F方法等仅仅须要5个參数,此时在设计API时就涉及5个參数的入口。
这些方法在业务发展的过程中被很多地方所复用。
某一天。我们发现F方法须要加一个參数,这个參数在A方法的入口參数中有,此时,假设要改中间方法牵涉面会非常大。并且不知道改动后会不会有Bug。
作为程序猿的我们可能会随性一想,ThreadLocal反正是全局的,就放这里吧。确实好解决。
可是此时你会发现系统中这样的方式有点像在贴补丁。越贴越多,我们必需要求调用相关的代码都使用ThreadLocal传递这个參数,有可能会搞得乱七八糟的。
换句话说,并非不让用。而是我们要明白它的入口和出口是可控的。
诡异的ThreadLocal最难琢磨的是“作用域”,尤其是在代码设计之初非常乱的情况下,假设再添加很多ThreadLocal。
系统就会逐渐变成神龙见首不见尾的情况。有了这样一个省事的东西。
可能很多小伙伴更加不在意设计,由于大家都觉得这些问题都能够通过变化的手段来解决。胖哥觉得这是一种恶性循环。
- 解决:
对于这类业务场景。应当提前有所准备。须要粗粒度化业务模型。即使要用ThreadLocal,也不是加一个參数就加一个ThreadLocal变量。
比如,我们能够设计几种对象来封装入口參数,在接口设计时入口參数都以对象为基础。
或许一个类无法表达全部的參数意思,并且那样easy导致强耦合。
通常我们依照业务模型分解为几大类型对象作为它们的參数包装,
而且将依照对象属性共享情况进行抽象,在继承关系的每个层次各自扩展对应的參数,
或者说加參数就在对象中加,共享參数就在父类中定义,这种參数就逐步规范化了。
四、ThreadLocal的坑
通过上面的分析。我们能够认识到ThreadLocal事实上是与线程绑定的一个变量,
如此就会出现一个问题:假设没有将ThreadLocal内的变量删除(remove)或替换,它的生命周期将会与线程共存。
因此,ThreadLocal的一个非常大的“坑”就是当使用不当时,导致使用者不知道它的作用域范围。
大家可能觉得线程结束后ThreadLocal应该就回收了。假设线程真的注销了确实是这种,可是事实有可能并不是如此。
比如在线程池中对线程管理都是采用线程复用的方法(Web容器通常也会采用线程池)。
在线程池中线程非常难结束甚至于永远不会结束。这将意味着线程持续的时间将不可预測,甚至与JVM的生命周期一致。
那么对应的ThreadLocal变量的生命周期也将不可预測。
或许系统中定义少量几个ThreadLocal变量也无所谓。
由于每次set数据时是用ThreadLocal本身作为Key的,同样的Key肯定会替换原来的数据。原来的数据就能够被释放了,理论上不会导致什么问题。
但世事无绝对,假设ThreadLocal中直接或间接包装了集合类或复杂对象,每次在同一个ThreadLocal中取出对象后,再对内容做操作,
那么内部的集合类和复杂对象所占用的空间可能会開始膨胀。
抛开代码本身的问题。
举一个极端的样例。
假设不想定义太多的ThreadLocal变量,就用一个HashMap来存放,这貌似没什么问题。
由于ThreadLocal在程序的不论什么一个地方都能够用得到,在某些设计不当的代码中非常难知道这个HashMap写入的源头,在代码中为了保险起见。
一般会先检查这个HashMap是否存在,若不存在,则创建一个HashMap写进去。若存在,通常也不会替换掉。
由于代码编写者一般会“害怕”由于这样的替换会丢掉一些来自“其它地方写入HashMap的数据”。从而导致很多不可预见的问题。
在这种情况下。HashMap第一次放入ThreadLocal中或许就一直不会被释放,而这个HashMap中可能開始存放很多Key-Value信息,
假设业务上存放的Key值在不断变化(比如,将业务的ID作为Key),那么这个HashMap就開始不断变长,并且非常可能在每一个线程中都有一个这种HashMap,逐渐地形成了间接的内存泄漏。
以前有非常多人吃过这个亏,并且吃亏的时候发现这种代码可能不是在自己的业务系统中。而是出如今某些二方包、三方包中(开源并不保证没有问题)。
要处理这样的问题非常复杂,只是首先要保证自己编写的代码是没问题的。要保证没问题不是说我们不去用ThreadLocal。甚至不去学习它。
由于它肯定有其应用价值。
在使用时要明确ThreadLocal最难以捉摸的是“不知道哪里是源头”(一般是代码设计不当导致的),仅仅有知道了源头才干控制结束的部分。
或者说我们从设计的角度要让ThreadLocal的set、remove有始有终,通常在外部调用的代码中使用finally来remove数据,
仅仅要我们细致思考和抽象是能够达到这个目的的。
有些是二方包、三方包的问题,对于这些问题我们须要学会的是找到问题的根源后解决,关于二方包、三方包的执行跟踪,
补充:在不论什么异步程序中(包含异步I/O、非堵塞I/O),ThreadLocal的參数传递是不靠谱的,由于线程将请求发送后。
就不再等待远程返回结果继续向下运行了,真正的返回结果得到后,处理的线程可能是还有一个。
五、总结
ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。
ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,
这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal不能使用原子类型,只能使用Object类型。ThreadLocal的使用比synchronized要简单得多。
ThreadLocal和Synchonized都用于解决多线程并发访问。但是ThreadLocal与synchronized有本质的区别。
synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。
而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。
而Synchronized却正好相反,它用于在多个线程间通信时能够获得数据共享。
Synchronized用于线程间的数据共享,而ThreadLocal则用于线程间的数据隔离。
- 当然ThreadLocal并不能替代synchronized,它们处理不同的问题域。Synchronized用于实现同步机制,比ThreadLocal更加复杂。
六、ThreadLocal使用的一般步骤
- 1、在多线程的类(如ThreadDemo类)中,创建一个ThreadLocal对象threadXxx,用来保存线程间需要隔离处理的对象xxx。
- 2、在ThreadDemo类中,创建一个获取要隔离访问的数据的方法getXxx(),
- 在方法中判断,若ThreadLocal对象为null时候,应该new()一个隔离访问类型的对象,并强制转换为要应用的类型。
- 3、在ThreadDemo类的run()方法中,通过getXxx()方法获取要操作的数据,这样可以保证每个线程对应一个数据对象,在任何时刻都操作的是这个对象。