多线程进阶学习09------ThreadLocal详解

简介: 多线程进阶学习09------ThreadLocal详解

ThreadLocal:提供线程的局部变量,对于线程共享变量如果使用ThreadLocal则无需加锁,更省事省心。

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

synchronized到ThreadLocal的演进

①. synchronized或者lock,有个管理员,好比,现在大家签到,多个同学(线程),但是只有一只笔,只能同一个时间,只有一个线程(同学)签到,加锁(同步机制是以时间换空间,执行时间不一样,类似于排队)

d0790130232046ce92acf62871d7ab5a.png

②. ThreadLocal,人人有份,每个同学手上都有一支笔,自己用自己的,不用再加锁来维持秩序(同步机制是以空间换时间,为每一个线程都提供了一份变量的副本,从而实现同时访问,互不干扰同时访问,肯定效率高啊)

36ddb59e0b9b4f6a89bf2260555e836c.png

访问这个变量的每个线程都会有这个变量的本地副本

通常情况下,我们创建的变量是可以被任何一个线程访问并修改的。如果想实现每一个线程都有自己的专属本地变量该如何解决呢?

JDK 中提供的ThreadLocal类正是为了解决这样的问题。 ThreadLocal类主要解决的就是让每个线程绑定它自己局部的值。

如果内存当中存在一个ThreadLocal变量,那么访问这个变量的每个线程都会有这个变量的本地副本,这也是ThreadLocal变量名的由来。

372e1b92822b4a0488e3b7a125224b53.png

可以使用 get() 和 set() 方法来获取默认值或将其值更改为当前线程所存的副本的值,从而避免了线程安全问题

应用案例一:

【强制】SimpleDateFormat 是线程不安全的类,一般不要定义为static变量,如果定义为static,必须加锁,或者使用DateUtils工具类

