ThreadLocal原理以及用法详解

学习本篇文章要求掌握多线程知识,否则学起来相当麻烦!!!

一、什么是ThreadLocal?

ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。

同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。
ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。

二、ThreadLocal与Synchronized的区别?

ThreadLocal和Synchonized都用于解决多线程并发访问。

Synchronized是利用锁的机制,使变量或代码块在某一时该只能被一个线程访问。而ThreadLocal为每一个线程都提供了变量的副本,使得每个线程在某一时间访问到的并不是同一个对象,这样就隔离了多个线程对数据的数据共享。

三、什么场景用ThreadLocal?

这里有一点需要屡明白,本身多线程之间就不存在变量共享,这里说的多线程不是手动创建的多个线程,而是用户多线程,就好比你访问两次接口,第二次访问的能访问到第一次的对象吗,是不能的,除非是static修饰的类变量。

比如有个方法,都有new Object(),然后进来了两条线程甚至更多,线程之间创建的对象,对象变量指向了堆当中不同位置的对象,在jvm层面来说的话,变量是存于栈当中,而实际的对象是存储在堆内存当中。

栈内存归属于单个线程,每个线程都会有一个栈内存,其存储的变量只能在其所属线程中可见,即栈内存可以理解成线程的私有内存,而堆内存中的对象对所有线程可见,堆内存中的对象可以被所有线程访问。

本身变量就不存在干扰,那什么时候用ThreadLocal呢?

针对于此主要列了以下使用场景:

(1)、在方法里直接new和使用ThreadLocal变量的生命周期是不一样的,new 的话假如对象存在引用,或者线程执行时间长都会导致对象长期存于堆中无法回收,而ThreadLocal是随着这个线程生命周期的,线程销毁,变量自然就不存在了,或者可以通过remove删除。
(2)、涉及到值传递的时候可以使用,例如:嵌套方法,我从a类执行到了e类,调到了f类得 d方法
d方法内我想拿到a类里边某个方法内得值,怎么办,一直传递过去吗,这时候可以考虑用static的ThreadLocal存值之后,直接取。
(3)、多线程情况下,不希望变量混淆,每个线程具有独立的线程可以使用。

下面我列了几个实际的应用场景,其中包括了源码上的,还有项目当中用到的:

1、Spring源码用到了

Spring采用Threadlocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。

Spring框架里面就是用的ThreadLocal来实现这种隔离,主要是在TransactionSynchronizationManager这个类里面,代码如下所示:

private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

	private static final ThreadLocal<Map<Object, Object>> resources =
			new NamedThreadLocal<>("Transactional resources");

	private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
			new NamedThreadLocal<>("Transaction synchronizations");

	private static final ThreadLocal<String> currentTransactionName =
			new NamedThreadLocal<>("Current transaction name");

  ……

2、项目全局添加ip用到了

需求背景:只要是操作数据就需要在表中存在操作者的ip

做的时候是在拦截器做的,用的ThreadLocal变量,也就是请求一过来只要登录校验成功,就向ThreadLocal存入该线程ip,ThreadLocal用的static修饰的,这样就可以在任意方法当中直接取ip,不涉及到传值等问题。然后在拦截器afterCompletion(在整个请求结束之后被调用)方法配置remove。

3、SimpleDateFormat线程安全问题

在这里插入图片描述
在这里插入图片描述
正常情况下看上述代码基本上也看不出来什么端倪,但是并发场景下就会出现报错等一系列问题,SimpleDateFormat不是线程安全的。

SimpleDataFormat的parse()方法,内部有一个Calendar对象,调用SimpleDataFormat的parse()方法会先调用Calendar.clear(),然后调用Calendar.add(),如果一个线程先调用了add()然后另一个线程又调用了clear(),这时候parse()方法解析的时间就不对了。

其实要解决这个问题很简单,让每个线程都new 一个自己的 SimpleDataFormat就好了,但是1000个线程难道new1000个SimpleDataFormat?

所以当时我们使用了线程池加上ThreadLocal包装SimpleDataFormat,再调用initialValue让每个线程有一个SimpleDataFormat的副本,从而解决了线程安全的问题,也提高了性能。

四、ThreadLocal用法

其实使用真的很简单,线程进来之后初始化一个可以泛型的ThreadLocal对象,之后这个线程只要在remove之前去get,都能拿到之前set的值,注意这里我说的是remove之前。

