ThreadLocal深度解析

在网上见过了太多了关于ThreadLocal的文章,各说各的理,都说是线程安全的,但又说不出哪里体现出线程安全,又说内存异常,又很难解释为什么会造成内存溢出,算了,还是自己看源码研究把,这篇博客就是自己在这个背景下完成的。

1. ThreadLocal的用途

1.1 保存线程的上下文信息,在任意需要的地方可以获取,但不被多线程共享

由ThreadLocal的特性可知,在同个线程里面,针对同个ThreadLocal对象的赋值,在线程中都是可以根据该ThreadLocal对象获取的,而ThreadLocal同时也是线程私有的,不用担心共享数据的问题。

  • 对服务器端来说,每次请求都是一条线程处理,可以把请求的数据放在ThreadLocal中,从controller --> service --> dao层,甚至RPC调用,都不用修改方法参数或类变量,直接通过ThreadLocal设置或获取即可
  • Spring的Connection管理(很多优秀框架都有使用到了ThreadLocal处理)
org.springframework.jdbc.datasource.DataSourceUtils#getConnection
	org.springframework.jdbc.datasource.DataSourceUtils#doGetConnection
		org.springframework.transaction.support.TransactionSynchronizationManager#getResource

在这里插入图片描述

1.2 线程安全的,避免某些需要考虑线程安全必须同步带来的性能损失

ThreadLocal针对线程的并发问题,是争论得比较多的,ThreadLocal其实无法处理共享数据的多线程并发问题的,它最多只是把共享数据在自己的线程内部拷贝了一个副本而已,针对共享数据的写操作还是会有并发问题的,线程的共享数据的拷贝也无法知道共享数据的最新的值。《阿里巴巴java开发手册》针对ThreadLocal中也有这样的描述:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-acHB6nmD-1585730826644)(http://image.domolife.cn/dev/xin/pic/image-20200401104026627.png)]

这句话怎么理解呢?以下面的代码作为示例:

在这里插入图片描述

避免某些需要考虑线程安全必须同步带来的性能损失这句话又该如何理解呢?以SimpleDateFormat为例,我们大家都知道SimpleDateFormat是线程不安全的,在多线程并发的时候会出现问题

SimpleDateFormat多线程共享
  • 工具类
public class DateHelpUtils{
	private final static SimpleDateFormat SDFTIMES = new SimpleDateFormat(
			"yyyyMMddHHmmss");
	private final static SimpleDateFormat testSdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
	
	/**
	 * 得到n天之后的日期
	 *
	 * @param days
	 * @return
	 */
	public static String getAfterDayDate(String days) {
		int daysInt = Integer.parseInt(days);

		Calendar canlendar = Calendar.getInstance();
		canlendar.add(Calendar.DATE, daysInt);
		Date date = canlendar.getTime();

		//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateStr = testSdf.format(date);

		return dateStr;
	}

	/**
	 * 得到几天前的时间  yyyy-MM-dd HH:mm:ss
	 * @param day
	 * @return
	 */
	public static String getDateBeforeTime(int day) throws ParseException {
		Date date = new Date();
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.DAY_OF_MONTH, -day);
		date = calendar.getTime();
		//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String value = testSdf.format(date);
		Date date2 = testSdf.parse(value);
		return value;
	}
	
	/**
	 * 几个小时后
	 * @param hour
	 * @return
	 */
	public static String getTimeByHour(int hour) throws ParseException {

		Calendar calendar = Calendar.getInstance();
		calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
		String date = testSdf.format(calendar.getTime());
		Date date2 = testSdf.parse(date);
		return date;
	}
}
  • 多线程运行
@Test
    public void testSimpleDateFormatThreadProblem() {
        int i = 0;
        int j = 0;
        int k = 0;
        while (i++ < 50) {
            System.out.println("i执行:" + i);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    DateHelpUtils.getAfterDayDate(new Random().nextInt(1000) + "");
                }
            }).start();
        }

        while (j++ < 50) {
            System.out.println("j执行:" + j);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        DateHelpUtils.getDateBeforeTime(new Random().nextInt(1000));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }

        while (k++ < 50) {
            System.out.println("k执行:" + k);
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        DateHelpUtils.getTimeByHour(new Random().nextInt(1000));
                    } catch (ParseException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
        }
    }
  • 运行结果:

在这里插入图片描述

在这里插入图片描述

