Java--ThreadLoacal类

ThreadLocal类

1.ThreadLocal定义

This class provides thread-local variables. These variables
differ from their normal counterparts in that each thread that
accesses one (via its get or set method) has its own,
independently initialized copy of the variable. ThreadLocal
instances are typically private static fields in classes that
wish to associate state with a thread (e.g.,a user ID or
Transaction ID).
 

上述生涩的英文是JavaDoc中对ThreadLocal的解释
接下来我们用自己的话翻译一下

ThreadLocal类的作用是提供一个属于线程的局部变量,这个局部变量在每个线程中都有不同的副本,可以通过get()和set()方法从里面得到或者设置这个局部变量。我们可以独立设置这个副本的初始值。ThreadLocal实例希望将类中的私有的 静态的成员的状态与线程联系起来。
我认为我翻译的也不是特别准确,所以将最正宗的、没有二次加工过的英文原文展示出来,希望大家能更好的理解。
在这里插入图片描述每一线程单独对应一个ThreadLocalMap

2.ThreadLocal的基本使用

下面的的内容仅仅从用法角度说明。拒听细节请您看源码分析

public class Test1 {
	public static void main(String[] args) {
		ThreadLocal threadLocal = new ThreadLocal<Integer>();
		
		int num = 5;
	
		threadLocal.set(num);
		System.out.println("num: " + threadLocal.get());
	}	
}

运行结果:
在这里插入图片描述如果将Test1中的threadLocal传入另一个线程中,那我们还会得到我们set进去的值吗?答案肯定是得不到的,因为set进去的值属于主线程,在其他线程是得不到的。threadLocal中提供的是属于线程的变量。

public class Test1 {
	public static void main(String[] args) {

		ThreadLocal threadLocal = new ThreadLocal<Integer>();
		
		int num = 5;
	
		threadLocal.set(num);
		System.out.println("num: " + threadLocal.get());
		new Test2(threadLocal);
	}	
	

}

class Test2 extends Thread{
	ThreadLocal local = new ThreadLocal<Integer>();
	public Test2(ThreadLocal local) {
		this.local = local;
		System.out.println("在主线程中输出:" + local.get());
		//开启一个子线程
		start();
	}

	@Override
	public void run() {
		System.out.println("子线程中输出:" + local.get());
	}
	
	
}

运行结果:
在这里插入图片描述如果我没有set值进去,直接threadLocal.get()并且输出它,毫无疑问输出的肯定是null,如果我们想默认让他输出一个我们给定的值,请看下面的代码:

public class Test1 {
	public static void main(String[] args) {
		//给get()方法提供一个在该线程中没有执行set()方法的前提下所得到的初始值,这个初始值并不会仅仅便随当前线程,而是伴随与这个ThreadLocal的实例。
		ThreadLocal threadLocal = new ThreadLocal<Integer>() {

			@Override
			protected Integer initialValue() {
				
				return 100;
			}
			
		};
		new Test2(threadLocal);
	}	
	

}

class Test2 extends Thread{
	ThreadLocal local = new ThreadLocal<Integer>();
	public Test2(ThreadLocal local) {
		this.local = local;
		System.out.println("在主线程中输出:" + local.get());
		//开启一个子线程
		start();
	}

	@Override
	public void run() {
		System.out.println("子线程中输出:" + local.get());
	}
	
	
}

运行结果:
在这里插入图片描述

3.ThreadLocal部分源码解析

3.1ThreadLocal的一些基本属性

    //每一个ThreadLocal对象都对应一个HashCode,在ThreadLocalMap中有用
    private final int threadLocalHashCode = nextHashCode();

    /**
     得到下一个HashCode,第一次得到的是0
     */
    private static AtomicInteger nextHashCode =
        new AtomicInteger();

    /**
     0x61c88647是斐波那契散列乘数,它的优点是通过它散列(hash)出来的结果分布会比较均匀,可以很大程度上避免hash冲突,总之没必要太纠结为问什么是这个数
     */
    private static final int HASH_INCREMENT = 0x61c88647;

    /**
     *返回下一个HashCode,每次的值增加0x61c88647,
     */
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

3.2 ThreadLocalMap

