Java并发编程 --- ThreadLocal

ThreadLocal叫做线程本地变量。

作用:每个线程往ThreadLocal中读写是线程隔离的,互相之间不会印象

ThreadLocal为什么是线程安全的

ThreadLocal存储数据时实际上是存储在ThreadLocalMap中,而每个线程自己都有一个ThreadLocalMap,所以没有线程安全问题

Thread

每个Thread内部都有一个ThreadLocalMap

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

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

ThreadLocalMap

Entry

      static class Entry extends WeakReference<ThreadLocal<?>> {
            //往ThreadLocal里面塞入的值
            Object value;

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

Entry是ThreadLocalMap的存储元素,其中它的key:ThreadLocal是弱引用。

为什么要使用弱引用

如果使用普通的key-value形式,会造成节点的生命周期跟线程的强绑定,这样就会影响到垃圾回收,而使用弱引用的话,在进行垃圾回收时,可以被回收,为垃圾回收提供了便利。

类变量

//初始容量 必须为2的幂
private static final int INITIAL_CAPACITY = 16;

private Entry[] table;

//table内的实际数量
private int size = 0;

//重新分配表大小的阙值
private int threshold; // Default to 0

table

image.png

我们把table数组在程序逻辑上理解为环形结构。

目的:使用线性探测法来解决散列冲突。

为什么使用线性探测法

主要是为了提高性能和减少内存碎片。

减少内存碎片:线性探测法在遇到冲突时,会在离哈希桶较近的位置查找,可以减少频繁插入和删除造成的内存空洞。

提高性能:单纯使用数组,可以有效利用CPU的缓存。

且ThreadLocal的应用场景本身只会存放自己的数据,所以数据量不会太多,造成的冲突概率较少。

为什么使用链表不能很好的利用CPU缓存:

链表的每个节点在内存中通常都是不连续的,这种不连续,所以需要多次访问内存,降低了缓存的命中率。

为什么使用数组能够很好的利用CPU缓存:

数组是内存紧凑型的,在64位计算机的前提下,有int arr[100],访问int[0],int大小占了4个字节,而CPU Cache有64字节,所以可以缓存16个,也就是0~15。增大了缓存的命中率。

线性探测法

插入:如果发现要插入的槽位有元素,那就往后移动,直到找到空位置。

删除:计算相应的槽位,并跟相应的槽位的key进行比较,直到key相同,删除相应槽位的元素,同时该槽位后面的元素要进行rehash。如下图

原始:

image.png

删除:

image.png

rehash:

image.png

关于删除的具体代码

简单来说:往后遍历,遇到空为止,在遍历过程中,如果发现该Entry的key==null,表示可以继续清除,将他的value和自身都设置为null,如果发现key不为null,则证明它是还存在的,则需要对他进行rehash

/**
 * 这个函数是ThreadLocal中核心清理函数,它做的事情很简单:
 * 就是从staleSlot开始遍历,将无效(弱引用指向对象被回收)清理,即对应entry中的value置为null,将指向这个entry的table[i]置为null,直到扫到空entry。
 * 另外,在过程中还会对非空的entry作rehash。
 * 可以说这个函数的作用就是从staleSlot开始清理连续段中的slot(断开强引用,rehash slot等)
 */
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // 因为entry对应的ThreadLocal已经被回收,value设为null,显式断开强引用
    tab[staleSlot].value = null;
    // 显式设置该entry为null,以便垃圾回收
    tab[staleSlot] = null;
    size--;

    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len); (e = tab[i]) != null; i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 清理对应ThreadLocal已经被回收的entry
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            /*
             * 对于还没有被回收的情况,需要做一次rehash。
             * 
             * 如果对应的ThreadLocal的ID对len取模出来的索引h不为当前位置i,
             * 则从h向后线性探测到第一个空的slot,把当前的entry给挪过去。
             */
            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.
                 *
                 * 这段话提及了Knuth高德纳的著作TAOCP(《计算机程序设计艺术》)的6.4章节(散列)
                 * 中的R算法。R算法描述了如何从使用线性探测的散列表中删除一个元素。
                 * R算法维护了一个上次删除元素的index,当在非空连续段中扫到某个entry的哈希值取模后的索引
                 * 还没有遍历到时,会将该entry挪到index那个位置,并更新当前位置为新的index,
                 * 继续向后扫描直到遇到空的entry。
                 *
                 * ThreadLocalMap因为使用了弱引用,所以其实每个slot的状态有三种也即
                 * 有效(value未回收),无效(value已回收),空(entry==null)。
                 * 正是因为ThreadLocalMap的entry有三种状态,所以不能完全套高德纳原书的R算法。
                 *
                 * 因为expungeStaleEntry函数在扫描过程中还会对无效slot清理将之转为空slot,
                 * 如果直接套用R算法,可能会出现具有相同哈希值的entry之间断开(中间有空entry)。
                 */
                while (tab[h] != null) {
                    h = nextIndex(h, len);
                }
                tab[h] = e;
            }
        }
    }
    // 返回staleSlot之后第一个空的slot索引
    return i;
}