//创建
private ThreadLocal threadLocal = new ThreadLocal();
//一旦创建了ThreadLocal,就可以使用它的set()方法设置要存储在其中的值。
threadLocal.set("A thread local value");
//获取值
String threadLocalValue = (String) threadLocal.get();
//移除一个值
threadLocal.remove();

项目当中很多都是这么用的

public class UserContext {
    
    
	private static final ThreadLocal<UserInfo> userInfoLocal = new ThreadLocal<UserInfo>();

	public static UserInfo getUserInfo() {
    
    
		return userInfoLocal.get();
	}

	public static void setUserInfo(UserInfo userInfo) {
    
    
		userInfoLocal.set(userInfo);
	}

	public static void clear() {
    
    
		userInfoLocal.remove();
	}
}

五、ThreadLocal原理

ThreadLocalMap是ThreadLocal的内部类。

在Thread类当中有个ThreadLocalMap的属性,所以我们都说他是线程隔离的,因为他本身就是线程对象的属性而已。

ThreadLocal就是一个工具类,他是操作Thread对象当中ThreadLocalMap属性的工具类,实际上调用的set和get都是在对线程的map做操作。
在这里插入图片描述

1、set方法

set方法实际上就是操作的Thread 当中的那个map属性,假如不用ThreadLocal我们也可以自己去操作线程对象里面的map,无非就是比较麻烦,而ThreadLocal相当于封装好了。我们不用再去获取线程,然后调用线程里面的map了,程序讲究的是高内聚,低耦合,我只需要知道ThreadLocal里面有个set可以存值就可以了。

public void set(T value) {
    
    
	// 获取当前线程
    Thread t = Thread.currentThread();
    // 获取这个线程的map属性
    ThreadLocalMap map = getMap(t);
    // map不为空说明已经初始化过了
    if (map != null)
    	// ThreadLocal作为map的一个key
        map.set(this, value);
    else
    	// 初始化key
        createMap(t, value);
}

// 返回这个线程的属性ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    
    
    return t.threadLocals;
}

在Thread对象内,存了一个ThreadLocalMap。也就是ThreadLocalMap只是线程对象的一个属性,然后可以存在多个值,而ThreadLocal只是用来做为map的key存在。
在这里插入图片描述
而ThreadLocalMap其实是ThreadLocal的一个内部类。属性就是一个Entry数组。Entry就是一个key,value形式的一个对象。

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

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

     private static final int INITIAL_CAPACITY = 16;

     private Entry[] table;
}

接下来先看初始化map,也就是当线程对象当中的map属性为空,会进行创建。
说白了就是给Thread对象的ThreadLocalMap属性赋值。

void createMap(Thread t, T firstValue) {
    
    
	// 这里的this代表的就是ThreadLocal这个对象
    t.threadLocals = new ThreadLocalMap(this, firstValue);
}

新建这里会发现实际上他是将ThreadLocal这个对象作为了key,然后value作为Entry的value。

private final int threadLocalHashCode = nextHashCode();

private static AtomicInteger nextHashCode = new AtomicInteger();

// 目的:为了让哈希码能均匀的分布在2的N次方的数组里, 即 Entry[] table
private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    
    
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}
        
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
	// 将ThreadLocal当中的table引入这个数组
    table = new Entry[INITIAL_CAPACITY];
    // 调用nextHashCode实际上就是调用的AtomicInteger类当中的getAndAdd方法,计算出来数组下标
    int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
    // ThreadLocal对象作为key,也就意味着一个线程一个ThreadLocal变量只能存一个value值
    // 多个ThreadLocal变量,就可以存在多个Entry对象,因为key不一样。
    table[i] = new Entry(firstKey, firstValue);
    size = 1;
    setThreshold(INITIAL_CAPACITY);
}

看完初始化这时候开始看set方法,也就是map存在了,他是如何操作的。

对于hash冲突的,他是用的开放定址法来解决的。
如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。

private void set(ThreadLocal<?> key, Object value) {
    
    
   // map当中的数组
   Entry[] tab = table;
   int len = tab.length;
   int i = key.threadLocalHashCode & (len-1);
   // 如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
   for (Entry e = tab[i];
        e != null;
        e = tab[i = nextIndex(i, len)]) {
    
    
       ThreadLocal<?> k = e.get();
	   // key值一样,不管value有没有值直接覆盖
       if (k == key) {
    
    
           e.value = value;
           return;
       }
	   // 如果当前位置是空的,就初始化一个Entry对象放在位置i上
       if (k == null) {
    
    
           replaceStaleEntry(key, value, i);
           return;
       }
   }
   tab[i] = new Entry(key, value);
   int sz = ++size;
   if (!cleanSomeSlots(i, sz) && sz >= threshold)
       rehash();
}