ThreadLocal
ThreadLocalMap是一个自定义的哈希散列表,该表是一个Entry类型的数组,每一个Entry对象都对应着一对键值,键为Treadlocal对象,值为Object类型。
每一个Thread对象都拥有一个ThreadLocalMap对象,默认值为null
在这里插入图片描述ThreadLocalMap哈希表基本算法思想如下:

  1. 每一个ThreadLocal对象都有一个ThreadLocalHashCode,对这个数据与ThreadLocalMap的 容量 - 1取余,假设容量为16,就是拿ThreadLocalHashCode对15取余,我们最终会得到一个0~15的下标值,当然源码中为了提高运算效率,用的是与位运算。实际就是取余的意思。
  2. 不同的对象ThreadLocalHashCode虽然不同,但是也有可能计算出的下标是相同的,这就叫做哈希表的冲突问题。为了解决这种冲突,ThreadLocalMap并没有像HashMap一样用红黑树解决,而是用了一种相对简单的方式。如果产生了冲突,就将下标加一,如果还有冲突就继续加一,直到不冲突时,将Entry对象放入其中。这也就间接反映了一个事实,计算出相同下标的元素,在数组这个储存结构上j尽量是连续的。
    我认为ThreadLocalMap分为五个部分:
  3. 成员及其构造方法
  4. 删除模块
  5. 扩容模块
  6. set模块
  7. get模块
3.2.1成员及其构造方法
 /**
        Entry代表着一对键值
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;

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

        /**
         * 这个Map的初始容量
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * 存储多对键值
         */
        private Entry[] table;

        /**
         * table中实际拥有的元素个数
         */
        private int size = 0;

        /*
         * 负载因子,就是哈希表中一个反映空间使用情况的值,因为哈希表存在冲突
           即计算的下标相同的情况,如果数组剩余空间比较少,他的散列情况就会比较差,计算出相同下标的可能性就越大
           所以一般当一个容器使用了2/3容量时,会进行扩容         
         */
        private int threshold; 

        /**
         * Set the resize threshold to maintain at worst a 2/3 load factor.
         调整threshold负载因子
         */
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }

        /**
         * 返回一个下标
           如果i + 1比len小,返回i + 1,否则返回0,这样可以构成一个循环数组
         */
        private static int nextIndex(int i, int len) {
            return ((i + 1 < len) ? i + 1 : 0);
        }

        /**
         * 返回一个下标
           如果i - 1大于等于0,返回 i - 1,否则返回len - 1,这样可以构成一个循环数组
         */
        private static int prevIndex(int i, int len) {
            return ((i - 1 >= 0) ? i - 1 : len - 1);
        }

        /**
         * 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.
         */
        /*
        构造方法
        第一个参数作为ThreadLocalMap的键
        第二个参数作为ThreadLocalMap的值
        */
        ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
            //初始化Entry数组,容量为初始大小16
            table = new Entry[INITIAL_CAPACITY];
            /*
            使用上面提到threadLocalHashCode的值来计算哈希码
            算法为取出threadLocalHashCode的低4位,二进制的四位可以表示0~15,刚好对应容量为16的数组的每个下标
            这也是问什么初始化容量必须是2的幂次方
            就拿四位比特来说 
            二进制 1000 对应 十进制 8
            将任意一个数和  B1000 - 1 进行与为运算, 皆有可能得到0~B1000 - 1即0~7的人一个数,数的值取决于那个任意数的低三位
            说白了这是用位运算来表示取余运算%
            */  
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //将一对键值放入数组中
            table[i] = new Entry(firstKey, firstValue);
            //因为这是第一次加入,所以一开始size = 0,加入一个后size = 1
            size = 1;
            //计算新的负载因子 16 * 2/3 = 10
            setThres10hold(INITIAL_CAPACITY);
        }

        /**
         *构造方法
         使用一个ThreadLocalMap对象来初始化
         */
        private ThreadLocalMap(ThreadLocalMap parentMap) {
            //得到所有的键值对
            Entry[] parentTable = parentMap.table;
            //得到数组的长度
            int len = parentTable.length;
            //使用这个长度计算负载因子
            setThreshold(len);
            //初始化一张新表
            table = new Entry[len];

            /*
            遍历parentTable
            以为Entry对象本质是一个指针,如果把它直接放入新的表中,是很不合理的。
            因为这两个表的元素都指同一个空间。
            所以要新初始化Entry对象,把父Entry对象的值传入新Entry对象,最后将新Entry对象传入新表
            */
            for (int j = 0; j < len; j++) {
                Entry e = parentTable[j];
                if (e != null) {
                    @SuppressWarnings("unchecked")
                    ThreadLocal<Object> key = (ThreadLocal<Object>) e.get();
                    if (key != null) {
                        Object value = key.childValue(e.value);
                        Entry c = new Entry(key, value);
                        //c 放入表中的位置还是由它threadLocalHashCode来决定
                        int h = key.threadLocalHashCode & (len - 1);
                        //计算出下标后,先判断下表的在数组中对应的位置有没有元素
                        //有的话下标++
                        while (table[h] != null)
                            h = nextIndex(h, len);
                        table[h] = c;
                        //成功加入后,实际元素数量++
                        size++;
                    }
                }
            }
        }
