9、详解ThreadLocal

目录

1、ThreadLocal

1.1 是什么

1.2 能干什么

2、ThreadLocal使用

2.1 API

2.2 API 使用

2.3 API 使用注意事项

2.4 不用remove方法的后果

2.5 小总结

3、ThreadLocal源码分析

3.1 Thread、ThreadLocal、ThreadLocalMap关系

4、ThreadLocal内存泄漏问题-非常重要

4.1 引出的问题

4.2 什么是内存泄漏

4.3 有几种引用(强引用、软引用、弱引用、虚引用)

4.4 强引用(默认支持模式)

4.5 软引用(SoftReference)

4.6 弱引用(weakReference)

4.7 虚引用(PhantomReterence)

4.8 四大引用小总结

5、为什么要用弱引用?不用如何?

5.1 为什么源代码用弱引用?

5.2 弱引用就万事大吉了吗? 

6、最佳实践

7、面试题

7.1 ThreadLocal中ThreadLoaclMap的数据结构和关系

7.2 ThreadLocal的key是弱引用,这是为什么?

7.3 ThreadLocal内存泄漏问题你知道吗?

7.4 如何解决ThreadLocal的内存泄漏? 

8、ThreadLocalMap 如何解决哈希冲突(补充)

9、ThreadLocalMap 初始容量以及扩容机制(补充)

9.1 初始容量

9.2 扩容机制


1、ThreadLocal

1.1 是什么

        ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。 ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。

1.2 能干什么

        实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),

        主要解决了让每个线程绑定自己的值,通过使用get()和set()方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题。

2、ThreadLocal使用

2.1 API

2.2 API 使用

需求:5个销售卖房子,集团高层只关心销售总量的准确统计数,按照总销售额统计,方便集团公司发奖金

群雄逐鹿起纷争——为了数据安全只能加锁

public class ThreadLocalDemo {
    public static void main(String[] args) {
        House house = new House();
        for(int i = 1;i <= 5;i ++){
            new Thread(()->{
                int size = new Random().nextInt(5) + 1;
                System.out.println(size);
                for(int j = 1;j <= size;j ++){
                    house.houseAdd();
                }
            },String.valueOf(i)).start();
        }

        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"共计卖出多少套:"+ house.count);
    }
}

class House{
    int count = 0;

    public synchronized void houseAdd(){
        ++count;
    }
}

需求变化:希望各自分灶吃饭,各凭销售本事提成,按照出单数各自统计;比如某房产中介销售都有自己的销售额指标,自己专属于自己的,不和别人掺和。

public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        House2 house = new House2();
        for(int i = 1;i <= 5;i ++){
            new Thread(()->{
                int size = new Random().nextInt(5) + 1;
                for(int j = 1;j <= size;j ++){
                    house.houseAdd();
                    house.saleVolumeByThreadLocal();
                }
                System.out.println(Thread.currentThread().getName()+"\t"+"号销售卖出:"+house.saleVolume.get());
            },String.valueOf(i)).start();
        }

        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+"\t"+"共计卖出多少套:"+ house.count);
    }
}
class House2{
    int count = 0;

    public synchronized void houseAdd(){
        ++count;
    }

    //两个都是创建一个线程局部变量并返回初始值
    /**
     * 一个比较老式的写法(这个阿里巴巴手册里也也有),initialValue()这个api已经淘汰了
     */
    /*ThreadLocal<Integer> saleVolume =  new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue(){
            return 0;
        }
    };*/
    /**
     *  java8之后带来的新写法:withInitial当前常被用来初始化
     */
    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);

    public void saleVolumeByThreadLocal(){
        saleVolume.set(1+saleVolume.get());
    }
}

再举例子:

public class ThreadLocalDemo5 {
    private String content;

    private String getContent() {
        return content;
    }