通过加锁方式优化
/**
	 * 得到n天之后的日期
	 *
	 * @param days
	 * @return
	 */
	public synchronized static String getAfterDayDate(String days) {
		int daysInt = Integer.parseInt(days);

		Calendar canlendar = Calendar.getInstance();
		canlendar.add(Calendar.DATE, daysInt);
		Date date = canlendar.getTime();

		//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateStr = testSdf.format(date);

		return dateStr;
	}

	/**
	 * 得到几天前的时间  yyyy-MM-dd HH:mm:ss
	 * @param day
	 * @return
	 */
	public synchronized static String getDateBeforeTime(int day) throws ParseException {
		Date date = new Date();
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.DAY_OF_MONTH, -day);
		date = calendar.getTime();
		//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String value = testSdf.format(date);
		Date date2 = testSdf.parse(value);
		return value;
	}

	/**
	 * 几个小时后
	 * @param hour
	 * @return
	 */
	public synchronized static String getTimeByHour(int hour) throws ParseException {

		Calendar calendar = Calendar.getInstance();
		calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
		String date = testSdf.format(calendar.getTime());
		Date date2 = testSdf.parse(date);
		return date;
	}
通过ThreadLocal方式优化
private final static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){
		@Override
		protected SimpleDateFormat initialValue() {
			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		}
	};
	/**
	 * 得到n天之后的日期
	 *
	 * @param days
	 * @return
	 */
	public  static String getAfterDayDate(String days) {
		int daysInt = Integer.parseInt(days);

		Calendar canlendar = Calendar.getInstance();
		canlendar.add(Calendar.DATE, daysInt);
		Date date = canlendar.getTime();

		//SimpleDateFormat sdfd = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String dateStr = sdf.get().format(date);

		return dateStr;
	}

	/**
	 * 得到几天前的时间  yyyy-MM-dd HH:mm:ss
	 * @param day
	 * @return
	 */
	public  static String getDateBeforeTime(int day) throws ParseException {
		Date date = new Date();
		Calendar calendar = Calendar.getInstance();
		calendar.setTime(date);
		calendar.add(Calendar.DAY_OF_MONTH, -day);
		date = calendar.getTime();
		//SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		String value = sdf.get().format(date);
		Date date2 = sdf.get().parse(value);
		return value;
	}

	/**
	 * 几个小时后
	 *kai.wang
	 * @param hour
	 * @return
	 */
	public  static String getTimeByHour(int hour) throws ParseException {

		Calendar calendar = Calendar.getInstance();
		calendar.set(Calendar.HOUR_OF_DAY, calendar.get(Calendar.HOUR_OF_DAY) + hour);
		String date = sdf.get().format(calendar.getTime());
		Date date2 = sdf.get().parse(date);
		return date;
	}

通过ThreadLocal的方式进行优化后的代码也能实现线程安全(通过线程隔离的方式),能减少因为加锁所带来的性能损失。也许有人会有疑惑,通过ThreadLoal实现的方式,和我通过在方法中实例一个局部SimpleDateFormat变量有什么区别呢?区别就在于,一个线程里面可能会调用多个方法,每个方法中都会调用SimpleDateFormat变量,通过ThreadLocal这个线程里面中不同的方法可以共享同一个SimpleDateFormat变量,如果在方法中实例一个SimpleDateFormat局部变量,在一个线程中只调用一个方法效果是一样的,但是如果在一个线程中调用多个方法,那就要实例化多个SimpleDateFormat变量,最终目的也能达到,但是牺牲了性能和空间。
在这里插入图片描述

总的来说,ThreadLocal适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法间或类间共享的场景。

1.3 ThreadLocal在JDK中的官方注释

This class provides thread-local variables. These variables differ from
their normal counterparts in that each thread that accesses one (via its {@code get} or {@code set} method) has its own, independently initialized copy of the variable. {@code ThreadLocal} instances are typically private static fields in classes that wish to associate state with a thread (e.g., a user ID or Transaction ID).

Each thread holds an implicit reference to its copy of a thread-local variable as long as the thread is alive and the {@code ThreadLocal} instance is accessible; after a thread goes away, all of its copies of thread-local instances are subject to garbage collection (unless other references to these copies exist).

ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

2. ThreadLocal的原理

每个Thread都有一个ThreadLocal.ThreadLocalMap的受保护的对象

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

ThreadLocalMap虽然也叫Map,但是不像HashMap一样实现了Map接口,而是包含了一定数量的Entry对象,ThreadLocalMapHashMap一样是通过数组定位到Entry对象,但是和HashMap不同的是,ThreadLocalMap没有链表结构,如果发生哈希冲突了,那么就会以index++的方式往下找到一个可以存储Entry的槽位

