ThreadLocal之强、弱、软、虚引用(上)

简介: ThreadLocal之强、弱、软、虚引用

1.ThreadLocal简介


ThreadLocal提供线程局部变量。这些变量与正常的变量不同,因为每一个线程在访问ThreadLocal实例的时候(通过其get或set方法)都有自己的、独立初始化的变量副本。ThreadLocal实例通常是类中的私有静态字段,使用它的目的是希望将状态(例如,用户ID或事务ID)与线程关联起来。


实现每一个线程都有自己专属的本地变量副本(自己用自己的变量不麻烦别人,不和其他人共享,人人有份,人各一份),主要解决了让每个线程绑定自己的值,通过使用get() 和set() 方法,获取默认值或将其值更改为当前线程所存的副本的值从而避免了线程安全问题

1673416690176.jpg


2.永远的helloworld


class House //资源类
{
    int saleCount = 0;
    public synchronized void saleHouse() {
        ++saleCount;
    }
    /*ThreadLocal<Integer> saleVolume = new ThreadLocal<Integer>(){
        @Override
        protected Integer initialValue()
        {
            return 0;
        }
    };*/
    ThreadLocal<Integer> saleVolume = ThreadLocal.withInitial(() -> 0);
    public void saleVolumeByThreadLocal() {
        saleVolume.set(1 + saleVolume.get());
    }
}
/**
 * 需求1: 5个销售卖房子,集团高层只关心销售总量的准确统计数。
 * <p>
 * 需求2: 5个销售卖完随机数房子,各自独立销售额度,自己业绩按提成走,
 */
public class ThreadLocalDemo {
    public static void main(String[] args) throws InterruptedException {
        House house = new House();
        for (int i = 1; i <= 5; i++) {
            new Thread(() -> {
                int size = new Random().nextInt(5) + 1;
                try {
                    for (int j = 1; j <= size; j++) {
                        house.saleHouse();
                        house.saleVolumeByThreadLocal();
                    }
                    System.out.println(Thread.currentThread().getName() + "\t" + "号销售卖出:" + house.saleVolume.get());
                } finally {
                    house.saleVolume.remove();
                }
            }, String.valueOf(i)).start();
        }
        //暂停毫秒
        try {
            TimeUnit.MILLISECONDS.sleep(300);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "\t" + "共计卖出多少套: " + house.saleCount);
    }
}


小总结:


因为每个 Thread 内有自己的实例副本且该副本只由当前线程自己使用


既然其它 Thread 不可访问,那就不存在多线程间共享的问题。


统一设置初始值,但是每个线程对这个值的修改都是各自线程互相独立的


加入synchronized或者Lock控制资源的访问顺序

人手一份,大家各自安好,没必要抢夺


阿里ThreadLocal规范开始

1673416731745.jpg

class MyData
{
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    public void add()
    {
        threadLocalField.set(1 + threadLocalField.get());
    }
}
/**
.【强制】必须回收自定义的 ThreadLocal 变量,尤其在线程池场景下,线程经常会被复用,如果不清理
自定义的 ThreadLocal 变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用
try-finally 块进行回收。
 */
public class ThreadLocalDemo2
{
    public static void main(String[] args) throws InterruptedException
    {
        MyData myData = new MyData();
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        try
        {
            for (int i = 0; i < 10; i++) {
                threadPool.submit(() -> {
                    try {
                        Integer beforeInt = myData.threadLocalField.get();
                        myData.add();
                        Integer afterInt = myData.threadLocalField.get();
                        System.out.println(Thread.currentThread().getName()+"\t"+"beforeInt:"+beforeInt+"\t afterInt: "+afterInt);
                    } finally {
                        myData.threadLocalField.remove(); //不及时清理,会导致计算结果错误
                    }
                });
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            threadPool.shutdown();
        }
    }
}

1673416754414.jpg

1673416761707.jpg


3.Thread,ThreadLocal,ThreadLocalMap 关系


Thread和ThreadLocal

1673416883117.jpg

各自线程,人手一份


ThreadLocal和ThreadLocalMap

1673416891982.jpg

三者总概括

1673416899915.jpg

threadLocalMap实际上就是一个以threadLocal实例为key,任意对象为value的Entry对象(k,v键值对)。

1673416926113.jpg

当我们为threadLocal变量赋值,实际上就是以当前threadLocal实例为key,值为value的Entry往这个threadLocalMap中存放


近似的可以理解为:ThreadLocalMap从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以ThreadLocal为Key),不过是经过了两层包装的ThreadLocal对象

1673416914540.jpg

JVM内部维护了一个线程版的Map<Thread,T>(通过ThreadLocal对象的set方法,结果把ThreadLocal对象自己当做key,放进了ThreadLoalMap中),每个线程要用到这个T的时候,用当前的线程去Map里面获取,通过这样让每个线程都拥有了自己独立的变量,人手一份,竞争条件被彻底消除,在并发模式下是绝对安全的变量。


4.ThreadLocal内存泄露问题

1673416938167.jpg


1.什么是内存泄漏


不再会被使用的对象或者变量占用的内存不能被回收,就是内存泄露。


2.ThreadLocalMap与WeakReference


ThreadLocalMap 从字面上就可以看出这是一个保存ThreadLocal对象的map(其实是以它为Key),不过是经过了两层包装的ThreadLocal对象: (1)第一层包装是使用 WeakReference<ThreadLocal <?>> 将 ThreadLocal 对象变成一个弱引用的对象; (2)第二层包装是定义了一个专门的类 Entry 来扩展 WeakReference <ThreadLocal <?>>


1673416960592.jpg

3.强引用、软引用、弱引用、虚引用分别是什么?

关于引用的整体架构

1673416969232.jpg

一般new对象,就是Reference(强引用)

