ThreadLocal的使用规则和底层源码解析,以及造成OOM的原因和解决方案

ThreadLocal

此类提供线程局部变量。这些变量不同于普通的对应变量,因为每个访问一个(通过其get或set方法)的线程都有自己独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态(private static (在java当中可以理解static就是全局的意思,只不过static是同类所有对象共享,而ThreadLocal是线程全局共享))字段,它们希望将状态与线程相关联(例如,用户ID或事务ID)。

典型场景
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。

例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。

在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。

比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。

本质上,ThreadLocal是通过空间来换取时间,从而实现每个线程当中都会有一个变量的副本,这样每个线程都会操作该副本这样每个线程都会操作该副本,从而完全规避了多线程的并发问题

 import java.util.concurrent.atomic.AtomicInteger;
  
   public class ThreadId {
    
    
       // Atomic integer containing the next thread ID to be assigned
       private static final AtomicInteger nextId = new AtomicInteger(0);
  
       // Thread local variable containing each thread's ID
       //确保当前类的所有实例都能访问到静态的Thread的变量
       private static final ThreadLocal<Integer> threadId =
           new ThreadLocal<Integer>() {
    
    
               @Override protected Integer initialValue() {
    
    
                   return nextId.getAndIncrement();
           }
       };
  
       // Returns the current thread's unique ID, assigning it if necessary
       public static int get() {
    
    
           return threadId.get();
       }
   }

构造方法

    /**
         构建一个最初包含(firstKey,firstValue)的新映射。ThreadLocalMaps是延迟构造的,
         因此只有在至少有一个条目要放入时才创建一个。
         */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
    
    
        	// 创建一个初始化容量为16的的Entry
            table = new Entry[INITIAL_CAPACITY];
            //通过当前线程的ThreadLocal生成的hashCode 和容量生成索引值 跟hashMap生成作用差不多
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //赋值给Entry[] 数组
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

ThreadLocalMap

ThreadLocalMap是一个定制的哈希映射,只适合维护线程本地值。在ThreadLocal类之外不导出任何操作(为了辅助ThreadLocal存储数据,数据存储在ThreadLocalMap的entry中可以理解为(ThreadLocalMap做key,存储数据为value))。该类是包私有的,以允许在类线程中声明字段。为了帮助处理非常大和长期的使用,哈希表条目使用weakreference作为键。但是,由于不使用引用队列,因此只有当表空间开始不足时,才能保证删除过时的条目。
在这里插入图片描述


        /**
        改静态内部类主要是用来组装和存储当前线程的数据 ThreadLocal 做key 变量做value
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
    
    
            /** The value associated with this ThreadLocal. */
            //与此ThreadLocal关联的值。
            Object value;

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

        /**
         * 初始容量——必须是2的幂次方。
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         *  底层Entry 数组用来存储 各个线程以及各个线程的变量副本
         * 由于每个线程都对应管理的ThreadLocal 和值
         *  */
        private Entry[] table;

        /**
         * 表中的条目数。
         */
        private int size = 0;

        /**
         * 要调整大小的下一个大小值。
         */
        private int threshold; // Default to 0

我们要知道 private Entry[] table;该成员变量 存储各个线程的Entry对象 即 线程的ThreadLocal 和对应副本 的集合数组
示例


/*
    ThreadLocal

    本质上,ThreadLocal是通过空间来换取时间,从而实现每个线程当中都会有一个变量的副本,这样每个线程都会操作该副本
    这样每个线程都会操作该副本,从而完全规避了多线程的并发问题


    Thread 和ThreadLocal 是通过 ThreadLocalMap 来进行关联交互的

    java中存在四种类型的引用:

    1. 强引用(strong) 对象被强引用 garbage collector是如论如何都不会回收该对象
    2. 软引用(soft)   可用内存不足 才会回收 前提强引用不指向该对象
    3. 弱引用(weak)    下一次scavenge GC 对象才会被回收 前提强引用不指向该对象
    4. 虚引用(phantom) 当GC时 虚引用的对象 后收到通知 准备后续的一些处理

    上述除了 强引用时直接new 其他的都是需要继承Reference 并将一些方法实现
 */

public class MyTest3 {
    
    

    public static void main(String[] args) {
    
    
        ThreadLocal<String>  threadLocal = new ThreadLocal<>();
        threadLocal.set(" hello world");
        System.out.println(threadLocal.get());
    }
}


ThreadLocal.set

