Java基础- ThreadLocal

ThreadLocal的数据结构

在这里插入图片描述

  • 有点像HashMap,可以保存"key : value"键值对,但是一个ThreadLocal只能保存一个,并且各个线程的数据互不干扰。
    • 在线程1中初始化了一个ThreadLocal对象localName,并通过set方法,保存了一个值,同时在线程1中通过 localName.get()可以拿到之前设置的值,但是如果在线程2中,拿到的将是一个null。
    • 可以看下面的set(T value)和 get()方法的源码
      • 可以发现,每个线程中都有一个 ThreadLocalMap数据结构,当执行set方法时,其值是保存在当前线程的 threadLocals变量中,当执行set方法中,是从当前线程的 threadLocals变量获取。
      • 所以在线程1中set的值,对线程2来说是拿不到的,而且在线程2中重新set的话,也不会影响到线程1中的值,保证了线程之间不会相互干扰。
  • ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)
  • ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。
  • 我们还要注意Entry, 它的key是ThreadLocal<?> k ,继承自WeakReference, 也就是我们常说的弱引用类型
使用场景

ThreadLocal用在多线程场景下,保存线程上下文信息,再任意需要的地方可以获取

线程安全的,避免某些情况下需要考虑线程安全同步带来的性能损失

  • 它能让线程拥有了自己内部独享的变量
  • 每一个线程可以通过get、set方法去进行操作
  • 可以覆盖initialValue方法指定线程独享的值
  • 通常会用来修饰类里private static final的属性,为线程设置一些状态信息,例如user ID或者Transaction ID
  • 每一个线程都有一个指向threadLocal实例的弱引用,只要线程一直存活或者该threadLocal实例能被访问到,都不会被垃圾回收清理掉
源码分析

threadLocal.get()

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;
        }
    }
    return setInitialValue();
}
  • 获取当前线程内部的ThreadLocalMap
  • map存在则获取当前ThreadLocal对应的value值
  • map不存在或者找不到value值,则调用setInitialValue,进行初始化

setInitialValue()

private T setInitialValue() {
    T value = initialValue();
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
    	// this指的是threadLocal
        map.set(this, value);
    else
        createMap(t, value);
    return value;
}

// 默认返回null
protected T initialValue() {
    return null;
}
  • 调用initialValue方法,获取初始化值【调用者可以通过覆盖该方法,设置自己的初始化值】
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

threadLocal.set(T value)

public void set(T value) {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
        createMap(t, value);
}
  • 获取当前线程内部的ThreadLocalMap
  • map存在则把当前ThreadLocal和value添加到map中
  • map不存在则创建一个ThreadLocalMap,保存到当前线程内部

threadLoacl.remove()

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

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;
        }
    }
}

// 清空threadMap中的key
public void clear() {
    this.referent = null;
}

// 清空threadMap中的value
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;
 }
  • 获取当前线程内部的ThreadLocalMap,存在则从map中删除这个ThreadLocal对象,将指向这个threadLoacl的弱引用清理掉。
ThreadLocalMap
static class ThreadLocalMap {
 
     // hash map中的entry继承自弱引用WeakReference,指向threadLocal对象
     // 对于key为null的entry,说明不再需要访问,会从table表中清理掉
     // 这种entry被成为“stale entries”
     static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         Object value;
 
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }

    private static final int INITIAL_CAPACITY = 16;

    private Entry[] table;

    private int size = 0;

    private int threshold; // Default to 0

    private void setThreshold(int len) {
        threshold = len * 2 / 3;
    }

    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类型的数组,初始化大小为16,阈值threshold为数组长度的2/3,Entry类型为WeakReference,有一个弱引用指向ThreadLocal对象。
  • 为了能够保存大量且存活时间较长的threadLocal实例,hash table entries采用了WeakReferences作为key的类型, 一旦hash table运行空间不足时,key为null的entry就会被清理掉

为什么Entry采用WeakReference类型?

