教程视频:https://www.bilibili.com/video/BV1fA411b7SX?from=search&seid=3920039686913857888
从47分钟开始讲threadLocal

强引用

image.pngimage.png

finalize方法

c++转java的程序员有可能会在finalize()中手动释放资源(c++需要手动释放资源,但是java有gc回收机制会自动回收资源),如果在finalize()中写很多释放资源的逻辑,如回收对象、关闭远程连接(需要得到对方反馈)的操作。

我的想法:在内存资源不足时,gc才会开始工作,gc工作时会调用finalize(),如果在finalize()中写很多释放资源的逻辑会导致GC负担更大,程序运行效率更差,甚至会报错,fgc和oom。

  1. @Override
  2. protected void finalize() throws Throwable {
  3. super.finalize();
  4. }

软引用

  1. SoftReference<byte[]> m = new SoftReference<>(new byte[1024*1024*10]);

image.png

控制台输出软引用对象

image.png

手动调用gc看看软引用是否会被回收

image.png

设置JVM初始堆内存只有20m,生成m对象需要10m内存,再生成b对象需要15m内存

image.pngimage.png
自己测试的时候会报oom
image.png
image.png

如果是再创建一个软引用会怎么样

image.png

弱引用

image.png

ThreadLocal

原理

可以看一下熬丙的文章:https://www.zhihu.com/question/341005993
ThreadLocalMap的底层用到了弱引用,底层结构是数组,数组中每个元素是一个弱引用的指针,
image.png
image.png
用数组是因为,我们开发过程中可以一个线程可以有多个TreadLocal来存放不同类型的对象的,但是他们都将放到你当前线程的ThreadLocalMap里,所以肯定要数组来存。

  1. private void set(ThreadLocal<?> key, Object value) {
  2. Entry[] tab = table;
  3. int len = tab.length;
  4. int i = key.threadLocalHashCode & (len-1);
  5. for (Entry e = tab[i];
  6. e != null;
  7. e = tab[i = nextIndex(i, len)]) {
  8. ThreadLocal<?> k = e.get();
  9. if (k == key) {
  10. e.value = value;
  11. return;
  12. }
  13. if (k == null) {
  14. replaceStaleEntry(key, value, i);
  15. return;
  16. }
  17. }
  18. tab[i] = new Entry(key, value);
  19. int sz = ++size;
  20. if (!cleanSomeSlots(i, sz) && sz >= threshold)
  21. rehash();
  22. }

从源码里面看到ThreadLocalMap在存储的时候会给每一个ThreadLocal对象一个threadLocalHashCode,在插入过程中,根据ThreadLocal对象的hash值,定位到table中的位置i,int i = key.threadLocalHashCode & (len-1)。
然后会判断一下:如果当前位置是空的,就初始化一个Entry对象放在位置i上;

if (k == null) {
    replaceStaleEntry(key, value, i);
    return;
}

如果位置i不为空,如果这个Entry对象的key正好是即将设置的key,那么就刷新Entry中的value;

if (k == key) {
    e.value = value;
    return;
}


如果位置i的不为空,而且key不等于entry,那就找下一个空位置,直到为空为止。
image.png

这样的话,在get的时候,也会根据ThreadLocal对象的hash值,定位到table中的位置,然后判断该位置Entry对象中的key是否和get的key一致,如果不一致,就判断下一个位置,set和get如果冲突严重的话,效率还是很低的。

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

 private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
            Entry[] tab = table;
            int len = tab.length;
// get的时候一样是根据ThreadLocal获取到table的i值,然后查找数据拿到后会对比key是否相等  if (e != null && e.get() == key)。
            while (e != null) {
                ThreadLocal<?> k = e.get();
              // 相等就直接返回,不相等就继续查找,找到相等位置。
                if (k == key)
                    return e;
                if (k == null)
                    expungeStaleEntry(i);
                else
                    i = nextIndex(i, len);
                e = tab[i];
            }
            return null;
        }

ThreadLocal自身有一个计数器,用于生产每一个threadLocal对象的hash值,该值会和ThreadLocalMap中的数据长度进行hash运算,确定threadLocal的在数组中的存储位置

threadLocal.set()执行流程

image.png
ThreadLocal使用场景:
spring的@transactional注解,每个事务中,数据库connection都是放在自己的线程中的,不同的事务数据库connection是不一样的,同一个事务是使用一个数据库connection。具体代码是在org.springframework.transaction.support.TransactionSynchronizationManager中
自己在项目中使用的场景

  1. 重试机制
  2. 一个线程经常遇到横跨若干方法调用,需要传递的对象

为什么ThreadLocalMap的key要设计成弱引用

public void func1() {
        ThreadLocal tl = new ThreadLocal<Integer>();
         tl.set(100); 
         tl.get(); 
}

image.png
当func1方法执行完毕后,栈帧销毁,强引用 tl 也就没有了,但此时线程的ThreadLocalMap里某个entry的 k 引用还指向这个对象。若这个k 引用是强引用,就会导致k指向的ThreadLocal对象及v指向的对象不能被gc回收,造成内存泄漏;但是弱引用就不会有这个问题(弱引用的对象一旦遇到gc,如果该对象没有被强引用,则会被回收)。使用弱引用,entry的k会被gc回收,就可以使ThreadLocal对象在方法执行完毕后顺利被回收。

注意:虽然弱引用,保证了k指向的ThreadLocal对象能被及时回收,但是v指向的对象没有被回收,虽然ThreadLocalMap调用get、set时发现k为null时会去回收整个entry、value,但是有可能长时间没有调用get、set方法,因此弱引用不能保证内存完全不泄露。我们要在不使用某个ThreadLocal对象后,手动调用remoev方法来删除它

内存泄漏问题

image.png

image.png
ThreadLocal在保存的时候会把自己当做Key存在ThreadLocalMap中,正常情况应该是key和value都应该被外界强引用才对,但是现在key被设计成WeakReference弱引用了。
弱引用的对象一旦遇到gc,如果该对象没有被强引用,则会被回收
这就导致了一个问题,ThreadLocal在没有外部强引用时,发生GC时会被回收,如果创建ThreadLocal的线程一直持续运行,那么这个Entry对象中的value指向的对象就有可能一直得不到回收,发生内存泄露。
就比如线程池里面的线程,线程都是复用的,那么之前的线程实例处理完之后,出于复用的目的线程依然存活,所以,ThreadLocal设定的value值被持有,导致内存泄露。
按照道理一个线程使用完,ThreadLocalMap是应该要被清空的,但是现在线程被复用了。
当然,如果线程不存在被复用的情况,就不会有这种内存泄漏的问题

解决方法

在代码的最后使用remove就好了,我们只要记得在使用的最后用remove把值清空就好了。

ThreadLocal<String> localName = new ThreadLocal();
try {
    localName.set("张三");
    ……
} finally {
    localName.remove();
}