JUC 五. ThreadLocal 与 四大引用

一. 基础

  1. 什么是ThreadLocal: 前线程的线程私有的本地变量,每个线程在通过get(),set()访问ThreadLocal时,都有自己的独立初始化的变量副本
  2. 你用ThreadLocal实现了什么功能:利用每个线程都有自己的ThreadLocal,多线程操作是互不影响线程安全的特性
  3. ThreadLocal常见面试题示例
  1. ThreadLocal中ThreadLocalMap的数据结构和关系了解吗
  2. ThreadLocal的key是什么
  3. ThreadLocal内存泄漏问题讲一下,怎么防止
  4. ThreadLocal中最后为什么要加remove方法
  1. 常用api
    在这里插入图片描述
  2. 示例
import java.util.concurrent.TimeUnit;
public class ThreadLocalDemo {
    
    //1.初始化方式1
    //原使用该方式创建ThreadLocal
    private static ThreadLocal<Object> threadLocal = new ThreadLocal<Object>() {
        @Override
        protected Object initialValue() {
            return "";
        }
    };

    //初始化方式2
    //使用JDK1.8提供的withInitial()
    private static ThreadLocal<Integer> threadLocal1 = ThreadLocal.withInitial(() -> 0);

  	//2.获取ThreadLocal值
    public static Integer get() {
        return threadLocal1.get();
    }

    //3.设置值
    public static void set(int value) {
        threadLocal1.set(value);
    }

    //4.清除值(ThreadLocal使用完毕后必须在finally中remove,
    //防止内存泄漏,需考虑线程池下线程会被复用)
    public static void clear() {
        threadLocal1.remove();
    }

    //5.测试运行,线程t1,t2分别设置自己的ThreadLocal
    public static void main(String[] args) throws InterruptedException {
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                ThreadLocalDemo.set(i);
            }
            System.out.println(Thread.currentThread().getName() + "线程,获取ThreadLocal值" + ThreadLocalDemo.get());
        }, "t1").start();

        new Thread(() -> {
            for (int i = 0; i < 10; i++) {
                ThreadLocalDemo.set(i);
            }
            System.out.println(Thread.currentThread().getName() + "线程,获取ThreadLocal值" + ThreadLocalDemo.get());
        }, "t2").start();

        TimeUnit.SECONDS.sleep(5L);
        System.out.println("主线程执行完毕,主线程的ThreadLocal值" + ThreadLocalDemo.get());
    }
}

使用ThreadLocal存储数据时的几个注意点

  1. 如果使用SimpleDateFormat,SimpleDateFormat是线程不安全的,不要定义为static,如果必须定义为staitc,必须加锁,或使用DateUtils工具类,下方代码中在执行第三步main方法时,会报错,
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;

public class ThreadLocalDeom2 {

    //1.format格式(正常思维:多个位置使用同一个日期格式,防止每次使用都需要创建对象,将format格式定义为static类型)
    private static SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:class");

    //2.提供日期字符串转日期类工具方法
    public static Date parse(String dateStr) throws ParseException {
        return sdf.parse(dateStr);
    }

    //3.多线程运行测试
    public static void main(String[] args) {

        for (int i = 0; i < 5; i++) {
            new Thread(() -> {
                try {
                    ThreadLocalDeom2.parse("2021-09-23 11:20:33");
                } catch (ParseException e) {
                    e.printStackTrace();
                }
            }).start();
        }

    }
}
  1. 原因: SimpleDateFormat类内部又一个Calender对象引用,它用来存储SimpleDateFormat相关的时间信息,当SimpleDateFormat使用static修饰时,那么多个线程之间共享SimpleDateFormat,同时也会共享这个Calendar,Caleandar内部会多次引用与clear(),是线程不安全的,存在bug,所以会报错
  2. 使用ThreadLocal方式每个线程都有自己的SimpleDateFormat,不存在多线程问题(另外还有不使用static方式,加锁方式等,但是性能低,或使用DateUtils工具类)
    在这里插入图片描述
  3. 一定要清除ThreadLocal(ThreadLocal使用完毕后必须在finally中remove,防止内存泄漏,需考虑线程池下线程会被复用)