import java.text.SimpleDateFormat;
import java.util.Random;
public class ThreadLocalExample implements Runnable{
    private static final ThreadLocal<SimpleDateFormat> formatter = ThreadLocal.withInitial(() -> new SimpleDateFormat("yyyyMMdd HHmm"));
    public static void main(String[] args) throws InterruptedException {
        ThreadLocalExample obj = new ThreadLocalExample();
        for(int i=0 ; i<10; i++){
            Thread t = new Thread(obj, ""+i);
            Thread.sleep(new Random().nextInt(1000));
            t.start();
        }
    }
    @Override
    public void run() {
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" default Formatter = "+formatter.get().toPattern());
        try {
            Thread.sleep(new Random().nextInt(1000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        //formatter pattern is changed here by thread, but it won't reflect to other threads
        formatter.set(new SimpleDateFormat());
        System.out.println("Thread Name= "+Thread.currentThread().getName()+" formatter = "+formatter.get().toPattern());
    }
}
//输出
Thread Name= 0 default Formatter = yyyyMMdd HHmm
Thread Name= 0 formatter = yy-M-d ah:mm
Thread Name= 1 default Formatter = yyyyMMdd HHmm
Thread Name= 2 default Formatter = yyyyMMdd HHmm
Thread Name= 1 formatter = yy-M-d ah:mm
Thread Name= 3 default Formatter = yyyyMMdd HHmm
Thread Name= 2 formatter = yy-M-d ah:mm
Thread Name= 4 default Formatter = yyyyMMdd HHmm
Thread Name= 3 formatter = yy-M-d ah:mm
Thread Name= 4 formatter = yy-M-d ah:mm
Thread Name= 5 default Formatter = yyyyMMdd HHmm
Thread Name= 5 formatter = yy-M-d ah:mm
Thread Name= 6 default Formatter = yyyyMMdd HHmm
Thread Name= 6 formatter = yy-M-d ah:mm
Thread Name= 7 default Formatter = yyyyMMdd HHmm
Thread Name= 7 formatter = yy-M-d ah:mm
Thread Name= 8 default Formatter = yyyyMMdd HHmm
Thread Name= 9 default Formatter = yyyyMMdd HHmm
Thread Name= 8 formatter = yy-M-d ah:mm
Thread Name= 9 formatter = yy-M-d ah:mm

从输出中可以看出,Thread-0 已经改变了 formatter 的值,但仍然是 thread-2 默认格式化程序与初始化值相同,其他线程也一样。

上面有一段代码用到了创建 ThreadLocal 变量的那段代码用到了 Java8 的知识,因为 ThreadLocal 类在 Java 8 中扩展,使用一个新的方法withInitial(),将 Supplier 功能接口作为参数。所以它等于下面这段代码。

private static final ThreadLocal<SimpleDateFormat> formatter = new ThreadLocal<SimpleDateFormat>(){
        @Override
        protected SimpleDateFormat initialValue()
        {
            return new SimpleDateFormat("yyyyMMdd HHmm");
        }
    };

应用案例二:

每个销售员可以出售多少套房子

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

②. 既然其他Thread不可访问,那就不存在多线程共享的问题

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

package com.bilibili.juc.tl;
import lombok.Getter;
import sun.font.FontRunIterator;
import java.util.Random;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicLong;
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());
    }
}
/**
 * @auther zzyy
 * @create 2021-12-31 15:46
 *
 * 需求1: 5个销售卖房子,集团高层只关心销售总量的准确统计数。
 *
 * 需求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"+"共计卖出多少套:saleCount "+house.saleCount);
        System.out.println(Thread.currentThread().getName()+"\t"+"共计卖出多少套:saleVolume "+house.saleVolume.get());
    }
}
//输出
D:\java8u211\jdk\bin\java.exe "-javaagent:D:\Program Files\IntelliJ IDEA 2020.3.1\lib\idea_rt.jar=57334:D:\Program Files\IntelliJ IDEA 2020.3.1\bin" -Dfile.encoding=GBK -classpath D:\java8u211\jdk\jre\lib\charsets.jar;D:\java8u211\jdk\jre\lib\deploy.jar;D:\java8u211\jdk\jre\lib\ext\access-bridge-64.jar;D:\java8u211\jdk\jre\lib\ext\cldrdata.jar;D:\java8u211\jdk\jre\lib\ext\dnsns.jar;D:\java8u211\jdk\jre\lib\ext\jaccess.jar;D:\java8u211\jdk\jre\lib\ext\jfxrt.jar;D:\java8u211\jdk\jre\lib\ext\localedata.jar;D:\java8u211\jdk\jre\lib\ext\nashorn.jar;D:\java8u211\jdk\jre\lib\ext\sunec.jar;D:\java8u211\jdk\jre\lib\ext\sunjce_provider.jar;D:\java8u211\jdk\jre\lib\ext\sunmscapi.jar;D:\java8u211\jdk\jre\lib\ext\sunpkcs11.jar;D:\java8u211\jdk\jre\lib\ext\zipfs.jar;D:\java8u211\jdk\jre\lib\javaws.jar;D:\java8u211\jdk\jre\lib\jce.jar;D:\java8u211\jdk\jre\lib\jfr.jar;D:\java8u211\jdk\jre\lib\jfxswt.jar;D:\java8u211\jdk\jre\lib\jsse.jar;D:\java8u211\jdk\jre\lib\management-agent.jar;D:\java8u211\jdk\jre\lib\plugin.jar;D:\java8u211\jdk\jre\lib\resources.jar;D:\java8u211\jdk\jre\lib\rt.jar;E:\Project\source-bulldozer\JavaConcurrent\target\classes;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter-web\2.5.8\spring-boot-starter-web-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter\2.5.8\spring-boot-starter-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot\2.5.8\spring-boot-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-autoconfigure\2.5.8\spring-boot-autoconfigure-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter-logging\2.5.8\spring-boot-starter-logging-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\ch\qos\logback\logback-classic\1.2.9\logback-classic-1.2.9.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\ch\qos\logback\logback-core\1.2.9\logback-core-1.2.9.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\apache\logging\log4j\log4j-to-slf4j\2.17.0\l
4 号销售卖出:2
1 号销售卖出:5
2 号销售卖出:5
3 号销售卖出:2
5 号销售卖出:2
main  共计卖出多少套:saleCount 16
main  共计卖出多少套:saleVolume 0
Process finished with exit code 0

ThreadLocal 源码

Thread类源码#仅仅定义HashMap

public class Thread implements Runnable {
 ......
//与此线程有关的ThreadLocal值。由ThreadLocal类维护
ThreadLocal.ThreadLocalMap threadLocals = null;
//与此线程有关的InheritableThreadLocal值。由InheritableThreadLocal类维护
ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
 ......
}

从上面Thread类 源代码可以看出Thread 类中有一个 threadLocals 和 一个 inheritableThreadLocals 变量,它们都是 ThreadLocalMap 类型的变量,我们可以把 ThreadLocalMap 理解为ThreadLocal 类实现的定制化的 HashMap。

默认情况下这两个变量都是 null,只有当前线程调用 ThreadLocal 类的 set或get方法时才创建它们。

ThreadLocal类源码#初始化HashMap

但为什么不通过Thread类调用getset,而是通过ThreadLocal 自己比如ThreadLocal formatter.set(value).getset呢?

●Thread类里面只是声明了ThreadLocalMap 但并未初始化

●ThreadLocal 在自己的getset方法内初始化了ThreadLocalMap ,并且传递了最终的变量值value,以及对value进行装填

public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
    public T get() {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        return setInitialValue();
    }   
    private T setInitialValue() {
        T value = initialValue();
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
        return value;
    }
    protected T initialValue() {
        return null;
    }

1、最后, ThreadLocalMap 的key是ThreadLocal ,多个线程Thread可以共用一个ThreadLocal 作为key,而不同的value才是最终的值。

2、相同的key并不会产生ThreadLocalMap当中Entry覆盖的问题,因为多个Thread并不会公用一个ThreadLocalMap,而是有几个Thread就会对应几个ThreadLocalMap

还是这张图:我们加深下对上述最后两点的理解

126b683320744387852b785f9eda90b5.png


ThreadLocal的静态内部类ThreadLocalMap如下:

static class ThreadLocalMap {
        /**
         * The entries in this hash map extend WeakReference, using
         * its main ref field as the key (which is always a
         * ThreadLocal object).  Note that null keys (i.e. entry.get()
         * == null) mean that the key is no longer referenced, so the
         * entry can be expunged from table.  Such entries are referred to
         * as "stale entries" in the code that follows.
         */
        static class Entry extends WeakReference<ThreadLocal<?>> {
            /** The value associated with this ThreadLocal. */
            Object value;
            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

ThreadLocal可能造成的问题

【强制】必须回收自定义的ThreadLocal变量,尤其在线程池场景下,线程经常会被复用,
如果不清理自定义的 ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。尽量在代理中使用try-finally块进行回收。 正例:
objectThreadLocal.set(userInfo);
try {
// ...
} finally {
objectThreadLocal.remove();
}

读懂阿里开发规范,由于线程池场景下的线程经常会被复用

●结果:影响后续业务逻辑

●结果:或造成内存泄露等问题

后续业务逻辑混乱问题

package com.bilibili.juc.tl;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
class MyData
{
    ThreadLocal<Integer> threadLocalField = ThreadLocal.withInitial(() -> 0);
    public void add()
    {
        threadLocalField.set(1 + threadLocalField.get());
    }
}
/**
 * @auther zzyy
.【强制】必须回收自定义的 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();
        }
    }
}
//输出
D:\java8u211\jdk\bin\java.exe "-javaagent:D:\Program Files\IntelliJ IDEA 2020.3.1\lib\idea_rt.jar=57644:D:\Program Files\IntelliJ IDEA 2020.3.1\bin" -Dfile.encoding=GBK -classpath D:\java8u211\jdk\jre\lib\charsets.jar;D:\java8u211\jdk\jre\lib\deploy.jar;D:\java8u211\jdk\jre\lib\ext\access-bridge-64.jar;D:\java8u211\jdk\jre\lib\ext\cldrdata.jar;D:\java8u211\jdk\jre\lib\ext\dnsns.jar;D:\java8u211\jdk\jre\lib\ext\jaccess.jar;D:\java8u211\jdk\jre\lib\ext\jfxrt.jar;D:\java8u211\jdk\jre\lib\ext\localedata.jar;D:\java8u211\jdk\jre\lib\ext\nashorn.jar;D:\java8u211\jdk\jre\lib\ext\sunec.jar;D:\java8u211\jdk\jre\lib\ext\sunjce_provider.jar;D:\java8u211\jdk\jre\lib\ext\sunmscapi.jar;D:\java8u211\jdk\jre\lib\ext\sunpkcs11.jar;D:\java8u211\jdk\jre\lib\ext\zipfs.jar;D:\java8u211\jdk\jre\lib\javaws.jar;D:\java8u211\jdk\jre\lib\jce.jar;D:\java8u211\jdk\jre\lib\jfr.jar;D:\java8u211\jdk\jre\lib\jfxswt.jar;D:\java8u211\jdk\jre\lib\jsse.jar;D:\java8u211\jdk\jre\lib\management-agent.jar;D:\java8u211\jdk\jre\lib\plugin.jar;D:\java8u211\jdk\jre\lib\resources.jar;D:\java8u211\jdk\jre\lib\rt.jar;E:\Project\source-bulldozer\JavaConcurrent\target\classes;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter-web\2.5.8\spring-boot-starter-web-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter\2.5.8\spring-boot-starter-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot\2.5.8\spring-boot-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-autoconfigure\2.5.8\spring-boot-autoconfigure-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\springframework\boot\spring-boot-starter-logging\2.5.8\spring-boot-starter-logging-2.5.8.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\ch\qos\logback\logback-classic\1.2.9\logback-classic-1.2.9.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\ch\qos\logback\logback-core\1.2.9\logback-core-1.2.9.jar;E:\dev_dir_temp\apache-maven-3.3.9\respository\org\apache\logging\log4j\log4j-to-slf4j\2.17.0\l
pool-1-thread-3 beforeInt:0  afterInt: 1
pool-1-thread-2 beforeInt:0  afterInt: 1
pool-1-thread-1 beforeInt:0  afterInt: 1
pool-1-thread-3 beforeInt:1  afterInt: 2
pool-1-thread-1 beforeInt:1  afterInt: 2
pool-1-thread-2 beforeInt:1  afterInt: 2
pool-1-thread-1 beforeInt:2  afterInt: 3
pool-1-thread-1 beforeInt:3  afterInt: 4
pool-1-thread-3 beforeInt:2  afterInt: 3
pool-1-thread-2 beforeInt:2  afterInt: 3
Process finished with exit code 0

内存泄露问题

每个Thread都有一个ThreadLocal.ThreadLocalMap类型的HashMap,该Map的key为ThreadLocal实例,他是一个弱引用,我们清楚弱引用有利于GC回收。

当ThreadLocal作为Map的key为null的时候,GC就会回收这一部分空间,但是value却不一定能够被回收,因为对于线程池当中的线程会被复用线程对象迟迟不会结束,value还与Current Thread存在一个强引用关系,如下图所示:

2dc686af6abd4250b201777656314438.png

ThreadLocalMap 中使用的 key 为 ThreadLocal 的弱引用,而 value 是强引用。所以,如果 ThreadLocal 没有被外部强引用的情况下,在垃圾回收的时候,key 会被清理掉,而 value 不会被清理掉。这样一来,ThreadLocalMap 中就会出现 key 为 null 的 Entry。假如我们不做任何措施的话,value 永远无法被 GC 回收,这个时候就可能会产生内存泄露。ThreadLocalMap 实现中已经考虑了这种情况,使用完 ThreadLocal方法后最好手动调用remove()方法会清理掉 key 为 null 的记录。

弱引用介绍:如果一个对象只具有弱引用,那就类似于可有可无的生活用品。弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它 所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。不过,由于垃圾回收器是一个优先级很低的线程, 因此不一定会很快发现那些只具有弱引用的对象。

为什么用弱引用?

ThreadLocal为每一个线程提供了一个私有的变量,这个现象是依托于ThreadLocalMap来实现的,map中的每一个Entry的key都是指向了一个threadlocal,这是一个弱引用,当外界对指向threadlocal的强引用回收之后,就说明这个threadlocal就没用了,但是此时还有map中的key也指向了它,若是这个key是一个强引用,那么我们就无法对threadlocal进行回收,就有可能造成一个内存泄漏的问题,所以使用了弱引用来解决这个问题,只有弱引用指向的对象,在下次垃圾回收时就会被回收

强软弱虚四大引用扩展讲解

强引用:最常见

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

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

因此强引用是造成Java内存泄漏的主要原因之一。

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

package com.bilibili.juc.tl;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
class MyObject
{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable
    {
        // finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
        System.out.println("-------invoke finalize method~!!!");
    }
}
/**
 * @auther zzyy
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        MyObject myObject = new MyObject();
        System.out.println("gc before: "+myObject);
        myObject = null;
        System.gc();//人工开启GC,一般不用
        //暂停毫秒
        try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
        System.out.println("gc after: "+myObject);
    }
}

软引用:看情况

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

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

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

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

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

2991c5dac0fe48b496884bdde0a3922b.png

package com.bilibili.juc.tl;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
class MyObject
{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable
    {
        // finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
        System.out.println("-------invoke finalize method~!!!");
    }
}
/**
 * @auther zzyy
 */
public class ReferenceDemo
{
    public static void main(String[] args)
        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];//20MB对象
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            System.out.println("-----gc after内存不够: "+softReference.get());
        }
    }
}

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

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

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

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

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

