• ThreadLocal学习


    正文

      之前在项目中与看到过ThreadLocal出现,但是一直不明白什么意思。而且最近也在从新学习多线程。正好有学到ThreadLocal。在次做一个记录。

    ThreadLocal是什么意思?
    ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。它采用采用空间来换取时间的方式,解决多线程中相同变量的访问冲突问题。
    先来看一段代码

    class Data {
        public Integer count = 0;
    
        public Integer getNumber() {
            return ++count;
        }
    
    }
    
    public class ThreadLocalDemo extends Thread {
        private Data data;
    
        public ThreadLocalDemo(Data data) {
            this.data = data;
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 3; i++) {
                System.out.println(Thread.currentThread().getName() + "," + data.getNumber());
            }
        }
    
        public static void main(String[] args) {
            Data data= new Data();
            ThreadLocalDemo t1 = new ThreadLocalDemo(data);
            ThreadLocalDemo t2 = new ThreadLocalDemo(data);
            t1.start();
            t2.start();
        }
    }
    

    通过这个图可以看到两个线程操作了一个变量,这样在实际情况下是不行的。一个线程在改变变量的时候另外一个线程也在改变这个变量。这样就会出现多线程中相同变量的访问冲突问题。

    我们可以通过创建两个实例对象来给变这样

    这种情况解决了相同变量的访问冲突问题。但是我们还可以使用ThreadLocal来解决这个问题。ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。
    ThreadLocal的四个方法:
      void set(Object value)设置当前线程的线程局部变量的值。
      public Object get()该方法返回当前线程所对应的线程局部变量。
      public void remove()将当前线程局部变量的值删除,目的是为了减少内存的占用,该方法是JDK 5.0新增的方法。需要指出的是,当线程结束后,对应该线程的局部变量将自动被垃圾回收,所以显式调用该方法清除线程的局部变量并不是必须的操作,但它可以加快内存回收的速度。
      protected Object initialValue()返回该线程局部变量的初始值,该方法是一个protected的方法,显然是为了让子类覆盖而设计的。这个方法是一个延迟调用方法,在线程第1次调用get()或set(Object)时才执行,并且仅执行1次。ThreadLocal中的缺省实现直接返回一个null。

    **使用ThreadLocal来改变刚刚的代码

    **

    class Data {
    
       public static ThreadLocal<Integer> threadLocal = new ThreadLocal<Integer>() {
           protected Integer initialValue() {
               return 0;
           };
       };
    
       public Integer getNumber() {
           int count = threadLocal.get() + 1;
           threadLocal.set(count);
           return count;
    
       }
    
    }
    
    public class ThreadLocalDemo extends Thread {
       private Data data;
    
       public ThreadLocalDemo(Data data) {
           this.data = data;
       }
    
       @Override
       public void run() {
           for (int i = 0; i < 3; i++) {
               System.out.println(Thread.currentThread().getName() + "," + data.getNumber());
           }
       }
    
       public static void main(String[] args) {
           Data res = new Data();
           ThreadLocalDemo t1 = new ThreadLocalDemo(res);
           ThreadLocalDemo t2 = new ThreadLocalDemo(res);
           t1.start();
           t2.start();
       }
    }
    

    这样就可以解决变量冲突。但是我没有搞懂ThreadLocal 与 给每个线程实例传递一个新的变量,这两种做法的区别。如果有小伙伴知道的话可以帮忙告知一下。

    ThreadLocal内存溢出的问题和如何避免
     ThreadLocal的原理:Thread内部维护ThreadLocalMap,它其实是一个Map,这个map的Key是一个弱引用也就是ThreadLocal的本身,Value才是真正存储的线程变量Object.而弱引用的生命周期只能存活到下次GC之前

      内存泄漏的原因:因为ThreadLocalMap中是以一个弱引用身份被Entry中的Key引用的,所以当ThreadLocal没有外部强引用来引用的话,那在下次Gc的时候就会被回收。这个时候Key已经被回收了,出现了null Key。也无法根据Null Key 找到Value。如果当前线程生命周期很长的话就会出现一条强引用链:Thread --> ThreadLocalMap-->Entry-->Value,这条强引用链会导致Entry不会回收,Value也不会回收,但Entry中的Key却已经被回收的情况,造成内存泄漏。JVM团队也考虑到了这样的情况,所以每次在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程,这样ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。这样就尽量避免了内存泄漏。

     static class Entry extends WeakReference<ThreadLocal<?>> {
    
                Object value;
             //ThreadLocal为key,真正需要存储的对象为value
                Entry(ThreadLocal<?> k, Object v) {
                    super(k);
                    value = v;
                }
            }
    

    可以看下具体的源码
    1、set()

        public void set(T value) {
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
        }
    

    ThreadLocal在调用set方法时,如果 getMap返回的为null,那么表示该线程的 ThreadLocalMap 还没有初始化,所以调用createMap进行初始化:t.threadLocals = new ThreadLocalMap(this, firstValue);

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

    初始化16的数组,并将firstKey、firstValue存入map。
    如果getMap没有返回NULL

     private void set(ThreadLocal<?> key, Object value) {
                Entry[] tab = table;
                int len = tab.length;
            //定位hash桶的位置
                int i = key.threadLocalHashCode & (len-1);
          //发生hash碰撞时如果碰撞的位置上已经有Entry,且原有的key没有被回收,就查找数组下一个位
                for (Entry e = tab[i];
                     e != null;
                     e = tab[i = nextIndex(i, len)]) {
                    ThreadLocal<?> k = e.get();
              //Key存在就替换原来的value值
                    if (k == key) {
                        e.value = value;
                        return;
                    }
              //key为空就替换并清除过期的Entry
                    if (k == null) {
                        replaceStaleEntry(key, value, i);
                        return;
                    }
                }
          //在空的位置上放入Entry之前先判断是否需要扩容
                tab[i] = new Entry(key, value);
                int sz = ++size;
                if (!cleanSomeSlots(i, sz) && sz >= threshold)
                    rehash();
            }
    

    2、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();
        }
    

    同样需要根据map是否为空来进行出来,如果没有初始化ThreadLocalMap就会返回setInitialValue()

    /**
    * setInitialValue方法很简单,定义一个value指向null,如果ThreadLocalMap 不为空,就插入value;如果ThreadLocalMap为空,先调用createMap初始化ThreadLoaclMap,再插入value。最后返回的就是value。
    */
    
     private T setInitialValue() {
            T value = initialValue();
            Thread t = Thread.currentThread();
            ThreadLocalMap map = getMap(t);
            if (map != null)
                map.set(this, value);
            else
                createMap(t, value);
            return value;
        }
    

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

    先定位hash桶的位置,然后根据桶位置找到Entry,如果Entry不为null且相同就返回对应的值。如果不符合调用getEntryAfterMiss()在进行处理

    private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
                Entry[] tab = table;
                int len = tab.length;
    //从当前位置向下寻找
                while (e != null) {
                    ThreadLocal<?> k = e.get();
    //相同就直接返回结果
                    if (k == key)
                        return e;
    //如果为null调用expungeStaleEntry()处理
                    if (k == null)
                        expungeStaleEntry(i);
    //继续寻找下一个位置
                    else
                        i = nextIndex(i, len);
                    e = tab[i];
                }
    //最后没找到返回NULL
                return null;
            }
    
    

    expungeStaleEntry()其实就是将Entry删除。防止内存泄漏。但是这样并不能完全保证内存不发生泄漏,如果使用了static的ThreadLocal,延长了生命周期也是有可能导致内存泄漏的。
    3、remove()

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

    也是找到hash桶的位置在遍历找到key然后找到相应的Entry并清理。最后也是调用了expungeStaleEntry()

    但是有个问题,为什么key要使用弱引用那?

      表面上看导致内存泄漏是因为key使用了弱引用,使Entry的key为null之后没有主动清理value导致的。

    其实可以分成两种情况讨论一下

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

      key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
    比较两种情况,我们可以发现:由于ThreadLocalMap的生命周期跟Thread一样长,如果都没有手动删除对应key,都会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

    因此,ThreadLocal内存泄漏的根源是:由于ThreadLocalMap的生命周期跟Thread一样长,如果没有手动删除对应key的value就会导致内存泄漏,而不是因为弱引用。

    所以综上所述,每次用完ThreadLocal,都调用remove(),清除数据。

  • 相关阅读:
    keras 报错 ValueError: Tensor conversion requested dtype int32 for Tensor with dtype float32: 'Tensor("embedding_1/random_uniform:0", shape=(5001, 128), dtype=float32)'
    redis 安装启动及设置密码<windows>
    mysql配置主从数据库
    将已有的项目提交到GitHub
    Top 5 SSH Clients for Windows (Alternatives of PuTTY)
    jQuery 插件写法示例
    Spring 定时操作业务需求
    eclipse 修改js文件无法编译到项目中
    linux 目录结构图解
    MongoDB 概念解析
  • 原文地址:https://www.cnblogs.com/yangk1996/p/12656484.html
Copyright © 2020-2023  润新知