## ThreadLocal的set方法
public void set(T value) {
        Thread t = Thread.currentThread();
  			/***
  				获取该线程绑定的ThreadLocalMap(即threadLocals),如果ThreadLocalMap在该线程
  				还未初始化,则初始化一个ThreadLocalMap并赋值threadLocals
  			***/
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

## ThreadLocakMap的初始化方法
   ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  					//创建指定长度的Entry数组,默认是16
            table = new Entry[INITIAL_CAPACITY];
  					/***
  						每个ThreadLocal在初始化的时候都会分配一个HashCode,这个hashCode是通过一个从0开始
  						自增的整型每次递增0x61c88647实现的
  						无论hashCode怎么实现,最终的目的都是希望能尽量均匀的分布对象,减少哈希冲突
  					***/
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
  					//当数量到达Threshold规定的长度时,扩容并reHash
            setThreshold(INITIAL_CAPACITY);
        }

## ThreadLocalMap的Entry
     static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }
2.1 ThreadLocal的结构图

在这里插入图片描述

用法
public void test() {
  ThreadLocal<String> threadLocal1 = new ThreadLocal<String>();
  ThreadLocal<String> threadLocal2 = new ThreadLocal<String>();
  
  threadLocal1.set("threadLocal1");
  threadLocal2.set("threadLocal2");
  
  Stirng value1 = threadLocal1.get();
  String value2 = threadLocal2.get();
}

以上的代码是ThreadLocal的最简单的用法,通过ThreadLocal对象存储一个字符串,然后又通过该ThreadLocal对象获取到字符串。它和我们之前看到的key-value的用法不同的是,它以ThreadLocal对象调用set方法,然后以调用对象本身作为key,如果要在同个线程中存储多个,那么就要实例化多个ThreadLocal对象,如原理图所示。ThreadLocal对象在Entry中是通过一个弱引用指向它的,至于为什么要使用弱引用,我们会在下面讲解到。

2.2 initialValue()方法

在ThreadLocal对象没有调用set方法然后直接调用get()方法会直接返回一个null值,那是因为在ThreadLocal的get方法中,如果发现当前的ThreadLocalMap为空,会调用setInitialValueprivate方法,该方法会调用initialValueprotected方法,然后通过initialValue获取到的值初始化map,并返回initialValue的值,由于initialValue方法默认的返回值是null,所以针对一个直接调用get的threadLocal会返回null。由于setInitialValue是private方法,我们可以在初始化ThreadLocal对象的时候通过覆盖initialValue方法设置初始值,这样在直接调用get方法的时候,如果当前ThreadLocal对象的map为空,就会使用initialValue返回值

private final static ThreadLocal<SimpleDateFormat> sdf = new ThreadLocal<SimpleDateFormat>(){
		@Override
		protected SimpleDateFormat initialValue() {
			return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
		}
	};

3. ThreadLocal的key为什么要使用弱引用以及ThreadLocal的内存溢出问题

由以上的内容可知,ThreadLocal对象在调用set方法的时候会把自己作为Entry的key,而Entry的key是作为弱引用存在的,至于什么是弱引用,可以参考下图:

在这里插入图片描述

对于一个对象来说,我们平时的new方法都是在栈中新建一个对堆中对象的一个强引用,该对象被回收的前提是,GC Root不可达,即没有强引用指向该对象。如果一个对象仅仅(注意这里的仅仅)被弱引用指向,那么在下一次垃圾回收的时候,该对象必定会被回收,弱引用可作为配置文件的初始化的时候使用(个人见解)

在这里插入图片描述

行了,说了那么多关于弱引用的,大家对弱引用应该有了一个基本的了解了。要理解ThreadLocalMap的Entry为什么要使用弱引用,我们来想这个问题,如果ThreadLocalMap的Entry不使用弱引用做key会怎样

3.1 如果ThreadLocalMap的Entry不使用弱引用做key?

ThreadLocalMapprotected类型的,也就是说我们无法在其包外访问ThreadLocalMap,针对ThreadLocal,其开放出来的方法其实是很少的,如果我们无法访问ThreadLoclMap,其里面的Entry更是无从访问,所以一旦下面的代码执行,那么该Entry是无法访问到的,而作为key的ThreadLocal对象也是无法被回收的