ThreadLocal的使用非常简单我们来看ThreadLocal的Set方法

 	 /**
		将此线程局部变量的当前线程副本设置为指定值。大多数子类不需要重写这个方法,
		只依赖initialValue方法来设置线程局部变量的值。
		
		参数:
		value–要存储在此线程本地的当前线程副本中的值。
     */
    public void set(T value) {
    
    
    	//获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的对应的ThreadLocalMap  副本数据
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else //没有就创建 并将其存入ThreadLocalMap中的Entry[]数组中
            createMap(t, value);
    }

在这里插入图片描述
实例化对象 具体查看构造方法内容

ThreadLocalMap.getMap

    /**
     *获取与ThreadLocal关联的映射。
     * 在InheritableThreadLocal中重写。
     *
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
    
    
        return t.threadLocals;
    }
    

在这我们可以看到每个线程对应都有一个ThreadLocalMap 副本数据 由此可以看出每个线程都是互相隔离的并不存在并发问题
在这里插入图片描述

 /**
    创建与ThreadLocal关联的映射。在InheritableThreadLocal中重写。

	参数:
		t–当前线程
		firstValue–地图初始条目的值
     */
    void createMap(Thread t, T firstValue) {
    
    
    	//
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

由ThreadLocal.createMap 该方法我们可以看出 。Thread 和ThreadLocal 是通过 ThreadLocalMap 来进行关联交互的将当前存储了ThreadLocalMap放置到当前线程对象当中

ThreadLocal.get

   /**
     返回此线程局部变量的当前线程副本中的值。如果变量对于当前线程没有值,
     则首先将其初始化为调用initialValue方法返回的值。
     *
     * @return the current thread's value of this thread-local
     */
    public T get() {
    
    
        Thread t = Thread.currentThread();
        //获取ThreadLocalMap 
        ThreadLocalMap map = getMap(t);
        if (map != null) {
    
    
        //拿到当前线程的副本数据
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
    
    
                @SuppressWarnings("unchecked")
                T result = (T)e.value;//返回副本数据
                return result;
            }
        }
        //返回 初始值null 
        return setInitialValue();
    }

ThreadLocalMap.getEntry

  private Entry getEntry(ThreadLocal<?> key) {
    
    
  			//threadLocalHashCode 和存储 副本数组的索引 来推断出当前线程的下标
            int i = key.threadLocalHashCode & (table.length - 1);
            //拿到当前线程的 Entry 数据
            Entry e = table[i];
            if (e != null && e.get() == key)
                return e; //返回当前线程对应的副本数据
            else
            	//找不到使用getEntryAfterMiss 方法 还找不到就返回null
                return getEntryAfterMiss(key, i, e);
        }

由上我们可以大致知道 ThreadLocal的布局
在这里插入图片描述

由图我们可以看出来 Entry引用当前线程的ThreadLocal ,他们之间的关系是弱引用的关系

**1、为什么 Entry extends WeakReference?Entry引用当前线程的ThreadLocal ,他们之间是弱引用的关系? **

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

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

假设Entry引用当前线程的ThreadLocal是强引用关系
在这里插入图片描述
当我们当前线程在方法中销毁对ThreadLocal的引用,本应该销毁当前线程的ThreadLocal然而由于 Entry数组中始终对当前线程的ThreadLocal牢牢强引用的关系;GC无法销毁当前的ThreadLocal 此时就会导致Entry中的key value对 永远无法释放 导致内存泄,当我们使用弱引用时下一次scavenge GC 对象就会被回收。
在这里插入图片描述
此时又会存在一个问题由于弱引用的关系当K被GC回收变成null 此时V还存在就会导致当前V始终存在并且无法被获取,此时该Entry[] 数组就存在很对key为null 的键值对 此时还是会造成内存泄露;ThreadLocal如果解决的? 答案是当ThreadLocal在get,set和remove的时候会去检查Entry数组中是否有ley为null的键值对 如果有就会删除

在这里插入图片描述

在这里插入图片描述

ThreadLocal使用规范

我们在日常开发的使用规范应该遵循如下:

    public class Test{
    
    
        private static final ThreadLocal<String> t1 = new ThreadLocal();

        public void Test(){
    
    
	        try{
    
    
	        .....
	        .....
	         }finally{
    
    
			/*
			删除对t1堆的引用和清除ThreadLocalMap中Entry[] 数组中 key为null的键值对。
			 
			 如果不调用极有可能造成内存泄漏 重点
			*/
	       	 t1.remove(); 
	         }
	     }
    }




猜你喜欢

转载自blog.csdn.net/qq_42261668/article/details/110749095