二 ThreadLocal 源码分析

  1. 先提出一个问题: Thread, ThreadLocal, ThreadLocalMap 三者有什么关系(了解一下整体结构)
    在这里插入图片描述
  1. Thread 是一个线程类,在Thread线程类中有一个"ThreadLocal.ThreadLocalMap threadLocals = null;" 属性,这说明每个线程中都有一个ThreadLocal,是线程的局部变量
  2. 查看 ThreadLocalMap, 是ThreadLocal的内部的"static class ThreadLocalMap" 静态内部类
  3. 而ThreadLocalMap中还有一个静态内部类 “static class Entry extends WeakReference<ThreadLocal<?>>”
  1. 由这个整体关系图我们了解到: ThreadLocal底层使用ThreadLocalMap存储,ThreadLocalMap底层使用一个静态内部类Entry进行存储,由Entry继承自WeakReference了解到ThreadLocal使用弱引用,由Entry结构了解到,ThreadLocal底层存储的是key value键值对
  2. 进而提出问题
  1. ThreadLocal底层使用key value键值对存储, key是什么, value是什么
  2. 什么是弱引用,Entry 中的key为什么使用弱引用
  1. 先了解一下ThreadLocalMap: 实际就是一个以当前ThreadLocal实例为key,存储的任意数据对象为value的Entry,当我们为ThreadLocal变量赋值时,实际就是当前ThreadLocal为key,值为value的entry放入ThreadLocalMap中
  2. 可以这样理解在JVM中维护了一个线程版的Map,该Map结构是Map<ThreadLocal,Value>, 通过ThreadLocal的set方法存储数据时,结果是把ThreadLocal自身作为key放进了ThreadLocalMap中,在用到这个数据时通过get方法去这个map中寻找,不同的线程由不同的ThreadLocal,这样每个线程都拥有了自己的独立变量

通过 ThreadLocal 的 get() 方法进行源码分析

  1. 查看ThreadLocal 的get()方法源码, 在get() 方法中会获取当前线程,获取当前线程的ThreadLocal.ThreadLocalMap,如果获取不到说明未被初始化,调用setInitialValue()方法进行初始化
  2. 在setInitialValue() 方法中 重点是调用 “createMap(t, value)” 创建 ThreadLocalMap
	public T get() {
		//1.获取当前线程
        Thread t = Thread.currentThread();
        //2.获取当前线程中ThreadLocal.ThreadLocalMap属性值
        ThreadLocalMap map = getMap(t);
		//3.判断获取到的ThreadLocalMap是否为null(当前使用的ThreadLocal是否初始化)
        if (map != null) {
        	//5.不为空,获取Entry参数
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                @SuppressWarnings("unchecked")
                T result = (T)e.value;
                return result;
            }
        }
        //4.未被初始化调用 setInitialValue()初始化
        return setInitialValue();
    }


	//第四步中初始化方法
	private T setInitialValue() {
		//1.初始化ThreadLocal
        T value = initialValue();
        //2.获取当前线程
        Thread t = Thread.currentThread();
        //3.获取当前线程中的TheadLocal.ThreadLocalMap
        ThreadLocalMap map = getMap(t);
        //4.如果获取到的ThreadLocalMap不为null,将当前初始化ThreadLocal设置到ThreadLocalMap中
        if (map != null)
            map.set(this, value);
        else
        	//5.创建ThreadLocalMap
            createMap(t, value);
        return value;
    }
  1. 查看setInitialValue()中创建ThreadLocalMap方法createMap(),前面我们了解过在ThreadLocal中有一个静态内部类ThreadLocalMap, 在ThreadLocalMap中有一个静态内部类Entry, 在createMap()方法中会调用ThreadLocalMap构造器,首先初始化长度INITIAL_CAPACITY为16,以当前ThreadLocal为key,存储的值为value,创建Entry,获取一个下标位置"firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);",将创建的Entry存储到数组中
	//1.创建ThreadLocalMap,内部调用它的构造函数
	void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

	//初始化table也就是Entry数组的长度
	private static final int INITIAL_CAPACITY = 16;
	
	//ThreadLocalMap构造器
	ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
			//1.初始化Entry数组长度 "INITIAL_CAPACITY = 16"
            table = new Entry[INITIAL_CAPACITY];
            //2.获取到value变量存储下标
            int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
            //3.以当前ThreadLocal自身为key, 存储的值为value创建Enrty进行存储
            table[i] = new Entry(firstKey, firstValue);
            //3.第一次初始化存储完毕设置size属性值为1
            size = 1;
            //4.设置threshold属性值
            setThreshold(INITIAL_CAPACITY);
   }
	private int threshold; // Default to 0
   	private void setThreshold(int len) {
       threshold = len * 2 / 3;
   	}

