【多线程与并发】ThreadLocal与强软弱虚引用

一、概述

ThreadLocal:线程本地变量,针对同一变量可以保存线程私有的值,保证多线程之间的数据隔离性。为什么不用方法内的局部变量?局部变量作用域为当前方法,而ThreadLocal可以跨方法获取。例如框架作者可以事先在threadlocal中设置好一些经常用到的变量,使用者就可以直接在自己的业务方法中获取到。列举几个在spring中的应用场景:

  1. spring的事务管理:每个请求处理对应的是一个线程,该线程中不同方法对数据库的操作都可以复用一个连接和事务。
  2. spring的国际化:每个请求到来后都会将语言信息保存起来,框架使用者可以直接通过LocaleContextHolder的静态方法获取语言、地区的上下文。

二、结构

ThreadLocal本身不会存储线程设置的值,而是通过往Thread对象的成员变量threadLocals(一个ThreadLocalMap类)增加键值对来实现的。不知道作者为什么是这样实现的。如果是我的话,就直接在ThreadLocal中加一个线程安全map,然后将线程对象作为map的key,线程设置的值为value。不过这样子做的话就会把重心从线程对象往ThreadLocal对象转移了。

与HashMap相类似,ThreadLocalMap也是一个entry数组,存储键值对,其中key是ThreadLocal对象,value是一个object对象。不过它没有链表和红黑树,几个核心的数据结构见如下源码。需要注意的是:entry中对key的引用是弱引用,为什么需要用到这个引用呢?这个会在下文”弱引用“小节分析。

每个ThreadLocal对象会自动生成一个threadLocalHashCode,用于决定它在ThreadLocalMap中的位置。当hash冲突时采取线性查询(即下标递增)的方式找寻空位。因此ThreadLocalMap不适合存储大量的键值对。和HashMap一样,ThreadLocalMap也有一个负载因子为2/3,当map容量超过table.length*2/3时会触发扩容。

public class ThreadLocal<T> {
	// 和HashMap一样hash值用于决定元素的位置
    private final int threadLocalHashCode = nextHashCode();

    private static AtomicInteger nextHashCode = new AtomicInteger();

    private static final int HASH_INCREMENT = 0x61c88647;
	// hash增长策略
    private static int nextHashCode() {
        return nextHashCode.getAndAdd(HASH_INCREMENT);
    }
    
	//...
	
    static class ThreadLocalMap {
		// key是弱引用
        static class Entry extends WeakReference<ThreadLocal<?>> {
            Object value;

            Entry(ThreadLocal<?> k, Object v) {
                super(k);
                value = v;
            }
        }

        private static final int INITIAL_CAPACITY = 16;
		// 核心数据结构
        private Entry[] table;
		// 存储kv对的数量
        private int size = 0;

        private int threshold; // Default to 0
        // 容量超过阈值时会触发扩容
        private void setThreshold(int len) {
            threshold = len * 2 / 3;
        }
        
		//...
	}
}	

三、源码分析

1.设置ThreadLocal变量的值

当使用ThreadLocal.set的时候,会检查当前线程是否已经有一个map了,如果有的话就把kv放到map中,如果没有就创建一个map并置入kv。

// java.lang.ThreadLocal#set
public void set(T value) {
    Thread t = Thread.currentThread();
    // 获取当前线程的ThreadLocalMap
    ThreadLocalMap map = t.threadLocals;
    if (map != null)
    	// 当前线程的ThreadLocalMap不为空时,将kv对插入该map,该set方法类似于put,详细步骤见下面一个方法
        map.set(this, value);
    else
    	// 当前线程的ThreadLocalMap为空时(第一次设置threadLocal变量),构造该线程的ThreadLocalMap并初始化
        t.threadLocals = new ThreadLocalMap(this, firstValue);
}

// java.lang.ThreadLocal.ThreadLocalMap#set
private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;
    // kv对位置的算法
    int i = key.threadLocalHashCode & (len-1);

	/* 
		递增i,线性查询空位子。有两种情况进入for循环:
		1. 重新设置ThreadLocal变量
		2. 哈希冲突
	*/
    for (Entry e = tab[i];
         e != null;
         // i递增,直到找到空位置
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

		// 重新设置ThreadLocal变量的场景
        if (k == key) {
            e.value = value;
            return;
        }
		// ThreadLocal已经被置为null
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }
	// 找到空位子,插入该键值对
    tab[i] = new Entry(key, value);
    int sz = ++size;
    // 超出阈值时扩容两倍,并重新计算entry位置,阈值为table.length*2/3其中2/3可以看做是负载因子
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

