Java并发编程---ThreadLocal的不安全性和内存泄漏分析

389 阅读4分钟

这是我参与更文挑战的第9天,活动详情查看: 更文挑战

Java并发编程---ThreadLocal源码解析

辨析Java的四种引用,那我走?

ThreadLocal在上篇文章里介绍了,他是干什么的,具体的使用方法,还有在源码层次分析了一下它的原理,链接在上边↑

但是ThreadLcoal使用不当,也是回发生内存泄漏和线程不安全的

1、ThreadLcoal内存泄漏分析

我们先来一个不使用ThreadLcoal的例子,分析一下内存占用情况

我们先把堆内存最大值设为256M!

public class ThreadLocalOOM {
    private static final int TASK_LOOP_SIZE = 500;

    final static ThreadPoolExecutor poolExecutor
            = new ThreadPoolExecutor(5, 5, 1,
            TimeUnit.MINUTES,
            new LinkedBlockingQueue<>());

    static class LocalVariable {
        private byte[] a = new byte[1024*1024*5];/*5M大小的数组*/
    }

    ThreadLocal<LocalVariable> localVariable;

    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    new LocalVariable();
                    System.out.println("use local varaible");
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

}

我们运行玩main方法然后用Java visual Vm走一下看一下堆内存的情况

image.png

我们发现堆内存的大小,最高也就25M左右,完全是符合我们的预期的,但是我们上班的代码还没有用ThreadLcoal呢

 public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    ThreadLocalOOM oom=new ThreadLocalOOM();
                    oom.localVariable=new ThreadLcoal<>();
                    oom.localVariable(new LocalVariable());
                    System.out.println("use local varaible");
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

这次我们再看看一下内存的使用情况

image.png 哦吼,内存竟然达到了150M甚至达到了200M

XDM,难倒我们加了ThreadLcoal以后,占用内存如此之大吗?

肯定是发生了内存泄露了,我们再加一句代码,再观察一下

 public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < TASK_LOOP_SIZE; ++i) {
            poolExecutor.execute(new Runnable() {
                public void run() {
                    ThreadLocalOOM oom=new ThreadLocalOOM();
                    oom.localVariable=new ThreadLcoal<>();
                    oom.localVariable(new LocalVariable());
                    System.out.println("use local varaible");
                    oom.localVariable.remove();
                }
            });

            Thread.sleep(100);
        }
        System.out.println("pool execute over");
    }

image.png

nice运行中内存大小的波动恢复到没有用ThreadLocal之前了,这是怎么回事呢?

我们再去ThreadLcoal的源码里看一下

image.png

我们看到ThreadLcoal里的ThreadLocalMap里的Entry的ThreadLcoal也就是key是一个弱引用,不知道Java四种引用是咋回事的XDM,去看Java四种引用的辨析,开头有链接

给XDM画张图就知道咋回事了

image.png 我们在开头就把堆大小设成了256

然后ThreadLcoal又是弱引用,所以在垃圾回收的时候会把ThreadLocal给回收掉,key也就回收掉了,但是我们的当前线程还引用了Map,Map还引用到了Entry,所以挡ThreadLocal被回收掉之后,value就不能通过ThreadLocal来访问了,所以就剩下了

这时候又有XDM说了,我们循环了500次,按这样说那我们的内存泄漏的大小应该比200M要大,为什么只到200M呢?

我们再去看ThreadLcoalMap的set方法和get方法

image.png

image.png

image.png

它会把 key为null的 Entry给清除一下,只不过这个方法在set的时候并不是每次都执行,也就是说回收的不及时,所以造成了一定程度上的内存泄漏

我们再去看remove方法

image.png

哦吼!remove出克e.clean之后也调用了清除key为null的Entry、

如果我们的Entry里的key用的是强引用会发生什么,如果是强引用,就会发生更多的内存泄漏,threadLocal的引用为null的话,因为是强引用threadlocal在垃圾回收的时候,虽然它和栈里的引用断了,但是他所在的Entry还被ThreadLocalMap持有呢,所以一定会发生内存泄漏

2、ThreadLcoal的不安全性

还是老样子,错误的使用方法,也是回导致线程不安全的,

public class ThreadLocalUnsafe implements Runnable {

    public static Number number = new Number(0);

    public void run() {
        //每个线程计数加一
        number.setNum(number.getNum()+1);
      //将其存储到ThreadLocal中
        value.set(number);
        SleepTools.ms(2);
        //输出num值
        System.out.println(Thread.currentThread().getName()+"="+value.get().getNum());
    }

    public static ThreadLocal<Number> value = new ThreadLocal<Number>() {
    };

    public static void main(String[] args) {
        for (int i = 0; i < 5; i++) {
            new Thread(new ThreadLocalUnsafe()).start();
        }
    }

    private static class Number {
        public Number(int num) {
            this.num = num;
        }

        private int num;

        public int getNum() {
            return num;
        }

        public void setNum(int num) {
            this.num = num;
        }

        @Override
        public String toString() {
            return "Number [num=" + num + "]";
        }
    }

}

上面的使用就是不安全的,如果对引用和对象,堆和栈不清楚的XDM,会犯这个错误,使用ThreadLocal的时候set的时候是一个引用,但是这五个线程他们操作的对象却是一个,这样的话,这个Number对象就被五个线程共享了,也就不安全了,原本是一个线程一个房间的,但是上边的用法导致,五个线程拿到的不是五个房间而是五个一样的门牌号,操作的时候是在一间房子里操作的、

3、总结

ThreadLcoal的源码分析内存泄漏的原因,这样我们使用的时候就能很大限度的避免内存泄漏,介绍了ThreadLocal不安全的一种用法!如有错误之处,请大佬们在评论区指出!大佬们一键三连!