多线程(七)ThreadLocal源码

12 篇文章 0 订阅

参考文章

1.简单聊一聊什么是ThreadLocal?以及它的大概用途 (第一节 0~8:13)

(1) 定义 (2)作用

ThreadLocal的作用是提供局部变量给线程内部使用。也就是说,它使用了一套机制保证:你new了一个变量threadLocal,在一个线程里,给threadLocal变量set一个别的线程无法访问使用的类型A的实例a,然后一段时间后,你可以从threadLocal变量中get出实例a,重点是这个threadLocal变量是可以跨线程的。同时,JDK建议你把这个threadLocal变量设置为static,因为他需要在多线程中保持全局唯一,他也有能力在全局唯一的情况下,在多线程中提供局部变量。

  • 总结:
  1. 线程并发: 在多线程并发的场景下
  2. 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
  3. 线程隔离: 每个线程的变量都是独立的,不会互相影响
  • 首先,ThreadLocal 不是用来解决共享对象的多线程访问问题的,一般情况下,通过ThreadLocal.set() 到线程中的对象是该线程自己使用的对象,其他线程是不需要访问的,也访问不到的。各个线程中访问的是不同的对象。
  • 另外,说ThreadLocal使得各线程能够保持各自独立的一个对象,并不是通过ThreadLocal.set()来实现的,而是通过每个线程中的new 对象 的操作来创建的对象,每个线程创建一个,不是什么对象的拷贝或副本。通过ThreadLocal.set()将这个新创建的对象的引用保存到各线程的自己的一个map中,每个线程都有这样一个map,执行ThreadLocal.get()时,各线程从自己的map中取出放进去的对象,因此取出来的是各自自己线程中的对象,ThreadLocal实例是作为map的key来使用的。
  • 如果ThreadLocal.set()进去的东西本来就是多个线程共享的同一个对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题。
  • 总之,ThreadLocal不是用来解决对象共享访问问题的,而主要是提供了保持对象的方法和避免参数传递的方便的对象访问方式。归纳了两点:
  1. 每个线程中都有一个自己的ThreadLocalMap类对象,可以将线程自己的对象保持到其中,各管各的,线程可以正确的访问到自己的对象。
  2. 将一个共用的ThreadLocal静态实例作为key,将不同对象的引用保存到不同线程的ThreadLocalMap中,然后在线程执行的各处通过这个静态ThreadLocal实例的get()方法取得自己线程保存的那个对象,避免了将这个对象作为参数传递的麻烦。

2.ThreadLocal的实现原理是什么?

(1)猜测原理(第一节 8:13~11:40) (2)实际情况(第一节 11:40~17:45) (3)设计优势是什么?(第一节 17:45~20:59)
在这里插入图片描述

这样设计的好处

(1) 这样设计之后每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。

(2) 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。

  1. 线程自己保留自己的数据
  2. 对key进行了优化,采用了弱引用

ThreadLocal的实现(第二节)

1.初步

  • threadLoaclHashCode
    //线程获取threadLocal.get()时,如果是第一次在某个 threadLocal对象上get时,会给当前线程分配一个value
	//这个value 和 当前的threadLocal对象 被包装成为一个 entry。其中 key是 threadLocal对象,value是threadLocal对象给当前线程生成的value
	//这个entry存放到,当前线程 threadLocals 这个map的哪个桶位? 与当前 threadLocal对象的threadLocalHashCode 有关系。
	// 使用 threadLocalHashCode & (table.length - 1) 得到的位置,就是当前 entry需要存放的位置。
	private final int threadLocalHashCode = nextHashCode();
  • nextHashCode
    /**
	 * The next hash code to be given out. Updated atomically. Starts at
	 * zero.
	 * 创建ThreadLocal对象时 会使用到,每创建一个threadLocal对象 就会使用nextHashCode 分配一个hash值给这个对象。
	 */
	private static AtomicInteger nextHashCode =
			new AtomicInteger();
  • HASH_INCREMENT
	/**
	 * The difference between successively generated hash codes - turns
	 * implicit sequential thread-local IDs into near-optimally spread
	 * multiplicative hash values for power-of-two-sized tables.
	 * 每创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode 这个值就会增长 0x61c88647 。
	 * 这个值 很特殊,它是 斐波那契数 ,也叫 黄金分割数。hash增量为 这个数字,带来的好处就是 hash分布非常均匀。
	 */
	private static final int HASH_INCREMENT = 0x61c88647;
  • nextHashCode()
	/**
	 * Returns the next hash code.
	 * 创建新的ThreadLocal对象时  会给当前对象分配一个hash,使用这个方法。
	 */
	private static int nextHashCode() {
		return nextHashCode.getAndAdd(HASH_INCREMENT);
	}
  • initialValue()
	/* 
	* 为这个线程局部变量返回当前线程的“初始值”。该方法将在线程第一次使用 get 
	* 方法访问变量时被调用,除非线程之前调用了 set 方法,在这种情况下是 initialValue  方法不会为线程调用。
	* 通常,每个线程最多调用此方法一次,但在后续调用 remove 后跟get的情况下可能会再次调用。
	* 这个实现只返回null 如果程序员希望线程局部变量具有null以外的初始值,则必须将ThreadLocal子类化,并重写此方法。通常,将使用匿名内部类。
	* @return 此线程本地的初始值
	* 默认返回null,一般情况下 咱们都是需要重写这个方法的。
	*/
	protected T initialValue() {
		return null;
	}