Java垃圾回收时,看一个对象需不需要回收,就是看这个对象是否可达。什么是可达,就是能不能通过引用去访问到这个对象。
jdk1.2以后,引用就被分为四种类型:强引用、弱引用、软引用和虚引用。强引用就是我们常用的Object obj = new Object(),obj就是一个强引用,指向了对象内存空间。当内存空间不足时,Java垃圾回收程序发现对象有一个强引用,宁愿抛出OutofMemory错误,也不会去回收一个强引用的内存空间。而弱引用,即WeakReference,意思就是当一个对象只有弱引用指向它时,垃圾回收器不管当前内存是否足够,都会进行回收。反过来说,这个对象是否要被垃圾回收掉,取决于是否有强引用指向。ThreadLocalMap这么做,是不想因为自己存储了ThreadLocal对象,而影响到它的垃圾回收,而是把这个主动权完全交给了调用方,一旦调用方不想使用,设置ThreadLocal对象为null,内存就可以被回收掉。

内存泄漏问题

当使用ThreadLocal保存一个value时,会在ThreadLocalMap中的数组插入一个Entry对象,按理说key-value都应该以强引用保存在Entry对象中,但在ThreadLocalMap的实现中,key被保存到了WeakReference对象中。
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value就有可能一直得不到回收,发生内存泄露。

public class MemoryLeak {
     public static void main(String[] args) {
         new Thread(new Runnable() {
             @Override
             public void run() {
                 for (int i = 0; i < 1000; i++) {
                     TestClass t = new TestClass(i);
                     t.printId();
                    t = null;
                }
            }
        }).start();
    }

    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

// 运行结果
0
1
2
3
...省略...
440
441
442
443
444
Exception in thread "Thread-0" java.lang.OutOfMemoryError: Java heap space
    at com.gentlemanqc.MemoryLeak$TestClass.<init>(MemoryLeak.java:33)
    at com.gentlemanqc.MemoryLeak$1.run(MemoryLeak.java:16)
    at java.lang.Thread.run(Thread.java:745)

对上面的代码稍作修改

public class MemoryLeak {
     public static void main(String[] args) {
         new Thread(new Runnable() {
             @Override
             public void run() {
                 for (int i = 0; i < 1000; i++) {
                     TestClass t = new TestClass(i);
                     t.printId();
                    t.threadLocal.remove();
                }
            }
        }).start();
    }

    static class TestClass{
        private int id;
        private int[] arr;
        private ThreadLocal<TestClass> threadLocal;
        TestClass(int id){
            this.id = id;
            arr = new int[1000000];
            threadLocal = new ThreadLocal<>();
            threadLocal.set(this);
        }

        public void printId(){
            System.out.println(threadLocal.get().id);
        }
    }
}

// 运行结果
0
1
2
3
...省略...
996
997
998
999
  • 一个内存泄漏,一个正常完成,对比代码只有一处不同:t = null改为了t.threadLocal.remove();
  • 示例中执行一次for循环里的代码后,对应的内存状态:
    在这里插入图片描述
  • t为创建TestClass对象返回的引用,临时变量,在一次for循环后就执行出栈了
  • thread为创建Thread对象返回的引用,run方法在执行过程中,暂时不会执行出栈
  • 调用t=null后,虽然无法再通过t访问内存地址,但是当前线程依旧存活,可以通过thread指向的内存地址,访问到Thread对象,从而访问到ThreadLocalMap对象,访问到value指向的内存空间,访问到arr指向的内存空间,从而导致Java垃圾回收并不会回收int[1000000]@541这一片空间。那么随着循环多次之后,不被回收的堆空间越来越大,最后抛出java.lang.OutOfMemoryError: Java heap space。

为什么调用remove可以防止内存泄漏
在这里插入图片描述
来看下调用remove方法之后的内存状态:
在这里插入图片描述

  • 因为remove方法将referent和value都被设置为null,所以ThreadLocal@540和Memory$TestClass@538对应的内存地址都变成不可达,Java垃圾回收自然就会回收这片内存,从而不会出现内存泄漏的错误

ThreadLocalMap之番外篇

ThreadLocalMap如何降低哈希冲突

回顾ThreadLocalMap添加元素的源码:

