当前位置:   article > 正文

并发—ThreadLocal笔记_threadlocal可见性

threadlocal可见性

背景:

并发处理很多时候可以提高处理的速度,但是如果是对共享变量操作时,也会带来一些问题(导致无法满足可见性,有序性,原子性),处理这些问题,第一种常见的方法是加锁,第二种常见的方法是通过CAS(保证标记变量操作的原子性) + Volitle(可见性),第三种常见的处理方法让每个线程都有自己单独的一份,例如ThreadLocal的使用。

使用示例:

  1. public class Test {
  2. public static class MyRunnable implements Runnable {
  3. private ThreadLocal<Integer> threadLocal = new ThreadLocal<>();
  4. @Override
  5. public void run() {
  6. threadLocal.set((int) (Math.random() * 100D));
  7. try {
  8. Thread.sleep(2000);
  9. } catch (InterruptedException e) {
  10. }
  11. System.out.println(threadLocal.get());
  12. }
  13. }
  14. public static void main(String[] args) {
  15. MyRunnable sharedRunnableInstance = new MyRunnable();
  16. Thread thread1 = new Thread(sharedRunnableInstance);
  17. Thread thread2 = new Thread(sharedRunnableInstance);
  18. thread1.start();
  19. thread2.start();
  20. }
  21. }

输出:

  1. 63
  2. 82

Thread、ThreadLocal、ThreadLocalMap、Entry之间的关系:

这里写图片描述

简述:一个Thread中只有一个ThreadLocalMap,一个ThreadLocalMap值有一个Entry类型的数组(数组被初始化后默认大小为16),set操作本质就是将ThreadLocal对象为键,value为值封装成Entry对象存入ThreadLocalMap中的数组中。而get操作就是以ThreadLocal对象为键找到ThreadLocalMap中Entry数组的索引,然后返回ThreadLocal对象对应的value值。无论是set(存入)还是get(获取)都要根据key(ThreadLocal对象)找到Entry数组的下标,在这里采用的是开放定址法,而HashMap中使用的拉链法。

ThreadLocal各类引用关系图:

1)疑惑:设置ThreadLocal设置为强引用,那么在将ThreadLocalRef设置为空后,ThreadLocal对象并不会被回收,会导致内存泄漏。如果ThreadLocal是弱引用,此时将ThreadLocalRef设置为空,此后出发GC后,ThreadLocal对象会被回收,但是此时Entry中的value值依旧无法回收,依然导致了内存泄漏。所以每次使用ThreadLocal过后会调用remove方法并将ThreadLocalRef设置为空,此时即使ThreadLocal是强引用,ThreadLocal对象和Entry对象都会被回收,那为啥还要将ThreadLocal设置为弱引用呢。

2)思考:ThreadLocalMap没有提供Api给我们调用,如果提供了调用方法,就可以直接使用字符串作为Entry的key了,我感觉那样也很方便。

这里使用ThrealLocal实例作为Map的Key,但实际上,它并不真的持有ThreadLocal的引用,而是弱引用,而当ThreadLocal的外部强引用被收回后,在垃圾回收时,弱引用会被回收,但value值依然存在,如果后面有set,get方法时,对应的value值会被清除,或当线程执行完后,整个map对象会被回收(Entry数组是Map的成员属性)。假设线程没有被销毁,且后面没有set和get操作,那么此时就会导致内存泄漏。

源码:

类Thread:

  1. class Thread{
  2. ThreadLocal.ThreadLocalMap threadLocals = null; //有一个ThreadLocalMap属性
  3. .......
  4. }

创建的线程里都有一个threadLocal对象。

类ThreadLocal:

  1. class ThreadLocal{
  2. //ThreadLocalMap类
  3. static class ThreadLocalMap{
  4. private static final int INITIAL_CAPACITY = 16; //默认数组大小为16
  5. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  6. table = new Entry[INITIAL_CAPACITY]; //Entry数组初始大小为16
  7. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  8. table[i] = new Entry(firstKey, firstValue); //Entry对象放入数组中
  9. size = 1;
  10. setThreshold(INITIAL_CAPACITY);
  11. }
  12. static class Entry extends WeakReference<ThreadLocal<?>> {
  13. .....
  14. }
  15. //用来存储Entry对象的数组
  16. private Entry[] table;
  17. //以当前ThreadLocal对象为键,设置value值到Entry对象中
  18. public void set(T value){
  19. .......
  20. }
  21. //获取value
  22. public T get(){
  23. .......
  24. }
  25. }
  26. }

set()方法:

set()方法源码:

  1. public void set(T value) {
  2. Thread t = Thread.currentThread();        //首先获取当前线程对象
  3. ThreadLocalMap map = getMap(t);           //通过getMap(t)拿到xianc
  4. if (map != null)
  5. map.set(this, value);                 //key是ThreadLocal当前对象,value是我们设置的值
  6. else
  7. createMap(t, value); //如果map没有初始化,就先初始化t.threadLocals
  8. }

