小白都能看的懂的ThreadLocal详解

最近有小伙伴,想让我写篇博客,来总结下关于ThreadLocal的内容。ThreadLocal也是一个比较高频的面试知识点了吧,之前关于ThreadLocal的内容一直躺在我的印象笔记里,那么今天我就写篇博客讲解下ThreadLocal的基本原理。

概述

废话不多说,学习之前先要知道ThreadLocal是干啥的。能帮我们干什么?为什么平时会使用到ThreadLocal。

ThreadLocal和synchronized一样,都是用来解决线程安全问题的。只不过ThreadLocal和synchronized解决线程安全问题的解决方案不同:synchronized是通过牺牲时间解决线程安全问题,ThreadLocal是通过牺牲空间解决线程安全问题。

关于synchronized,想必大家都很清楚了,我就不赘述了。如果使用synchronized修饰,那么同一时刻只有一个线程去操作数据,此时肯定是线程安全的,因为只有一个线程操作。但是其他获取不到资源的线程就要阻塞挂起,直到别人释放资源,才能去获取资源操作数据。但是挂起线程或者唤醒线程都会使CPU会从用户态切换到内核态,比较浪费性能。

ThreadLocal基本用法

并且使用ThreadLocal保证线程安全,使用起来也很简单,如果想放到ThreadLocal一个int值,例子如下:

ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
threadLocal .set(1);//存储值到ThreadLocal
threadLocal .get();//从ThreadLocal获取值,返回1

那么他的原理是什么呢?
上面说到ThreadLocal是牺牲空间解决线程安全问题,那么他到底是怎么解决的呢?

ThreadLocal原理分析

其实,每个线程都对应了一个ThreadLocal,那么想一下我们如果将数据存储到ThreadLocal这个对象中,那么岂不是就解决了线程安全问题(因为每个线程都对应了自己的ThreadLocal,那么每个线程都操作自己的ThreadLocal不就不会产生线程安全问题了)。

其实真正存储数据的并不是ThreadLocaMap这个类,而是ThreadLocal的一个静态内部类ThreadLocalMap,这个东西类似一个Map,但是又有一些不一样,接下来会详细讲述。

上边就是ThreadLocal的基本原理了,那么到底具体是咋做的呢?想学会,看源码是必不可少的。come on!!

ThreadLocal最常用的方法就是

public T get();//获取ThreadLocal存储的数据,T是泛型,表示返回的结果类型
public void set(T value);//向ThreadLocal存储数据,其中T类型的value表示想要存储的数据

ThreadLocal的set()方法

首先,来看看ThreadLocal的set()操作,看看ThreadLocal是如何帮我们将数据存储在每个线程内的。
当我们家调用了set方法,会发生什么呢?set方法源码如下:

public void set(T value) {
	//获取当前线程
	Thread t = Thread.currentThread();
	//根据当前线程获取ThreadLocalMap
	//ThreadLocalMap是ThreadLocal的一个静态内部类,下边会讲,可以暂时理解成一个HashMap(是暂时哦~~)
	ThreadLocalMap map = getMap(t);
	if (map != null)
		//map不是null的话(已经初始化)就调用ThreadLocalMap的get()方法将这个value设置进去。
		//注意!!!!此时的值时value,键是this,this表示这个对象,也就是调用set方法的TheadLocal对象
	    map.set(this, value);
	else
		//如果为空,就去初始化一个map
	    createMap(t, value);
}

然后看上边源码第三行的getMap方法:

ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

可以看到返回一个ThreadLocalMap,返回的是一个Thread的变量threadLocals,那么近Thread类看看:
在这里插入图片描述
这也证明了我们上边的所说的原理。

不知道大家是不是被转晕了,总结一下:

  • 当我们调用ThreadLocal的set方法之后,会先根据当前的线程获取一个ThreadLocalMap对象(每个Thread都对应一个ThreadLocalMap);
  • 如果这个map不是空的话,就将数据存储到这个Map,其中key是这个ThreadLocal对象,value就是我们想要存储的值。

ThreadLocal的get()方法

如果理解了上边的set方法,那么get方法就是洒洒水的事情。
老规矩,上源码:

public T get() {
	//获取当前线程
   Thread t = Thread.currentThread();
   //根据当前线程获取ThreadLocalMap
	//ThreadLocalMap是ThreadLocal的一个静态内部类,下边会讲,可以暂时理解成一个HashMap(是暂时哦~~)
   ThreadLocalMap map = getMap(t);
   if (map != null) {
   		//调用ThreadLocalMap的get()方法获取数据
   		//注意此时get()方法内的参数是this,也就是当前调用get方法的ThreadLocal对象
       ThreadLocalMap.Entry e = map.getEntry(this);
       if (e != null) {
           @SuppressWarnings("unchecked")
           T result = (T)e.value;
           return result;
       }
   }
   return setInitialValue();
}

