对于并发的学习-ThreadLocal

ThreadLocal对象可以提供线程局部变量,每个线程Thread拥有一份自己的副本变量,多个线程互不干扰。

ThreadLocal的数据结构

对于并发的学习-ThreadLocal_第1张图片

Thread类有一个类型为ThreadLocal.ThreadLocalMap的实例变量threadLocals,也就是说每个线程有一个自己的ThreadLocalMap。

ThreadLocalMap有自己的独立实现,可以简单地将它的key视作ThreadLocal,value为代码中放入的值(实际上key并不是ThreadLocal本身,而是它的一个弱引用)。

每个线程在往ThreadLocal里放值的时候,都会往自己的ThreadLocalMap里存,读也是以ThreadLocal作为引用,在自己的map里找对应的key,从而实现了线程隔离。

ThreadLocalMap有点类似HashMap的结构,只是HashMap是由数组+链表实现的,而ThreadLocalMap中并没有链表结构。

我们还要注意Entry, 它的key是ThreadLocal k ,继承自WeakReference, 也就是我们常说的弱引用类型。

GC后key是否为null

复习Java中的四种引用:

  • 强引用:我们常常new出来的对象就是强引用类型,只要强引用存在,垃圾回收器将永远不会回收被引用的对象,哪怕内存不足的时候
  • 软引用:使用SoftReference修饰的对象被称为软引用,软引用指向的对象在内存要溢出的时候被回收
  • 弱引用:使用WeakReference修饰的对象被称为弱引用,只要发生垃圾回收,若这个对象只被弱引用指向,那么就会被回收
  • 虚引用:虚引用是最弱的引用,在 Java 中使用 PhantomReference 进行定义。虚引用中唯一的作用就是用队列接收对象即将死亡的通知

看一下利用反射得到ThreadLocal中的情况和GC后的情况

public class ThreadLocalDemo { 
    public static void main(String[] args) throws NoSuchFieldException, IllegalAccessException, InterruptedException { 
        Thread t = new Thread(()->test("abc",false)); 
        t.start(); 
        t.join(); 
        System.out.println("--gc后--"); 
        Thread t2 = new Thread(() -> test("def", true)); 
        t2.start(); 
        t2.join(); 
    } 
    private static void test(String s,boolean isGC) { 
        try { 
            new ThreadLocal<>().set(s);
            if (isGC) { 
                System.gc(); 
            } 
            Thread t = Thread.currentThread(); 
            Class clz = t.getClass(); 
            Field field = clz.getDeclaredField("threadLocals"); 
            field.setAccessible(true); 
            Object ThreadLocalMap = field.get(t); 
            Class tlmClass = ThreadLocalMap.getClass(); 
            Field tableField = tlmClass.getDeclaredField("table");        
            tableField.setAccessible(true); 
            Object[] arr = (Object[]) tableField.get(ThreadLocalMap); 
            for (Object o : arr) { 
                if (o != null) { 
                    Class entryClass = o.getClass(); 
                    Field valueField =  entryClass.getDeclaredField("value"); 
                    Field referenceField = entryClass.getSuperclass().getSuperclass().getDeclaredField("referent");
                    valueField.setAccessible(true); 
                    referenceField.setAccessible(true);
                    System.out.println(String.format("弱引用key:%s,值:%s", referenceField.get(o), valueField.get(o))); 
                } 
            } 
        } catch (Exception e) { e.printStackTrace(); } 
    } 
}

输出的结果如下:

对于并发的学习-ThreadLocal_第2张图片

这里直接使用 new ThreadLocal<>().set(s);生成ThreadLocal,key只有个弱引用,GC后被回收。

如果这里使用对象引用一下 ThreadLocal threadLocal = new ThreadLocal<>(); threadLocal.set(s);

GC后因为threadLocal指向key存在一个强引用,key并不会被GC回收。

如果不使用强引用的话,GC回收掉key就只剩value不会被回收永远存在,出现内存泄漏。

ThreadLocalMap Hash算法

既然ThreadLocal存在一个key-value的Map结构,那ThreadLocalMap也会实现自己的hash算法来解决散列表数组冲突的问题。

int i = key.threadLocalHashCode & (len - 1);

ThreadLocalMap中的hash方法很简单就是key的hashCode和数组的长度 - 1做与操作,这里 i 就是对应数组的下标位置。

而关键的是threadLocalHashCode的计算,这里有使用一个属性叫做HASH_INCREMENT = 0x61c88647

每当创建一个ThreadLocal对象,这个ThreadLocal.nextHashCode值就增加0x61c88647。

而这个值非常特殊,它就是斐波那契数也就是黄金分割数。hash的增量是这个数字带来的好处就是hash会分布的非常均匀。

ThreadLocalMap的Hash冲突

即便是用黄金分割数作为hash计算因子,大大的减少了冲突的概率,但是仍然会可能存在冲突。

HashMap中解决冲突是使用了数组结构上又构建了一个链表,拉链式的解决的。

ThreadLocalMap并没有使用这种,它是使用一种线性的解决办法。

查询到 i 为如果当前位置的数据是有效的,比较key是否相同,相同就替换,不相同检查 i + 1位置是否为空不断的向后查找。

对于并发的学习-ThreadLocal_第3张图片

在这个过程中,如果遇到了key被回收的桶,也会执行一个replaceStaleEntry()方法,该方法含义是替换过期数据的逻辑,以index=7位起点开始遍历,进行探测式数据清理工作。

对于并发的学习-ThreadLocal_第4张图片

你可能感兴趣的:(学习,java,jvm)