table的扩容机制

负载因子:2/3

当Entry的数量满足总容量的2/3时,就会触发table的扩容机制。

具体流程:

1、生成一个比原容量大一倍的Entry数组。

2、将原数组的所有元素进行rehash。

3、当原数组无数据后,使用新数组,销毁原数组。

        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 {
                        //rehash
                        int h = k.threadLocalHashCode & (newLen - 1);
                        while (newTab[h] != null)
                            h = nextIndex(h, newLen);
                        newTab[h] = e;
                        count++;
                    }
                }
            }

            setThreshold(newLen);
            size = count;
            table = newTab;
        }

实现环状的逻辑结构

把数据当成环主要的一点就是遍历

/**
 * 环形意义的下一个索引
 */
private static int nextIndex(int i, int len) {
    return ((i + 1 < len) ? i + 1 : 0);
}

/**
 * 环形意义的上一个索引
 */
private static int prevIndex(int i, int len) {
    return ((i - 1 >= 0) ? i - 1 : len - 1);
}

getEntry()方法

private Entry getEntry(ThreadLocal<?> key) {
    // 根据key这个ThreadLocal的ID来获取索引,也即哈希值
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    // 对应的entry存在且未失效且弱引用指向的ThreadLocal就是key,则命中返回
    if (e != null && e.get() == key) {
        return e;
    } else {
        // 因为用的是线性探测,所以往后找还是有可能能够找到目标Entry的。
        return getEntryAfterMiss(key, i, e);
    }
}

/*
 * 调用getEntry未直接命中的时候调用此方法
 */
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;
   
    // 基于线性探测法不断向后探测直到遇到空entry。
    while (e != null) {
        ThreadLocal<?> k = e.get();
        // 找到目标
        if (k == key) {
            return e;
        }
        if (k == null) {
            // 该entry对应的ThreadLocal已经被回收,调用expungeStaleEntry来清理无效的entry
            expungeStaleEntry(i);
        } else {
            // 环形意义下往后面走
            i = nextIndex(i, len);
        }
        e = tab[i];
    }
    return null;
}

set()方法

核心:找到一个槽位为null的可以进行存放。

但在找的过程中,有两种情况是可以直接使用不为null的槽位:

1、该槽位本身就是它自己的,那对于这个情况只需要进行值修改就可以。

2、该槽位的key为null,那就代表这个槽位其实已经没用了,只不过还没进行垃圾回收,所以存在。

若在两种情况都不符合,那遍历到最后,它也肯定找到了一个为null的槽位。

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;
            }
            //代表当前槽位没用只不过还没进行垃圾回收,可直接用该位置
            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 void replaceStaleEntry(ThreadLocal<?> key, Object value,
                               int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;
    Entry e;

    // 向前扫描,查找最前的一个无效slot
    int slotToExpunge = staleSlot;
    for (int i = prevIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = prevIndex(i, len)) {
        if (e.get() == null) {
            slotToExpunge = i;
        }
    }

    // 向后遍历table
    for (int i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();

        // 找到了key,将其与无效的slot交换
        if (k == key) {
            // 更新对应slot的value值
            e.value = value;

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

            /*
             * 如果在整个扫描过程中(包括函数一开始的向前扫描与i之前的向后扫描)
             * 找到了之前的无效slot则以那个位置作为清理的起点,
             * 否则则以当前的i作为清理起点
             */
            if (slotToExpunge == staleSlot) {
                slotToExpunge = i;
            }
            // 从slotToExpunge开始做一次连续段的清理,再做一次启发式清理
            cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
            return;
        }

        // 如果当前的slot已经无效,并且向前扫描过程中没有无效slot,则更新slotToExpunge为当前位置
        if (k == null && slotToExpunge == staleSlot) {
            slotToExpunge = i;
        }
    }

    // 如果key在table中不存在,则在原地放一个即可
    tab[staleSlot].value = null;
    tab[staleSlot] = new Entry(key, value);

    // 在探测过程中如果发现任何无效slot,则做一次清理(连续段清理+启发式清理)
    if (slotToExpunge != staleSlot) {
        cleanSomeSlots(expungeStaleEntry(slotToExpunge), len);
    }
}

