请你谈谈对ThreadLocal的理解?

1理解Java的强引用、软引用、弱引用和虚引用

在这里插入图片描述

1强引用(StrongReference)

强引用是使用最普遍的引用,如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足时,Java虚拟机宁愿抛出OutOfMemoryError错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。 如果强引用对象不使用时,需要弱化从而使GC能够回收,如下:

    strongReference = null;

方法的内部使用强引用:这个引用保存在Java栈中,而真正的引用内容(Object)保存在Java堆中。 当这个方法运行完成后,就会退出方法栈,则引用对象的引用数为0,这个对象会被回收。

strongReference是全局变量时:就需要在不用这个对象时赋值为null,因为强引用不会被垃圾回收。

public class TO1_NormalReference{
	public static void main(String[] args) throws IOException {
		M m= new M();
		m = null;
		System.gc(); //DisableExplicitGC
		//System. out. printIn(m) ; 
		System. in.read();//阻塞main线程,给垃圾回收线程时间执行
	}
}

2软引用(SoftReference)

如果一个对象只具有软引用,则内存空间充足时,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。软引用可用来实现内存敏感的高速缓存。

public class TO2_ _SoftReference {
	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( millis: 500) ;
		} catch (Inter ruptedException e) {
			e.printStackTrace() ;
		}
		System. out . println(m.get()); // 没有被GC回收
		
		//再分配一个数组, heap将装不下,这时候系统会垃圾回收,先回收一次,如果不够, 会把软引用回收。
		byte[] b = new byte [1024* 1024*15] ;
		System.out . println(m.get());
		// 软引用可用来实现内存敏感的高速缓存。
	}
}

3弱引用(WeakReference)

弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存。

public class TO3_ WeakReference {
	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());
		
		/*ThreodLocal<M> tl = new ThreadLocal<>();
		tl.set(new M());
		tl. remove();*/
	}
}

4虚引用(PhantomReference)

虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动,管理直接内存使用的。

public class TO4_ PhantomReference {
	private static final List<object> LIST = new LinkedList<>();
	private static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>(); 
	
	public static void main(String[] args) {
		PhantomReference<M> phantomReference = new PhantomReference<> (new M(), QUEUE) ;
		new Thread(() -> {
			while (true) {
				LIST.add(new byte[10241024]);
			try{
				Thread.sleep( millis: 1000);
			} catch (InterruptedException e) {
				e.printStackTrace();
				Thread . currentThread(). interrupt();
			}
			System. out . println(phantomReference.get());
			}
		}).start();
		new Thread(() -> {
			while (true) {
				Reference<? extends M> poll = QUEUE.poll();
				if (poll != null) {
					System.out .println("---虚引用对象被jvm回收了---- " + poll);
				}
			}
		}).start();
		try {
			Thread.sleep( millis: 500);
		} catch (InterruptedException e) {
			e.printStackTrace() ;
		}
	}
}

2 理解ThreadLocal

package com.zs.thread;


public class WriteReadTest {
    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread(() -> add10ByThreadLocal());
            t.start();
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }
}

在这里插入图片描述

Thread类声明了成员变量threadLocals,threadLocals才是真正的线程本地变量,因此每个 Thread 都有自己的线程本地变量,所以线程本地变量拥有线程隔离特性,也就是天生的线程安全。

public class Thread implements Runnable {
	ThreadLocal.ThreadLocalMap threadLocals = null;
}

threadLocals 成员变量类是 ThreadLocal.ThreadLocalMap,即是 ThreadLocal 提供的内部类,因此 Thread 线程本地变量的创建、新增、获取、删除实现核心,必然是围绕 threadLocals,所以开发者也是围绕 threadLocals 实现功能,为了后续重复使用,还会对代码实现进行封装复用,而 ThreadLocal 就是线程本地变量工具类,由 J D K 提供,线程本地变量的功能都已经实现好了,开箱即用。
在这里插入图片描述
一个 Thread可以拥有多个 ThreadLocal键值对(存储在ThreadLocalMap结构),又因为 ThreadLocalMap 依赖当前Thread,Thread销毁时 ThreadLocalMap 也会随之销毁,所以 ThreadLocalMap 的生命周期与 Thread 绑定。
在这里插入图片描述「本地线程变量的作用域,属于当前线程整个范围,一个线程可以跨越多个方法使用本地线程变量」,当你希望某些变量在某 Thread 的多个方法中共享并保证线程安全,那就大胆的使用ThreadLocal。

2.1 ThreadLocal源码

package com.zs.thread;

class ThreadLocalTest {
    private ThreadLocal<ThreadVariable> threadLocal = new ThreadLocal<ThreadVariable>(){
        @Override
        protected ThreadVariable initialValue() {
            return new ThreadVariable("threadVariable initialValue()");
        }
    };

