ThreadLocal原理分析及内存泄漏解决方法

目录

目录

一、并发问题

二、ThreadLocal实现原理

1. 引用(reference )

1.1 强引用(Strong Reference)

1.2 软引用(Soft Reference)

1.3 弱引用(Weak Reference)

1.4 虚引用

2.ThreadLocal原理

3. 内存泄漏

4. 避免内存泄漏的方法

三、总结


一、并发问题

回顾之前写过的关于多线程并发的博客,并发问题的原因主要有三种:

  • CPU缓存导致的可见性问题
  • JVM优化导致的CPU指令执行顺序问题
  • 线程切换导致的原子性问题

并发问题究其原因,是对线程之间共享对象操作导致的。那么如果特定情况下线程都访问自己的变量,是不是就可以解决线程不安全的问题呢?这个线程的本地变量就是ThreadLocal来实现的。

二、ThreadLocal实现原理

在了解ThreadLocal之前,需要先了解对象引用的概念

准备

public class GroovyDto {
    public GroovyDto(String groovyName, Integer size) {
        this.groovyName = groovyName;
        this.size = size;
    }

    private String groovyName;
    private Integer size;

    public String getGroovyName() {
        return groovyName;
    }

    public void setGroovyName(String groovyName) {
        this.groovyName = groovyName;
    }

    public Integer getSize() {
        return size;
    }

    public void setSize(Integer size) {
        this.size = size;
    }

    @Override
    public String toString() {
        return "GroovyDto{" +
                "groovyName='" + groovyName + '\'' +
                ", size=" + size +
                '}';
    }
}

创建一个测试类ReferenceDemo

1. 引用(reference )

1.1 强引用(Strong Reference)

Java语言中默认声明的就是强引用,这种引用,只要引用还指向对象,那么对象就会一直存活,例如:

Object obj = new Object();

手动设置为null

obj = null;

 这种情况obj变量就只是一个引用,存放在栈中,占两个字节,当方法出栈的时候释放掉。而曾经指向的对象,只要没有其他引用指向它,就会被GC。

测试:

public class ReferenceDemo {
    public static void main(String[] args) {
        //强引用
        GroovyDto groovy = new GroovyDto("hello",123);
        //手动置空
        groovy = null;
        //试图回收这个对象
        System.gc();
        System.out.println(groovy);
    }
}

//output : null
//对象被回收

1.2 软引用(Soft Reference)

这类引用是非必须,但还有用的对象。只存在软引用时,内存足够的情况下进行垃圾回收,软引用对象不会被回收,反之会被回收。

就比如设计一个博客系统,一个用户查看可一篇文章退出后,再次查看这篇文章的时候就没必要去数据库中再次查询,而是直接取内存中的对象。还有网页、图片的缓存。这样有效的减少数据库查询,提高性能。

如果用强引用来缓存,内存占满时,我们找不到合适的机会去做垃圾回收。

测试

public class ReferenceDemo {
    public static void main(String[] args) {
        //强引用
        GroovyDto groovy = new GroovyDto("hello",123);
        //软引用
        SoftReference<GroovyDto> softRef = new SoftReference<GroovyDto>(groovy);
        //手动置空去掉强引用
        groovy = null;
        //试图回收这个对象
        System.gc();
        System.out.println(softRef.get());
    }
}

//output : GroovyDto{groovyName='hello', size=123}
//对象没被回收

1.3 弱引用(Weak Reference)

GC一旦发现一块内存上是有弱引用,那肯定会被回收,不管空间是否足够。

例如一个电商的优惠券系统,用了一个弱引用的用户List与优惠券Coupon对象绑定

 这时候如果一个用户注销掉,使用强引用的情况下,我们就需要解决,Coupon和User解绑的动作,一旦忘记删除,那就会出现问题。

反之如果使用弱引用,在下次垃圾回收的时候List里的弱引用对象就会被回收,实现自动更新的效果。

测试

public class ReferenceDemo {
    public static void main(String[] args) {
        //强引用
        GroovyDto groovy = new GroovyDto("hello",123);
        //软引用
        WeakReference<GroovyDto> weakRef = new WeakReference<GroovyDto>(groovy);
        //手动置空去掉强引用
        groovy = null;
        //试图回收这个对象
        System.gc();
        System.out.println(weakRef.get());
    }
}

//output : null
//对象被回收

1.4 虚引用

用的不多,不做介绍

2.ThreadLocal原理

每个Thread里面都有一个ThreadLocalMap对象

public class Thread implements Runnable {
/* ThreadLocal values pertaining to this thread. This map is maintained
     * by the ThreadLocal class. */
    ThreadLocal.ThreadLocalMap threadLocals = null;
}

 每个ThreadLocalMap对象都包含一个Entry[] 数组(java.lang.ThreadLocalMap中的静态内部类),数组中的key是ThreadLocal的弱引用,value是存储的值

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

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

value是通过java.lang.ThreadLocal的set方法写入的

    public void set(T value) {
        //获取当前线程
        Thread t = Thread.currentThread();
        //获取当前线程的ThreadLocalMap 对象
        ThreadLocalMap map = getMap(t);
        if (map != null)
            //不为空就将value设置到ThreadLocalMap 中
            //this表示ThreadLocal对象
            map.set(this, value);
        else
            //如为空即首次设置ThreadLocalMap 的值
            createMap(t, value);
    }

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

    void createMap(Thread t, T firstValue) {
        //t.threadLocals 指向新创建的ThreadLocalMap
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

3. 内存泄漏

ThreadLocal本身不存储值,作为一个key让线程在ThreadLocalMap中取值,这里的key是ThreadLocal对象的弱引用,如果外部没有强引用指向这个ThreadLocal,那么GC的时候该对象就一定会被回收。到时候Entry中就会存在key为null的值,这样的值我们是取不到的。

那么如果这个线程如果是线程池里的线程,或者是个很耗时的线程,那么这个key为null的Entry的value就会一直存在,因为存在一个强引用链,这样就会造成内存泄漏。

4. 避免内存泄漏的方法

主动调用ThreadLocal中的remove方法删除对象的值。算然ThreadLocal中的set和get方法都使用了排空处理,但如果没用到set和get方法依然会造成内存泄漏。所以务必使用ThreadLocal中的remove方法将设置的线程本地变量值删除

     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }
      /**
        *ThreadLocalMap中的remove方法
         * Remove the entry for key.
         */
        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;
                }
            }
        }

因为是值传递,所以多线程在set同一个对象进ThreadLocal会存在并发问题,所以要确保每次set进去的对象都是一个全新的对象。如果想规避这个方法可以重写set方法,实现对象的深拷贝。

三、总结

  1. ThreadLocal用于存储线程本地变量,解决线程间变量值共享导致的线程安全问题
  2. 主动调用remove方法删除对象,避免造成内存泄漏
  3. 使用static的ThreadLocal延长了ThreadLocal的生命周期会导致内存泄漏
  4. 分配使用了ThreadLocal,但没有调用get(),set(),remove()方法,会导致内存泄漏
  5. ThreadLocal默认是值传递,一定要避免多线程共享一个对象,不然会造成线程安全问题

 

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值