2.获取ThreadLocal变量的值

获取ThreadLocal变量就是从当前线程的threadLocalMap中找到该threadLocal对象对应的value

// java.lang.ThreadLocal#get
public T get() {
    Thread t = Thread.currentThread();
    ThreadLocalMap map = t.threadLocals;
    if (map != null) {
    	// 通过key获取到entry
        ThreadLocalMap.Entry e = map.getEntry(this);
        if (e != null) {
            T result = (T)e.value;
            return result;
        }
    }
    // 构建ThreadLocalMap,并用null初始化
    return setInitialValue();
}

// java.lang.ThreadLocal#setInitialValue
private T setInitialValue() {
    T value = null;
    Thread t = Thread.currentThread();
    ThreadLocalMap map = getMap(t);
    if (map != null)
        map.set(this, value);
    else
	    // 构建ThreadLocalMap,并用null初始化
        createMap(t, value);
    return value;
}

3.删除ThreadLocal变量的值

类似的,删除threadLocal变量的值的流程就是从当前线程的ThreadLocalMap中获取该threadLocal对应的entry然后将key和value置为null

// java.lang.ThreadLocal#remove
public void remove() {
    ThreadLocalMap m = getMap(Thread.currentThread());
    if (m != null)
    	// 取出当前线程的ThreadLocalMap,然后移除键值对
        m.remove(this);
}

// java.lang.ThreadLocal.ThreadLocalMap#remove
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)]) {
        // 考虑到hash冲突的情况需要判断key是否是同一个(比较地址)
        if (e.get() == key) {
            e.clear();
            expungeStaleEntry(i);
            return;
        }
    }
}

// java.lang.ThreadLocal.ThreadLocalMap#expungeStaleEntry
private int expungeStaleEntry(int staleSlot) {
    Entry[] tab = table;
    int len = tab.length;

    // key和value置为null等待gc,size减一
    tab[staleSlot].value = null;
    tab[staleSlot] = null;
    size--;

    // 重新计算entry的位置
    Entry e;
    int i;
    for (i = nextIndex(staleSlot, len);
         (e = tab[i]) != null;
         i = nextIndex(i, len)) {
        ThreadLocal<?> k = e.get();
        // 手动清理过期数据
        if (k == null) {
            e.value = null;
            tab[i] = null;
            size--;
        } else {
            int h = k.threadLocalHashCode & (len - 1);
            if (h != i) {
                tab[i] = null;


                while (tab[h] != null)
                    h = nextIndex(h, len);
                tab[h] = e;
            }
        }
    }
    return i;
}

四、引用分类

强软弱虚四种引用的引入和jvm的内存回收密不可分。
引用分类图

1.强引用

强引用是我们日常使用最普遍的引用方式例如Object o = new Object(),只要强引用存在,其指向的对象无论何时都不会被jvm回收,换句话说只要引用不指向null该对象就会一直存活。

 /*
输出:
	 obj cleaned...
 */
public static void testStrongRef() throws InterruptedException {
    MyObj myObj = new MyObj("obj", null);
    System.gc();
    myObj = null;
    System.gc();
    TimeUnit.MILLISECONDS.sleep(100);
}


static class MyObj {
    private String name;
    public Reference<byte[]> padding;

    MyObj(String name, Reference<byte[]> padding) {
        this.name = name;
        this.padding = padding;
    }

    @Override
    public String toString() {
        return name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(this.name + " cleaned...");
    }
}

2.软引用

软引用指向的对象在jvm堆内存不足时会被释放。比较适用于有用但不是必须的对象,可以用作缓存。