    private void setContent(String content) {
        this.content = content;
    }
    public static void main(String[] args) {
        ThreadLocalDemo5 demo = new ThreadLocalDemo5();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                demo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

// 线程0--->线程2的数据
// 线程2--->线程2的数据
// 线程3--->线程2的数据
// 线程1--->线程1的数据
// 线程4--->线程4的数据
public class ThreadLocalDemo5 {
    ThreadLocal<String> t1 = new ThreadLocal<>();
    private String content;

    private String getContent() {
//        return content;
        return t1.get();
    }

    private void setContent(String content) {
//        this.content = content;
        t1.set(content);
    }
    public static void main(String[] args) {
        ThreadLocalDemo5 demo = new ThreadLocalDemo5();
        for (int i = 0; i < 5; i++) {
            Thread thread = new Thread(() -> {
                demo.setContent(Thread.currentThread().getName() + "的数据");
                System.out.println(Thread.currentThread().getName() + "--->" + demo.getContent());
            });
            thread.setName("线程" + i);
            thread.start();
        }
    }
}

// 线程1--->线程1的数据
// 线程2--->线程2的数据
// 线程0--->线程0的数据
// 线程3--->线程3的数据
// 线程4--->线程4的数据

2.3 API 使用注意事项

1、先初始化(这个看业务需求)

2、利用set get方法

3、调用remove()方法,防止内存泄漏

 

2.4 不用remove方法的后果

        主要演示线程池情况下,线程池中的线程会复用(不会自动清空),而上面的都是新建一个Thread。

class MyData{
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    public void add(){
        threadLocalField.set(1+ threadLocalField.get());
    }
}

/**
 * 根据阿里规范,需要对自定义的ThreadLocal进行回收,否则容易造成内存泄漏和业务逻辑问题(因为线程池中的线程会复用)
 */
public class ThreadLocalDemo2 {
    public static void main(String[] args) {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try {
            for(int i = 0;i < 10;i ++){
                threadPool.submit(()->{
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName()+"\t"+"beforeInt"+beforeInt+"\t afterInt"+afterInt);
                    } finally {
                        myData.threadLocalField.remove();
                    }
                });
            }
        } catch (Exception e) {
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}
//没有remove---出现了累积
//pool-1-thread-1  beforeInt0   afterInt1
//pool-1-thread-3  beforeInt0   afterInt1
//pool-1-thread-2  beforeInt0   afterInt1
//pool-1-thread-2  beforeInt1   afterInt2
//pool-1-thread-2  beforeInt2   afterInt3
//pool-1-thread-2  beforeInt3   afterInt4
//pool-1-thread-2  beforeInt4   afterInt5
//pool-1-thread-2  beforeInt5   afterInt6
//pool-1-thread-3  beforeInt1   afterInt2
//pool-1-thread-1  beforeInt1   afterInt2
//有remove-不会出现累积的情况
//pool-1-thread-1  beforeInt0   afterInt1
//pool-1-thread-3  beforeInt0   afterInt1
//pool-1-thread-2  beforeInt0   afterInt1
//pool-1-thread-1  beforeInt0   afterInt1
//pool-1-thread-3  beforeInt0   afterInt1
//pool-1-thread-1  beforeInt0   afterInt1
//pool-1-thread-2  beforeInt0   afterInt1
//pool-1-thread-1  beforeInt0   afterInt1
//pool-1-thread-3  beforeInt0   afterInt1
//pool-1-thread-2  beforeInt0   afterInt1

2.5 小总结

1、因为每个Thread内有自己的实例副本并且该副本只由当前线程自己使用

2、既然其他Thread不可访问,那就不存在多线程间共享的问题。

3、统一设置初始值,但是每个线程对这个值的修改都是各自线程相互独立的

4、因为在开发中用的都是线程池,线程会复用,会出现线程中的实例副本出现公用的情况,所以在使用完之后要remove;因为出现复用,不remove也会出现内存溢出的情况。

一句话如何才能不争抢

        1、假如synchronized或者Lock控制资源的访问顺序

        2、利用ThreadLocal人手一份,大家各自安好,没必要抢夺

3、ThreadLocal源码分析

3.1 Thread、ThreadLocal、ThreadLocalMap关系

根据官方API,Thread是程序中执行的线程ThreadLocal类提供线程局部变量

 三者关系:

 本人感觉这么画更容易理解他们之间的关系,但是老师画的更复合代码中的逻辑

 

        threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象。

        当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放

4、ThreadLocal内存泄漏问题-非常重要

4.1 引出的问题

1、什么是内存泄漏?

2、为什么要用弱引用?

3、不用如何?

4.2 什么是内存泄漏

        不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄漏。

为什么会内存泄漏?

 

        ThreadLocalMap从字面上就可以看出这是一个保存ThreadlLocal对象的map(以ThreadLocal为Key,不过是经过了两层包装的ThreadLocal对象:

(1)第一层包装是使用 WeakReference> 将ThreadLocal对象变成一个弱引用的对象。

(2)第二层包裝是定义了一个专门的类 Entry 来扩展 WeakReference>。

4.3 有几种引用(强引用、软引用、弱引用、虚引用)

前置java知识条件:

        Java技术允许使用finalize()方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

        官方API:finalize()的通常目的是在对象被不可撤销地丢弃之前执行清理操作。

Reference是强引用
SoftReference是软引用
WeakReference是弱引用
PhantomReference是虚引用

4.4 强引用(默认支持模式)

        当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收


        强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收因此强引用是造成Java内存泄漏的主要原因之一


        对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应(强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

public class ReferenceDemo {
    public static void main(String[] args) {
        MyObject myObject = new MyObject();
        System.out.println("gc before" + myObject);

        //new 一个对象是一个强引用,如果不把他指为null,垃圾回收回收不了他
        myObject = null;
        System.gc();//人工开启gc 一般不用

        System.out.println("gc after "+ myObject);
    }
}

class MyObject{
    @Override
    protected void finalize() throws Throwable{
        //finalize的通常目的是在对象被不可撤销的丢弃之前进行清理操作
        System.out.println("finalize()被调用-------invoke finalize");
    }
}

//gc beforecom.zhang.admin.controller.MyObject@2f4d3709
//gc after null
//finalize()被调用-------invoke finalize      -------这不就是在对象丢弃之前进行一个清理操作,这里确实清理了

4.5 软引用(SoftReference)

        软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。

        对于只有软引用的对象来说,当系统内存充足时它不会 被回收,当系统内存不足时它会被回收

        软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

调整运行内存配置

 

public class ReferenceDemo {
    public static void main(String[] args) {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        System.gc();
        try {TimeUnit.SECONDS.sleep(1);} catch (InterruptedException e) {e.printStackTrace();}
        System.out.println("-------gc after内存够用"+softReference.get());

        try {
            // 创建20M内存大小
            byte[] bytes = new byte[20 * 1024 * 1024];
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            System.out.println("---------gc after内存不够"+softReference.get());
        }
    }
}

class MyObject{
    @Override
    protected void finalize() throws Throwable{
        //finalize的通常目的是在对象被不可撤销的丢弃之前进行清理操作
        System.out.println("finalize()被调用-------invoke finalize");
    }
}
//-------gc after内存够用com.zhang.admin.controller.MyObject@2f4d3709
//---------gc after内存不够null,(因为是软引用,在内存不足时被清理了)
//finalize()被调用-------invoke finalize
//Exception in thread "main" java.lang.OutOfMemoryError: Java heap space
//  at com.zhang.admin.controller.referenceDemo.main(referenceDemo.java:22)

4.6 弱引用(weakReference)

        弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

        对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存

public class ReferenceDemo {
    public static void main(String[] args) {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-------gc after内存够用"+weakReference.get());
        System.gc();
        System.out.println("---------gc after内存够用"+weakReference.get());
    }
}

class MyObject{
    @Override
    protected void finalize() throws Throwable{
        //finalize的通常目的是在对象被不可撤销的丢弃之前进行清理操作
        System.out.println("finalize()被调用-------invoke finalize");
    }
}

//-----gc before 内存够用 com.zhang.admin.controller.MyObject@2f4d3709
//finalize()被调用-------invoke finalize
//----gc after内存够用 null ------- (不管怎么样都会清除,这即是弱引用)

适用场景

        假如有一个应用需要读取大量的本地图片:

                如果每次读取图片都从硬盘读取则会严重影响性能

                如果一次性全部加载到内存中又可能造成内存溢出

此时使用软引用可以解决这个问题:

        设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

4.7 虚引用(PhantomReterence)

1、虚引用必须和引用队列 (ReferenceQueue)联合使用

        虚引用需要java.lang.ret.PhantomReterence类来实现,顾名思义, 就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有院引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收,它不能单独使用也不能通过它访问对象,虚引用必须和引用队列(ReferenceQueue)联合使用

2、PhantomReference的get方法总是返回null

        虚引用的主要作用是跟踪对象被垃圾回收的状态。仅仅是提供了一和确保对象被 finalize以后,做某些事情的通知机制。

        PhantomReference的get方法总是返回null,因此无法访问对应的引用对象。

3、处理监控通知使用

        换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用来实现比finalize机制更灵活的回收操作。

构造方法

引用队列

 

 

class MyObject{
    @Override
    protected void finalize() throws Throwable{
        //finalize的通常目的是在对象被不可撤销的丢弃之前进行清理操作
        System.out.println("finalize()被调用-------invoke finalize");
    }
}
public class ReferenceDemo {
    public static void main(String[] args) {
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(new MyObject(), referenceQueue);
        // System.out.println(phantomReference.get());//这里就是个null--虚引用的get()就是null

        List<byte[]> list = new ArrayList<>();

        new Thread(() -> {
            //模拟一个无限循环
            while (true){
                list.add(new byte[1 * 1024 * 1024]);
                try { TimeUnit.MILLISECONDS.sleep(600); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(phantomReference.get());
            }
        },"t1").start();

        new Thread(() -> {
            while (true){
                Reference<? extends MyObject> reference = referenceQueue.poll();
                if (reference != null) {
                    System.out.println("有虚对象加入队列了");
                }
            }
        },"t2").start();
    }
}

//null
//finalize()被调用-------invoke finalize
//null
//null
//null
//null
//null
//有虚对象加入队列了  ------(说明被干掉之后进入了这个引用队列)
//Exception in thread "t1" java.lang.OutOfMemoryError: Java heap space
//  at com.zhang.admin.controller.referenceDemo.lambda$main$0(referenceDemo.java:30)
//  at com.zhang.admin.controller.referenceDemo$$Lambda$1/1108411398.run(Unknown Source)
//  at java.lang.Thread.run(Thread.java:748)

4.8 四大引用小总结

 关系:

         每个Thread对象维护着一个ThreadLoaclMap的引用

        ThreadLocalMap是ThreadLocal的内部类,用Entry来进行存储

        调用ThreadLocal的set()方法时,实际上就是往ThreadLocalMap设置值,key就是ThreadLocal对象,值Calue时传递进来的对象

        调用ThreadLoaal的get()方法时,实际上就是往ThreadLoaclMap获取值,key是ThreadLocal对象,ThreadLocal本身并不存储值,它知识自己作为一个key来让线程从TheadLoaclMap获取value,正因为这个原理,所以ThreadLoacl能实现"数据隔离",获取当前线程的局部变量值,不受其他线程影响。

5、为什么要用弱引用?不用如何?

5.1 为什么源代码用弱引用?

         当function01方法执行完毕后,栈帧销毁强引用 tl 也就没有了。但此时线程的ThreadLocalMap里某个entry的key引用还指向这个对象。

        若这个key引用是强引用就会导致key指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;

        若这个key引用是弱引用,就大概率会减少内存泄漏的问题(还有一个key为null的雷,后面讲)。使用弱引用,就可以使ThreadLocal对象在方法执行完毕后顺利被回收且Entry的key引用指向为null

        总结:弱引用可以让key被gc回收

5.2 弱引用就万事大吉了吗? 

         当我们为threadLocal变量赋值,实际上就是当前的Entry(threadLocal实例为key,值为value)往这个threadLocalMap中存放。Entry中的key是弱引用,当threadLocal外部强引用被置为null(tl=null),那么系统 GC 的时候,根据可达性分析,这个threadLocal实例就没有任何一条链路能够引用到它,这个ThreadLocal势必会被回收,这样一来,ThreadLocalMap中就会出现key为null的Entry,就没有办法访问这些key为null的Entry的value,如果当前线程再迟迟不结束的话(这个tl就不会被干掉),这些key为null的Entry的value就会一直存在一条强引用链:Thread Ref -> Thread -> ThreaLocalMap -> Entry -> value永远无法回收,造成内存泄漏。

        当然,如果当前thread运行结束,threadLocal,threadLocalMap,Entry没有引用链可达,在垃圾回收的时候都会被系统进行回收。

        但在实际使用中我们有时候会用线程池去维护我们的线程,比如在Executors.newFixedThreadPool()时创建线程的时候,为了复用线程是不会结束的,所以threadLocal内存泄漏就值得我们小心

6、最佳实践

  1. 一定要进行初始化避免空指针问题ThreadLocal.withInitial(()- > 初始化值);
  2. 建议把ThreadLocal修饰为static
  3. 用完记得手动remove

7、面试题

7.1 ThreadLocal中ThreadLoaclMap的数据结构和关系

        每个线程都有一个ThreadLocalMap,ThreadLocalMap中保存该线程中所有的ThreadLoacl,而ThreadLocal本身并不保存值,值都是保存再ThreadLocalMap中的,其中ThreadLoacl为ThreadLoaclMap中的key。

7.2 ThreadLocal的key是弱引用,这是为什么?

        如果ThreadLocal的 key是强引用,是会发生内存泄漏的。如果ThreadLocal的key是强引用,引用的ThreadLocal 的对象被回收了,但是 ThreadLocalMap还持有ThreadLocal 的强引用,如果没有手动删除,ThreadLocal 不会被回收,发生内存泄漏。

        如果是弱引用的话,引用的ThreadLocal的对象被回收了,即使没有手动删除,ThreadLocal也会被回收。key为null的value也会在ThreadLocalMap调用set() 、get()、remove()的时候会被清除。

        保证Key被回收,不发生内存泄漏。

7.3 ThreadLocal内存泄漏问题你知道吗?

        因为ThreadLocal中的key是弱引用,而value是强引用。当ThreadLocal没有被强引用时,在进行垃圾回收时,key会被清理掉,而value不会被清理掉,这时如果不做任何处理,value将永远不会被回收,产生内存泄漏。

7.4 如何解决ThreadLocal的内存泄漏? 

        其实在ThreadLocal在设计的时候已经考虑到了这种情况,在调用set()、get() 、remove()等方法时就会清理掉key为null 的记录,所以在使用完ThreadLocal 后最好手动调用remove()方法

8、ThreadLocalMap 如何解决哈希冲突(补充)

        ThreadLocalMap 类的底层数据接口是一个Entry类型的数组,也可以看出来他就是一个单纯的数组

ThreadLocal中的set方法

public void set(T value) {
  // 获取当前线程
  Thread t = Thread.currentThread();
  // 获取 Thread 类中 ThreadLocal.ThreadLocalMap 类型的 threadLocals 变量
  ThreadLocalMap map = getMap(t);
  // 若 threadLocals 变量不为空,进行赋值;否则新建一个 ThreadLocalMap 对象来存储
  if (map != null)
    map.set(this, value);
  else
    createMap(t, value);
}

ThreadLocalMap中的set方法

private void set(ThreadLocal<?> key, Object value) {
  // 获取 ThreadLocalMap 的 Entry 数组对象
  Entry[] tab = table;
  int len = tab.length;
  // 基于斐波那契散列法获取当前 ThreadLocal 对象的散列值 : 可以理解找到该ThreadLocal的位置
  int i = key.threadLocalHashCode & (len-1);
  // 解决哈希冲突,线性探测法: 如果当前位置有中有Entry对象
  for (Entry e = tab[i]; e != null; e = tab[i = nextIndex(i, len)]) {
    ThreadLocal<?> k = e.get();
    // 如果当前 ThreadLocal 对象正好等于 Entry 对象中的 key 属性,直接更新 ThreadLocal 中 value 的值
    if (k == key) {
      e.value = value;
      return;
    }
    // 如果当前 ThreadLocal 对象不等于 Entry 对象中的 key 属性,
    // 并且 Entry 对象的 key 是空的,这里进行的逻辑其实是 设置键值对,
    // 同时清理无效的 Entry (一定程序防止内存泄漏,下文会有详细介绍)
    if (k == null) {
      replaceStaleEntry(key, value, i);
      return;
    }
  }
  // 如果在遍历中没有发现当前 TheadLocal 对象的散列值,也没有发现 Entry 对象的 key 为空的情况,
  // 而是满足了退出循环的条件,即 Entry 对象为空时,那么就会创建一个 新的 Entry 对象进行存储 ,
  // 同时做一次 启发式清理 ,将 Entry 数组中 key 为空,value 不为空的对象的 value 值释放;
  tab[i] = new Entry(key, value);
  int sz = ++size;
  if (!cleanSomeSlots(i, sz) && sz >= threshold)
    rehash();
}

解决哈希冲突方式:

        如果在 set 时遇到哈希冲突,ThreadLocal 会通过线性探测法尝试在数组下一个索引位置进行存储,同时在 set 过程中 ThreadLocal 会释放 key 为 NULL,value 不为 NULL 的脏 Entry对象的 value 属性来防止内存泄漏

9、ThreadLocalMap 初始容量以及扩容机制(补充)

9.1 初始容量

// ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
  // 初始化 Entry 数组
  table = new Entry[INITIAL_CAPACITY];
  int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
  table[i] = new Entry(firstKey, firstValue);
  size = 1;
  // 设置扩容条件
  setThreshold(INITIAL_CAPACITY);
}
// 初始化容量
private static final int INITIAL_CAPACITY = 16;

9.2 扩容机制

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

        rehash 会触发一次全量清理,[扩容出发条件]如果数组已存内容大于等于原来数组的二分之一(数组长度的三分之二的再四分之三 ),则进行 resize(扩容)

// 扩容条件
private void rehash() {
  expungeStaleEntries();

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

        进行扩容时,Entry 数组为扩容为 原来的2倍 ,重新计算 key 的散列值,如果遇到 key 为 NULL 的情况,会将其 value 也置为 NULL,帮助虚拟机进行GC。

// 具体的扩容函数
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;
}

        如果ThreadLocalMap中数组的长度达到原数组的二分之一的容量后,会扩容到原来数组长度的两倍

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

郭吱吱

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值