ThreadLocal使用和原理

ThreadLocal是线程本地变量,用来解决并发下数据隔离性的问题,不能解决共享。

他可以将一个变量拷贝的线程内,线程调用时再线程内进行使用,相当于给每个线程复制一个副本供各个线程使用。

ThreadLocal简单使用

他的目的很简单,就是让每个线程操控同一个变量做不同的事情,互不干扰

我们这里使用栈进行对比,用来查看高并发下的问题

@Slf4j
public class TestThreadLocal {
    public static void main(String[] args) {
        ThreadLocal<String> local = new ThreadLocal<>();
        Deque<String> stack = new ArrayDeque<>();
        for (int i = 0; i < 10; i++) {
            final int j = i;
            new Thread(()->{
                String s = Thread.currentThread().getName()+"::"+j;
                local.set(s);
                stack.push(s);
                log.debug("local--->{}",local.get());
                //解决内存泄露问题
                local.remove();
                if(!stack.isEmpty()){
                    log.debug("stack-->{}",stack.pop());
                }else{
                    log.debug("null---");
                }

            },"t"+i).start();
        }
    }
}

运行结果:

# 这里使用local没出现问题,每个线程打印的都是自己存入的数据
15:03:56.862 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t8::8
15:03:56.862 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t6::6
15:03:56.862 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t9::9
15:03:56.862 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t1::1
15:03:56.862 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t7::7
# 这里就已经出问题了,t9线程打印了t6线程存的数据
15:03:56.866 [t9] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t6::6
15:03:56.866 [t6] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t9::9
15:03:56.866 [t7] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t7::7
15:03:56.862 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t2::2
15:03:56.862 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t4::4
15:03:56.866 [t8] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t8::8
15:03:56.862 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t3::3
15:03:56.866 [t4] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t3::3
15:03:56.862 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t5::5
15:03:56.866 [t3] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t2::2
15:03:56.866 [t5] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t1::1
15:03:56.862 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - local--->t0::0
15:03:56.866 [t1] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t5::5
15:03:56.866 [t2] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t4::4
15:03:56.866 [t0] DEBUG JUCTest.ThreadLocalTest.TestThreadLocal - stack-->t0::0

上面的例子应该已经帮我们很好的理解了ThreadLocal的作用,以及我们使用其他存储的对比,接下来我们看看ThreadLocal内部是怎么实现的

原理

看原理之前我们先了解一下ThreadLocal的存储结构,以及Thread、ThreadLocal、ThreadLocalMap之间的关系:

ThreadLocal使用和原理_第1张图片

我们看Thread的属性

/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;

Thread内部属性默认是有ThreadLocalMap的,不过默认为null罢了,而ThreadLocalMap中存储的键值结构是,所以实际可以用多个ThreadLocal存取不同的值

ThreadLocal结构

class ThreadLocal{
    static class ThreadLocalMap {
        static class Entry extends WeakReference<ThreadLocal<?>> {
            //...
        }
    }
    //....
}

每个ThreadLocal内有一个ThreadLocalMap,ThreadLocalMap类似HashMap,但是也有不同,其中包含一个Entry,类似hashmap的Entry,不过有区别,下面会详细说。

我们使用ThreadLocal无非就那几个方法,所以我们直接挑重点看

  1. set
public void set(T value) {
    //获取到当前调用的线程
    Thread t = Thread.currentThread();
    //查看当前线程是否已经实例化过ThreadLocalMap
    ThreadLocalMap map = getMap(t);
    //如果已经实例化过,那么就将当前ThreadLocal作为key,value作为值加入map
    if (map != null)
        map.set(this, value);
    else
        //否则创建ThreadLocalMap
        createMap(t, value);
}
  • getMap,用来返回线程的ThreadLocalMap
 	ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
  • createMap
 	void createMap(Thread t, T firstValue) {
        //实例化当前ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

如果该线程的ThreadLocalMap还没被创建,那么在这将会实例化一次,并且该线程的ThreadLocalMap以后会有永远拥有,另一个ThreadLocal来存储的时候会直接使用该ThreadLocalMap

  • 实例化ThreadLocalMap
		//可以看到 local会是key,value作为值
		ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            table = new Entry[INITIAL_CAPACITY];
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            table[i] = new Entry(firstKey, firstValue);
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

整个ThreadLocalMap内包含了一个Entry数组,用于存放不同的键值

所以这里set方法的流程是:

  • 获取当前线程对象

  • 查看该线程内的ThreadLocalMap是否被实例化过

    • 如果没有被实例化,那么进行实例化,并将当前ThreadLocal和值存进去

    • 如果被实例化过,那么将当前ThreadLocal找个合适的位置存入,map.set(this, value);

    • 那么我们看这个方法:map.set(this, value)

		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.
			//获取当前map内的entry数组
            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();
				
                //如果当前ThreadLocal已经存入值,那么进行覆盖并返回
                if (k == key) {
                    e.value = value;
                    return;
                }
                
				//如果当前值过期,也就是弱引用失效,将进行替换,将当前key替换进去,这里不展开讲
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			
            //到这说明当前ThreadLocal没有被放入过,所以这里直接new
            tab[i] = new Entry(key, value);
            int sz = ++size;
            //解决hash冲突,如果ThreadLocal有很多的情况下
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

如果当前线程的ThreadLocalMap已经存在了,再次加入ThreadLocal和value无非两种情况,之前加入过和没加入过,加入过的找到并替换,没加入的直接new一个新的对象出来

到这里set方法基本结束

接下来看get方法

  1. get
	public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //被实例化过
        if (map != null) {
            //找当前ThreadLocal存放的位置
            ThreadLocalMap.Entry e = map.getEntry(this);
            //如果已经找到,直接将值返回,没找到说明以前就没存过该值
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //没实例化过先实例化,然后将null设置进去,返回null
        return setInitialValue();
    }
  • setInitialValue
	private T setInitialValue() {
        T value = initialValue();  //initialValue()返回值为null
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }

接下来我们看怎么找到的该ThreadLocal对应的值

  • getEntry
 		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);
        }
  • 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)
                    //如果为null,往后寻找一段,找到脏entry进行清理,直到找到null为止
                    expungeStaleEntry(i);
                else
                    //开发地址法找i下标
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

以上是ThreadLocal常用方法的原理

差别

前文我们说了,ThreadLocalMap和HashMap类似,但是也有差别:

  • HashMap实现方式为数组+链表
    • Entry内保存key和value值,是强引用关系
    • 采用拉链法寻值
  • ThreadLocalMap实现方式为数组
    • Entry仅仅存储了value值
    • 采用开放地址法寻值,当前位置有hash冲突时就会一直往下找,直到找到可用的为止

内存泄漏

我们知道ThreadLocal是弱引用关系,那么当GC之后ThreadLocal被清理掉后,value还没被清理掉,此时会造成内存泄露。

之所以是弱引用,是因为每个Entry的key虽然是ThreadLocal,但是并不是直接指向,Entry内没有让ThreadLocal存储的位置,都在Entry的父类里

解决内存泄露的方法也很简单,就是在使用完之后手动remove就可以了

你可能感兴趣的:(java,开发语言)