ThreadLocal&InheritableThreadLocal
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-SyXNECV6-1691832575541)(\java_source_imgs/threadlocal_img.jpg)]
ThreadLocal 的用处
ThreadLocal使用场合主要解决多线程中数据数据因并发产生不一致问题。ThreadLocal为每个线程的中并发访问的数据提供一个副本,通过访问副本来运行业务,这样的结果是耗费了内存,单大大减少了线程同步所带来性能消耗,也减少了线程并发控制的复杂度。
ThreadLocal不能使用基本数据类型,只能使用Object类型。
二者的区别
ThreadLocal声明的变量是线程私有的成员变量,每个线程都有该变量的副本,线程对变量的修改对其他线程不可见
InheritableThreadLocal声明的变量同样是线程私有的,但是子线程可以使用同样的InheritableThreadLocal类型变量从父线程继承InheritableThreadLocal声明的变量,父线程无法拿到其子线程的。即使可以继承,但是子线程对变量的修改对父线程也是不可见的。
public static void main(String... args) throws InterruptedException {
Practice practice = new Practice();
ThreadLocal<Integer> num = new ThreadLocal<>();
InheritableThreadLocal<Integer> num2 = new InheritableThreadLocal<>();
num2.set(30);
num.set(20);
Thread th1 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(Thread.interrupted());
}
practice.printInfoNum(num); // null 因为th1 是 main的子线程,但是不能访问
});
Thread th2 = new Thread(() -> {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println(Thread.interrupted());
}
practice.printInfoNum2(num2); // 30,子线程可以访问 父线程
});
th1.start();
th2.start();
}
public void printInfoNum(ThreadLocal<Integer> num) {
System.out.println(num.get()); //
}
public void printInfoNum2(ThreadLocal<?> num) {
System.out.println(num.get());
}
底层试下是 包含键值对的数组, 初始容量为 16,size 用于记录当前存放元素的个数
threshold 与其他集合相同,扩容的临界值
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* The number of entries in the table.
*/
private int size = 0;
/**
* The next size value at which to resize.
*/
private int threshold; // Default to 0
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Increment i modulo len.
*/
private static int nextIndex(int i, int len) {
return ((i + 1 < len) ? i + 1 : 0);
}
/**
* Decrement i modulo len.
*/
private static int prevIndex(int i, int len) {
return ((i - 1 >= 0) ? i - 1 : len - 1);
}
1. 面试官:知道ThreadLocal嘛?谈谈你对它的理解?(基于jdk1.8)
为什么 ThreadLocalMap 的 key 要设置成 弱引用
因为 我们知道 threadlocalmap 和线程相同的生命周期,如果,threadlocal 对象 被赋值 null, 但是 threadlocal 实例仍旧不能被回收,因为 threadlocalmap 始终包含指向 threadlocal 的对象,因此,要将 key 设置成 弱引用,一旦 threadlocal 对象设为空,并且 进行垃圾回收, thradlocal 在堆中的对象就会被回收。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8yCfM1Bb-1691832575544)(java_source_imgs/why_threadlocal_weakreference.png)]
ThreadLocal 为什么会引起内存泄漏
如果,我们将 ThreadLocal 对象设为空, 因为 key 是 弱引用,这个时候, 就会将 key 指向的threadlocal 对象回收掉,也就是 key 指向了 null, 但是 value 是强引用,不会被回收,并且该对象不会被访问就造成了 内存泄漏, 并且 线程池的某些线程可能永远不被关闭,如果这样的对象很多,造成内存泄漏很 easy.
2. InheritableThreadLocal详解为什么可以继承父线程的
3. 为什么非得创建ThreadLocal 对象,而不是直接调用ThreadLocalMap threadLocals ,
因为这些成员变量都是包级别访问权限
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
/*
* InheritableThreadLocal values pertaining to this thread. This map is
* maintained by the InheritableThreadLocal class.
*/
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
4. get() 方法设置默认值 initialValue 具有线程隔离性
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-AZy7vhU2-1691832575544)(java_source_imgs/ThreadLocal.get.jpg)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-K3T0acDs-1691832575546)(java_source_imgs/ThreadLocal.get2.jpg)]
get 方法首先查看 当前线程 ThreadLocal.ThreadLocalMap 是否为空,如果为空,就会创建 map
public T get() {
Thread t = Thread.currentThread();
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;
}
}// 如果 value == null or map == null;
return setInitialValue();
}
private T setInitialValue() {
T value = initialValue(); // 调用 获取默认值(该方法可以被重写,返回默认值)
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value; // 返回默认值
}
/*
根据元素 获取 entry
*/
private Entry getEntry(ThreadLocal<?> key) {
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
return e;
else
return getEntryAfterMiss(key, i, e);
}
// 如果,没有获取元素,说明,在插入的时候,因为解决冲突,元素后置
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
expungeStaleEntry(i);
else
i = nextIndex(i, len);
e = tab[i];
}
return null;
}
// 檫除元素,将元素重新定位
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
/**
* Allocates a new {@code Thread} object. This constructor has the same
* effect as {@linkplain #Thread(ThreadGroup,Runnable,String) Thread}
* {@code (null, null, gname)}, where {@code gname} is a newly generated
* name. Automatically generated names are of the form
* {@code "Thread-"+}<i>n</i>, where <i>n</i> is an integer.
*/
public Thread() {
init(null, null, "Thread-" + nextThreadNum(), 0);
}
/**
* Initializes a Thread with the current AccessControlContext.
* @see #init(ThreadGroup,Runnable,String,long,AccessControlContext,boolean)
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize) {
init(g, target, name, stackSize, null, true);
}
/**
* Initializes a Thread.
*
* @param g the Thread group
* @param target the object whose run() method gets called
* @param name the name of the new Thread
* @param stackSize the desired stack size for the new thread, or
* zero to indicate that this parameter is to be ignored.
* @param acc the AccessControlContext to inherit, or
* AccessController.getContext() if null
* @param inheritThreadLocals if {@code true}, inherit initial values for
* inheritable thread-locals from the constructing thread
*/
private void init(ThreadGroup g, Runnable target, String name,
long stackSize, AccessControlContext acc,
boolean inheritThreadLocals) {
if (name == null) {
throw new NullPointerException("name cannot be null");
}
this.name = name;
Thread parent = currentThread(); // 我们知道一定会在一个线程中创建线程,也就是新创建的线程子线程, currentThread 就是父线程,
this.group = g;
this.daemon = parent.isDaemon();
this.priority = parent.getPriority();
this.target = target;
setPriority(priority);
// 这里将 父线程 inheritableThreadLocals 直接赋值给新的线程 inheritableThreadLocals
if (inheritThreadLocals && parent.inheritableThreadLocals != null)
this.inheritableThreadLocals =
ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);
/* Stash the specified stack size in case the VM cares */
this.stackSize = stackSize;
/* Set thread ID */
tid = nextThreadID();
}
set
就是计算 Key 的hash ,然后插入数组,如果 定址已经有了元素,就往后找,如果,位置上有脏数据,则替换,否则,就是当前位置上无数据,直接创建插入
每次插入之后,先进行清除数据,然后在比较 和 threshold 的大小,减少扩容的次数
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();
// 如果,key 已经存在,则替换
if (k == key) {
e.value = value;
return;
}
// 如果, key == null, 也就是是脏数据,进行替换
if (k == null) {
replaceStaleEntry(key, value, i);
return;
}
}
// 执行到这一步,说明是 直接插入
tab[i] = new Entry(key, value);
int sz = ++size;
if (!cleanSomeSlots(i, sz) && sz >= threshold)
rehash();
}
cleanSomeSlots
expungeStaleEntry 返回的是 tab[i] == null 的下标, 也就是 i
private boolean cleanSomeSlots(int i, int n) {
boolean removed = false;
Entry[] tab = table;
int len = tab.length;
do {
i = nextIndex(i, len);
Entry e = tab[i];
if (e != null && e.get() == null) {
n = len;
removed = true;
// 清除
i = expungeStaleEntry(i);
}
} while ( (n >>>= 1) != 0);
return removed;
}
replaceStaleEntry
private void replaceStaleEntry(ThreadLocal<?> key, Object value,
int staleSlot) {
Entry[] tab = table;
int len = tab.length;
Entry e;
// Back up to check for prior stale entry in current run.
// We clean out whole runs at a time to avoid continual
// incremental rehashing due to garbage collector freeing
// up refs in bunches (i.e., whenever the collector runs).
int slotToExpunge = staleSlot;
for (int i = prevIndex(staleSlot, len);
(e = tab[i]) != null;
i = prevIndex(i, len))
if (e.get() == null)
slotToExpunge = i;
// Find either the key or trailing null slot of run, whichever
// occurs first
for (int i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
//如果我们找到key,那么我们需要将它与陈旧的条目进行交换,以保持哈希表的顺序。然后,可以将新过期的槽或在它上面遇到的任何其他过期槽发送到expungeStaleEntry,以删除或重新哈希所有正在运行的其他条目。
if (k == key) {
e.value = value;
tab[i] = tab[staleSlot];
tab[staleSlot] = e;
// Start expunge at preceding stale entry if it exists
if (slotToExpunge == staleSlot)
slotToExpunge = i;
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
return;
}
// 如果在向后扫描时没有找到过时的条目,那么在扫描键时看到的第一个过时条目就是在运行时仍然存在的第一个过时条目。
if (k == null && slotToExpunge == staleSlot)
slotToExpunge = i;
}
// If key not found, put new entry in stale slot
tab[staleSlot].value = null;
tab[staleSlot] = new Entry(key, value);
// If there are any other stale entries in run, expunge them
if (slotToExpunge != staleSlot)
cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
}
expungeStaleEntry
檫除当前位置上的脏数据,并且对数据进行重新 hash
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
// 檫除当前位置上的数据
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// size --
// Rehash until we encounter null
Entry e;
int i;
// 依次后查,
for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
// 如果当前 为脏数据, 直接清除
if (k == null) {
e.value = null;
tab[i] = null;
size--;
} else {
// 如果,不是脏数据,但是,希望将 hash 相同的值尽可能放在一起
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
remove
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;
}
}
}
rehash
其实每次扩容之前, 都会清空脏数据,从而减少扩容的次数
threshold = len * 2 / 3
threshold= 3/ 4 threshold
只要 size >= 1 / 2 tab 的容量,就会进行扩容
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-VoDd37mO-1691832575547)(java_source_imgs\threadlocal_rehash.jpg)]
/**
* Set the resize threshold to maintain at worst a 2/3 load factor.
*/
private void setThreshold(int len) {
threshold = len * 2 / 3;
}
/**
* Re-pack and/or re-size the table. First scan the entire
* table removing stale entries. If this doesn't sufficiently
* shrink the size of the table, double the table size.
*/
private void rehash() {
expungeStaleEntries();
// Use lower threshold for doubling to avoid hysteresis
if (size >= threshold - threshold / 4)
resize();
}
/**
* Double the capacity of the table.
*/
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
InheritableThreadLocal
-
可以继承父线程的 ThreadLocal 变量
public static void main(String... args) { Practice practice = new Practice(); Person person1 = new Person("CaoBourne1"); InheritableThreadLocal<Person> personLocal = new InheritableThreadLocal<>(); personLocal.set(person1); Thread th1 = new Thread(() -> { practice.printInfo(personLocal); try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println(Thread.interrupted()); } }); th1.start(); Thread.sleep(500); } public void printInfo(ThreadLocal<Person> childLocal) { System.out.println("child\t" + childLocal.get()); // person1 }
-
子线程修改了 ThreadLocal ,父线程仍是旧值
子线程还是输出原有的数据
public static void main(String... args) throws InterruptedException, IllegalAccessException, InstantiationException { Practice practice = new Practice(); Person person1 = new Person("CaoBourne1"); Person person2 = new Person("CaoBourne2"); InheritableThreadLocal<Person> personLocal = new InheritableThreadLocal<>(); personLocal.set(person1); Thread th1 = new Thread(() -> { for (int i = 0; i < 10; i++) { practice.printInfo(personLocal); try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println(Thread.interrupted()); } } }); th1.start(); Thread.sleep(500); personLocal.set(person2); System.out.println("main" + personLocal.get()); } public void printInfo(ThreadLocal<Person> childLocal) { System.out.println("child\t" + childLocal.get()); }
-
父线程修改了 ThreadLocal, 子线程仍是旧值
父线程也只能输出旧值
public static void main(String... args) throws InterruptedException, IllegalAccessException, InstantiationException { Practice practice = new Practice(); Person person1 = new Person("CaoBourne1"); Person person2 = new Person("CaoBourne2"); InheritableThreadLocal<Person> personLocal = new InheritableThreadLocal<>(); personLocal.set(person1); Thread th1 = new Thread(() -> { for (int i = 0; i < 10; i++) { practice.printInfo(personLocal); try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println(Thread.interrupted()); } if(i == 5) { personLocal.set(person2); } } }); th1.start(); Thread.sleep(500); System.out.println("main" + personLocal.get()); } public void printInfo(ThreadLocal<Person> childLocal) { System.out.println("child\t" + childLocal.get()); }
-
属性修改,父线程和子线程可以相互影响
修改了 person1 的属性值,会影响到子线程
public static void main(String... args) throws InterruptedException, IllegalAccessException, InstantiationException { Practice practice = new Practice(); Person person1 = new Person("CaoBourne1"); Person person2 = new Person("CaoBourne2"); InheritableThreadLocal<Person> personLocal = new InheritableThreadLocal<>(); personLocal.set(person1); Thread th1 = new Thread(() -> { for (int i = 0; i < 10; i++) { practice.printInfo(personLocal); try { Thread.sleep(50); } catch (InterruptedException e) { System.out.println(Thread.interrupted()); } if(i == 5) { person1.setName("CaoBourne9"); } } }); th1.start(); Thread.sleep(500); personLocal.set(person2); System.out.println("main" + personLocal.get()); } public void printInfo(ThreadLocal<Person> childLocal) { System.out.println(Thread.currentThread().getName() + "child\t" + childLocal.get()); }
ThreadLocalMap 和 HashMap 的区别
底层实现:
ThreadLocalMap 数组, hash 定制,线性探测再散列。但是 Key 弱引用。
HashMap 数组,链表,红黑二叉树
扩容:
ThreadLocalMap
的扩容大小 2/3 进行扩容,利用 Hash & len - 1 定址,解决冲突,但是在扩容的时候,会重新清理多余的 过期元素,并且,会 重新判断 size >= 3/4; 因为扩容首先要开辟空间,并且要重新定址,浪费性能。
son1.setName(“CaoBourne9”);
}
}
});
th1.start();
Thread.sleep(500);
personLocal.set(person2);
System.out.println(“main” + personLocal.get());
}
public void printInfo(ThreadLocal childLocal) {
System.out.println(Thread.currentThread().getName() + “child\t” + childLocal.get());
}
# ThreadLocalMap 和 HashMap 的区别
> 底层实现:
>
> ThreadLocalMap 数组, hash 定制,线性探测再散列。但是 Key 弱引用。
>
> HashMap 数组,链表,红黑二叉树
>
> 扩容:
>
> `ThreadLocalMap` 的扩容大小 2/3 进行扩容,利用 Hash & len - 1 定址,解决冲突,但是在扩容的时候,会重新清理多余的 过期元素,并且,会 重新判断 size >= 3/4; 因为扩容首先要开辟空间,并且要重新定址,浪费性能。
>
> `HashMap` 扩容:大于 Threshold 直接扩 原来的二倍,重新定址。