Java面试之ThreadLocal的使用
Posted 龙鸣丿
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Java面试之ThreadLocal的使用相关的知识,希望对你有一定的参考价值。
ThreadLocal解决了什么问题?内部源码是怎么样的?
作用:实现在线程的上下文传递对象,为每个线程创建一个副本。
案例:
public class ThreadLocalTest
private static ThreadLocal<Long> 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来保存键值对。
ThreadLocal的使用:
案例一:
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<SimpleDateFormat> 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<User> holder = new ThreadLocal<>();
class User
String name;
public User(String name)
this.name = name;
执行结果:
service2拿到的用户:jack
service3拿到的用户:jack
ThreadLocal的应用场景# 数据库连接:
public Connection initialValue()
return DriverManager.getConnection(DB_URL);
;
public static Connection getConnection()
return connectionHolder.get();
ThreadLocal的应用场景# Session管理:
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;
ThreadLocal的应用场景# 多线程:
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。
以上是关于Java面试之ThreadLocal的使用的主要内容,如果未能解决你的问题,请参考以下文章