三 通过 ThreadLocal 引出四大引用

  1. 什么是内存泄漏: 不会再被使用的对象或变量占用内存不能被回收,就是内存泄漏
  2. 前面我们了解到在ThreadLocal中底层实际存储数据的是一个静态内部类ThreadLocalMap中的的静态内部类Entry,这个Entry继承了 WeakReference<ThreadLocal<?>> 父类,说明Entry的key是个弱引用
    在这里插入图片描述

四大引用

  1. 在java中分为: 强引用Reference, 软引用SoftReference, 弱引用WeakReference, 虚引用PhantomReference, (下图中的ReferenceQueue引用队列)
    在这里插入图片描述
  2. 复习JVM: 在java中如果一个对象被判定为不可达对象时,在执行垃圾回收该对象会被回收掉,在该对象被回收前会执行finalize()方法,进行指定的清理工作(一般这个方法工作中不用)
    在这里插入图片描述
    在这里插入图片描述

默认强引用Reference

  1. 示例代码: 只要对象还是被引用对象不会回收,当对象不再被引用,例如第二步中设置为null,执行gc,该对象才会被回收
import java.util.concurrent.TimeUnit;

//创建的对象默认就是强引用类型
class MyReference {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("gc, finalize() invoked");
    }
}

public class ReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        //1.创建强引用对象
        MyReference my = new MyReference();
        System.out.println("gc 前 my:" + my);
        //2.设置对象不再引用
        my = null;
        //3.手动gc
        System.gc();
        //4.防止主线程马上停止休眠
        TimeUnit.SECONDS.sleep(5L);
        System.out.println("gc 后 my:" + my);
    }
}
  1. 把一个对象赋值给引用变量,这个引用变量就是一个强引用,当一个对象被强引用变量引用时,它处于可达状态,只要被判定为可达对象,哪怕内存不足,发送OOM,也不会被垃圾回收,只有在判定为不可达对象时例如设置为null才会被回收,既强引用是产生内存泄漏的原因之一

软引用SoftReference

  1. 软引用的特点: 内存够用不会被回收,内存不够时会被回收,即使该对象还在引用中
  2. 方便测试设置手动设置内存为10m
    在这里插入图片描述
    在这里插入图片描述
  3. 示例:软引用使用 SoftReference 实现,创建软引用对象 SoftReference softReference, 当内存不够用时,例如上面设置内存为10m,在第三步中创建一个9m对象,此时会出现内存溢出问题,MyReference中的finalize会执行,在第四步获取对象,打印为null,说明软引用对象在内存不够用时会被回收掉
public class ReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        //1.使用SoftReference将MyReference对象设置为软引用
        SoftReference<MyReference> softReference = new SoftReference<>(new MyReference());

        //(防止异常无法打印对象,通过finally包一下)
        try {
            //2.手动创建一个9m的对象,制造内存不够用的场景
            byte[] bytes = new byte[9 * 1024 * 1024];
            //3.防止主线程马上停止休眠
            TimeUnit.SECONDS.sleep(5L);
        } finally {
            //4.获取对象并打印,输出null
            System.out.println("my: " + softReference.get());
        }
    }
}

弱引用WeakReference

  1. 弱引用使用WeakReference实现,对于弱引用来说只要垃圾回收执行,不管JVM内存够不够用,弱引用对象都会被回收
  2. 示例,在第二步手动触发gc(), 被修饰为弱引用的MyReference对象会被回收掉
public class ReferenceDemo {
    public static void main(String[] args) throws InterruptedException {
        //1.使用WeakReference将MyReference对象设置为弱引用
        WeakReference<MyReference> weakReference = new WeakReference<>(new MyReference());
        //2.手动触发垃圾回收
        System.gc();
        //3.防止主线程马上停止休眠
        TimeUnit.SECONDS.sleep(5L);
        //4.获取对象并打印,输出null
        System.out.println("my: " + weakReference.get());
    }
}
  1. 软引用与弱引用的适用场景:例如将图片文件等对象数据作为软/弱引用,当内存不住或,垃圾回收时,将该对象回收掉,防止出现oom等等

虚引用PhantomReference

在这里插入图片描述

四. ThreadLocal 中存在的内存泄漏问题与

