五、架构师-高并发与多线程-ThreadLocal java的四种引用类型 强软弱虚

从JDK1.2版本开始,把对象的引用分为四种级别,从而使程序能更加灵活的控制对象的生命周期。java的引用有强软弱虚四种类型,其中弱引用类型和ThreadLocal密切相关。ThreadLocal:线程本地变变量。以下会对这几个概念原理和使用场景进行分析。

强引用

强引用类型是普通的引用,是最常用的。比如:Object o = new Object()中引用o就是强引用类型。当一个对象具有强引用的时候,GC是不会对其回收的,就算内存不够用,宁可抛出OutOfMemoryError错误也不会回收它。只有这个这个对象不被引用的时候才会被回收。

public class ThreadLocal01 {
    public static void main(String[] args) throws IOException {
        M m = new M();
        System.gc();
        System.out.println("有引用时执行gc后 M对象 :" + m);

        m = null;
        System.gc();
        System.out.println("没有引用的时候执行gc后 M对象:" + m);

        System.in.read();
    }
}

public class M {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize ....被回收");
        super.finalize();
    }
}

在这里插入图片描述

软引用

软引用的创建需要借助jdk中java.lang.ref.SoftReference这个类来创建。也就是说,我们的变量是先引用到SoftReference这个对象,SofReference这个对象再去引用我们想要设置为软引用的对象。
当堆内存够用时,被软引用指向的对象不会被GC回收。
当堆内存不够用时,被软引用指向的对象自动的被GC回收。
例如:

/**
 * 运行时 把堆大小设置为30M  -Xmx30M
 */
public class Reference01 {

    public static void main(String[] args) throws InterruptedException {
        // 10M
        SoftReference<byte[]> m = new SoftReference<>(new byte[1024 * 1024 * 10]);
        System.out.println(m.get());
        System.gc();

        TimeUnit.SECONDS.sleep(2);

        System.out.println(m.get());
        // 又申请15M
        byte[] b = new byte[1024 * 1024 * 15];
        System.out.println(m.get());
    }

}

在这里插入图片描述
运行时分配了30M的堆内存,可以看到当内存不够用时,即使SoftReference中指向的对象被引用也会被收回。

应用场景:JVM本地缓存可以使用。

弱引用

弱引用的创建方式与软引用类似,需要借助于jdk中java.lang.ref.WeakReference类去创建。
只有弱引用的对象不管内存够不够用,只要有GC都会被回收。
例子:

public class Reference02 {
    public static void main(String[] args) {
        WeakReference<M> m = new WeakReference<M>(new M());

        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
        
    }
}

结果:
在这里插入图片描述
弱引用的一个经典用法就是ThreadLocal类。下面会着重分析一下ThreadLocal类。

ThreadLocal

ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:

  • 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
  • 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。

三个重要的方法:

  • public T get()
  • public void set(T value);
  • public void remove();
    
    public static final ThreadLocal<Object> th = new ThreadLocal<>();

    public static void main(String[] args) throws IOException {
        
        th.set(new Object());

        Object o = th.get();

     	th.remove();

    }

实现原理源码分析

  1. 首先看一下Thread类
public class Thread implements Runnable {
   ......(其他源码)
    /* 
     * 当前线程的ThreadLocalMap,主要存储该线程自身的ThreadLocal
     */
    ThreadLocal.ThreadLocalMap threadLocals = null;

    /*
     * InheritableThreadLocal,自父线程集成而来的ThreadLocalMap,
     * 主要用于父子线程间ThreadLocal变量的传递
     */
    ThreadLocal.ThreadLocalMap inheritableThreadLocals = null;
    ......(其他源码)
}

Thread类中包含 threadLocalsinheritableThreadLocals 两个变量,其中threadLocals里面存放的变量只对该线程可见,* inheritableThreadLocals* 主要存储可自动向子线程中传递的ThreadLocal.ThreadLocalMap,不仅对该线程自己可见,其子线程也可见

inheritableThreadLocal对子线程可见的实现:

if (inheritThreadLocals && parent.inheritableThreadLocals != null)
            this.inheritableThreadLocals =
                ThreadLocal.createInheritedMap(parent.inheritableThreadLocals);

不过本文重点所关注的是ThreadLocalThreadLocal.ThreadLocalMap

在这里插入图片描述
在Thread中threaLocals属性只在ThreadLocal被使用;而这两处代码分别是ThreadLocal类中的ThreadLocalMap getMap(Thread t)void createMap(Thread t, T firstValue)方法调用。

    /**
     * Get the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     * 返回Thread中的threadLocals
     * @param  t the current thread
     * @return the map
     */
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

    /**
     * Create the map associated with a ThreadLocal. Overridden in
     * InheritableThreadLocal.
     * 给Thread中的threadLocals设置初始对象
     * @param t the current thread
     * @param firstValue value for the initial entry of the map
     */
    void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }
  1. public void set(T value)
    public void set(T value) {
    	// 获取当前线程
        Thread t = Thread.currentThread();
        // 获取当前线程中的ThreadLocalMap对象
        ThreadLocalMap map = getMap(t);
        // 设置当前线程中的ThreadLocalMap的值,其中key是当前ThreadLocal对象
        if (map != null)
            map.set(this, value);
        else
        	// map不存在则创建一个
            createMap(t, value);
    }

