一点点ThreadLocal的原理分析

什么是ThreadLocal

        ThreadLocal实际上一种线程隔离机制,它是JDK提供的除了锁以外的又一种保证在多线程环境下对于共享变量的访问的安全性的解决方案

ThreadLocal的简单使用

        下面的例子中,开启5个线程,在每个线程内部获取到了本地变量的值,然后调用print方法打印当前本地变量的值,打印之后调用本地变量的remove方法会删除本地内存中的变量。代码如下所示

public class ThreadLocalDemo {

    static ThreadLocal<Integer> local=new ThreadLocal<Integer>(){
        // 重写initialValue方法,赋初始值
        protected Integer initialValue(){
            return 0; 
        }
    };

    public static void main(String[] args) {
        Thread[] thread=new Thread[5];
        for (int i=0;i<5;i++){
            thread[i]=new Thread(()->{
                int num=local.get(); // 调用get方法,获得的值都是0
                local.set(num+=5); // 通过set方法设置到local中  thread[0] ->thread[1] ->
                System.out.println(Thread.currentThread().getName()+"-"+num);
                local.remove(); // 通过remove方法删除
            });
        }
        for (int i = 0; i < 5; i++) {
            thread[i].start();
        }
    }
}

其实对于ThreadLocal的使用来说就这么简单,创建ThreadLocal的时候,通过initialValue方法赋初始值,通过get方法取值,set方法赋值,remove方法删除

ThreadLocal和Thread的关系

ThreadLocalMap