在这里插入图片描述

  1. Thread 内部维护了一个ThreadLocalMap的引用, 而ThreadLocalMap是ThreadLocal的内部类,底层使用Entry结构存储数据,在ThreadLocal调用set()方法时,实际就是往ThreadLocalMap中存储数据,Key是ThreadLocal本身,Value是传递进来的值通过这两个key value创建Entry对象,当调用get()方法时,就是通过ThreadLocal本身为key,让线程在ThreadLocalMap中获取对应的value值,进而实现每个线程之间相互隔离的本地局部变量
  2. 那么结合前面了解到的四大引用的特点,思考一下ThreadLocalMap中的key为什么要使用弱引用,以下方代码为案例进行解释: 创建了ThreadLocal 变量,存储了数据"111",此时t1是强引用,当setVal()方法执行完毕后,栈帧销毁,那么t1对象跟随销毁掉,但是前面我们看源码了解到在ThreadLocal底层使用ThreadLocal自身为key,存储数据为value创建Entry对象后存储为ThreadLocalMap,如果这个key是强引用,会出现key引用指向的这个ThreadLocal与对应这个key的value都在引用中,即使setVal()方法执行完毕也不能被判定为不可达对象,进而不会被回收造成内存泄漏问题
	public void setVal() {
		//1.创建ThreadLocal对象t1, t1为强引用
        ThreadLocal<Integer> t1= new ThreadLocal<>();
        t1.set(111);
        Integer i = t1.get();
    }
  1. 上面我们知道ThreadLoca使用弱引用的原因" Entry extends WeakReference<ThreadLocal<?>>" 防止一直是强引用,不能回收对象造成内存泄漏, 但是当外部的ThreadLocal被设置为null,根据可达性分析,这个ThreadLocal实例不在被任何链路引用,此时就会被回收掉,进而又引出一个问题ThreadLocal被回收为null,造成key为null了那么这个ThreadLocalMap中就变成了"< null, value >", 假如说持有这个ThreadLocalMap的线程迟迟不结束(例如使用线程池情况下),这些key为null的Entry就会一直存在,也会造成无法回收的问题,最终造成内存泄漏: 因此弱引用并不能百分百保证内存不泄漏问题,所以在使用ThreadLocal对象后,需要手动调用remove方法来删除它,防止线程池场景下线程一直不结束,大量key为null的Entry出现,或复用线程获取到上一个业务逻辑中创建的ThreadLocal数据
  2. 查看get(), set(), remove()源码会发现,在执行时会调用 expungeStaleEntry() 检查key是否为null,如果为null会设置对应的Entry为null, (下方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();
                    //检查并清除key为null的Entry
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

五. 总结

  1. 在Thread中持有ThreadLocal.ThreadLocalMap 属性, ThreadLocalMap是ThreadLocal的静态内部类, 在ThreadLocalMap中有一个继承了WeakReference的静态内部类Entry, 说明这个Entry是弱引用,查看ThreadLocal中的get()方法的源码会发现, 在调用get方法获取数据时,首先会调用currentThread()获取到当前线程,获取该线程中的ThreadLocal.ThreadLocalMap属性,如果获取为空,会执行setInitialValue()方法初始化ThreadLocal, 最终会执行到一个createMap(t, value)方法,创建ThreadLocalMap, 在该方法中,首先会创建一个长度为16的Entry数组,创建一个Entry对象,key是当前ThreadLocal本身,value为存储的值,创建完成后,将这个Entry放入数组中
  2. 那么ThreadLocalMap中的Entry为什么设置为弱引用, 首先要了解一下四大引用: 强引用Reference, 软引用SoftReference, 弱引用WeakReference, 虚引用PhantomReference, 几种引用的特点
  1. 强引用对象: 只要还存在引用,垃圾回收时不会给回收掉
  2. 软引用对象: 垃圾回收时,如果内存不足才会被回收掉
  3. 弱引用对象: 只要垃圾回收都会被回收掉
  1. 前面了解到在存储ThreadLocal数据时ThreadLocalMap中以当前ThreadLocal本身做为key创建了Entry对象,就会造成key引用指向的这个ThreadLocal与对应这个key的value都在引用中,即使存储方法setVal()执行完毕也不能被判定为不可达对象,进而不会被回收造成内存泄漏问题, 所以将ThreadLocalMap中的Entry内部类设置为弱引用,只要执行垃圾回收都会被回收调
  2. 这样又引出一个问题,ThreadLocal被回收为null,造成key为null了那么这个ThreadLocalMap中就变成了"< null, value >" key为null的键值对, 假设持有当前ThreadLocalMap的线程一直不结束,这些key为null的Entry就会一直存在,也会造成无法回收的问题,最终造成内存泄漏: 因此弱引用并不能百分百保证内存不泄漏问题,所以在使用ThreadLocal对象后,需要手动调用remove方法来删除它,防止线程池场景下线程一直不结束,大量key为null的Entry出现,或复用线程获取到上一个业务逻辑中创建的ThreadLocal数据
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值