  • 方式一:构造方法
  • 方式二:set方法

其中i就是ThreadLocal在ThreadLocalMap中存放的索引,计算方式为:key.threadLocalHashCode & (len-1)。我们先来看threadLocalHashCode是什么?

private final int threadLocalHashCode = nextHashCode();

也就是说,每一个ThreadLocal都会根据nextHashCode生成一个int值,作为哈希值,然后根据这个哈希值&(数组长度-1),从而获取到哈希值的低N位(以len为16,16-1保证低四位都是1,从而获取哈希值本身的低四位值),从而获取到在数组中的索引位置。那它是如何降低哈希冲突的呢?玄机就在于这个nextHashCode方法。

private static AtomicInteger nextHashCode = new AtomicInteger();

private static final int HASH_INCREMENT = 0x61c88647;

private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

0x61c88647是什么?转化为十进制是1640531527。2654435769转换成int类型就是-1640531527。2654435769等于(根号5-1)/2乘以2的32次方。(根号5-1)/2是什么?是黄金分割数,近似为0.618。也就是说0x61c88647理解为一个黄金分割数乘以2的32次方。有什么好处?它可以神奇的保证nextHashCode生成的哈希值,均匀的分布在2的幂次方上,且小于2的32次方。来看例子:

public class ThreadLocalHashCodeTest {
 
     private static AtomicInteger nextHashCode =
             new AtomicInteger();
 
     private static final int HASH_INCREMENT = 0x61c88647;
 
     private static int nextHashCode() {
         return nextHashCode.getAndAdd(HASH_INCREMENT);
    }

    public static void main(String[] args){
        for (int i = 0; i < 16; i++) {
            System.out.print(nextHashCode() & 15);
            System.out.print(" ");
        }
        System.out.println();
        for (int i = 0; i < 32; i++) {
            System.out.print(nextHashCode() & 31);
            System.out.print(" ");
        }
        System.out.println();
        for (int i = 0; i < 64; i++) {
            System.out.print(nextHashCode() & 63);
            System.out.print(" ");
        }
    }
}

// 输出
0 7 14 5 12 3 10 1 8 15 6 13 4 11 2 9 
16 23 30 5 12 19 26 1 8 15 22 29 4 11 18 25 0 7 14 21 28 3 10 17 24 31 6 13 20 27 2 9 
16 23 30 37 44 51 58 1 8 15 22 29 36 43 50 57 0 7 14 21 28 35 42 49 56 63 6 13 20 27 34 1 48 55 62 5 12 19 26 33 40 47 54 61 4 11 18 25 32 39 46 53 60 3 10 17 24 31 38 45 52 59 2 9 

元素索引值完美的散列在数组当中,并没有出现冲突

如何解决哈希冲突
ThreadLocalMap采用黄金分割数的方式,大大降低了哈希冲突的情况,但是这种情况还是存在的,那如果出现,它是怎么解决的呢?请看:
threadLocalMap.set(K k, V v)

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();

        // 如果是同一个对象,则覆盖value值
        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();
}

当出现哈希冲突时,它的做法看是否是同一个对象或者是是否可以替换,否则往后移动一位,继续判断

private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

扩容
通过set方法里的代码,我们知道ThreadLocalMap扩容有两个前提:

  • !cleanSomeSlots(i, sz)
  • size >= threshold

元素个数大于阈值进行扩容,这个很好理解,那么还有一个前提是什么意思呢?我们来看cleanSomeSlots()做了什么:

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;
}
  • 方法上注释写的很明白,从当前插入元素位置,往后扫描数组中的元素,判断是否是“stale entry”。在前面将ThreadLocalMap类声明信息的时候讲过,“stale entry”表示的是那些key为null的entry。cleanSomeSlots方法就是找到他们,调用expungeStaleEntry方法进行清理。如果找到,则返回true,否则返回false。
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;
}
  • entry不为空,key为空时,调用expungeStaleEntry(int index)把当前节点删除,size–, 再for循环遍历所有节点,清除所有key为空的节点,返回最后一个清除的节点下标i。
    如果有清除节点操作也就是removed=true,那么就不会扩容操作,否则调用rehash()。此时还未扩容:
private void rehash() {
    // 先清理stale entry,会导致size变化
    expungeStaleEntries();

    // 如果size大于等于3/4阈值,则扩容
    if (size >= threshold - threshold / 4)
        resize();
}

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);
    }
}

rehash方法中会再次判断是否有无效节点(e != null && e.get() == null),如果有就清理无效节点,如果清理之后,如果size大于等于3/4阈值,则扩容

/**
* 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;
}
  • 两倍长度扩容,重新计算索引,扩容的同时也顺便清理了key为null的元素,即stale entry,不再存入扩容后的数组中。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值