2.获取当前线程副本数据 get() 方法源码深入解析

  • get()方法
	/**
	 * Returns the value in the current thread's copy of this
	 * thread-local variable.  If the variable has no value for the
	 * current thread, it is first initialized to the value returned
	 * by an invocation of the {@link #initialValue} method.
	 *
	 *  @return the current thread's value of this thread-local
	 *
	 * 返回当前线程与当前ThreadLocal对象相关联的 线程局部变量,这个变量只有当前线程能访问到。
	 * 如果当前线程 没有分配,则给当前线程去分配(使用initialValue()方法)
	 */
	public T get() {
		//获取当前线程
		Thread t = Thread.currentThread();
		//获取到当前线程Thread对象的 threadLocals,即当前线程ThreadLocalMap对象的引用
		ThreadLocalMap map = getMap(t);
		//map!=null条件成立:说明当前线程已经拥有自己的 ThreadLocalMap 对象了
		if (map != null) {
			//key:(this)当前threadLocal对象
			//调用map.getEntry() 方法,获取threadLocalMap 中该threadLocal关联的 entry
			ThreadLocalMap.Entry e = map.getEntry(this);
			//条件成立:说明当前线程 初始化过 与当前threadLocal对象相关联的 线程局部变量
			if (e != null) {
				@SuppressWarnings("unchecked")
				T result = (T)e.value;
				//返回value
				return result;
			}
		}

		//执行到这里有几种情况?
		//1.当前线程对应的threadLocalMap为null
		//2.当前线程与当前threadLocal对象没有生成过相关联的 线程局部变量

		//setInitialValue()方法,初始化当前线程与当前threadLocal对象 相关联的value。
		//且 当前线程如果没有threadLocalMap的话,还会初始化创建map。
		return setInitialValue();
	}
  • initialValue() 初始化线程副本变量
   /*
    * 默认返回null,一般情况下 咱们都是需要重写这个方法的。
	*/
	protected T initialValue() {
		return null;
	}
  • setInitialValue() 设置线程副本变量
	/**
	 * Variant of set() to establish initialValue. Used instead
	 * of set() in case user has overridden the set() method.
	 *
	 * setInitialValue()方法,初始化当前线程与当前threadLocal对象 相关联的value。
	 * 且 当前线程如果没有threadLocalMap的话,还会初始化创建map。
	 * @return the initial value
	 */
	private T setInitialValue() {
		//调用的当前ThreadLocal对象的initialValue方法,这个方法大部分情况下咱们都会重写,返回初始化value的值。
		//value 就是当前ThreadLocal对象与当前线程相关联的 线程局部变量。
		T value = initialValue();
		//获取当前线程对象
		Thread t = Thread.currentThread();
		//获取当前线程内部的threadLocals,即threadLocalMap对象。
		ThreadLocalMap map = getMap(t);
		//条件成立:说明当前线程内部已经初始化过 threadLocalMap对象了。 (各自线程的threadLocals 只会初始化一次。)
		if (map != null) {
			//保存 当前threadLocal 与 当前线程生成的线程局部变量。
			//key: 当前threadLocal对象   value:线程与当前threadLocal相关的局部变量
			map.set(this, value);
		} else {
			//执行到这里,说明当前线程内部还未初始化 threadLocalMap ,这里调用createMap 给当前线程创建map

			//参数1:当前线程   参数2:线程与当前threadLocal相关的局部变量
			createMap(t, value);
		}

		//返回线程与当前threadLocal相关的局部变量
		return value;
	}
  • createMap() 初始化线程ThreadLocalMap变量
	void createMap(Thread t, T firstValue) {
		//传递t 的意义就是:要访问 当前这个线程 t.threadLocals 字段,给这个字段初始化。

		//new ThreadLocalMap(this, firstValue)
		//创建一个ThreadLocalMap对象,初始 k-v 为: this <当前threadLocal对象> ,线程与当前threadLocal相关的局部变量
		t.threadLocals = new ThreadLocalMap(this, firstValue);
	}

流程图

在这里插入图片描述

3.修改当前线程副本数据 set() 方法源码深入解析

	/**
	 * Sets the current thread's copy of this thread-local variable
	 * to the specified value.  Most subclasses will have no need to
	 * override this method, relying solely on the {@link #initialValue}
	 * method to set the values of thread-locals.
	 *
	 * @param value the value to be stored in the current thread's copy of
	 *        this thread-local.
	 *
	 * 修改当前线程与当前threadLocal对象相关联的 线程局部变量。
	 */
	public void set(T value) {
		//获取当前线程
		Thread t = Thread.currentThread();
		//获取当前线程的threadLocalMap对象
		ThreadLocalMap map = getMap(t);
		//条件成立:说明当前线程的threadLocalMap已经初始化过了
		if (map != null) {
			//调用threadLocalMap.set方法 进行重写 或者 添加。
			map.set(this, value);
		} else {
			//执行到这里,说明当前线程还未创建 threadLocalMap对象。

			//参数1:当前线程   参数2:线程与当前threadLocal相关的局部变量
			createMap(t, value);
		}
	}