弱引用:无法豁免

弱引用需要用java.lang.ref.WeakReference类来实现,它比软引用的生存期更短,

对于只有弱引用的对象来说,只要垃圾回收机制一运行,不管JVM的内存空间是否足够,都会回收该对象占用的内存。

package com.bilibili.juc.tl;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
class MyObject
{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable
    {
        // finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
        System.out.println("-------invoke finalize method~!!!");
    }
}
/**
 * @auther zzyy
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        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());
    }
}

虚引用

  • 引用队列:虚引用必须和引用队列 (ReferenceQueue)联合使用 虚引用需要java.lang.ref PhantomReference类来实现,顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的 生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用不能单独使用也不 能通过它访问对象,虚引用必须和引用队列 (ReferenceQueue)联合使用。 对象被回收之后,有东西将被装入引用队列 (ReferenceQueue)中。gc之后!!!
  • 总是返回null:PhantomReference的get方法总是返回null 虚引用的主要作用是跟踪对象被垃圾回收的状态。 PhantomReference的get方法总是返回null,因此无法访问对应的引用对象
  • 通知机制:虚引用仅仅是提供了一种确保对象被 finalize以后,做某些事情的通知机制。处理监控通知使用,换句话说,设置虚引用关联对象的唯一目的,就是在这个对象被收集器回收的时候收到一个系统通知或者后续添加进一步的处理,用 来实现比finalize机制更灵活的回收操作
  • 7b4489facfeb4543bc0d1e05c815f3b3.png
package com.bilibili.juc.tl;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.TimeUnit;
class MyObject
{
    //这个方法一般不用复写,我们只是为了教学给大家演示案例做说明
    @Override
    protected void finalize() throws Throwable
    {
        // finalize的通常目的是在对象被不可撤销地丢弃之前执行清理操作。
        System.out.println("-------invoke finalize method~!!!");
    }
}
/**
 * @auther zzyy
 */