3.2.2 删除模块

有一个名词在JavaDoc中的叫做stale Entries,翻译过来差不多时陈旧条目的意思,我们姑且叫它陈旧条目。
陈旧条目:陈旧条目的来源就是来自remove方法,该方法中有一条代码 e.clear();大家看一下源码:
在这里插入图片描述clear方法并不是将entry对象删除,而是将对象中的Entry的键赋值为null,所以Entry[]数组中的Entry对象依然存在,并没有删除,所以留下的这不完整的Entry对象就叫做陈旧条目。


/**
         remove方法就是陈旧条目的来源,因为对于一个 Entry e对象
         e.clear()作用是将键的值改为null。
         */
        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中的键赋值为null
                    e.clear();
                    //将陈旧条目清除
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

expungeStaleEntry方法
它不仅有清除陈旧条目的功能,还可以保证相同哈希码的元素的连续性

/*
        删除下标为staleSlot的Entry对象
        删除按道理来说直接删掉就可以,但是这么想就错了,
        这个哈希表有一个特点,相同哈希码的Entry对象一定是连续的,
        如果你删除了一个,有可能会破坏这种连续性,所以要将后面的对象统一前移
         */
        private int expungeStaleEntry(int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;

            /*
            只有Entry对象中的键为空时才会调用这个发法
            将值变为null
            将Entry赋值为null
            这就算删除了一个Entry对象。所以size--
            */
            tab[staleSlot].value = null;
            tab[staleSlot] = null;
            size--;

            /*
                从该 下标 + 1 开始遍历这个循环数组,直至下标所对应的对象为null
                如果存在Entry对象,但是对象中的键为null;
                就把这个对象删除

                如果Entry对象完整,即键不为null
                取出它的 threadLocalHashCode 求出哈希码
                求出的下标h如果和当前的遍历的下标不一致,
                将它以
                while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                这种方式重新加入数组,并且删除掉以前的
            */
            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;

                        while (tab[h] != null)
                            h = nextIndex(h, len);
                        tab[h] = e;
                    }
                }
            }
            //最终返会一个空间为null的下标
            return i;
        }

expungeStaleEntries()方法
清楚所有的陈旧条目

		/**
         * 删除有陈旧的条目
           陈旧的条目:存在Entry对象,但是键是null
         */
        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);
            }
        }
    }
3.2.3 扩容模块

resize()方法

 /**
         *这个方法用于扩容,每次扩容会扩容两倍
         */
        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; // 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;
        }

rehash()方法
把基本的扩容操作再封装一下,扩容前先清除所有的陈旧条目

/**
         先清除所有的陈旧条目
         如果实际拥有的元素数量大于threshold - threshold / 4时我们进行扩容
         如果抛开int类型取整的特点,用纯数学的角度来分析:
         threshold = 2/3 len
         threshold - threshold / 4 = 1/2 len
         也就是说容器的容量用到大约一半时,会进行扩容
         */
        private void rehash() {
            expungeStaleEntries();

            // Use lower threshold for doubling to avoid hysteresis
            if (size >= threshold - threshold / 4)
                resize();
        }
3.2.4 set模块

该模块主要是对Hash表存入值