在set时,首先获得当前线程对象,然后通过getMap()拿到线程的ThreadLocalMap,并将值设置到ThreadLocalMap中。map现在简单理解成是Map,存入以ThreadLocal对象为键,value为值的键值对。如果map为null(没有初始化)就初始化t.threadLocals,详细的说明如下:

getMap函数

  1. ThreadLocalMap getMap(Thread t) {
  2. return t.threadLocals; //threadLocals是类Thread的属性
  3. }

getMap返回Thread实例的threadLoclas属性赋值给map。Thread中ThreadLocalMap类型的属性ThreadLocals声明如下:

  1. class Thread implements Runnable{                //threadLocals是Thread类中的属性
  2. ThreadLocal.ThreadLocalMap threadLocals = null;
  3. }

如果map为空,就执行createMap函数,createMap函数的定义如下:

  1. void createMap(Thread t, T firstValue) {
  2. t.threadLocals = new ThreadLocalMap(this, firstValue); //key是当前ThreadLocal对象
  3. }

ThreadLocalMap的构造函数的源码:

  1. ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  2. table = new Entry[INITIAL_CAPACITY]; //默认初始容量为16
  3. int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  4. table[i] = new Entry(firstKey, firstValue); //Entry对象存入数组
  5. size = 1; //Entry对象的数量为1
  6. setThreshold(INITIAL_CAPACITY);
  7. }

如果map不为空,也就是threadLocals属性已经被初始化过了,就直接使用set方法赋新值:

map.set(this,value);

set方法的源码:

  1. /**
  2. * Set the value associated with key.
  3. *
  4. * @param key the thread local object
  5. * @param value the value to be set
  6. */
  7. private void set(ThreadLocal key, Object value) {
  8. // We don't use a fast path as with get() because it is at
  9. // least as common to use set() to create new entries as
  10. // it is to replace existing ones, in which case, a fast
  11. // path would fail more often than not.
  12. Entry[] tab = table; //table是ThreadLocalMap中的属性
  13. int len = tab.length;
  14. int i = key.threadLocalHashCode & (len-1); //通过key的hashCode来计算索引的位置i
  15. //不同于HashMap,HashMap中使用的是拉链法,这里使用的是开放地址法
  16. for (Entry e = tab[i];e != null;e = tab[i = nextIndex(i, len)]) { //取出索引为i处的值,如果不为空,就将索引向后移动一格,直到找到空的位置
  17. ThreadLocal k = e.get(); //获取键值
  18. if (k == key) { //如果key(ThreadLocal)的值相同,那么就将value值替换,然后直接返回
  19. e.value = value;
  20. return;
  21. }
  22. if (k == null) { //数组i出的Entry不为空,但是Entry的key(ThreadLocal)为空
  23. replaceStaleEntry(key, value, i); //此时直接将值放在数组的该位置
  24. return;
  25. }
  26. }
  27. tab[i] = new Entry(key, value); //如果找到索引i处,数组为空,就赋新值
  28. int sz = ++size;
  29. if (!cleanSomeSlots(i, sz) && sz >= threshold) //判断当前存储的对象个数是否已经超过了阈值
  30. rehash(); //如果超过了阈值,就要进行扩容并将所有的对象重新计算位置
  31. }

关于set方法的简要说明:首先第17行代码根据key值得到数组中索引的位置 i ,使用开放定址法,从索引处开始判断该处是不是为空(没有Entry对象),如果该位置有Entry对象,此时会进一步判断,如果该Entry对象的key值与新插入的Entry对象的key值相同,那么就直接将原先对象的value值替换成新的value值。如果该Entry对象的key为空,此时就执行第27行代码(出现Entry对象不为空,而key为空,是因为执行key的引用为弱引用,每次GC都会被回收),此时执行第27行代码。在从索引 i 处向后挪动的过程中,如果遇到数组某个下标出没有Entry对象,此时就直接执行32行,将新的Entry对象存入该处。在第34行会判断数组中存储的Entry对象的个数是否已经超过了阈值的大小,如果超过了,需要重新扩充并将所有的对象重新计算位置(rehash函数实现)。

ThreadLocalMap中存储的是Entry对象本质上是一个弱引用WeakReference<ThreadLocal>,也就是说ThreadLocal里面存储的对象本质是一个ThreadLocal对象的弱引用,该ThreadLocal随时可能倍被回收(弱引用每次GC后,都会回收),即导致ThreadLocalMap里面对应的Value的Key是null,我们使用expungeStateEntries函数来清理掉这些key为空的Entry对象。