/*
说明:
	MyObj类就是上面那个片段代码中的类
	需要增加虚拟机参数-Xmx10M
	下面不会输出'soft3 cleaned...',因为软引用对象本身不会被回收,回收的是它指向的对象,即字节数组
	
输出:
	null
	[B@2f4d3709
	null
	null
	[B@4e50df2e
	null
	null
	[B@1d81eb93
*/
public static void testSoftRef() throws InterruptedException {

    MyObj myObj1 = new MyObj("soft1", new SoftReference<>(new byte[5 * 1024 * 1024]));
    MyObj myObj2 = new MyObj("soft2", new SoftReference<>(new byte[5 * 1024 * 1024]));
    System.out.println(myObj1.padding.get());
    System.out.println(myObj2.padding.get());

    SoftReference<byte[]> bytes1 = new SoftReference<>(new byte[5 * 1024 * 1024]);
    SoftReference<byte[]> bytes2 = new SoftReference<>(new byte[5 * 1024 * 1024]);
    System.out.println(myObj2.padding.get());
    System.out.println(bytes1.get());
    System.out.println(bytes2.get());

    SoftObj softObj1 = new SoftObj("soft3", new byte[5 * 1024 * 1024]);
    SoftObj softObj2 = new SoftObj("soft4", new byte[5 * 1024 * 1024]);
    System.out.println(bytes2.get());
    System.out.println(softObj1.get());
    System.out.println(softObj2.get());

    TimeUnit.MILLISECONDS.sleep(100);
}

static class SoftObj extends SoftReference<byte[]> {
    private String name;

    SoftObj(String name, byte[] bytes) {
        super(bytes);
        this.name = name;
    }

    @Override
    protected void finalize() throws Throwable {
        System.out.println(this.name + " cleaned...");
    }
}

3.弱引用

弱引用指向的对象只要在gc之后就会被释放回收,即使这个gc不是由内存不足产生的,或者gc也没有清除弱引用指向的对象。

/*
输出:
	[B@2f4d3709
	null
*/
public static void testWeakRef(){
    WeakReference<byte[]> weak = new WeakReference<>(new byte[1]);
    System.out.println(weak.get());
    System.gc();
    System.out.println(weak.get());
}

像上文ThreadLocalMap中对key的引用就是弱引用,目的是为了防止内存泄露。因为当threadLocal的引用被指向null后,若ThreadLocalMap的key还是强引用的话则这对entry就永远得不到释放了,可以思考下面一段代码。
tl.set(”test“)会在当前线程内部的threadLocalMap设置一对kv(key为tl对象,v为test字符串),当tl指向null后,若threadLocalMap对key(也就是tl对象)的引用是强引用则这一对kv永远也释放不了了,即使该tl不再使用了。

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

    public void testThreadLocal() {
        tl.set("test");
        System.out.println(tl.get());
        tl = null;
    }

4.虚引用

虚引用是四类引用中最为特殊的一种,它创建的时候除了要传递包裹的对象外还要增加一个引用队列参数。并且你无法获取虚引用指向的对象。那么虚引用究竟有什么作用呢?它是jvm管理直接内存(direct memory,是堆外内存)的一种方式,直接内存的分配和回收都是有Unsafe类去操作,java在申请一块直接内存之后,会在堆内存分配一个对象保存这个堆外内存的引用,这个对象被垃圾收集器管理,一旦这个对象被回收,相应的用户线程会收到通知并对直接内存进行清理工作。工作方式类似于下面这段代码。

/*
说明:
	phantomReference.get()永远返回null
	当虚引用指向的对象被gc时,虚引用对象就会进入到引用队列中。
*/
public static void testPhantomRef() throws InterruptedException {
    Object obj = new Object();	// 可以看做是堆外内存
    ReferenceQueue<Object> referenceQueue = new ReferenceQueue<>();
    PhantomReference<Object> phantomReference = new PhantomReference<>(obj, referenceQueue);
    System.out.println(phantomReference.get());
    System.out.println(referenceQueue.poll());

    Thread thread = new Thread(() -> {
        Reference reference;
        for (reference = referenceQueue.poll(); reference == null; ) {
            reference = referenceQueue.poll();
        }
        System.out.println(reference);	// 感知到堆外内存被gc了,unsafe清理堆外内存
    });

    thread.start();
    obj = null;
    System.gc();	//堆外内存被gc
    thread.join();

}

五、小结

对象被回收时间 用途
强引用 永远不会 对象常用的引用方式
软引用 内存不足时 缓存可以缓存但非必须缓存的对象
弱引用 gc发生后 ThreadLocal
虚引用 所指向的对象被gc jvm管理直接内存

六、参考

Java:强引用,软引用,弱引用和虚引用
强软弱虚引用,只有体会过了,才能记住

猜你喜欢

转载自blog.csdn.net/hch814/article/details/107458407
今日推荐