强引用又有三个子类:SoftReference(软),WeakReference(弱),PhantomReference(虚)


java 技术允许使用 finalize() 方法在垃圾收集器将对象从内存中清除出去之前做必要的清理工作。

1673416981574.jpg

1.强引用(默认支持模式)


当内存不足,JVM开始垃圾回收,对于强引用的对象,就算是出现了OOM也不会对该对象进行回收,死都不收。


强引用是我们最常见的普通对象引用,只要还有强引用指向一个对象,就能表明对象还“活着”,垃圾收集器不会碰这种对象。在 Java 中最常见的就是强引用,把一个对象赋给一个引用变量,这个引用变量就是一个强引用。当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到,JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。


对于一个普通的对象,如果没有其他的引用关系,只要超过了引用的作用域或者显式地将相应 (强)引用赋值为 null,一般认为就是可以被垃圾收集的了(当然具体回收时机还是要看垃圾收集策略)。

class MyObject
{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable
    {
        // finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
        System.out.println("-------invoke finalize method~!!!");
    }
}
public static void strongReference()
{
    MyObject myObject = new MyObject();
    System.out.println("-----gc before: "+myObject);
    myObject = null;
    System.gc();
    try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
    System.out.println("-----gc after: "+myObject);
}


2.软引用


软引用是一种相对强引用弱化了一些的引用,需要用java.lang.ref.SoftReference类来实现,可以让对象豁免一些垃圾收集。


对于只有软引用的对象来说,


当系统内存充足时它 不会 被回收,


当系统内存不足时它 会 被回收。


软引用通常用在对内存敏感的程序中,比如高速缓存就有用到软引用,内存够用的时候就保留,不够用就回收!

private static void softReference()
    {
        SoftReference<MyObject> softReference = new SoftReference<>(new MyObject());
        //System.out.println("-----softReference:"+softReference.get());
        System.gc();
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after内存够用: "+softReference.get());//输出有值
        try
        {
            byte[] bytes = new byte[20 * 1024 * 1024];//设置JVM启动内存小于20M,创建20MB对象
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());//输出:null,内存不够才会回收
        }
    }


1673416288187.jpg

3.弱引用


弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

private static void weakReference()
    {
        WeakReference<MyObject> weakReference = new WeakReference<>(new MyObject());
        System.out.println("-----gc before 内存够用: "+weakReference.get());//有值
        System.gc();
        //暂停几秒钟线程
        try { TimeUnit.SECONDS.sleep(1); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("-----gc after 内存够用: "+weakReference.get());//输出:null
    }


软引用和弱引用的适用场景


假如有一个应用需要读取大量的本地图片:


如果每次读取图片都从硬盘读取则会严重影响性能,


如果一次性全部加载到内存中又可能造成内存溢出。


此时使用软引用可以解决这个问题。


设计思路是:用一个HashMap来保存图片的路径和相应图片对象关联的软引用之间的映射关系,在内存不足时,JVM会自动回收这些缓存图片对象所占用的空间,从而有效地避免了OOM的问题。

Map<String, SoftReference<Bitmap>> imageCache = new HashMap<String, SoftReference<Bitmap>>();

软引用:内存充足 不回收,内存不充足 回收


弱引用:只要gc执行,内存不管是否充足,都会回收

相关文章
|
7月前
|
存储 监控 安全
解锁ThreadLocal的问题集:如何规避多线程中的坑
解锁ThreadLocal的问题集:如何规避多线程中的坑
347 0
|
5月前
|
存储 缓存 算法
(五)JVM成神路之对象内存布局、分配过程、从生至死历程、强弱软虚引用全面剖析
在上篇文章中曾详细谈到了JVM的内存区域,其中也曾提及了:Java程序运行过程中,绝大部分创建的对象都会被分配在堆空间内。而本篇文章则会站在对象实例的角度,阐述一个Java对象从生到死的历程、Java对象在内存中的布局以及对象引用类型。
137 8
|
存储 监控 安全
ThreadLocal之强、弱、软、虚引用(下)
ThreadLocal之强、弱、软、虚引用
ThreadLocal之强、弱、软、虚引用(下)
|
Java
强、软、弱、虚引用
强、软、弱、虚引用
92 0
|
存储 Java
Java并发编程 - 线程封闭之 ThreadLocal(一)
Java并发编程 - 线程封闭之 ThreadLocal(一)
135 0
Java并发编程 - 线程封闭之 ThreadLocal(一)
|
存储 Java
Java并发编程 - 线程封闭之 ThreadLocal(二)
Java并发编程 - 线程封闭之 ThreadLocal(二)
146 0
Java并发编程 - 线程封闭之 ThreadLocal(二)
|
前端开发 Java
线程上下文类加载器ContextClassLoader内存泄漏隐患
今天(2020-01-18)在编写Netty相关代码的时候,从Netty源码中的ThreadDeathWatcher和GlobalEventExecutor追溯到两个和线程上下文类加载器ContextClassLoader内存泄漏相关的Issue,两个Issue分别是两位前辈在2017-12的时候提出的,描述的是同一类问题,最后被Netty的负责人采纳,并且修复了对应的问题从而关闭了Issue。这里基于这两个Issue描述的内容,对ContextClassLoader内存泄漏隐患做一次复盘。
175 0
线程上下文类加载器ContextClassLoader内存泄漏隐患
|
存储 IDE 数据可视化
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(下)
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(下)
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(下)
|
安全 IDE Java
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(上)
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(上)
hashCode竟然不是根据对象内存地址生成的?还对内存泄漏与偏向锁有影响?(上)
|
算法 Java 数据库连接
Java并发编程 - 线程封闭之 ThreadLocal(三)
Java并发编程 - 线程封闭之 ThreadLocal(三)
160 0