ThreadLocal介绍

ThreadLocal变量可以理解为线程内部私有的共享变量。在Java的内存模型中每个线程拥有自己的工作线程,这个变量是一个map结构的专门用于线程私有变量的存储,方便于解耦+防止并发。StackOverFlow上有段解释较为通俗。When and how can we use ThreadLocal variable?

  • When an object is not thread-safe, instead of synchronization which hampers the scalability, give one object to every thread and keep it thread scope, which is ThreadLocal. One of most often used but not thread-safe objects are database Connection and JMSConnection.
  • One example is Spring framework uses ThreadLocal heavily for managing transactions behind the scenes by keeping these connection objects in ThreadLocal variables. At high level, when a transaction is started it gets the connection ( and disables the auto commit ) and keeps it in ThreadLocal. on further db calls it uses same connection to communicate with db. At the end, it takes the connection from ThreadLocal and commits ( or rollback ) the transaction and releases the connection.I think log4j also uses ThreadLocal for maintaining MDC.

通常线程池这种并发的资源模型会面临多线程竞争,可以将线程的每个连接初始化到每个线程中避免竞争。这时候会将这个连接放置到ThreadLocal变量中。

ThreadLocal API

ThreadLocal变量实际上是以当前线程为key,可以指定一个value的map对象,只能容放一个kv键值对。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package com.souche.study.TreadLocal;


/**
* 1.ThreadLocal变量一般定义为private static。
* 2.可以通过无参构造函数初始化or函数式初始化。
* 3.ThreadLocal以当前线程为key,value可以指定类型,只能存储一个值。
* 4.ThreadLocal初始化之后的值不能被remove掉。
*/
public class ThreadLocalTest {
//类加载的时候直接初始化,并且初始值不会被remove
private static ThreadLocal<Integer> threadLocal=ThreadLocal.withInitial(()->1);
//类加载的时候未被初始化,只提供无参构造函数,只有默认值null
private static ThreadLocal<Integer> threadLocal1=new ThreadLocal<>();

public static void main(String[] args) {
System.out.println("get有初始化ThreadLocal:"+threadLocal.get());
threadLocal.remove();
System.out.println("remove初始化的ThreadLocal:"+threadLocal.get());

threadLocal.set(2);
System.out.println("get重新赋值的ThreadLocal:"+threadLocal.get());

threadLocal.remove();
System.out.println("remove重新赋值的ThreadLocal:"+threadLocal.get());

//threadLocal1
System.out.println("get无初始化ThreadLocal1:"+threadLocal1.get());
threadLocal1.set(2);
System.out.println("get重新赋值的ThreadLocal1:"+threadLocal1.get());

threadLocal1.remove();
System.out.println("remove重新赋值的ThreadLocal1:"+threadLocal1.get());
}
}

结果:

1
2
3
4
5
6
7
get初始化ThreadLocal:1
remove初始化的ThreadLocal:1
get重新赋值的ThreadLocal:2
remove重新赋值的ThreadLocal:1
get初始化ThreadLocal1:null
get重新赋值的ThreadLocal1:2
remove重新赋值的ThreadLocal1:null

Storing User Data in a Map

我们假定每个线程都需要存储它的登录信息,这里给定一个用户信息类:

1
2
3
4
5
6
7
public class Context {
private String userName;

public Context(String userName) {
this.userName = userName;
}
}

我们要实现每个userId对应一个context就需要将这个userId作为key存储在map中,同时需要时concurrentHashMap满足多线程并发的线程安全。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class SharedMapWithUserContext implements Runnable {

public static Map<Integer, Context> userContextPerUserId
= new ConcurrentHashMap<>();
private Integer userId;
private UserRepository userRepository = new UserRepository();

@Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContextPerUserId.put(userId, new Context(userName));
}

// standard constructor
}

然后我们通过测试类去测试:

1
2
3
4
5
6
SharedMapWithUserContext firstUser = new SharedMapWithUserContext(1);
SharedMapWithUserContext secondUser = new SharedMapWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

assertEquals(SharedMapWithUserContext.userContextPerUserId.size(), 2);

这种方式也是可以的,相对来说需要去做一个线程安全的东西成本比较大。

Storing User Data in ThreadLocal

向上面这种情况我们可以将这个context存在每个线程自有的ThreadLocal变量中,不用考虑线程安全的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ThreadLocalWithUserContext implements Runnable {

private static ThreadLocal<Context> userContext
= new ThreadLocal<>();
private Integer userId;
private UserRepository userRepository = new UserRepository();

@Override
public void run() {
String userName = userRepository.getUserNameForUserId(userId);
userContext.set(new Context(userName));
System.out.println("thread context for given userId: "
+ userId + " is: " + userContext.get());
}

// standard constructor
}

同样的,我们可以进行测试:

1
2
3
4
5
6
ThreadLocalWithUserContext firstUser 
= new ThreadLocalWithUserContext(1);
ThreadLocalWithUserContext secondUser
= new ThreadLocalWithUserContext(2);
new Thread(firstUser).start();
new Thread(secondUser).start();

可以得到一个结果:

1
2
thread context for given userId: 1 is: Context{userNameSecret='18a78f8e-24d2-4abf-91d6-79eaa198123f'}
thread context for given userId: 2 is: Context{userNameSecret='e19f6a0a-253e-423e-8b2b-bca1f471ae5c'}

ThreadLocal如何实现

很多blog都介绍了ThreadLocal主要作用是解耦+防止并发。实际上我自己将其理解为一个全局map变量,全局变量的存在减少了不必要的参数传递,也就是解耦,这个全局变量我们一般通过定义变量为private static实现,需要通过类直接访问可以定义为public static。通过维护当前线程id作为map的key,保证了只有当前key的value对当前线程可见,其他线程无法访问到这个value的资源,那么这个value便可以理解为线程的私有资源,实际上这些对象都还是放在堆中,只不过用key标示了每个资源的归属 ,也就是每个工作线程都有自己的私有空间。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
*
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}

一般我们的框架在处理了登录之后,例如单点登录,一个请求进入到tomcat,分配线程资源,进入到后台,通过拦截器分发的对应的sso服务。服务返回登录信息,这个时候会写一个ThreadLocal变量(AuthNHolder)以当前线程id为key,保存用户信息到value里面,之后线程在任何地方想获取这个用户信息可以直接从ThreadLocal中拿到,线程请求完成之后回到拦截器会将这个value清空。这里应该可以体会到解耦+防止并发。

总结

其实我们在使用这个变量的时候,通常也要防止在线程池的情况下,可能会导致某一个线程同时消费很多任务,如果在一个任务结束之后没有将线程的工作空间进行清除,那么这个工作空间会存储两个任务的信息,这可能会产生问题。我们可以通过登录保存用户信息这个案例更好的了解ThreadLocal的应用。详细可以参考optimus中AuthHolder的实现