JUC学习笔记 - 05强软弱虚引用以及ThreadLocal

377 阅读5分钟

ThreadLocal

初识

首先看一个小程序:

public class TestThreadLocal {
    volatile static Person p = new Person();

    public static void main(String[] args) {

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(p.name);
        }, "t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            p.name = "p2";
        }, "t2").start();
    }
}

class Person {
    String name = "p1";
}

这个程序最终打印的是p2,很好理解,t2线程先于t1线程修改了同一个对象,所以最终打印的是修改过后的值。如果想让这个对象在每个线程里独有一份,那就可以使用ThreadLocal了:

public class ThreadLocal {
    static ThreadLocal<Person> tl = new ThreadLocal<>();

    public static void main(String[] args) {
        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(2);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            System.out.println(tl.get());
        }, "t1").start();

        new Thread(() -> {
            try {
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            tl.set(new Person());
        }, "t2").start();
    }

    static class Person {
    }
}

上述代码最终输出的是null,说明t2线程设置的值在t1线程中无法读取到。我们尝试读ThreadLocal的源码来理解。

ThreadLocal源码

首先进入set方法看一下:

public class ThreadLocal<T> {
    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;
    }
}

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

首先获取当前线程,然后从当前线程中拿到ThreadLocalMap,最后将value值设置到该ThreadLocalMap中。可以看到ThreadLocalMap是由线程维护的,也就是说ThreadLocalMap是在Thread里,而不是在ThreadLocal里。看到这里就可以明白为什么在当一个线程中的ThreadLocal设置的值在另一个线程无法读取到了。

内存泄漏问题:

key:key本身对应ThreadLocal,若局部方法执行完毕,线程指向ThreadLocalMap,key又指向ThreadLocal对象,如果是强引用则不会被回收。所以在设计时,key的引用为弱引用;

value:普通线程使用ThreadLocal,不remove其实也没事儿,线程销毁就没引用指向ThreadLocalMap,自然也可以回收。但如果线程池中的核心线程使用了ThreadLocal,那就必须remove,因为核心线程不会销毁,会导致内存泄漏。

Java中的四种应用:强软弱虚

首先重写一下finalize()方便System.gc()的时候观察观察:

public class Observation {
    @Override
    protected void finalize() throws Throwable {
        System.out.println("finalize");
    }
}

强引用

默认的引用就是强引用,只要有一个应用指向某个对象,那么垃圾回收的时候一定不回回收它。举个例子:

    public static void main(String[] args) throws IOException {
        Observation observation = new Observation();
//        observation = null;
        System.gc();

        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

可以看到并没有输出“finalize”,说明gc时没有回收observation对象。如果将observation = null,才会触发垃圾回收。

软引用

假如要创建一个Observation对象的软引用,则需要这样写:SoftReference<Observation> m = new SoftReference<>(new Observation())。先试一下如下代码:

    public static void main(String[] args) {
        SoftReference<Observation> m = new SoftReference<>(new Observation());
        m = null;
        System.gc();

        try {
            Thread.sleep(Long.MAX_VALUE);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

可以看到正常打印了“finalize”,那么软引用到底是干啥的呢?再来看看下面这段代码:

    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(500);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(m.get());

        byte[] b = new byte[1024 * 1024 * 15];
        System.out.println(m.get());
    }

运行上述代码需要设置堆内存为20M:-Xmx20M。可以看到前两个输出都有内容,最后一个输出为null。第一次创建字节数组时分配了10MB,gc时不回回收该对象。第二次创建数组时需要分配15MB,由于内存不够了所以会回收第一个数组,所以最后输出了null。即当有一个对象被一个软引用所指向的时候,如果系统内存不够用的时候就会回收它。

弱引用

弱引用只要遭遇到gc就会回收,举个例子:

    public static void main(String[] args) {
        WeakReference<Observation> m = new WeakReference<>(new Observation());
        System.out.println(m.get());
        System.gc();
        System.out.println(m.get());
    }

可以看到gc后第二次打印的是null

弱引用最典型的应用就是ThreadLocal,回到ThreadLocalMapset方法:

    private void set(ThreadLocal<?> key, Object value) {
        // We don't use a fast path as with get() because it is at
        // least as common to use set() to create new entries as
        // it is to replace existing ones, in which case, a fast
        // path would fail more often than not.
        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)]) {
            ThreadLocal<?> k = e.get();
            if (k == key) {
                e.value = value;
                return;
            }
            if (k == null) {
                replaceStaleEntry(key, value, i);
                return;
            }
        }
        tab[i] = new Entry(key, value);
        int sz = ++size;
        if (!cleanSomeSlots(i, sz) && sz >= threshold)
            rehash();
    }

    static class Entry extends WeakReference<ThreadLocal<?>> {
        /** The value associated with this ThreadLocal. */
        Object value;

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

可以看到set方法中实际存放的是new Entry(key, value),而这个Entry继承自WeakReference,且这个弱引用装的是ThreadLocal,也就是说Entrykey是一个弱引用。假如这里是个强引用,那么当最开始指向new ThreadLocal<>()的引用消失时,ThreadLocal里的对象是无法被回收的,这样就会造成内存泄漏。但如果这个key是弱引用就不会有这样的问题了。

但是这里的key被回收后就会变成null,且原本对应的value值无法访问,依旧存在内存泄漏的问题。所以使用ThreadLocal时如果对象不用了,一定要使用remove方法避免内存泄露。

虚引用

虚引用时用于堆外内存管理的,且构造方法有两个,其中第二个是队列。可以看到虚引用是get不到值的。一旦虚引用里的对象被垃圾回收,那么QUEUE会接收到通知,即被回收的对象会被放入该队列。

public class TestPhantomReference {
    private static Object o = new Object();
    private static final ReferenceQueue<Object> QUEUE = new ReferenceQueue<>();
    private static PhantomReference<Object> phantomReference = new PhantomReference<>(o, QUEUE);

    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                try {
                    System.out.println(phantomReference.get());
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            o = null;
            System.gc();
        }).start();

        new Thread(() -> {
            while (true) {
                Reference<? extends Object> poll = QUEUE.poll();
                if (poll != null) {
                    System.out.println("--- 虚引用对象被jvm回收了 ---- " + poll);
                }
            }
        }).start();
    }
}