4.删除当前线程副本数据 remove() 方法源码深入解析

	/**
	 * Removes the current thread's value for this thread-local
	 * variable.  If this thread-local variable is subsequently
	 * {@linkplain #get read} by the current thread, its value will be
	 * reinitialized by invoking its {@link #initialValue} method,
	 * unless its value is {@linkplain #set set} by the current thread
	 * in the interim.  This may result in multiple invocations of the
	 * {@code initialValue} method in the current thread.
	 * 
	 * @since 1.5
	 * 
	 * 移除当前线程与当前threadLocal对象相关联的 线程局部变量。
	 */
	public void remove() {
		//获取当前线程的 threadLocalMap对象
		ThreadLocalMap m = getMap(Thread.currentThread());
		//条件成立:说明当前线程已经初始化过 threadLocalMap对象了
		if (m != null)
			//调用threadLocalMap.remove( key = 当前threadLocal)
			m.remove(this);
	}

内核分析:(第三节)

  • ThreadLocal的核心其实是ThreadLocalMap对象

1)静态内部类 Entry 分析(0~12:58)

弱引用

在这里插入图片描述

内部类Entry

/**
		 * The entries in this hash map extend WeakReference, using
		 * its main ref field as the key (which is always a
		 * ThreadLocal object).  Note that null keys (i.e. entry.get()
		 * == null) mean that the key is no longer referenced, so the
		 * entry can be expunged from table.  Such entries are referred to
		 * as "stale entries" in the code that follows.
		 *
		 * 什么是弱引用呢?
		 * A a = new A();     //强引用
		 * WeakReference weakA = new WeakReference(a);  //弱引用
		 *
		 * 强引用  a = null; 时
		 * 下一次GC时,对象a就被回收了,不管是否还有 弱引用 关联在这个对象上。
		 *
		 * key 使用的是弱引用保留,key保存的是threadLocal对象。
		 * value 使用的是强引用,value保存的是 threadLocal对象与当前线程相关联的 value。
		 *
		 * entry的key 这样设计有什么好处呢?
		 * 当threadLocal对象失去强引用且对象被GC回收后,散列表中与 threadLocal对象相关联的 entry的key 就为null
		 * 再次去key.get() 时,拿到的是null。
		 * 站在map角度就可以区分出哪些entry是过期的,哪些entry是非过期的。
		 */
		static class Entry extends WeakReference<ThreadLocal<?>> {
			/** The value associated with this ThreadLocal. */
			Object value;

			Entry(ThreadLocal<?> k, Object v) {
				//通过 super(k); 调用了WeakReference的构造方法,实现弱引用
				super(k);
				value = v;
			}
		}

2)ThreadLocalMap 字段分析/简单方法setThreadshold()、nextIndex()、prevIndex()(12:58~22:50)

  • 散列表数组的初始长度
		/**
		 * The initial capacity -- MUST be a power of two.
		 * 初始化当前map内部 散列表数组的初始长度 16
		 */
		private static final int INITIAL_CAPACITY = 16;
  • threadLocalMap 内部散列表数组引用
		/**
		 * The table, resized as necessary.
		 * table.length MUST always be a power of two.
		 * threadLocalMap 内部散列表数组引用,数组的长度 必须是 2的次方数
		 */
		private Entry[] table;
  • 散列表数组占用情况
		/**
		 * The number of entries in the table.
		 * 当前散列表数组 占用情况,存放多少个entry。
		 */
		private int size = 0;
  • 扩容触发阈值
		/**
		 * The next size value at which to resize.
		 * 扩容触发阈值,初始值为: len * 2/3
		 * 触发后调用 rehash() 方法。
		 * rehash() 方法先做一次全量检查全局 过期数据,把散列表中所有过期的entry移除。
		 * 如果移除之后 当前 散列表中的entry 个数仍然达到  threshold - threshold/4  就进行扩容。
		 */
		private int threshold; // Default to 0

setThreadshold()

		/**
		 * Set the resize threshold to maintain at worst a 2/3 load factor.
		 * 将阈值设置为 (当前数组长度 * 2)/ 3。
		 */
		private void setThreshold(int len) {
			threshold = len * 2 / 3;
		}

nextIndex

		/**
		 * Increment i modulo len.
		 * 获取当前位置的下一个位置下标
		 * 参数1:当前下标
		 * 参数2:当前散列表数组长度
		 */
		private static int nextIndex(int i, int len) {
			//当前下标+1 小于散列表数组的话,返回 +1 后的值
			//否则 情况就是 下标+1 == len ,返回0
			//实际形成一个环绕式的访问。
			return ((i + 1 < len) ? i + 1 : 0);
		}