    public ThreadVariable getThreadVariable() {
        return threadLocal.get();
    }
    public void setThreadLocalVariable(ThreadVariable threadLocalVariable) {
        threadLocal.set(threadLocalVariable);
    }
    public void clear() {
        threadLocal.remove();
    }
}

class ThreadVariable {
    private String name;

    public ThreadVariable(String name) {
        this.name = name;
    }
}

2.2 ThreadLocalMap结构

在这里插入图片描述
ThreadLocalMap 结构已经非常清晰,发现 ThreadLocal 竟被弱引用持有?为什么ThreadLocal会被弱引用?
在这里插入图片描述

2.2.1 get 获取变量
    public T get() {
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程本地变量
        ThreadLocalMap map = getMap(t);
        if (map != null) {
            //如果线程本地变量不为空,通过当前ThreadLocal作为索引获取对应的Entry对象
            ThreadLocalMap.Entry e = map.getEntry(this);
            if (e != null) {
                //如果Entry对象不为空,获取value值并返回
                T result = (T)e.value;
                return result;
            }
        }
        /如果本地线程变量为null或者value为空,执行初始化value
        return setInitialValue();
    }

步骤如下:

1、获取当前线程
2、获取当前线程的本地变量
3、线程本地变量没有被创建,执行setInitialValue方法进行初始化,并返回value值
4、线程本地变量存在,ThreadLocal计算成索引从 本地线程变量 获取Entry,如果Entrynull,执行setInitialValue方法进行初始化,并返回value值,否则通过Entry获取value返回
2.2.2 initialValue方法
    private T setInitialValue() {
        // 执行initialValue方法,获取value
        T value = initialValue();
        // 获取当前线程
        Thread t = Thread.currentThread();
        // 获取线程本地变量
        ThreadLocalMap map = getMap(t);
        // 线程本地变量不为空,当前ThreadLocal作为索引设置映射的value
        if (map != null)
            map.set(this, value);
        else
            // 如果线程本地变量为空,创建线程本地变量,并把当前ThreadLocal作为索引设置映射的value
            createMap(t, value);
        return value;
    }

步骤如下:

通过get方法触发
执行初始化,获取到value
获取当前线程
获取当前线程本地变量
如果当前线程本地变量存在 ,ThreadLocal计算成索引设置映射的value,否则创建线程本地变量再做后续的设置操作
返回value值
2.2.3 set 设置变量
    public void set(T value) {
        Thread t = Thread.currentThread();
        ThreadLocalMap map = getMap(t);
        if (map != null)
            map.set(this, value);
        else
            createMap(t, value);
    }

步骤如下:

获取当前线程
获取线程本地变量
本地变量不为空,当前ThreadLocal为索引设置映射的value,否则创建线程本地变量再做后续的设置操作
2.2.4 remove 清除变量
        private void remove(ThreadLocal<?> key) {
            /获取Entry数组
            Entry[] tab = table;
            int len = tab.length;
            // 计算出当前ThreadLocal的数组下标
            int i = key.threadLocalHashCode & (len-1);
            for (Entry e = tab[i];
                 e != null;
                 e = tab[i = nextIndex(i, len)]) {
                //遍历,直到找到Entry中key为当前对象key的那个元素
                if (e.get() == key) {
                    e.clear();
                    expungeStaleEntry(i);
                    return;
                }
            }
        }

步骤如下:

获取Entry数组
当前ThreadLocal计算出索引
根据索引获取Entry元素(若是第一次没有命中,就循环直到null)
清除Entry元素

核心就三样:ThreadLocal线程本地变量工具类(同时作为索引)、Entry基本元素(由弱引用包装类ThreadLocal与value组成),Entry数组容器,到这里流程很清晰了。ThreadLocal计算出数组索引,用 ThreadLocal 与 value 构建出 Entry 元素,最终放入 Entry 容器中。

在这里插入图片描述
Entry的key都是一个弱引用:如果这个变量不再被其他对象使用时,可以自动回收这个ThreadLocal对象,避免可能的内存泄露(注意,Entry中的value,依然是强引用)。

理解ThreadLocal中的内存泄漏问题

虽然ThreadLocalMap中的key是弱引用,当不存在外部强引用的时候(tl = null),就会自动被回收,但是Entry中的value依然是强引用。

只有当Thread被回收时,这个value才有被回收的机会,否则,只要线程不退出,value总是会存在一个强引用。但是,要求每个Thread都会退出,是一个极其苛刻的要求,对于线程池来说,大部分线程会一直存在在系统的整个生命周期内,那样的话,就会造成value对象出现泄漏的可能。处理的方法是,在ThreadLocalMap进行set(),get(),remove()的时候,都会进行清理。

ThreadLocal并不能100%保证不发生内存泄漏,如果get()方法总是访问固定几个一直存在的ThreadLocal,那么清理动作就不会执行;如果你没有机会调用set()和remove(),那么这个内存泄漏依然会发生。当你不需要这个ThreadLocal变量时,主动调用remove(),这样对整个系统是有好处的。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值