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的使用的主要内容,如果未能解决你的问题,请参考以下文章

Java面试之ThreadLocal的使用

Java面试必问,ThreadLocal终极篇

Java 200+ 面试题补充 ThreadLocal 模块

java ThreadLocal 实践与面试要点

广州某小公司:ThreadLocal面试

Java多线程之ThreadLocal总结2