这里注意:阈值threshold等于table数组长度的2/3,而扩容的条件是数组中Entry对象的个数超过了阈值的3/4。

  1. private void rehash() {
  2. expungeStaleEntries(); //keynull的Entry对象清除
  3. // Use lower threshold for doubling to avoid hysteresis
  4. if (size >= threshold - threshold / 4) //当大小大于阈值的四分之三时,就扩容
  5. resize();
  6. }

resize()扩容函数

  1. private void resize() {
  2. Entry[] oldTab = table;
  3. int oldLen = oldTab.length;
  4. int newLen = oldLen * 2; //新数组长度为原先数组的两倍
  5. Entry[] newTab = new Entry[newLen];
  6. int count = 0;
  7. for (int j = 0; j < oldLen; ++j) {
  8. Entry e = oldTab[j]; //逐个取出原先的Entry对象
  9. if (e != null) { //如果不为空
  10. ThreadLocal<?> k = e.get(); //获取key
  11. if (k == null) { //如果为空,就将value值也设置为空
  12. e.value = null; // Help the GC
  13. } else { //如果不为空
  14. int h = k.threadLocalHashCode & (newLen - 1); //重新根据hash值计算索引值
  15. while (newTab[h] != null) //如果冲突,往后移一步,直到遇到空格
  16. h = nextIndex(h, newLen);
  17. newTab[h] = e; //将Entry对象放入
  18. count++;
  19. }
  20. }
  21. }
  22. setThreshold(newLen); //设置阈值
  23. size = count;
  24. table = newTab;
  25. }

get()方法

  1. public T get() {
  2. Thread t = Thread.currentThread();                        //获取当前线程
  3. ThreadLocalMap map = getMap(t);                           //获取当前线程中的ThreadLocalMap
  4. if (map != null) {    
  5. ThreadLocalMap.Entry e = map.getEntry(this);          //通过key(当前的LocalThread)获取value值
  6. if (e != null) {
  7. @SuppressWarnings("unchecked")
  8. T result = (T)e.value;
  9. return result;
  10. }
  11. }
  12. return setInitialValue();
  13. }

get()方法获取当前线程的ThreadLocalMap类型的对象threadLoacals赋值给map,如果map不为空,就以ThreadLocal对象是键找到对应的Entry对象,如果Entry对象不为空,就返回Entry对象的value值。如果map为空就执行setInitialValue()

  1. private Entry getEntry(ThreadLocal<?> key) {
  2. int i = key.threadLocalHashCode & (table.length - 1);//根据hash值得到索引
  3. Entry e = table[i]; //取出索引处的Entry对象
  4. if (e != null && e.get() == key) //如果Entry对象不为空并且Entry对象的键值等于key说明找到了
  5. return e;
  6. else
  7. return getEntryAfterMiss(key, i, e);//否则执行这个函数
  8. }

根据哈希值找到索引,如果对应的Entry对象不为空,且Entry对象的key不为空,就返回Entry对象。否则执行第7行的函数

  1. private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {//如果Entry对象为空,或者Entry对象的键与key不相等
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. while (e != null) {
  5. ThreadLocal<?> k = e.get();
  6. if (k == key)
  7. return e;
  8. if (k == null)
  9. expungeStaleEntry(i); //键为空,就清理对应的value
  10. else
  11. i = nextIndex(i, len); //移到数组下一个位置
  12. e = tab[i];
  13. }
  14. return null; //如果Entry对象为空,直接返回null
  15. }

该函数主要是对Entry对象为空和Entry对象的key值为空做了处理,如果Entry对象为空,表明通过无法取到这个值,直接返回null,如果Entry对象的key为空,就将该Entry对象的value值清空,然后返回null,否则就在就在数组中移动一位重复上面的判断,直到找到Entry对象的键值等于key返回value值。

 

参考:

再有人问你什么是ThreadLocal,就把这篇文章甩给他!

谈谈ThreadLocal为什么被设计为弱引用 - 知乎

再有人问你什么是ThreadLocal,就把这篇文章甩给他!

LocalThread基本用法讲解:

http://ifeve.com/java-threadlocal%E7%9A%84%E4%BD%BF%E7%94%A8/

原理讲的比较好的

https://blog.csdn.net/huachao1001/article/details/51734973

https://blog.csdn.net/huachao1001/article/details/51970237

ThreadLocal的内存泄漏

https://blog.csdn.net/xlgen157387/article/details/78513735

https://www.cnblogs.com/onlywujun/p/3524675.html

声明:本文内容由网友自发贡献,不代表【wpsshop博客】立场,版权归原作者所有,本站不承担相应法律责任。如您发现有侵权的内容,请联系我们。转载请注明出处:https://www.wpsshop.cn/w/我家自动化/article/detail/294524
推荐阅读
  

闽ICP备14008679号