ThreadLocalMap map对象中的key是当前ThreadLocal实例,而不是当前线程。

  1. public T get()
    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;
            }
        }
        // 不存在则返回默认值,ThreadLocal默认值是null
        return setInitialValue();
    }
  1. public void remove()

    /**
     * Removes the current thread's value for this thread-local
     * variable.  If this thread-local variable is subsequently
     * {@linkplain #get read} by the current thread, its value will be
     * reinitialized by invoking its {@link #initialValue} method,
     * unless its value is {@linkplain #set set} by the current thread
     * in the interim.  This may result in multiple invocations of the
     * {@code initialValue} method in the current thread.
     *
     * @since 1.5
     */
     public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

可以看到这三个方法最终对数据的操作都是ThreadLocalMap类中的Entry数组。ThreadLocalMap是ThreadLocal的静态内部类,Entry是ThreadLocalMap的静态内部类;可以理解为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;
            }
        }

        /**
         * The initial capacity -- MUST be a power of two.
         */
        private static final int INITIAL_CAPACITY = 16;

        /**
         * The table, resized as necessary.
         * table.length MUST always be a power of two.
         */
        private Entry[] table;
        
	.......其他代码
}

所以最终ThreadLocal和Thread的关系应该是这样子的:
在这里插入图片描述

ThreadLocal与弱引用

在ThreadLocalMap中Enty继承了WeakReference类;

   static class Entry extends WeakReference<ThreadLocal<?>> {
         /** The value associated with this ThreadLocal. */
         Object value;
         
         Entry(ThreadLocal<?> k, Object v) {
             super(k);
             value = v;
         }
     }

Enty的构造函数调用了super(k)方法,那么这时此时Enty的key通过弱引用指向了ThreadLocal对象;所以当ThreadLocal对象只存在弱引用的时候,遇到垃圾回收的时候就会被清理;但是value是强引用,不会被清理掉,这样就会出现key为bull的value。

在ThreadLocalMap实现的时候已经考虑到了这种情况在调用set(),get(),remove()方法的时候,就会清理掉key为null的记录。如果说会出现内存泄漏,那么久只有在出现了key为null的记录后,没有手动调用remove()方法,之后也不再调用 get()、set()方法的情况下。

所以建议收回自定义的ThreadLocal对象,特别是在线程池的情况下,线程会被重复使用,如果不清理自定义的ThreadLocal变量,可能会影响后续业务逻辑和造成内存泄露等问题。 尽量在代理中使用try-finally块进行回收:

objectThreadLocal.set(userInfo); 
try {
    // ... 
} 
finally {
    objectThreadLocal.remove(); 
}

ThreadLocal 使用场景

  1. 存储用户Session
private static final ThreadLocal threadSession = new ThreadLocal();

    public static Session getSession() throws InfrastructureException {
        Session s = (Session) threadSession.get();
        try {
            if (s == null) {
                s = getSessionFactory().openSession();
                threadSession.set(s);
            }
        } catch (HibernateException ex) {
            throw new InfrastructureException(ex);
        }
        return s;
    }
  1. 解决线程安全问题:SimpleDateFormat是非线程安全的
public class DateUtil {
    private static ThreadLocal<SimpleDateFormat> format1 = new ThreadLocal<SimpleDateFormat>() {
        @Override
        protected SimpleDateFormat initialValue() {
            return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        }
    };

    public static String formatDate(Date date) {
        return format1.get().format(date);
    }
}
  1. 数据库连接session
    等等

虚引用

虚引用是一种十分特殊的引用,它主要用在堆外内存的管理,虚引用可以指向jvm堆中的对象,但是没有实际的意义。

public class Reference3 {
    private static final List<Object> LIST = new LinkedList<>();

    public static final ReferenceQueue<M> QUEUE = new ReferenceQueue<>();

    public static void main(String[] args) {
        PhantomReference<M> p = new PhantomReference<>(new M(), QUEUE);

        new Thread(() -> {
           while (true){
           	// 不停的加内存使其产生GC
               LIST.add(new byte[1024 * 1024]);
               try {
                   TimeUnit.SECONDS.sleep(1);
               } catch (InterruptedException e) {
                   e.printStackTrace();
               }
               System.out.println(p.get());
           }
        }).start();

        new Thread(() -> {
           while (true){
               Reference<? extends M> poll = QUEUE.poll();
               // 存放管理对外内存对象的QUEUE,如果有管理对外内存的对象被回收,那么该对象会被放到这个队列里
               if(poll != null){
                   System.out.println("---虚引用被jvm回收了");
               }
           }
        }).start();
    }
}

在这里插入图片描述

虚引用只要是用来管理对外内存的;由于对外内存不归JVM的GC管理,所以当jvm中的对象使用了堆外内存后,要主动的去释放掉。p引用指向一个PhantomReference对象,PhantomReference对象又指向了堆外内存;当GC对PhantomReference对象被回收的时候,并不能同时回收堆外内存;所以就用一个QUEUE队列来存放被GC回收掉的含有堆外内存引用的对象,然后由JVM调用对应的方法释放堆外内存。


版权声明:本文为weixin_36994557原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明。