Java面试-ThreadLocal你知道多少?

最近做的一个项目中用到了ThreadLocal,在拦截器中存储访问用户的信息,以便进行方法级别的权限验证。用了感觉一知半解的,空闲了查询了许多的资料,并参考了慕课网的《玩转Java并发工具》课程,发现ThreadLocal是面试中很容易考到的并发类,于是将ThreadLocal相关的内容整理并记录于此,方便自己和同样准备找工作的同学学习。

典型应用场景

场景1:每个线程需要一个独享的对象
  • 通常应用在线程不安全的工具类,如SimpleDateFormat,Random
  • 每个Thread内有自己的实例副本,不共享
  • 比喻:课本只有一本,一群人同时做笔记会发生冲突有线程安全问题。把课本复印成一人一本就没问题了

案例内容:

编写一个函数,计算1970年1.1 08:00:00 GMT后 seconds 秒后的时间,假设是1000个线程进行调用

  • 方案1
public class ThreadLocalNormalUsage02 {

    public static ExecutorService threadPool =
            Executors.newFixedThreadPool(10);

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 1000; i++) {
            int finalI = i;
            threadPool.submit(new Runnable() {
                @Override
                public void run() {
                    String date = new ThreadLocalNormalUsage02().date(finalI);
                    System.out.println(date);
                }
            });
        }
        threadPool.shutdown();
    }


    //获取1970年1.1 08:00:00 GMT后 seconds 的时间
    public String date(int seconds) {
        //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
        return dateFormat.format(date);

    }

}

这种方案存在一个问题,每次调用都需要创建一个SimpleDateFormat 对象,消耗太大了,有没有解决的方案呢

  • 方案2:
    将SimpleDateFormat 对象抽出来作为静态变量
	public static SimpleDateFormat dateFormat ;
	//获取1970年1.1 08:00:00 GMT后 seconds 的时间
    public String date(int seconds) {
        //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
        Date date = new Date(1000 * seconds);

        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);

    }

新的问题又出现了,运行结果存在相同的值,发生了线程安全问题

  • 方案3:
    将使用SimpleDateFormat对象的代码锁起来
	 public String date(int seconds) {
        //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
        Date date = new Date(1000 * seconds);

        String s;
        synchronized (ThreadLocalNormalUsage03.class){
            s = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(date);

        }
        return s;
    }

这种情况下还存在问题,在高并发下每个线程都需要排队获取,效率低,不适用

  • 方案4:利用ThreadLocal再次升级实现,线程安全且能并行执行
	//获取1970年1.1 08:00:00 GMT后 seconds 的时间
    public String date(int seconds) {
        //参数的单位是毫秒,从1970年1.1 08:00:00 gmt计时
        Date date = new Date(1000 * seconds);
        SimpleDateFormat dateFormat = ThreadStatrFormatter.dateFormatThreadLocal.get();

        return dateFormat.format(date);
    }

    class ThreadStatrFormatter {
        public static ThreadLocal<SimpleDateFormat>
                dateFormatThreadLocal =
                new ThreadLocal<SimpleDateFormat>() {
                    @Override
                    protected SimpleDateFormat initialValue() {
                        return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
                    }
                };
    }
场景2:当前用户信息需要被线程内所有方法共享

在应用开发中,有些参数需要被线程内许多方法使用,如权限管理,很多的方法都需要验证当前线程用户的身份信息

案例内容:一个系统中,user对象需要在很多server中进行使用

  • 方案1
    将user作为参数层层传递,从service1->service2->service3以此类推。这样会导致代码冗余难以维护

  • 方案2
    定义一个全局的static 的user,想要拿的时候直接获取。
    这是一种错误的方案!!
    因为我们现在的场景是多用户的系统,每个线程对应着不同的用户,每个线程的user是不同的

  • 方案3
    定义一个UserMap,每次访问从Map中获取用户的信息,多线程访问下加锁或者使用ConcurrentHashMap,但是对性能有影响

  • 方案4
    利用ThreadLocal,不需要锁,不影响性能。
    强调的是同一个请求内不同方法间的共享

    代码演示:

/**
 * 避免传递参数的麻烦
 * ThreadLocalan案例2
 * @author Chkl
 * @create 2020/3/10
 * @since 1.0.0
 */
public class ThreadLocalNormalUsage06 {
    public static void main(String[] args) {
        new Service1().process();
    }
}

class Service1 {
    public void process() {
        User user = new User("张三");
        UserContextHolder.holder.set(user);
        new Service2().process();
    }
}

class Service2 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service2:" + user.name);
        new Service3().process();
    }
}

class Service3 {
    public void process() {
        User user = UserContextHolder.holder.get();
        System.out.println("service3:" + user.name);
    }
}

class UserContextHolder {
    public static ThreadLocal<User> holder
            = new ThreadLocal<>();
}

class User {
    String name;
    public User(String name) {
        this.name = name;
    }
}

ThreadLocal的两个作用

  • 让某个需要用到的对象在线程间隔离(每个线程都有自己的独立的对象)
  • 同一线程中,在任何方法中都可以轻松获取到该对象

两种初始化方法使用场景

  • 场景1:initialValue
    如果在ThreadLocal第一次get的时候把对象给初始化时使用,对象的初始化时机受控制

  • 场景2:set
    如果需要保存到ThreadLocal的对象的生成时机不由我们随意控制,如访问拦截器生成用户信息的情况下使用

使用ThreadLocal的好处

  • 线程安全
  • 不需要加锁,执行效率高
  • 更高效的利用内存,节省开销
  • 避免传参的繁琐操作

ThreadLocal与Thread的关系

一张图搞懂Thread,ThreadLocal,ThreadLocalMap三者的关系:

-----

每个Thread对象都持有一个ThreadLocalMap成员变量
查看Thread的源码也可以发现确实存在这样一个变量
在这里插入图片描述
在这里插入图片描述

ThreadLocal的重要方法

  • initialValue()

    • 该方法返回当前线程对应的初始值,使用了延迟加载,当调用get()方法是才会触发
    • 当第一次使用get()方法时会调用此方法,如果调用前用set()方法设置了值就不会调用
    • 当调用remove()方法后再次调用get()方法依然会调用initialize
    • 如果不重写initialValue方法,直接调用get()会返回null
  • set() 为线程设置新的值

  • get()

    • 得到线程对应的value,如果首次调用,则会调用initialize
    • get方法是先取出当前线程的ThreadLocalMap,再通过map.getEntry(ThreadLocal)方法将本ThreadLocal的引用作为参数传入获取ThreadLocal的值
    • ThreadlocalMap这个Map是存放在Thread中而不是ThreadLocal中
  • remove() 删除线程所保持的值

    • remove()方法也是在ThreadlocalMap中进行操作,传入当前ThreadLocal对象的引用,删除m ap中的value的值

ThreadLocal注意点

  • 最后一次使用之后应该手动的调用remove()方法,防止内存溢出
  • 如果可以不使用ThreadLocal就解决问题,不要强行使用(如:任务数很少时)
  • 优先使用框架的支持,而不是自己创造
    Spring中,如果可以使用RequestContextHolder就不要用ThreadLocal

ThreadLocal 为什么会发生内存溢出?

ThreadLocal的存储实际是把当前线程作为key,存储数据当做value存储在ThreadLocalMap(内部实现为Entry)中,key使用的是弱引用,而value使用的是强引用。当任务执行结束后因为value没有回收导致数据不会被GC处理,会一直存在于线程中,积累到足够多就会发生内存溢出

如何解决内存溢出

最后一次使用之后应该手动的调用remove()方法

针对ThreadLocal 的内存溢出,也有相应的操作去解决,当调用set(),remove()方法时,会对没有使用的键值对进行处理(方便GC回收),所以操作完成后需要手动的对ThreadLocal 进行处理,调用threadLocal.remove()

发布了29 篇原创文章 · 获赞 53 · 访问量 3万+

猜你喜欢

转载自blog.csdn.net/qq_41170102/article/details/104778024
今日推荐