prevIndex

		/**
		 * Decrement i modulo len.
		 * 获取当前位置的上一个位置下标
		 * 参数1:当前下标
		 * 参数2:当前散列表数组长度
		 */
		private static int prevIndex(int i, int len) {
			//当前下标-1 大于等于0 返回 -1后的值就ok。
			//否则 说明 当前下标-1 == -1. 此时 返回散列表最大下标。
			//实际形成一个环绕式的访问。
			return ((i - 1 >= 0) ? i - 1 : len - 1);
		}

3)ThreadLocalMap 构造方法分析(22:50~30:00)

构造一个最初包含 (firstKey, firstValue) 的新映射。ThreadLocalMaps 是惰性构建的,因此我们只在至少有一个条目要放入时才创建

		/**
		 * Construct a new map initially containing (firstKey, firstValue).
		 * ThreadLocalMaps are constructed lazily, so we only create
		 * one when we have at least one entry to put in it.
		 *
		 * 因为Thread.threadLocals字段是延迟初始化的,只有线程第一次存储 threadLocal-value 时,才会创建 threadLocalMap对象。
		 *
		 * 参数firstKey : threadLocal对象
		 * 参数firstValue: 当前线程与threadLocal对象关联的value。
		 */
		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			//创建entry数组长度为16,表示threadLocalMap内部的散列表。
			table = new Entry[INITIAL_CAPACITY];
			
			//寻址算法:key.threadLocalHashCode & (table.length - 1)
			//table数组的长度一定是 2 的次方数。
			//2的次方数-1 有什么特征呢?  转化为2进制后都是1.    16==> 1 0000 - 1 => 1111
			//1111 与任何数值进行&运算后,得到的数值 一定是 <= 1111

			//i 计算出来的结果 一定是 <= B1111
			int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);

			//创建entry对象,存放到 指定位置的slot中。(数组中的每个单元,我们称为slot)
			table[i] = new Entry(firstKey, firstValue);
			//存放了一个元素,设置size=1
			size = 1;
			//设置扩容阈值:(当前数组长度 * 2)/ 3  => 16 * 2 / 3 => 10
			setThreshold(INITIAL_CAPACITY);
		}

从给定的父映射构造一个新映射,包括所有可继承的 ThreadLocals。仅由 createInheritedMap 调用。

		/**
		 * Construct a new map including all Inheritable ThreadLocals
		 * from given parent map. Called only by createInheritedMap.
		 *
		 * @param parentMap the map associated with parent thread.
		 */
		private ThreadLocalMap(ThreadLocalMap parentMap) {
			Entry[] parentTable = parentMap.table;
			int len = parentTable.length;
			setThreshold(len);
			table = new Entry[len];

			for (int j = 0; j < len; j++) {
				Entry e = parentTable[j];
				if (e != null) {
					@SuppressWarnings("unchecked")
					//获取父map中entry对象的key,即threadLocal对象
					ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
					if (key != null) {
						Object value = key.childValue(e.value);
						Entry c = new Entry(key, value);
						int h = key.threadLocalHashCode & (len - 1);
						while (table[h] != null)
							h = nextIndex(h, len);
						table[h] = c;
						size++;
					}
				}
			}
		}

4)ThreadLocalMap getEntry() 【快速查询】根据key获取Entry方法源码深入分析(30:00~38:20)

		/**
		 * Get the entry associated with key.  This method
		 * itself handles only the fast path: a direct hit of existing
		 * key. It otherwise relays to getEntryAfterMiss.  This is
		 * designed to maximize performance for direct hits, in part
		 * by making this method readily inlinable.
		 *
		 * @param  key the thread local object
		 * @return the entry associated with key, or null if no such
		 *
		 * ThreadLocal对象 get() 操作 实际上是由 ThreadLocalMap.getEntry() 代理完成的。
		 *
		 * key:某个 ThreadLocal对象,因为 散列表中存储的entry.key 类型是 ThreadLocal。
		 */
		private Entry getEntry(ThreadLocal<?> key) {
			//路由规则: ThreadLocal.threadLocalHashCode & (table.length - 1) ==> index
			int i = key.threadLocalHashCode & (table.length - 1);
			//访问散列表中 指定指定位置的 slot
			Entry e = table[i];

			//条件一:成立 说明slot有值
			//条件二:成立 说明 entry#key,与当前查询的key一致,返回当前entry 给上层就可以了。
			if (e != null && e.get() == key) {
				return e;
			} else {
				//有几种情况会执行到这里?
				//1.e == null
				//2.e.key != key

				//getEntryAfterMiss 方法,会继续向当前桶位后面继续搜索 e.key == key 的entry.
				//为什么这样做呢??
				//因为 存储时 发生hash冲突后,并没有在entry层面形成 链表。存储时的处理 就是线性的向后找到一个可以使用的slot,并且存放进去。(所以这里hash冲突的处理方法是线性探测法)
				return getEntryAfterMiss(key, i, e);
			}
		}

