ThreadLocal内存泄漏问题解析

一、ThreadLocal简介

1、ThreadLocal介绍

在JDK 1.2的版本中就提供java.lang.ThreadLocal,ThreadLocal为解决多线程程序的并发问题提供了一种新的思路。

当使用ThreadLocal维护变量时,ThreadLocal为每个使用该变量的线程提供独立的变量副本,所以每一个线程都可以独立地改变自己的副本,而不会影响其它线程所对应的副本。从线程的角度看,目标变量就象是线程的本地变量,这也是类名中“Local”所要表达的意思。

2、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。

在JDK5.0中,ThreadLocal支持泛型,该类的类名已经变为ThreadLocal。API方法也相应进行了调整,新版本的API方法分别是void set(T value)、T get()以及T initialValue()。

3、ThreadLocal是如何做到为每一个线程维护变量的副本的呢?

其实实现的思路很简单:在ThreadLocal类中有一个Map,用于存储每一个线程的变量副本,Map中元素的键为当前ThrealLocal对象,而值对应线程的变量副本。

4、threadLocal的作用

提供线程内的局部变量,不同 的线程之间不会相互干扰,这种变量在线程的生命周期内起作用,减少同一个线程内多个函数或组件之间一些公共变量传递的复杂度
1、线程并发:多线程环境下
2、传递数据:通过THreadLocal在同一线程,不同组件中传递公共变量
3、线程隔离:每个线程变量都是独立的,不会相互影响

二、java四种引用类型

/**
 * Author: 徐志
 * Date: 2020/8/9 10:25
 */
public class M {
    //重写finalize方法,当系统调用finalize()方法时,并不一定执行了垃圾回收,具体还的看JVM
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}

1、强引用

Object o=new Object();
强引用指向的对象不会被垃圾回收器回收。宁肯溢出

/**
 * Author: 徐志
 * Date: 2020/8/9 10:27
 * 强引用
 */

import java.io.IOException;

public class NormalReference {
    public static void main(String[] args) throws IOException {
        M m=new M();  //强引用
        m=null;
        System.gc(); //手动调用GC,真正执行还是得看垃圾回收器

        //main线程走完,垃圾回收线程也会退出,就可能出现垃圾会收还没执行运行就已经结束了
        System.in.read();//阻塞main线程,给垃圾回收线程时间执行
    }
}

2、软引用

在这里插入图片描述
SR是一个软引用对象,存放在堆上。只不过这个对象有些特殊, 它又指向一个数组。
当内存不够时,软引用指向的对象会被回收;内存够的时候,就不会回收。运行以下代码需要将堆内存调小一点

-Xmx20M    //设置堆内存
import java.lang.ref.SoftReference;

/**
 * Author: 徐志
 * Date: 2020/8/9 10:35
 * 软引用
 */
public class SoftReferenceTest {
    public static void main(String[] args) {
        SoftReference<byte[]> m=new SoftReference<>(new byte[1024*1024*10]);

        System.out.println(m.get());
        System.gc();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());

        //再分配一个数组,这是会堆装不下,系统会执行垃圾回收
        byte[] b=new byte[1024*1024*15];
        System.out.println(m.get());
    }

}
//软引用适合做缓存

3、弱引用

弱引用指向的对象,只要执行垃圾回收,就会被回收掉
在这里插入图片描述

import java.lang.ref.WeakReference;

/**
 * Author: 徐志
 * Date: 2020/8/9 10:52
 * 弱引用
 */

public class WeakReferenceTest {
    public static void main(String[] args) {
        WeakReference<M> m=new WeakReference<>(new M());

        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
    }
}

4、虚引用

一般用来管理内存,使用虚引用指向的对象拿不到值。虚引用仅仅只是在回收的时候发出一个信号

/**
 * Author: 徐志
 * Date: 2020/8/9 10:59
 */

import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;

/**
 * 虚引用,用来管理直接内存,凡是虚引用指向的对象,在垃圾回收的时候会先将对象
 * 放到队列里
 */
public class PhantomReferenceTest {

    private static final List<Object> LIST=new LinkedList<>();
    private static final ReferenceQueue<M> QUEUE=new ReferenceQueue<>();

    //运行前指定堆内存为20M
    public static void main(String[] args) {
        PhantomReference<M> phantomReference=new PhantomReference<>(new M(),QUEUE);
        System.out.println(phantomReference.get());

        new Thread(()->{
            while (true){
              //往List里存值,死循环,总会将内存占满
                LIST.add(new byte[1024*1024]);
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                    Thread.currentThread().interrupt();
                }
                System.out.println(phantomReference.get());
            }
        }).start();

        new Thread(()->{
            Reference<? extends M> poll=QUEUE.poll();
            if (poll!=null){
                System.out.println("虚引用对象被JVM回收了"+poll);
            }
        }).start();

        try {
            Thread.sleep(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

    }
}

ThreadLocal用法

1、传统的多线程

多个线程操作,线程与线程之间不隔离,有可能出现的结果是
线程2–>线程1的数据

/**
 * Author: 徐志
 * Date: 2020/8/9 14:59
 * 线程不隔离
 */

public class MyThreadDemo {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

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

传统的解决方法,使用synchronized关键字

/**
 * Author: 徐志
 * Date: 2020/8/9 14:59
 */

/**
 * 使用synchronized关键字解决
 */
public class MyThreadDemo2 {
    private String content;

    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }

    public static void main(String[] args) {
        MyThreadDemo2 demo=new MyThreadDemo2();
        for (int i = 0; i < 5; i++) {
            Thread thread=new Thread(new Runnable() {
                @Override
                public void run() {
                    //性能降低,程序失去并发性,串行执行
                    synchronized (MyThreadDemo2.class){
                    demo.setContent(Thread.currentThread().getName()+"的数据");
                    System.out.println("===============");
                    System.out.println(Thread.currentThread().getName()+"-->"+demo.getContent());
                }}
            });
            thread.setName("线程"+i);
            thread.start();
        }
    }
}

2、ThreadLocal方式

比使用synchronized关键字的效率高

/**
 * Author: 徐志
 * Date: 2020/8/9 14:59
 */

public class ThreadLocalDemo {

    ThreadLocal<String> tl=new ThreadLocal<>();

    public String getContent() {
        return tl.get();
    }

    public void setContent(String content) {
        tl.set(content);
    }

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

在这里插入图片描述

/**
 * Author: 徐志
 * Date: 2020/8/9 11:35
 */
public class ThreadLocalTest2 {
    static ThreadLocal<Person> t1=new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println(t1.get());
        }).start();

        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //每个线程对应一个map,线程与线程之间互不影响
            //以t1为key,person为值,放到一个map里
            t1.set(new Person());
        }).start();
    }

    static class Person{
        String name="zhangsna";
    }
}

在这里插入图片描述

三、JVM调优小案例

小米真实案例

场景:频繁Full GC,频繁OOM。
原因:一批C++程序员转行java程序员之后重写了finalize方法

1、为什么C++程序员会重写finalize方法
因为C++需要程序员手动释放内存,不需要垃圾回收器

2、为什么重写finalize方法会造成Full GC
将很多耗时的操作放在finalize方法里了,耗时操作会影响对象的回收

四、ThreadLocal内存泄漏的原因

使用ThreadLocal线程与线程隔离,一个线程对应一个Map
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 1
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值