作用:实现在线程的上下文传递对象,为每个线程创建一个副本。
案例:
public class ThreadLocalTest {
private static ThreadLocal threadLocal = new ThreadLocal<>();
public static void main(String[] args) throws InterruptedException {
Task task = new Task();
new Thread(task).start();
Thread.sleep(10);
new Thread(task).start();
}
static class Task implements Runnable{
@Override
public void run() {
Long result = threadLocal.get();
if(result == null){
threadLocal.set(System.currentTimeMillis());
}
System.out.println(Thread.currentThread().getName()+"->"+threadLocal.get());
}
}
}
输出的结果是不同的:
Thread-0->1607250402525
Thread-1->1607250402535
为什么可以给每个线程保存一个不同的副本
分析源码:
Long result = threadLocal.get();
public T get() {
//1.获取当前线程
Thread t = Thread.currentThread();
//2.获取当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null) {
//3.以threadLocal为key,获取到entry
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
//4.获取对应的entry的value,就是我们存放到里面的变量的副本
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
threadLocal.set(System.currentTimeMillis());
public void set(T value) {
//1.获取当前线程
Thread t = Thread.currentThread();
//2.获取当前线程对应的map
ThreadLocalMap map = getMap(t);
if (map != null)
//3.往map里存放一个键值对
//this:threadLocal
//value:存放的副本
map.set(this, value);
else
createMap(t, value);
}
每个线程都会有对应的map,map来保存键值对。
案例一:
package com.huawei.并发;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ThreadLocalDemo04 {
public static ExecutorService threadPool = Executors.newFixedThreadPool(16);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadPool.submit(()-> {
String data = new ThreadLocalDemo04().date(finalI);
System.out.println(Thread.currentThread().getName()+"->"+data);
});
}
threadPool.shutdown();
}
private String date(int seconds){
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = ThreadSafeFormater.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormater{
public static ThreadLocal dateFormatThreadLocal = ThreadLocal.withInitial(()->
new SimpleDateFormat("mm:ss"));
}
ThreadLocal给每个线程维护一个自己的simpleDateFormat对象,这个对象在线程之间是独立的,互相没有关系的。这也就避免了线程安全问题。与此同时,simpleDateFormat对象还不会创造过多,线程池一共只有 16 个线程,所以需要16个对象即可。
案例二:
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。
package com.huawei.并发;
public class ThreadLocalDemo05 {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1{
void service1(User user){
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2{
void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用户:"+user.name);
new Service3().service3();
}
}
class Service3{
void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用户:"+user.name);
UserContextHolder.holder.remove();
}
}
class UserContextHolder{
public static ThreadLocal holder = new ThreadLocal<>();
}
class User{
String name;
public User(String name){
this.name = name;
}
}
执行结果:
service2拿到的用户:jack
service3拿到的用户:jack
public Connection initialValue() {
return DriverManager.getConnection(DB_URL);
}
};
public static Connection getConnection() {
return connectionHolder.get();
}
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
public class ThreadLocalExsample {
/**
* 创建了一个MyRunnable实例,并将该实例作为参数传递给两个线程。两个线程分别执行run()方法,
* 并且都在ThreadLocal实例上保存了不同的值。如果它们访问的不是ThreadLocal对象并且调用的set()方法被同步了,
* 则第二个线程会覆盖掉第一个线程设置的值。但是,由于它们访问的是一个ThreadLocal对象,
* 因此这两个线程都无法看到对方保存的值。也就是说,它们存取的是两个不同的值。
*/
public static class MyRunnable implements Runnable {
/**
* 例化了一个ThreadLocal对象。我们只需要实例化对象一次,并且也不需要知道它是被哪个线程实例化。
* 虽然所有的线程都能访问到这个ThreadLocal实例,但是每个线程却只能访问到自己通过调用ThreadLocal的
* set()方法设置的值。即使是两个不同的线程在同一个ThreadLocal对象上设置了不同的值,
* 他们仍然无法访问到对方的值。
*/
private ThreadLocal threadLocal = new ThreadLocal();
@Override
public void run() {
//一旦创建了一个ThreadLocal变量,你可以通过如下代码设置某个需要保存的值
threadLocal.set((int) (Math.random() * 100D));
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
}
//可以通过下面方法读取保存在ThreadLocal变量中的值
System.out.println("-------threadLocal value-------"+threadLocal.get());
}
}
public static void main(String[] args) {
MyRunnable sharedRunnableInstance = new MyRunnable();
Thread thread1 = new Thread(sharedRunnableInstance);
Thread thread2 = new Thread(sharedRunnableInstance);
thread1.start();
thread2.start();
}
}
运行结果
-------threadLocal value-------38
-------threadLocal value-------88
在每个线程Thread内部有一个ThreadLocal.ThreadLocalMap类型的成员变量threadLocals,这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)。 初始时,在Thread里面,threadLocals为空,当通过ThreadLocal变量调用get()方法或者set()方法,就会对Thread类中的threadLocals进行初始化,并且以当前ThreadLocal变量为键值,以ThreadLocal要保存的副本变量为value,存到threadLocals。 然后在当前线程里面,如果要使用副本变量,就可以通过get方法在threadLocals里面查找。
实际的通过ThreadLocal创建的副本是存储在每个线程自己的threadLocals中的;
为何threadLocals的类型ThreadLocalMap的键值为ThreadLocal对象,因为每个线程中可有多个threadLocal变量,就像上面代码中的longLocal和stringLocal;
在进行get之前,必须先set,否则会报空指针异常;如果想在get之前不需要调用set就能正常访问的话,必须重写initialValue()方法。 因为在上面的代码分析过程中,我们发现如果没有先set的话,即在map中查找不到对应的存储,则会通过调用setInitialValue方法返回i,而在setInitialValue方法中,有一个语句是T value = initialValue(), 而默认情况下,initialValue方法返回的是null。