5)ThreadLocalMap getEntryAfterMiss() 快速查询失败后,继续向下探测查询方法源码深入分析(38:20~48:15)

		/**
		 * Version of getEntry method for use when key is not found in
		 * its direct hash slot.
		 *
		 * @param  key the thread local object            threadLocal对象,表示key
		 * @param  i the table index for key's hash code  key计算出来的index
		 * @param  e the entry at table[i]                table[index] 中的 entry
		 * @return the entry associated with key, or null if no such
		 */
		private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
			//获取当前threadLocalMap中的散列表 table
			Entry[] tab = table;
			//获取table长度
			int len = tab.length;

			//条件:e != null 说明 :向后查找的范围是有限的,碰到 slot == null 的情况,搜索结束。
			//e: 循环处理的当前元素
			while (e != null) {
				/* 获取当前slot 中entry对象的key,即threadLocal对象
				get()方法是Reference中的方法,
				e是Entry的实例对象,Entry继承WeakReference继承Reference */
				ThreadLocal<?> k = e.get();

				//条件成立:说明向后查询过程中找到合适的entry了,返回entry就ok了。
				if (k == key) {
					//找到的情况下,就从这里返回了。
					return e;
				}
				// 条件成立:说明当前slot中的entry#key 关联的 ThreadLocal对象已经被GC回收了。
				// 因为key 是弱引用, key = e.get() == null.
				if (k == null) {
					//做一次 探测式过期数据回收。
					expungeStaleEntry(i);
				}
				else {
					//获取下一个index,继续向后搜索。
					i = nextIndex(i, len);
				}
				//获取下一个slot中的entry。
				e = tab[i];
			}

			//执行到这里,说明关联区段内都没找到相应数据。
			return null;
		}

6)ThreadLocalMap expungeStaleEntry() 抹除过期数据段方法源码深入分析(48:15~1:05:36)

		/**
		 * Expunge a stale entry by rehashing any possibly colliding entries
		 * lying between staleSlot and the next null slot.  This also expunges
		 * any other stale entries encountered before the trailing null.  See
		 * Knuth, Section 6.4
		 *
		 * @param staleSlot index of slot known to have null key
		 * @return the index of the next null slot after staleSlot
		 * (all between staleSlot and this slot will have been checked
		 * for expunging).
		 *
		 * 参数 staleSlot:table[staleSlot] 就是一个过期数据(entry.key == null),以这个位置开始 继续向后查找过期数据,直到碰到 slot == null 的情况结束。
		 * 返回:staleSlot之后的下一个空槽的索引(所有在 staleSlot 和这个槽之间的都将被检查, 以进行清除)。
		 */
		private int expungeStaleEntry(int staleSlot) {
			//获取散列表
			Entry[] tab = table;
			//获取散列表当前长度
			int len = tab.length;

			// expunge entry at staleSlot
			// help gc (是过期数据,所以置为null)
			tab[staleSlot].value = null;
			//因为staleSlot位置的entry 是过期的 这里直接置为Null
			tab[staleSlot] = null;
			//因为上面干掉一个元素,所以 -1.
			size--;

			// Rehash until we encounter null
			//e:表示当前遍历节点
			Entry e;
			//i:表示当前遍历的index
			int i;

			//for循环,从 staleSlot 的下一个位置开始搜索过期数据,直到碰到 slot == null 结束。
			for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
				//进入到for循环里面,当前entry一定不为null

				//获取当前遍历节点 entry 的key.
				ThreadLocal<?> k = e.get();

				//条件成立:说明k表示的threadLocal对象 已经被GC回收了,当前entry属于脏数据了。
				if (k == null) {
					//help gc
					e.value = null;
					//脏数据对应的slot置为null
					tab[i] = null;
					//因为上面干掉一个元素,所以 -1.
					size--;
				} else {
					//执行到这里,说明当前遍历的slot中对应的entry 是非过期数据
					//因为前面有可能清理掉了几个过期数据,就可能空出来了位置。
					//如果下面的if条件成立,说明当前entry 存储时碰到hash冲突了,往后偏移存储了,
					//这个时候 应该去优化位置,找到离正确位置更近的位置
					//这样的话,查询的时候 效率才会更高!

					//重新计算当前entry对应的 index
					int h = k.threadLocalHashCode & (len - 1);

					//条件成立:说明当前entry存储时 就是发生过hash冲突,然后向后偏移过了。
					if (h != i) {
						//将entry当前位置 设置为null
						tab[i] = null;

						// Unlike Knuth 6.4 Algorithm R, we must scan until
						// null because multiple entries could have been stale.

						//h 是正确位置。

						//以正确位置h 开始,向后查找第一个 可以存放entry的位置。
						while (tab[h] != null)
							h = nextIndex(h, len);

						//将当前元素放入到 距离正确位置 更近的位置(有可能就是正确位置)。
						tab[h] = e;
					}
				}
			}
			//返回:staleSlot之后的下一个空槽的索引
			return i;
		}

在这里插入图片描述
串联总结4-6(1:05:36~1:07:54)

get流程图

在这里插入图片描述