原理和set()方法是一样的:

  • 当我们调用ThreadLocal的get方法之后,会先根据当前的线程获取一个ThreadLocalMap对象(每个Thread都对应一个ThreadLocalMap);
  • 调用ThreadLocaMap的get方法根据当前的ThreaLocal对象获取数据。

总结

那么ThreadLocal的原理就讲完了,存数据的时候让当前ThreadLocal对象作为一个key存到ThreadLocalMap中,获取数据的时候再从ThreadLocalMap中根据当前的ThreadLocal对象获取数据。因为同一个线程只对应一个ThreadLocalMap,所以他是线程安全的。

那么ThreadLocalMap是何方神圣呢?

ThreadLocalMap原理

上面只是说了类似一个HashMap,又有点不同,那么是哪里不同呢?

类似于HashMap,ThreadLocalMap中存储数据的也是一个entry数组。其他地方都是类似的,只有一个地方需要注意,就是这个entry类继承了弱引用,废话不多说,直接上源码:

static class Entry extends WeakReference<ThreadLocal<?>> {
    Object value;
	//entry类的构造函数
    Entry(ThreadLocal<?> k, Object v) {
        super(k);
        value = v;
    }
}

补充:如果一个对象引用是一个弱引用,那么在发生GC的时候该弱引用引用的对象就会被回收掉。
如果一个引用是一个强引用,那么即使发生OOM也不会被回收。

此时Entry对象中的key是一个弱引用,那么为什么设置成一个弱引用呢,这就跟我们经常说的内存泄漏有着很大的关联了?

内存泄漏

试想一下,如果ThreadLocalMap中的ThreadLocal使用强引用,那么此时对于一个ThreadLocal对象就会存在两个强引用。由于ThreadLocalMap中还存在一个该对象的强引用,即使原来的对象引用失效的时候,这个ThreadLocal对象也不会被回收。

但是如果使用弱引用的话,如果ThreadLocal的对象引用失效了,此时发生GC,因为ThreadLocalMap中使用弱引用,所以个对象会被回收。

但是这产生了另一个问题,因为我们ThreadLocalMap中的数据是以ThreadLocal为key进行存储的,获取数据也是根据这个ThreadLocal获取,如果发生GC,这个ThreadLocal对象被回收了(value不是弱引用,不会被回收),那么这个value我们就获取不到了,产生了内存泄漏

这可如何是好呀?

  1. 很显然我们还没有达到开发JDK的程度,JDK也想到了这个问题,当我们调用set()或者get()方法的时候,会帮我们自动清除掉为null的键值对,此时就避免了内存泄漏的情况。

  2. 但是我们不可能总是调用set或者get方法吧,还可以在使用ThreadLocal完成之后,调用ThreadLocal的remove()方法,这个方法会将存在ThreadLocalMap的数据移除,这样就不会由于GC之后ThreadLocal被回收造成的内存泄漏问题了。

  3. 另外一个避免内存泄漏的方法就是将ThreadLocal用static修饰,设置成静态的,这样就不会被GC掉,一直可以通过ThreadLocal对象获取到map中的数据。

项目应用

我的分布式电商项目中,不是使用ThreadLocal来解决线程安全问题的,而是利用了存在ThreadLocal中的数据在一个线程执行的任意时刻都能获取到的特性。

我们的许多模块都需要登录才能操作,比如订单模块。像下单这种操作必须使用到当前登陆的用户信息,当人可以存储在session中,用的时候去获取。但是如果service层想要使用呢?存在session的数据必须在controller层就获取到,如果service想使用,必须每次用的时候在controller获取,然后再作为参数传到service层,这样显然太麻烦。

我再项目中写了一个拦截器,通过session判断用户登录信息,然后将用户信息存储在ThreadLoca中,下次在service中用的时候就可以使用ThreadLocal获取数据了,因为从拦截器->controller->service都是一个线程,可以获取到。

线程是使用线程池获取,所以很可能造成内存泄漏,那么如何解决内存泄漏呢?

我使用static进行修饰,这样ThreadLocal对象就永远不会被回收,并且在类中只存在一个对象。下个用户使用同一个线程的时候,就会覆盖掉原来的数据。这样我们的系统的并发有多少,那么内存中就会有多少个ThreadLocal对象,还是可以接受的。

你可能感兴趣的:(并发编程,多线程,并发编程,java)