/**
 * 启发式地清理slot,
 * i对应entry是非无效(指向的ThreadLocal没被回收,或者entry本身为空)
 * n是用于控制控制扫描次数的
 * 正常情况下如果log n次扫描没有发现无效slot,函数就结束了
 * 但是如果发现了无效的slot,将n置为table的长度len,做一次连续段的清理
 * 再从下一个空的slot开始继续扫描
 * 
 * 这个函数有两处地方会被调用,一处是插入的时候可能会被调用,另外个是在替换无效slot的时候可能会被调用,
 * 区别是前者传入的n为元素个数,后者为table的容量
 */
private boolean cleanSomeSlots(int i, int n) {
    boolean removed = false;
    Entry[] tab = table;
    int len = tab.length;
    do {
        // i在任何情况下自己都不会是一个无效slot,所以从下一个开始判断
        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;
}


private void rehash() {
    // 做一次全量清理
    expungeStaleEntries();

    /*
     * 因为做了一次清理,所以size很可能会变小。
     * ThreadLocalMap这里的实现是调低阈值来判断是否需要扩容,
     * threshold默认为len*2/3,所以这里的threshold - threshold / 4相当于len/2
     */
    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) {
            /*
             * 个人觉得这里可以取返回值,如果大于j的话取了用,这样也是可行的。
             * 因为expungeStaleEntry执行过程中是把连续段内所有无效slot都清理了一遍了。
             */
            expungeStaleEntry(j);
        }
    }
}

remove()方法

/**
 * 从map中删除ThreadLocal
 */
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;
        }
    }
}

内存溢出问题

内存溢出:我们在系统的堆内存中为对象申请一块空间,该对象使用完了,却无法进行GC回收,这样就会造成内存溢出。

ThreadLocal中存在内存溢出场景

image.png

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

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

原因

ThreadLocalMap使用ThreadLocal的弱引用作为Key,如果ThreadLocal没有被外部强引用的话,那么它的Key肯定会被GC,但是value还存在强引用,所以无法GC,这样存在的value就造成了内存泄漏。只有当Thread线程退出后,value的强引用链才会断掉。

强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value

为什么Key不使用强引用呢

如果Key使用强引用,那么当外部回收ThreadLocal时,因为ThreadLocalMap还持有ThreadLocal的强引用,而且Thread持有ThreadLocalMap的强引用,所以ThreadLocal不会被回收。

解决

养成好习惯,使用完调用remove方法。

具体实现操作:1、先把ThreadLocal弱引用删掉。2、同时再把value设置为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.refersTo(key)) {
              //本地方法 删除key的引用
              e.clear();
              //将value置为null
              expungeStaleEntry(i);
              return;
          }
      }
 }

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

ThreadLocal父子线程数据传递方案

使用InheritableThreadLocal

public static void main(String[] args) {
     InheritableThreadLocal<String> inheritableThreadLocal = new InheritableThreadLocal<>();
     inheritableThreadLocal.set("123");
     System.out.println("父线程的值:"+inheritableThreadLocal.get());
     new Thread(new Runnable() {
         @Override
         public void run() {
             System.out.println("子线程的值:" + inheritableThreadLocal.get());
         }
     }).start();
}

结果:
父线程的值:123
子线程的值:123

源码解析 - InheritableThreadLocal

public class InheritableThreadLocal<T> extends ThreadLocal<T> {
    protected T childValue(T parentValue) {
        return parentValue;
    }
    //可以看出使用的ThreadLocalMap是inheritableThreadLocals
    ThreadLocalMap getMap(Thread t) {
       return t.inheritableThreadLocals;
    }

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

改类继承了ThreadLocal,变化较大的就是获取ThreadLocalMap使用的是inheritableThreadLocals。

源码解析 - Thread的创建

//本质上调用了init进行线程的创建 
public Thread() {
    init(null, null, "Thread-" + nextThreadNum(), 0);
}

//有些比较长 就省略掉
private void init(ThreadGroup g, Runnable target, String name,
                      long stackSize, AccessControlContext acc,
                      boolean inheritThreadLocals) {
 // 重点是 这串代码 如果父线程的inheritableThreadLocals不为null,则会将父线程的inheritableThreadLocals进行浅拷贝
  if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

}

private ThreadLocalMap(ThreadLocalMap parentMap) {
          Entry[] parentTable = parentMap.table;
          int len = parentTable.length;
          setThreshold(len);
          table = new Entry[len];

          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);
                      int h = key.threadLocalHashCode & (len - 1);
                      while (table[h] != null)
                          h = nextIndex(h, len);
                      table[h] = c;
                      size++;
                  }
              }
          }
}