7)ThreadLocalMap set() 写数据方法源码深入分析(1:07:54~1:16:38)

		/**
		 * Set the value associated with key.
		 *
		 * ThreadLocal 使用set方法 给当前线程添加 threadLocal-value   键值对。
		 *
		 * @param key the thread local object
		 * @param value the value to be set
		 */
		private void set(ThreadLocal<?> key, Object value) {
			//获取散列表
			Entry[] tab = table;
			//获取散列表数组长度
			int len = tab.length;
			//计算当前key 在 散列表中对应的位置
			int i = key.threadLocalHashCode & (len-1);


			//整个for循环的作业:以当前key对应的slot位置 向后查询,找到可以使用的slot。
			//什么样slot可以使用呢?
			// 1.k == key 说明是替换    660行
			// 2.k == null,说明碰到一个过期的 slot ,这个时候 咱们可以强行占用呗。  667行
			// 3.循环终止,说明查找过程中 碰到 slot == null 了。   677行
			for (Entry e = tab[i];
			     e != null;
			     e = tab[i = nextIndex(i, len)]) {

				//获取当前元素key
				ThreadLocal<?> k = e.get();

				//条件成立:说明当前set操作是一个替换操作。
				if (k == key) {
					//做替换value值的逻辑。
					e.value = value;
					return;
				}

				//条件成立:说明 向下寻找过程中 碰到entry#key == null 的情况了,说明当前entry 是过期数据。
				if (k == null) {
					//碰到一个过期的 slot ,这个时候 咱们可以强行占用呗。
					//替换过期数据的逻辑。
					replaceStaleEntry(key, value, i);
					return;
				}
			}

			//执行到这里,说明for循环碰到了 slot == null 的情况。
			//直接在合适的slot中 创建一个新的entry对象。
			tab[i] = new Entry(key, value);
			//因为是新添加,所以++size.
			int sz = ++size;

			//做一次启发式清理
			//条件一:!cleanSomeSlots(i, sz) 成立,说明启发式清理工作 未清理到任何数据..
			//条件二:sz >= threshold 成立,说明当前table内的entry已经达到扩容阈值了..会触发rehash操作。
			if (!cleanSomeSlots(i, sz) && sz >= threshold)
				rehash();
		}

8)ThreadLocalMap replaceStaleEntry() 写数据过程发现key == null Entry,执行替换过期Entry逻辑(1:16:38~1:37:33)

		/**
		 * Replace a stale entry encountered during a set operation
		 * with an entry for the specified key.  The value passed in
		 * the value parameter is stored in the entry, whether or not
		 * an entry already exists for the specified key.
		 *
		 * As a side effect, this method expunges all stale entries in the
		 * "run" containing the stale entry.  (A run is a sequence of entries
		 * between two null slots.)
		 *
		 * @param  key the key
		 * @param  value the value to be associated with key
		 * @param  staleSlot index of the first stale entry encountered while
		 *         searching for key.
		 * key: 键 threadLocal对象
		 * value: 与键关联的值
		 * staleSlot: 上层的set方法,在迭代查找时,发现的当前这个slot是一个过期的 entry。
		 */
		private void replaceStaleEntry(ThreadLocal<?> key, Object value,
		                               int staleSlot) {
			//获取散列表
			Entry[] tab = table;
			//获取散列表数组长度
			int len = tab.length;
			//临时变量
			Entry e;

			//表示 开始探测式清理过期数据的 起始下标。默认从当前 staleSlot开始。
			int slotToExpunge = staleSlot;

			//以传入的staleSlot开始,向上一个迭代查找,找有没有过期的数据。for循环一直到碰到null结束。
			for (int i = prevIndex(staleSlot, len);
			                  (e = tab[i]) != null;
			                 i = prevIndex(i, len)){

				//条件成立:说明向前找到了过期数据,更新 探测清理过期数据的起始下标为 i
				if (e.get() == null){
					slotToExpunge = i;
				}
			}

			//以传入的staleSlot开始,向下一个去查找,直到碰到null为止。
			for (int i = nextIndex(staleSlot, len);
						      (e = tab[i]) != null;
						     i = nextIndex(i, len)) {
				//获取当前元素 key
				ThreadLocal<?> k = e.get();

				//条件成立:说明是一个 替换逻辑。
				if (k == key) {
					//替换新数据。
					e.value = value;

					// 执行到这里是因为线性探测导致的。staleSlot离正确位置更近,且数据已过期过期,所以有下面的交换逻辑
					//交换位置的逻辑:
					//将table[staleSlot]这个过期数据 放到 当前循环到的 table[i] 这个位置。
					tab[i] = tab[staleSlot];
					//将tab[staleSlot] 中保存为 当前entry。 这样的话,咱们这个数据位置就被优化了
					tab[staleSlot] = e;

					//条件成立:有下面两点
					// 1.说明replaceStaleEntry 一开始时 的向前查找过期数据,并未找到过期的entry.
					// 2.向后检查过程中也未发现过期数据(780行,如果向后检查过程中发现过期数据,slotToExpunge会被更新)
					if (slotToExpunge == staleSlot)
						//开始探测式清理过期数据的下标 修改为 当前循环的index。
						slotToExpunge = i;

					//cleanSomeSlots :启发式清理
					cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
					return;
				}

				//条件1:k == null 成立,说明当前遍历的entry是一个过期数据
				//条件2:slotToExpunge == staleSlot 成立,一开始时 的向前查找过期数据 并未找到过期的entry.
				if (k == null && slotToExpunge == staleSlot)
					//因为向后查询过程中查找到一个过期数据了,更新slotToExpunge 为 当前循环的index。
					//前提条件是 前驱扫描时 未发现 过期数据..
					slotToExpunge = i;
			}

			//什么时候执行到这里呢?
			//向后查找过程中 并未发现 k == key 的entry,说明当前set没有替换操作,直接强行占用过期数据的位置

			//直接将新数据添加到 table[staleSlot] 对应的slot中。
			tab[staleSlot].value = null;
			tab[staleSlot] = new Entry(key, value);

			//条件成立:除了当前staleSlot 以外 ,还发现其它的过期slot了.. 所以要开启 清理数据的逻辑..
			if (slotToExpunge != staleSlot)
				cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
		}