set()方法

  /**
        将一对键值加入到哈希表中
         */
        private void set(ThreadLocal<?> key, Object value) {
            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();

                if (k == key) {
                    //如果键以存在则覆盖值
                    e.value = value;
                    return;
                }

                //Entry对象存在但是键不存在
                if (k == null) {
                    //跳过陈旧条目接着往下找,有没有存在同样键Entry对象
                    //如果存在进行值覆盖,并把该对象与陈旧条目替换位置,然后将陈旧条目清楚
                    replaceStaleEntry(key, value, i);
                    return;
                }
            }

            tab[i] = new Entry(key, value);
            int sz = ++size;
            //这里算是真正的扩容操作开始的地方
            //新加入的位置之后的几个元素如果没有陈旧条目 并且 size > 负载因子 就执行扩容
            if (!cleanSomeSlots(i, sz) && sz >= threshold)
                rehash();
        }

replaceStaleEntry()方法

  /*
        参数1 2是一对键值
        参数3 :下标
        */
        private void replaceStaleEntry(ThreadLocal<?> key, Object value,
                                       int staleSlot) {
            Entry[] tab = table;
            int len = tab.length;
            Entry e;

            //slotToExpunge可能与staleSlot相等,也有可能是离staleSlot逆时针最远的陈旧条目的下标
            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
            //从该成就条目开始,遍历数组,直至遍历到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.
            /*
            如果键相等,将值放入其中,把陈旧条目与当下做调换
            */
                if (k == key) {
                    e.value = value;

                    tab[i] = tab[staleSlot];
                    tab[staleSlot] = e;

                    //删除下标为i的陈旧条目
                    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;
            }

            //没有相同的键,将该陈旧条目直接替换成我们set进去的那对键值
            tab[staleSlot].value = null;
            tab[staleSlot] = new Entry(key, value);

            // 如果还有其他的陈旧条目,我们要对此进行清除
            if (slotToExpunge != staleSlot)
                cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
        }

cleanSomeSlots()方法

		/*
		如果陈旧条目被删除 返回true,
        清除从i+ 1开始的若干个陈旧条目
        */
        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;
        }
3.2.5 get模块

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方法

        /**
            参数1:ThreadLocal<?> key,键
            参数2:通过key对象中的threadLocalHashCode计算得到的下表
            参数3:通过该下表在数组中对应的Entry对象
            通过方法名getEntryAfterMiss我们就知道这个方法是的作用:
                在第一次通过下标直接查找没找到后接着找。
         */
        private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;

            /*
            如果 e == null 直接返回null,意思就是没找到
            */
            while (e != null) {
                //找到了e,判端是不是我们需要的e
                ThreadLocal<?> k = e.get();
                //是的话返回
                if (k == key)
                    return e;
                //如果存在Entry对象,但是里面的ThreadLocal<?>对象为
                if (k == null)
                    expungeStaleEntry(i);
                else
                    //该Entry对象并不是我们要找的,加标加1,接着找
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

3.3ThreadLocal

public T get() {
        //得到当前线程
        Thread t = Thread.currentThread();
        //得到当前线程的的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //当前线程ThreadLocalMap对象不为null
        if (map != null) {
            //将本类的对象作为键从该Map中读出Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            //Entry对象不为null
            if (e != null) {
                @SuppressWarnings("unchecked")
                //读出这对键值对的值
                T result = (T)e.value;
                //返回结果
                return result;
            }
        }
        //没有找到相关的ThreadLocalMap或者Entry对象时。
        //会把当前对象作为键,initialValue();的返回值作为值,加入到当前线程的Map中
        //如果连Map都没有,就使用这对键值初始化一个Map
        return setInitialValue();
    }

    /**
     * Variant of set() to establish initialValue. Used instead
     * of set() in case user has overridden the set() method.
     *
     * @return the initial value
     */
    private T setInitialValue() {
        //得到一个默认的初始值,如果你没覆盖initialValue方法,得到是null
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        //存在Map,将这个值set进去
        if (map != null)
            map.set(this, value);
        else
            //没有Map我们创建它
            createMap(t, value);
        //返回initialValue方法的返回值
        return value;
    }

    /**
     * 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.
     */
    public void set(T value) {
        //得到当前线程
        Thread t = Thread.currentThread();
        //得到当前线程的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        //存在map将键值对
        if (map != null)
            map.set(this, value);
        else
            //没有Map创建Map,并把键值对set进去
            createMap(t, value);
    }

    /**
     * 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
     */
     //删除当前线程的ThreadLocalMap中的键为当前ThreadLocal对象的那对键值对
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param  t the current thread
     * @return the map
     */
    //得到当前线程的ThreadLocalMap对象
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     *
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    //使用一对键值对当前线程中创建一个ThreadLocalMap对象
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值