线程池下的问题

public class InheritableThreadLocalTest {

    public static void main(String[] args) throws Exception{
        final ThreadLocal<Person> threadLocal=new InheritableThreadLocal<>();
        threadLocal.set(new Person("1"));
        System.out.println("初始值:"+threadLocal.get());
        Runnable runnable=()->{
            System.out.println("----------start------------");
            System.out.println("父线程的值:"+threadLocal.get());
            threadLocal.set(new Person("2"));
            System.out.println("子线程覆盖后的值:"+threadLocal.get());
            System.out.println("------------end---------------");
        };
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
    }
}

结果:
初始值:Person{name='1'}
----------start------------
父线程的值:Person{name='1'}
子线程覆盖后的值:Person{name='2'}
------------end---------------
----------start------------
父线程的值:Person{name='2'}
子线程覆盖后的值:Person{name='2'}
------------end---------------
----------start------------
父线程的值:Person{name='2'}
子线程覆盖后的值:Person{name='2'}
------------end---------------

原因

image.png

造成这一问题的原因是:第一次提交任务时,线程A会浅拷贝主线程的ThreadLocal值,而往后提交的任务,会拷贝线程池中前一个线程的ThreadLocal值。

使用TransmittableThreadLocal

public class TransmittableThreadLocalTest{

    public static void main(String[] args)  throws Exception{
        final ThreadLocal<Person> threadLocal=new TransmittableThreadLocal<>();
        threadLocal.set(new Person("1"));
        System.out.println("初始值:"+threadLocal.get());
        Runnable task=()->{
            System.out.println("----------start------------");
            System.out.println("父线程的值:"+threadLocal.get());
            threadLocal.set(new Person("2"));
            System.out.println("子线程覆盖后的值:"+threadLocal.get());
            System.out.println("------------end---------------");
        };
        ExecutorService executorService= Executors.newFixedThreadPool(1);
        Runnable runnable= TtlRunnable.get(task);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
        TimeUnit.SECONDS.sleep(1);
        executorService.submit(runnable);
    }
}

运行结果:
结果:
初始值:Person{name='1'}
----------start------------
父线程的值:Person{name='1'}
子线程覆盖后的值:Person{name='2'}
------------end---------------
----------start------------
父线程的值:Person{name='1'}
子线程覆盖后的值:Person{name='2'}
------------end---------------
----------start------------
父线程的值:Person{name='1'}
子线程覆盖后的值:Person{name='2'}
------------end---------------

原理

public final class TtlRunnable implements Runnable {
    private final AtomicReference<Map<TransmittableThreadLocal<?>, Object>> copiedRef;
    private final Runnable runnable;
    private final boolean releaseTtlValueReferenceAfterRun;

    private TtlRunnable(Runnable runnable, boolean releaseTtlValueReferenceAfterRun) {
        //从父类copy值到本类当中
        this.copiedRef = new AtomicReference<Map<TransmittableThreadLocal<?>, Object>>(TransmittableThreadLocal.copy());
        this.runnable = runnable;//提交的runable,被修饰对象
        this.releaseTtlValueReferenceAfterRun = releaseTtlValueReferenceAfterRun;
    }
    /**     
      * wrap method {@link Runnable#run()}.    
      */
    @Override
    public void run() {
        //获取父类的拷贝值
        Map<TransmittableThreadLocal<?>, Object> copied = copiedRef.get();
        if (copied == null || releaseTtlValueReferenceAfterRun && !copiedRef.compareAndSet(copied, null)) {
            throw new IllegalStateException("TTL value reference is released after run!");
        }
        //装载到当前线程
        Map<TransmittableThreadLocal<?>, Object> backup = TransmittableThreadLocal.backupAndSetToCopied(copied);
        try {
            runnable.run();//执行提交的task
        } finally {
        //clear
        TransmittableThreadLocal.restoreBackup(backup);
        }
 }}
posted @   ayu0v0  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 震惊!C++程序真的从main开始吗?99%的程序员都答错了
· 【硬核科普】Trae如何「偷看」你的代码?零基础破解AI编程运行原理
· 单元测试从入门到精通
· 上周热点回顾(3.3-3.9)
· winform 绘制太阳,地球,月球 运作规律
点击右上角即可分享
微信分享提示