// 如果下标不大于数组长度,让目前的数组下标+1
private static int nextIndex(int i, int len) {
    
    
    return ((i + 1 < len) ? i + 1 : 0);
}

2、get方法

public T get() {
    
    
    Thread t = Thread.currentThread();
    // 获取当前线程的map
    ThreadLocalMap map = getMap(t);
    if (map != null) {
    
    
    	// 根据ThreadLocal寻找Entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
    
    
            @SuppressWarnings("unchecked")
            T result = (T)e.value;
            return result;
        }
    }
    return setInitialValue();
}

3、remove方法

public void remove() {
    
    
     ThreadLocalMap m = getMap(Thread.currentThread());
     if (m != null)
         m.remove(this);
 }
     
private void remove(ThreadLocal<?> key) {
    
    
    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)]) {
    
    
        if (e.get() == key) {
    
    
        	// 实际上是调用的Reference的clear方法,就是标记为清除,gc的时候会直接在内存删掉
            e.clear();
            // 将map当中的引用直接置空为null
            expungeStaleEntry(i);
            return;
        }
    }
}

六、共享线程的ThreadLocal数据

使用InheritableThreadLocal可以实现多个线程访问ThreadLocal的值,我们在主线程中创建一个InheritableThreadLocal的实例,然后在子线程中得到这个InheritableThreadLocal实例设置的值。

public void test() {
    
        
	final ThreadLocal threadLocal = new InheritableThreadLocal();       
	threadLocal.set("没啥问题");    
	Thread t = new Thread() {
    
            
	    @Override        
	    public void run() {
    
                
	      System.out.println(threadLocal.get());    
	    }    
	  };          
	  t.start(); 
}  

InheritableThreadLocal和ThreadLocal是一样的,都是Thread对象的一个属性。
在这里插入图片描述
在Thread类当中有个init方法,其中包含了一段这个代码
在这里插入图片描述
如果父线程的inheritThreadLocals存在,那么我就把父线程的inheritThreadLocals给当前线程的inheritThreadLocals。

那么在什么情况下需要子线程可以获取父线程的threadLocal变量呢?

还挺多比如,子线程需要拿到存放在threadLocal变量中的用户登录信息,有的中间件需要把统一的id追踪到的整个调用链路记录下来。其实子线程使用父线程中的threadLocal方法由多种方式,比如创建线程时传入父线程中的变量,并将其复制到子线程中,或者在父线程中构造一个map作为参数传递给子线程,但是这些都改变了我们的使用习惯,所以在这些情况下InheritableThreadLocal就显得比较有用了

七、使用ThreadLocal需要注意的

1、弱引用

我们可以看下源码,看它哪里用了弱引用
在这里插入图片描述
弱引用:

只具有弱引用的对象拥有更短暂的生命周期,在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。
不过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

之所以key设置为弱引用是因为key才是真正的ThreadLocal对象,而我们一直说的map他是thread对象的一个属性,自然是随着线程生命周期。

2、内存泄漏

这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。设计成弱引用的目的是为了更好地对ThreadLocal进行回收。

3、内存泄漏如何避免

每次使用完ThreadLocal都调用它的remove()方法清除数据
尽可能不让他在线程存储值,避免使用线程池的时候值一直在线程对象存储。

八、总结

在这里插入图片描述
ThreadLocal的实现是这样的:每个Thread 维护一个 ThreadLocalMap 映射表,这个映射表的 key 是 ThreadLocal实例本身,value 是真正需要存储的 Object。

也就是说 ThreadLocal 本身并不存储值,它只是作为一个 key 来让线程从 ThreadLocalMap 获取 value。值得注意的是图中的虚线,表示 ThreadLocalMap 是使用 ThreadLocal 的弱引用作为 Key 的,弱引用的对象在 GC 时会被回收。

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的设计中已经考虑到这种情况,也加上了一些防护措施:在ThreadLocal的get(),set(),remove()的时候都会清除线程ThreadLocalMap里所有key为null的value。
当然做了这些也是无法避免的,毕竟他有的时候你不知道key什么时候会被回收,我们只能做的就是尽可能用完之后就remove掉。

猜你喜欢

转载自blog.csdn.net/weixin_43888891/article/details/119571259
今日推荐