在这里插入图片描述

9)ThreadLocalMap cleanSomeSlot() 启发式清理过期数据方法源码深入分析(1:37:33~1:52:28)

		/**
         * 启发式扫描一些单元格以查找过时的条目。 
         * 这在添加新元素或另一个陈旧元素已被删除时调用。
         * 它执行对数扫描次数,作为无扫描(快速但保留垃圾)和扫描次数与元素数量成正比之间的平衡
         * 这将找到所有垃圾但会导致某些插入花费 O(n) 时间。
		 *
		 * @param i 一个已知不会持有过时条目的位置。 扫描从 i 之后的元素开始。
		 *
		 * @param n 扫描控制:{@code log2(n)} 个单元格被扫描
		 * 除非找到一个陈旧的条目,在这种情况下{@code log2(table.length)-1} 个额外的单元格被扫描。
		 * 从插入中调用时,此参数是元素的数量
		 * 但从replaceStaleEntry 中调用时,它是表长度。 
		 * (注意:所有这些都可以通过对 n 进行加权而不是仅使用直接对数 n 来改变或多或少的激进。但这个版本简单、快速,而且似乎工作得很好。)
		 *
		 * 参数 i : 启发式清理工作开始位置
		 * 参数 n : 一般传递的是 table.length ,这里n 也表示结束条件。
		 * @return 如果已删除任何陈旧条目,则为 true。
		 */
		private boolean cleanSomeSlots(int i, int n) {
			//表示启发式清理工作 是否清除过过期数据
			boolean removed = false;
			//获取当前map的散列表引用
			Entry[] tab = table;
			//获取当前散列表数组长度
			int len = tab.length;

			do {
				//这里为什么不是从i就检查呢?
				//因为cleanSomeSlots(i = expungeStaleEntry(...), n)  , expungeStaleEntry(...)方法的返回值一定是null。

				//获取当前i的下一个 下标
				i = nextIndex(i, len);
				//获取table中当前下标为i的元素
				Entry e = tab[i];

				//条件一:e != null
				//条件二:e.get() == null 成立,说明当前slot中保存的entry 是一个过期的数据..
				if (e != null && e.get() == null) {
					//重新更新n为 table数组长度
					n = len;
					//表示清理过数据.
					removed = true;
					//以当前过期的slot为开始节点 做一次 探测式清理工作
					i = expungeStaleEntry(i);
				}

				// 假设table长度为16
				// 16 >>> 1 ==> 8
				// 8 >>> 1 ==> 4
				// 4 >>> 1 ==> 2
				// 2 >>> 1 ==> 1
				// 1 >>> 1 ==> 0
			} while ( (n >>>= 1) != 0);

			return removed;
		}

10)ThreadLocalMap rehash() -> resize() 散列表扩容方法源码分析(1:52:28~2:03:20)


		/**
		 * 重新包装和/或重新调整表格的大小。
		 * 首先扫描整个表,删除陈旧的条目。
		 * 如果这还不够缩小表格的大小,请将表格大小加倍。
		 */
		private void rehash() {
			//这个方法执行完后,当前散列表内的所有过期的数据,都会被干掉。
			expungeStaleEntries();

			// Use lower threshold for doubling to avoid hysteresis
			//条件成立:说明清理完 过期数据后,当前散列表内的entry数量仍然达到了 threshold * 3/4,真正触发 扩容!
			if (size >= threshold - threshold / 4)
				//扩容。
				resize();
		}
		/**
		 * 清除表中的所有陈旧条目
		 */
		private void expungeStaleEntries() {
			Entry[] tab = table;
			int len = tab.length;
			for (int j = 0; j < len; j++) {
				Entry e = tab[j];
				if (e != null && e.get() == null)
					expungeStaleEntry(j);
			}
		}
		/**
		 * Double the capacity of the table.
		 */
		private void resize() {
			//获取当前散列表
			Entry[] oldTab = table;
			//获取当前散列表长度
			int oldLen = oldTab.length;
			//计算出扩容后的表大小  oldLen * 2
			int newLen = oldLen * 2;
			//创建一个新的散列表
			Entry[] newTab = new Entry[newLen];
			//表示新table中的entry数量。
			int count = 0;

			//遍历老表,迁移数据到新表。
			for (int j = 0; j < oldLen; ++j) {
				//访问旧表的指定位置的slot
				Entry e = oldTab[j];
				//条件成立:说明旧表中的指定位置 有数据
				if (e != null) {
					//获取entry#key
					ThreadLocal<?> k = e.get();
					//条件成立:说明旧表中的当前位置的entry 是一个过期数据..
					if (k == null) {
						e.value = null; // Help the GC
					} else {
						//执行到这里,说明老表的当前位置的元素是非过期数据 正常数据,需要迁移到扩容后的新表。。

						//计算出当前entry在扩容后的新表的 存储位置。
						int h = k.threadLocalHashCode & (newLen - 1);
						//while循环:就是拿到一个距离h最近的一个可以使用的slot。
						while (newTab[h] != null)
							h = nextIndex(h, newLen);

						//将数据存放到 新表的 合适的slot中。
						newTab[h] = e;
						//数量+1
						count++;
					}
				}
			}

			//设置下一次触发扩容的指标。
			setThreshold(newLen);
			size = count;
			//将扩容后的新表 的引用保存到 threadLocalMap 对象的 table这里。
			table = newTab;
		}

