一、是什么
ThreadLocal翻译成中文应该是:线程局部变量。
ThreadLocal提供了线程的局部变量,如果创建一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的一个副本,每个线程都可以通过set()和get()来对这个局部变量进行操作,但不会和其他线程的局部变量进行冲突,实现了线程的数据隔离。
往ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。是一个以ThreadLocal对象为键、任意对象为值的存储结构。
从表面上看ThreadLocal相当于维护了一个map,key就是当前的线程,value就是需要存储的对象。实际上是ThreadLocal的静态内部类ThreadLocalMap为每个Thread都维护了一个数组table,ThreadLocal确定了一个数组下标,而这个下标就是value存储的对应位置。通过get和set方法就可以得到当前线程对应的值。
二、为什么
多线程访问同一个共享变量的时候容易出现并发问题,特别是多个线程对一个变量进行写入的时候,一般使用者在访问共享变量的时候需要进行额外的同步措施才能保证线程安全性。ThreadLocal是除了加锁这种同步方式之外的一种保证一种规避多线程访问出现线程不安全的方法。
三、怎么做
说了这么多,那ThreadLocal有哪些应用场景呢?
(1)Android源码的Lopper、ActivityThread以及AMS中都用到了ThreadLocal。
public final class Looper {
private static final String TAG = "Looper";
// sThreadLocal.get() will return null unless you've called prepare().
static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>();
private static void prepare(boolean quitAllowed) {
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
sThreadLocal.set(new Looper(quitAllowed));
}
....//省略
}
- Looper类就是利用了ThreadLocal的特性,保证每个线程只存在一个Looper对象。
- Android的Handler消息机制中,对于Handler来说,它需要获取当前线程的looper,Looper的作用域就是线程并且不同线程具有不同的Looper,这个时候通过ThreadLocal就可以轻松实现Looper在线程中的存取。
- 再例如开源框架EventBus,EventBus需要获取当前线程的PostingThreadState对象,不同的PostingThreadState同样作用于不同的线程,EventBus可以很轻松的获取当前线程下的PostingThreadState对象,然后进行相关操作。
(2)日常使用场景不多,当某些数据是以线程为作用域并且不同线程具有不同的数据副本的时候,可以考虑使用ThreadLocal。
比较经典的一个例子是SimpleDataFormat,实践证明sdf的parse(String to Date)有严重的线程安全问题,其内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。
有几种不同的解决方案:
①每次来都new新的。缺点:空间浪费比较大
②方法用synchronized修饰。缺点:并发上不来
③用jdk1.8中的日期格式类DateFormatter,DateTimeFormatter。缺点:改动比较大
④用ThreadLocal,一个线程一个SimpleDateFormat对象。代码如下
public class threadLocalTest {
// private static final SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
private static ThreadLocal<DateFormat> threadLocalData = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
public static Date parse(String dateStr) {
Date date = null;
try {
// date = sdf.parse(dateStr);
date = threadLocalData.get().parse(dateStr);
} catch (ParseException e) {
e.printStackTrace();
}
return date;
}
public static void main(String[] args) {
ExecutorService service = Executors.newFixedThreadPool(20);
for (int i = 0; i < 20; i++) {
service.execute(()->{
System.out.println(threadLocalTest.parse("2020-09-09 16:46:30"));
});
}
service.shutdown();
}
}
(3)复杂逻辑下对象传递,即解决共享参数
项目中存在一个线程经常遇到横跨若干方法调用,需要传递的对象,也就是上下文(Context),它是一种状态,经常就是是用户身份、任务信息等,就会存在过渡传参的问题。
方案①:使用到类似责任链模式,给每个方法增加一个context参数非常麻烦,而且有些时候,如果调用链有无法修改源码的第三方库,对象参数就传不进去了;
方案②为每一个线程定义一个静态变量监听器,如果是多线程的话,一个线程就需要定义一个静态变量,无法扩展
方案③:使用ThreadLocal,只需要在调用前在ThreadLocal中设置参数,其他地方get一下就好了。
//before:
void work(User user) {
getInfo(user);
checkInfo(user);
setSomeThing(user);
log(user);
}
//after:
void work(User user) {
try{
threadLocalUser.set(user);
// 在他们内部 User u = threadLocalUser.get(); 就好了
getInfo();
checkInfo();
setSomeThing();
log();
} finally {
threadLocalUser.remove();
}
}
【附】不完美的地方:
在传参过程中,A->B->C->D->E,若E想多加一个参数,此参数在A中有,是不是之前的BCD接口的参数都需要修改呢。牵涉面比较大,程序改动较大,而且不知道后续是否有BUG。
通常解决办法:将A的参数都放到ThreadLocal中,这样做是可以将眼前问题解决,但这就像贴布丁,越贴越多,搞得系统中调用相关的代码都要使用ThreadLocal传参,可能搞得乱七八糟。换句话说,不是不让用ThreadLocal,而是要明确它的出入口是可控的。
(4) 很多场景的cookie,session等数据隔离都是通过ThreadLocal去做实现的。
比如Hibernate的session获取场景:
private static final ThreadLocal<Session> threadLocal = new ThreadLocal<Session>();
//获取Session
public static Session getCurrentSession(){
Session session = threadLocal.get();
//判断Session是否为空,如果为空,将创建一个session,并设置到本地线程变量中
try {
if(session ==null&&!session.isOpen()){
if(sessionFactory==null){
rbuildSessionFactory();// 创建Hibernate的SessionFactory
}else{
session = sessionFactory.openSession();
}
}
threadLocal.set(session);
} catch (Exception e) {
// TODO: handle exception
}
return session;
}
(5)Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接。同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。Spring的事务也主要是ThreadLocal和AOP去做实现的。
我们使用数据库的时候首先就是建立数据库连接,用完后关闭就好了。
这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。
public final class ConnectionUtil {
private ConnectionUtil() {}
private static final ThreadLocal<Connection> conn = new ThreadLocal<>();
public static Connection getConn() {
Connection con = conn.get();
if (con == null) {
try {
Class.forName("com.mysql.jdbc.Driver");
con = DriverManager.getConnection("url", "userName", "password");
conn.set(con);
} catch (ClassNotFoundException | SQLException e) {
// ...
}
}
return con;
}
}
关于ThreadLocal实现原理以及内存泄漏问题相关总结在下章: