面试官:ThreadLocal的应用场景和注意事项有哪些?

摘要:
SimpleDateFormat的原因与此类似。我们如何解决这个问题?解决方案1:每次它出现时都是新的,空间浪费相对较大的publicclassDateUtil{publicstaticDateparse{SimpleDateFormat sdf=newSimpleDateFormat;Datedate=null;try{date=sdf.parse;}catch{e.printStackTrace();}return date;}解决方案2:使用synchronized和publicclassDateUtil{privatestaticfinalSimpleDateFormat sdf=newSimpleDateFormat;publicstaticsynchronizedDateparse{Datedate=null;try{date=sdf.parse;}catch{e.printStackTrace();}return date;}}解决方案3:在jdk1.8中使用日期格式类DateFormatter,DateTimeFormatterpublicclassDateUtil{privatestaticDateTimeFormatter format=DateTimeFormater.ofPattern;publicstaticLocalDateTimeparse{returnLocalDateTime.parse;}}解决方案4:使用ThreadLocal,一个线程有一个SimpleDateFormat对象publicclassDateUtil{privatestaticThreadLocal<DateFormat>threadLocal=threadLocal.withInitial;publicstaticDateparse{Datedate=null;try{date=threadLocal.get().parse;}catch{e.printStackTrace();}return date;}上面带有10的工具类可以更改为以下形式:publicclassNumUtil{privatestaticThreadLocal<Integer>addNumThreadLocal=newThreadLocal<>();publicstaticintadd10{addNumThreadLocal.set;try{TimeUnit.SECONDS.sleep;}catch{e.printStackTrace();}returnaddNumThreadLocal。get()+10;}}现在这两个工具类可以正常使用了。为什么?事实上,Thread类中已经有一个Map容器来存储变量。

前言

ThreadLocal主要有如下2个作用

  1. 保证线程安全
  2. 在线程级别传递变量

保证线程安全

最近一个小伙伴把项目中封装的日期工具类用在多线程环境下居然出了问题,来看看怎么回事吧

日期转换的一个工具类

public class DateUtil {

    private static final SimpleDateFormat sdf = 
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static Date parse(String dateStr) {
        Date date = null;
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

然后将这个工具类用在多线程环境下

public static void main(String[] args) {

    ExecutorService service = Executors.newFixedThreadPool(20);

    for (int i = 0; i < 20; i++) {
        service.execute(()->{
            System.out.println(DateUtil.parse("2019-06-01 16:34:30"));
        });
    }
    service.shutdown();
}

结果报异常了,因为部分线程获取的时间不对在这里插入图片描述
这个异常就不从源码的角度分析了,写一个小Demo,理解了这个小Demo,就理解了原因

一个将数字加10的工具类

public class NumUtil {

    public static int addNum = 0;

    public static int add10(int num) {
        addNum = num;
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return addNum + 10;
    }
}
public static void main(String[] args) {

	ExecutorService service = Executors.newFixedThreadPool(20);

	for (int i = 0; i < 20; i++) {
		int num = i;
		service.execute(()->{
			System.out.println(num + " " +  NumUtil.add10(num));
		});
	}
	service.shutdown();
}

然后代码的一部分输出为

0 28
3 28
7 28
11 28
15 28

什么鬼,不是加10么,怎么都输出了28?这主要是因为线程切换的原因,线程陆续将addNum值设置为0 ,3,7但是都没有执行完(没有执行到return addNum+10这一步)就被切换了,当其中一个线程将addNum值设置为18时,线程陆续开始执行addNum+10这一步,结果都输出了28。SimpleDateFormat的原因和这个类似,那么我们如何解决这个问题呢?

解决方案

解决方案1:每次来都new新的,空间浪费比较大

public class DateUtil {

    public static Date parse(String dateStr) {
        SimpleDateFormat sdf =
                new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = null;
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

解决方案2:方法用synchronized修饰,并发上不来

public class DateUtil {

    private static final SimpleDateFormat sdf =
            new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");

    public static synchronized Date parse(String dateStr) {
        Date date = null;
        try {
            date = sdf.parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

解决方案3:用jdk1.8中的日期格式类DateFormatter,DateTimeFormatter

public class DateUtil {

    private static DateTimeFormatter formatter = 
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

    public static LocalDateTime parse(String dateStr) {
        return LocalDateTime.parse(dateStr, formatter);
    }
}

解决方案4:用ThreadLocal,一个线程一个SimpleDateFormat对象

public class DateUtil {

    private static ThreadLocal<DateFormat> threadLocal = ThreadLocal.withInitial(
            ()-> new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));

    public static Date parse(String dateStr) {
        Date date = null;
        try {
            date = threadLocal.get().parse(dateStr);
        } catch (ParseException e) {
            e.printStackTrace();
        }
        return date;
    }
}

上面的加10的工具类可以改成如下形式(主要为了演示ThreadLocal的使用)

public class NumUtil {

    private static ThreadLocal<Integer> addNumThreadLocal = new ThreadLocal<>();

    public static int add10(int num) {
        addNumThreadLocal.set(num);
        try {
            TimeUnit.SECONDS.sleep(1);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        return addNumThreadLocal.get() + 10;
    }
}

现在2个工具类都能正常使用了,这是为啥呢?

原理分析

当多个线程同时读写同一共享变量时存在并发问题,如果不共享不就没有并发问题了,一个线程存一个自己的变量,类比原来好几个人玩同一个球,现在一个人一个球,就没有问题了,如何把变量存在线程上呢?其实Thread类内部已经有一个Map容器用来存变量了。它的大概结构如下所示

在这里插入图片描述
ThreadLocalMap是一个Map,key是ThreadLocal,value是Object

映射到源码就是如下所示:
ThreadLocalMap是ThreadLocal的一个静态内部类

public class Thread implements Runnable {
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

往ThreadLocalMap里面放值

// ThreadLocal类里面的方法,将源码整合了一下
public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    if (map != null)
        map.set(this, value);
    else
		t.threadLocals = new ThreadLocalMap(this, firstValue);
}

从ThreadLocalMap里面取值

// ThreadLocal类里面的方法,将源码整合了一下
public T get() {
	Thread t = Thread.currentThread();
	ThreadLocalMap map = t.threadLocals;
	if (map != null) {
		ThreadLocalMap.Entry e = map.getEntry(this);
		if (e != null) {
			@SuppressWarnings("unchecked")
			T result = (T)e.value;
			return result;
		}
	}
	return setInitialValue();
}

从ThreadLocalMap里面删除值

// ThreadLocal类里面的方法,将源码整合了一下
public void remove() {
	ThreadLocalMap m = Thread.currentThread().threadLocals;
	if (m != null)
		m.remove(this);
}

执行如下代码

public class InfoUtil {

    private static ThreadLocal<String> nameInfo = new ThreadLocal<>();
    private static ThreadLocal<Integer> ageInfo = new ThreadLocal<>();

    public static void setInfo(String name, Integer age) {
        nameInfo.set(name);
        ageInfo.set(age);
    }

    public static String getName() {
        return nameInfo.get();
    }

    public static void main(String[] args) {
        new Thread(() -> {
            InfoUtil.setInfo("张三", 10);
            // 张三
            System.out.println(InfoUtil.getName());
        }, "thread1").start();
        new Thread(() -> {
            InfoUtil.setInfo("李四", 20);
            // 李四
            System.out.println(InfoUtil.getName());
        }, "thread2").start();
    }
}

变量的结构如下图
在这里插入图片描述

在线程级别传递变量

假设有如下一个场景,method1()调用method2(),method2()调用method3(),method3()调用method4(),method1()生成了一个变量想在method4()中使用,有如下2种解决办法

  1. method 2 3 4的参数列表上都写上method4想要的变量
  2. method 1 往ThreadLocal中put一个值,method4从ThreadLocal中get出来

哪种实现方式比较优雅呢?相信我不说你也能明白了

我在生产环境中一般是这样用的,如果一个请求在系统中的处理流程比较长,可以对请求的日志打一个相同的前缀,这样比较方便处理问题

这个前缀的生成和移除可以配置在拦截器中,切面中,当然也可以在一个方法的前后

public class Main {

    public static final ThreadLocal<String> SPANID =
            ThreadLocal.withInitial(() -> UUID.randomUUID().toString());

    public static void start() {
        SPANID.set(UUID.randomUUID().toString());
        // 方法调用过程中可以在日志中打印SPANID表明一个请求的执行链路
        SPANID.remove();
    }
}

当然Spring Cloud已经有现成的链路追踪组件了。

ThreadLocal使用注意事项

ThreadLocal如果使用不当会造成如下问题

  1. 脏数据
  2. 内存泄露

脏数据

线程复用会造成脏数据。由于线程池会复用Thread对象,因此Thread类的成员变量threadLocals也会被复用。如果在线程的run()方法中不显示调用remove()清理与线程相关的ThreadLocal信息,并且下一个线程不调用set()设置初始值,就可能get()到上个线程设置的值

内存泄露

static class ThreadLocalMap {

	static class Entry extends WeakReference<ThreadLocal<?>> {
		Object value;

		Entry(ThreadLocal<?> k, Object v) {
			super(k);
			value = v;
		}
	}
}

ThreadLocalMap使用ThreadLocal的弱引用作为key,如果一个ThreadLocal没有外部强引用来引用它,那么系统 GC 的时候,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话,这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏

大白话一点,ThreadLocalMap的key是弱引用,GC时会被回收掉,那么就有可能存在ThreadLocalMap<null, Object>的情况,这个Object就是泄露的对象

其实,ThreadLocalMap的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value

解决办法

解决以上两个问题的办法很简单,就是在每次用完ThreadLocal后,及时调用remove()方法清理即可

欢迎关注

在这里插入图片描述

参考博客

神奇的ThreadLocal
[1]https://mp.weixin.qq.com/s/kuTspYfMkDK4AjlETifujg
还在使用SimpleDateFormat?你的项目崩没?
ThreadLocal为什么会内存泄漏
[2]https://blog.xiaohansong.com/ThreadLocal-memory-leak.html
[3]https://zhangzw.com/posts/20190503.html

免责声明:文章转载自《面试官:ThreadLocal的应用场景和注意事项有哪些?》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇MongoDB安装与配置(Ubuntu)供多处理器系统中的高速缓存同步中使用的转发状态下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

最全的iOS面试题及答案-转载

1. Object-c的类可以多重继承么?可以实现多个接口么?Category是什么?重写一个类的方式用继承好还是分类好?为什么? 答: Object-c的类不可以多重继承;可以实现多个接口,通过实现多个接口可以完成C++的多重继承;Category是类别,一般情况用分类好,用Category去重写类的方法,仅对本Category有效,不会影响到其他类与原...

浏览器工作原理和实践(三)——页面

《浏览器工作原理与实践》是极客时间上的一个浏览器学习系列,在学习之后特在此做记录和总结。 一、事件循环 消息队列是一种数据结构,可以存放要执行的任务。它符合队列“先进先出”的特点,也就是说要添加任务的话,添加到队列的尾部;要取出任务的话,从队列头部去取。 从上图可以看出,改造可以分为下面三个步骤: (1)添加一个消息队列; (2)IO 线程中产生的新任务...

Redis分布式锁实战

大家好,我是walking,感谢你打开这篇文章,请认真阅读下去吧,希望对你有帮助。文末送福利~ 今天我们聊聊Redis的一个实际开发的使用场景,那就是大名鼎鼎的分布式锁。 啥是分布式锁? 我们学习 Java 都知道锁的概念,例如基于 JVM 实现的同步锁 synchronized,以及 jdk 提供的一套代码级别的锁机制 lock,我们在并发编程中会经...

linux下用/proc/stat文件来计算cpu的利用率-c语言实现

proc文件系统介绍       /proc文件系统是一个伪文件系统,它只存在内存当中,而不占用外存空间。它以文件系统的方式为内核与进程提供通信的接口。用户和应用程序可以通过/proc得到系统的信息,并可以改变内核的某些参数。由于系统的信息,如进程,是动态改变的,所以用户或应用程序读取/proc目录中的文件时,proc文件系统是动态从系统内核读出所需信息并...

线程方法

使用多线程原因:1、当执行某个耗时任务时,需要开启多线程2、希望多个任务“同时”执行3、防止线程阻塞4、完成某个特定的任务 进程:正在执行的程序 线程:具有完成特定任务的一条执行路径,是CPU执行的最小单位(数据传输的基本单位是字节) 注意:CPU在某个时间刻度上只能够执行一条原子性语句(字节最小是bit位) 原子性语句:不可再分割的语句 CPU执...

Delphi 中多线程同步的一些处理方法

当创建了多个线程,并且多个线程都要访问同一资源,,就有可能出现混乱,于是用Synchronize来控制,使同一时间只有一个线程使用那部分资源,Synchronize参数里面的代码就是多线程需要公共的代码!线程是进程内一个相对独立的、可调度的执行单元。一个应用可以有一个主线程,一个主线程可以有多个子线程,子线程还可以有自己的子线程,这样就构成了多线程应用了。...