set流程图

在这里插入图片描述

11)ThreadLocalMap remove() 移除数据方法源码分析(2:03:20~2:04:30)

		/**
		 * Remove the entry for key.
		 */
		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) {
					e.clear();
					expungeStaleEntry(i);
					return;
				}
			}
		}

拓展:内存泄漏问题

参考文章
(1) 内存泄漏相关概念

  • Memory overflow:内存溢出,没有足够的内存提供申请者使用。
  • Memory leak: 内存泄漏是指程序中己动态分配的堆内存由于某种原因程序未释放或无法释放,造成系统内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出。

(2) 如果key使用强引用

  • ​假设ThreadLocalMap中的key使用了强引用,那么会出现内存泄漏吗?
  • 此时ThreadLocal的内存图(实线表示强引用)如下:

在这里插入图片描述

  • 假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  • 但是因为threadLocalMap的Entry强引用了threadLocal,造成threadLocal无法被回收。
  • 在没有手动删除这个Entry以及CurrentThread依然运行的前提下,

始终有强引用链 threadRef->currentThread->threadLocalMap->entry,
Entry就不会被回收(Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。

  • 也就是说,ThreadLocalMap中的key使用了强引用, 是无法完全避免内存泄漏的。

(5)如果key使用弱引用

  • 那么ThreadLocalMap中的key使用了弱引用,会出现内存泄漏吗?
  • 此时ThreadLocal的内存图(实线表示强引用,虚线表示弱引用)如下:
    在这里插入图片描述
    在这里插入图片描述
  • 同样假设在业务代码中使用完ThreadLocal ,threadLocal Ref被回收了。
  • 由于ThreadLocalMap只持有ThreadLocal的弱引用,没有任何强引用指向threadlocal实例, 所以threadlocal就可以顺利被gc回收,此时Entry中的key=null。
  • 但是在没有手动删除这个Entry以及CurrentThread依然运行的前提下,

也存在有强引用链 threadRef->currentThread->threadLocalMap->entry -> value ,
value不会被回收, 而这块value永远不会被访问到了,导致value内存泄漏。

  • 也就是说,ThreadLocalMap中的key使用了弱引用, 也有可能内存泄漏。

(6)出现内存泄漏的真实原因

  • 比较以上两种情况,我们就会发现,内存泄漏的发生跟ThreadLocalMap中的key是否使用弱引用是没有关系的。那么内存泄漏的的真正原因是什么呢?
  • 细心的同学会发现,在以上两种内存泄漏的情况中,都有两个前提:
    1. 没有手动删除这个Entry
    2. CurrentThread依然运行
  • 第一点很好理解,只要在使用完ThreadLocal,调用其remove方法删除对应的Entry,就能避免内存泄漏。
  • 第二点稍微复杂一点, 由于ThreadLocalMap是Thread的一个属性,被当前线程所引用,所以它的生命周期跟Thread一样长。那么在使用完ThreadLocal的使用,如果当前Thread也随之执行结束,ThreadLocalMap自然也会被gc回收,从根源上避免了内存泄漏。

综上,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key就会导致内存泄漏。

(7) 为什么使用弱引用

  • 根据刚才的分析, 我们知道了: 无论ThreadLocalMap中的key使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
  • 要避免内存泄漏有两种方式:
    1. 使用完ThreadLocal,调用其remove方法删除对应的Entry
    2. 使用完ThreadLocal,当前Thread也随之运行结束
  • 相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的。
  • 也就是说,只要记得在使用完ThreadLocal及时的调用remove,无论key是强引用还是弱引用都不会有问题。那么为什么key要用弱引用呢?
  • 事实上,在ThreadLocalMap中的set/getEntry方法中,会对key为null(也即是ThreadLocal为null)进行判断,如果为null的话,那么是会对value置为null的。
  • 这就意味着使用完ThreadLocal,CurrentThread依然运行的前提下,就算忘记调用remove方法,弱引用比强引用可以多一层保障:弱引用的ThreadLocal会被回收,对应的value在下一次ThreadLocalMap调用set,get,remove中的任一方法的时候会被清除,从而避免内存泄漏。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值