public void test() {
  //new ThreadLocal对象有一个强引用threadLocal1
  ThreadLocal<String> threadLocal = new ThreadLocal<String>();
  //假设threadLocal中的ThreadLocalMap用的是强引用,那么通过set方法,就会有另外个强引用指向new ThreadLocal对象。也就是说现在在栈中有2个强引用指向堆中的ThreadLocal对象
  threadLocal.set("threadLocal");
  //把外面的强引用置为null,即上面设置的Entry已经没有入口可以访问,但是堆中的ThreadLocal对象无法被回收,因为在Entry内部还有个强引用指向(假设key用的是强引用),那么时间久了之后(假设没手动调用remove方法),势必会造成内存溢出
  threadLocal = null;
}

按照上面的结论,Entry用的是ThreadLocal的弱引用,那在外面的ThreadLocal对象不可用的时候,ThreadLocal对象只有一个弱引用指向,那么就会在下次垃圾回收的时候被虚拟机回收。

所以通过上面的反例我们就能更好地理解了为什么ThreadLocal的Entry要用弱引用做key的原因了

3.2 关于内存溢出问题

由上面内容可知,我们已经通过弱引用解决了Entry的key的内存溢出问题,那么问题来了,Entry的value是强引用,Entry的key被回收后,该Entry已经是没用了,但是Entry的value是存在的,该value无法回收,Entry对象锁占用的内存也是无法回收的,而Entry的value因为对我们来说是不可见,自然就无法回收,所以就存在了内存溢出问题。说到这里,可能有人会问,为什么不把Entry的value也设置为弱引用呢?个人觉得可能value是Object类型的原因吧(这点没有去验证过,有知道的大佬可以评论中知道下,谢谢)

如何解决内存溢出问题呢?
  • ThreadLocal的set方法和get方法都有通过一定的方式来清理key为null但是entry为不为null的Entry对象,这个在下面说
  • 手动调用ThreadLocal对象的remove方法(强烈推荐的final块中调用)

4. ThreadLocal的set方法和get方法都是怎么处理的

ThreadLocal的set方法和get方法我是在看了半天的源码才基本理解

  • set()

    1. 通过hashcode定位到ThreadLocal对象在ThreadLocalMap中的下标
    2. 如果该下标所在的key和要赋值的key一样,直接替换value
    3. 如果该下标所在的Entry不存在,则直接新建一个Entry
    4. 如果该下标所在的key和赋值的key不一样,则一直往下寻找一个适合的槽位
    5. 在寻找的过程中顺带清理一批key为null但是entry不为null的对象,方便虚拟机下次回收
     /**
             * Set the value associated with key.
             *
             * @param key the thread local object
             * @param value the value to be set
             */
            private void set(ThreadLocal<?> key, Object value) {
    
                // We don't use a fast path as with get() because it is at
                // least as common to use set() to create new entries as
                // it is to replace existing ones, in which case, a fast
                // path would fail more often than not.
    
                Entry[] tab = table;
                int len = tab.length;
                int i = key.threadLocalHashCode & (len-1);
    
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
    
                    if (k == key) {
                        e.value = value;
                        return;
                    }
    
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
    
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    
  • get

    1. 通过hashcode定位到ThreadLocal对象在ThreadLocalMap中的下标
    2. 如果该下标所在的位置上有值,且该位置上的key和要查找的key一样,则返回该值
    3. 否则依次往下寻找key相同的
    4. 在寻找的过程中顺带清理一批key为null但是entry不为null的对象,方便虚拟机下次回收
set方法在哈希冲突时的简单流程图

在这里插入图片描述

由以上可知,ThreadLocalMap在哈希冲突严重的情况下的执行效率是很低的,因为每次都要遍历整个map

5. ThreadLocal实践

在使用了ThreadLocal之后,下次再调用set()get(),那么就有可能会清除ThreadLocalMap中key为null的Entry对象,这样对应的value就没有GC Roots可达了,下次GC的时候就可以被回收,但是如果没有再调用set()get()方法,或者说他们没完全清除可被回收对象,那么内存溢出的风险就很大,最后的做法是每次用完ThreadLocal之后手动remove

private final static ThreadLocal<String> threadLocal = new ThreadLocal<String>();
public void test() {
  try {
    threadLocal.set("threadLocal");
  } finally{
    threadLocal.remove();
  }
}
6. 参考文章
  • https://www.cnblogs.com/fengzheng/p/8690253.html
  • http://www.jasongj.com/java/threadlocal/
  • https://mp.weixin.qq.com/s/SysYihctu03RlUtI0pcG7w
  • https://mp.weixin.qq.com/s/1ccG1R3ccP0_5A7A5zCdzQ
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值