public class ReferenceDemo
{
    public static void main(String[] args)
    {
        MyObject myObject = new MyObject();
        ReferenceQueue<MyObject> referenceQueue = new ReferenceQueue<>();
        PhantomReference<MyObject> phantomReference = new PhantomReference<>(myObject,referenceQueue);
        //System.out.println(phantomReference.get());
        List<byte[]> list = new ArrayList<>();
        new Thread(() -> {
            while (true){
                list.add(new byte[1 * 1024 * 1024]);
                try { TimeUnit.MILLISECONDS.sleep(500); } catch (InterruptedException e) { e.printStackTrace(); }
                System.out.println(phantomReference.get()+"\t"+"list add ok");
            }
        },"t1").start();
        new Thread(() -> {
            while (true){
                Reference<? extends MyObject> reference = referenceQueue.poll();
                if(reference != null){
                    System.out.println("-----有虚对象回收加入了队列");
                    break;
                }
            }
        },"t2").start();
    }
}

52867cfc39a84a19a2d23e212ccd30d9.png



相关文章
|
6月前
|
存储 监控 安全
解锁ThreadLocal的问题集:如何规避多线程中的坑
解锁ThreadLocal的问题集:如何规避多线程中的坑
293 0
|
2月前
|
监控 Java 调度
【Java学习】多线程&JUC万字超详解
本文详细介绍了多线程的概念和三种实现方式,还有一些常见的成员方法,CPU的调动方式,多线程的生命周期,还有线程安全问题,锁和死锁的概念,以及等待唤醒机制,阻塞队列,多线程的六种状态,线程池等
134 6
【Java学习】多线程&JUC万字超详解
|
4月前
|
存储 SQL Java
(七)全面剖析Java并发编程之线程变量副本ThreadLocal原理分析
在之前的文章:彻底理解Java并发编程之Synchronized关键字实现原理剖析中我们曾初次谈到线程安全问题引发的"三要素":多线程、共享资源/临界资源、非原子性操作,简而言之:在同一时刻,多条线程同时对临界资源进行非原子性操作则有可能产生线程安全问题。
|
4月前
|
安全 Java
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
多线程线程安全问题之避免ThreadLocal的内存泄漏,如何解决
|
4月前
|
存储 安全 Java
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
多线程线程安全问题之ThreadLocal是什么,它通常用于什么场景
|
4月前
|
存储 缓存 Java
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
Java面试题:解释Java中的内存屏障的作用,解释Java中的线程局部变量(ThreadLocal)的作用和使用场景,解释Java中的锁优化,并讨论乐观锁和悲观锁的区别
53 0
|
4月前
|
并行计算 算法 安全
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
Java面试题:解释Java内存模型的内存屏障,并讨论其对多线程并发的影响,解释Java中的线程局部变量(ThreadLocal)的工作原理,解释Java中的ForkJoinPool的工作原理
42 0
|
5月前
|
NoSQL Redis
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
Redis系列学习文章分享---第五篇(Redis实战篇--优惠券秒杀,全局唯一id 添加优惠券 实现秒杀下单 库存超卖问题分析 乐观锁解决超卖 实现一人一单功能 集群下的线程并发安全问题)
127 0
|
5月前
|
调度 Python
Python多线程学习优质方法分享
Python多线程学习优质方法分享
26 0
|
5月前
|
安全 API C++
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
逆向学习Windows篇:C++中多线程的使用和回调函数的实现
187 0