在ThreadLocal类里面有个静态内部类ThreadLocalMap

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

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

        /**
         * 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;

		......
}

从这个代码我们大体可以看出ThreadLocalMap结构:
1. 是一个Entry数组
2. Entry对象是一个<key,value>键值对结构
3. Entry的key是一个ThreadLocal对象
4. Entry的key是一个ThreadLocal对象,而且是弱引用对象(弱引用导致了ThreadLocal的问题)

ThreadLocal和Thread类

在Thread类里面,可以发现两个ThreadLocal.ThreadLocalMap类型的变量

/* 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;

那他们的关系其实可以抽象为下图:
在这里插入图片描述
我们先看一下get方法的代码

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;
            }
        }
        return setInitialValue();
    }

然后看一下set方法的代码实现

 public void set(T value) {
 		// 获取当前线程
        Thread t = Thread.currentThread();
        // 以当前线程作为参数,去获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

我们发现,这两个方法都是获取到当前线程,然后当前线程为参数,调用getMap方法,对返回来的ThreadLocal结果操作,那么我们看一下getMap方法具体实现:

ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

        这个方法返回的就是我们上面说的,线程Thread里面定义的threadLocals参数的值,那么到这一步,应该大体清楚了两者之间的关系和实现了。
        Thread类中有两个变量threadLocals和inheritableThreadLocals,二者都是ThreadLocal内部类ThreadLocalMap(可以理解为类似HashMap一样的key/value结构数据)类型的变量,在默认情况下,每个线程中的这两个变量都为null。当线程第一次调用ThreadLocal的set或者get方法的时候才会创建他们。而且,每个线程的本地变量不是存放在ThreadLocal实例中,而是放在调用线程的threadLocals变量里面。也就是说,ThreadLocal类型的本地变量是存放在具体的线程空间上,其本身相当于一个装载本地变量的工具壳,通过set方法将value添加到调用线程的threadLocals中,当调用线程调用get方法时候能够从它的threadLocals中取出变量。
        那么根据我们的判断,remove方法是不是也是操作threadLocals变量,带着这个猜测去看源码:

public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

        发现果然也是获取到当前线程的threadLocals,然后把对应key的值删除

分析set方法的实现

 public void set(T value) {
 		// (1)获取当前线程
        Thread t = Thread.currentThread();
        // (2)以当前线程作为参数,去获取ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        // (3)如果map不为null,就直接添加本地变量,key为当前线程,值为添加的本地变量值
        if (map != null)
            map.set(this, value);
        else
        // (4)如果map为null,说明是首次添加,需要首先创建出对应的map
            createMap(t, value);
    }

        第2步getMap分析了,我们先从第4步,当map==null的时候表示首次进来,要创建map开始看

void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

        createMap方法创建了threadLocals,并且将本地变量值添加到了threadLocals中

ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			// 创建一个数组,初始容量为16
            table = new Entry[INITIAL_CAPACITY];
            // 这是一个根据哈希码和数组长度求元素放置的位置下标的算法
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            // 创建一个Entry节点,放在数组对应坐标位置
            table[i] = new Entry(firstKey, firstValue);
            // 初始化,所以数组长度为1
            size = 1;
            setThreshold(INITIAL_CAPACITY);
        }

        然后我们再看第3步,当map!=null的时候,更新对应的值

/**
         * Set the value associated with key.
         *
         * @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计算当前key在数组中的位置索引
            int i = key.threadLocalHashCode & (len-1);
			// 循环遍历数组
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                ThreadLocal<?> k = e.get();
				// 如果key相等,更新value值
                if (k == key) {
                    e.value = value;
                    return;
                }
				// 如果key==null,表示脏数据,线性替换解决hash冲突
                if (k == null) {
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }
			// 说明新的数据不在Entry[]数组里面,这时候就添加到数组里面就好了
            tab[i] = new Entry(key, value);
            // 数组长度+1
            int sz = ++size;
            // 当数组长度超过临界值的时候,进行扩容,类比hanshMap的reHash理解
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

        在这段代码里面,可能很多人不理解keynull的情况,我们看代码e != null但是keynull,说明什么,说明这个Entry节点存在,但是key为空,value不为空,那就说明key过期被回收掉了(别忘了key是弱引用),那这就属于脏数据,而且原来的key和现在的key在同一位置,说明发生了hash冲突,replaceStaleEntry(key, value, i);就是用线性探测来解决hash冲突的,这里不再细讲。

分析get方法的实现

下面是get方法代码

 public T get() {
 		// 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程的变量threadLocals的值
        ThreadLocalMap map = getMap(t);
        if (map != null) {
        	// 根据当前ThreadLocal实例,获取对应的Entry节点
            ThreadLocalMap.Entry e = map.getEntry(this);
            // 如果e不为空,则取对应的value返回
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        // 如果e==null,表示直接在线程里面get()方法了,在之前没有先调用set()方法,所以在此处设置初始值到当前线程
        return setInitialValue();
    }

setInitialValue()此方法跟set方法其实一样,不再赘述

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;
    }
 private Entry getEntry(ThreadLocal<?> key) {
 			// 计算索引
            int i = key.threadLocalHashCode & (table.length - 1);
            Entry e = table[i];
            // 如果存在非空e,并且key相等,则返回
            if (e != null && e.get() == key)
                return e;
            else
            	// 如果存在空e,或者key不相等,进行判断
                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();
                // e不为空,key相等,返回e
                if (k == key)
                    return e;
                // e不为空,key==null,属于脏数据,删除脏数据,并且rehash
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

分析remove方法的实现

先看下remove方法代码

public void remove() {
		// 获取当前线程对应的threadLocals变量值
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
         	// 1 如果不为空,则删除当前ThreadLocal实例对应的节点
             m.remove(this);
     }

进入 m.remove(this);方法

private void remove(ThreadLocal<?> key) {
			// 获取当前Entry[]数组
            Entry[] tab = table;
            // 获取当前数组长度
            int len = tab.length;
            // 获取当前key对应的位置索引
            int i = key.threadLocalHashCode & (len-1);
            // 循环
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                if (e.get() == key) {
                	// 如果key相等,清空引用
                    e.clear();
                    // 删除脏数据节点,并且重新rehash
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

ThreadLocal存在的问题-内存泄漏

原因:
        ThreadLocal的内部是ThreadLocalMap。ThreadLocalMap内部是由一个Entry数组组成。Entry类的构造函数为 Entry(弱引用的ThreadLocal对象, Object value对象)。因为Entry的key是一个弱引用的ThreadLocal对象,所以在 垃圾回收 之前,将会清除此Entry对象的key。那么, ThreadLocalMap 中就会出现 key 为 null 的 Entry,就没有办法访问这些 key 为 null 的 Entry 的 value。这些 value 被Entry对象引用,所以value所占内存不会被释放。
解决:
        其实分析源码我们可以看到,设计者在get()/set()/remove()方法中都涉及到对脏数据(key为null的数据)的清除,所以在确定不会再使用ThreadLocal的线程最后,调用remove()方法就好了
为什么要把key设计成弱引用?
        不妨反过来想想,如果使用强引用,当ThreadLocal对象的引用被回收了,ThreadLocalMap本身依然还持有ThreadLocal的强引用,如果没有手动删除这个key,则ThreadLocal不会被回收,所以只要当前线程不消亡,ThreadLocalMap引用的那些对象就不会被回收

相关学习链接:
弱引用的特点
线性探测解决Hash冲突

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).
            // 当前key的索引下标,因为key==null,定义为脏数据下标
            int slotToExpunge = staleSlot;
            // 循环向前查找,直到e==null为止
            for (int i = prevIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = prevIndex(i, len))
                if (e.get() == null)
                	// 每找到一个key==null的脏数据,把下标更改一下啊
                    slotToExpunge = i;

            // Find either the key or trailing null slot of run, whichever
            // occurs first
             // 循环向前查找,直到e==null为止
            for (int i = nextIndex(staleSlot, len);
                 (e = tab[i]) != null;
                 i = nextIndex(i, len)) {
                ThreadLocal<?> k = e.get();

                // If we find key, then we need to swap it
                // with the stale entry to maintain hash table order.
                // The newly stale slot, or any other stale slot
                // encountered above it, can then be sent to expungeStaleEntry
                // to remove or rehash all of the other entries in run.			
                 // 当key相等的时候,更新value
                if (k == key) {
                    e.value = value;
					// 交换值
					// tab[staleSlot]是当前要更新的值,按照key所计算出来的节点位置,但是当前为值是在那个数据,key==null,后面cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);方法会把当前为止的数据清楚,把当前节点e = null;
					// tab[i]是当前key值相等的位置节点,并且已经更新了这个节点的value值
				 // 如果不交换位置的话,下次更新这个节点的时候,当前节点e=null,就不会更新到tab[i]的位置的值,而是直接在此位置new Entry插入进去,这样就有重复的值了
					
                    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 we didn't find stale entry on backward scan, the
                // first stale entry seen while scanning for key is the
                // first still present in the run.
                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);
        }

本文是综合自己的认识和参考各类资料(书本及网上资料)编写,若有侵权请联系作者,所有内容仅代表个人认知观点,如有错误,欢迎校正; 邮箱:1354518382@qq.com 博客地址:https://blog.csdn.net/qq_35576976/

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
ThreadLocalJava中的一个线程本地变量,它提供了一种简单的解决多线程并发访问共享变量的方案。每个ThreadLocal对象维护了一个独立的变量副本,每个线程都可以访问自己的变量副本,从而避免了多线程之间对同一变量的竞争。 ThreadLocal原理可以简单概括为以下几个步骤: 1. 每个Thread对象内部都有一个ThreadLocalMap对象,ThreadLocalMap是ThreadLocal的核心数据结构,用于存储每个ThreadLocal对象对应的变量副本。 2. 在ThreadLocal对象中调用set()方法时,ThreadLocal首先获取当前线程的ThreadLocalMap对象,然后将当前ThreadLocal对象作为key,将要存储的变量副本作为value,存储到ThreadLocalMap中。 3. 在ThreadLocal对象中调用get()方法时,ThreadLocal首先获取当前线程的ThreadLocalMap对象,然后以当前ThreadLocal对象作为key获取对应的变量副本,从而实现了多线程之间的变量隔离。 4. 在ThreadLocal对象中调用remove()方法时,ThreadLocal首先获取当前线程的ThreadLocalMap对象,然后将当前ThreadLocal对象从ThreadLocalMap中删除。 需要注意的是,由于ThreadLocalMap是存储在线程中的,因此需要注意内存泄漏的问题。如果在ThreadLocal对象使用结束后没有调用remove()方法,会导致ThreadLocal对象一直存在于ThreadLocalMap中,从而引起内存泄漏。因此,在使用ThreadLocal对象时,需要注意及时调用remove()方法,以避免内存泄漏问题。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值