java四种引用方式与ThreadLocal解析

java四种引用类型

从jdk1.2以来,java把对象的引用定义为四种级别,从而使程序可以更加灵活地控制对象的生命周期。四种引用类型按照由强到弱的顺序分别为:强引用、软引用、弱引用、虚引用。

强引用

public static void main(String[] args) throws IOException {
    M m = new M();
    m = null;
    System.gc();
    System.in.read();
}

m就是持有堆内M对象的一个强引用,也是我们最常见的一种引用形式,属于不可回收资源,垃圾回收期不会主动回收它。当内存空间不足的时候,jvm会抛出OutOfMemoryError错误,也不会主动释放强引用对象。

如果想释放强引用对象,可以显示的把引用复制为null,这样jvm就会在合适的时间释放掉强引用对象。

软引用

SoftReference m = new SoftReference(new byte[1024 * 1024 * 10]);
System.out.println(m.get());
System.gc();
try {
    Thread.sleep(500);
} catch (InterruptedException e) {
    e.printStackTrace();
}
byte[] b = new byte[1024 * 1024 * 15];
System.out.println(m.get() );

如果内存空间足够,垃圾回收器不会回收软引用对象,如果内存空间不足,垃圾回收器一定会回收软引用的对象。

弱引用

public static void main(String[] args) {
    WeakReference m = new WeakReference<>(new M());
    System.out.println(m.get());  // 返回对象
    System.gc();
    System.out.println(m.get());  // 返回null
}

弱引用有更短的生命周期,垃圾回收器只要扫描到弱引用对象,就会清除弱引用对象。

虚引用

private static final List LIST = new LinkedList<>();
private static final ReferenceQueue QUEUE = new ReferenceQueue<>();

public static void main(String[] args) {
    PhantomReference phantomReference = new PhantomReference<>(new M(), QUEUE);
    System.out.println(phantomReference.get());

    new Thread(()->{
        for (int i = 0; i < 30; i++) {
            LIST.add(new byte[1024 * 1024]);
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                e.printStackTrace();
                Thread.currentThread().interrupt();
            }
            System.out.println(phantomReference.get());
        }

    }).start();

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

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

虚引用不影响对象的生命周期。虚引用需要和引用队列相关,当垃圾回收器准备回收一个对象,发现它还有虚引用,就会把虚引用加入到与之关联的引用队列中。

ThreadLocal与弱引用

ThreadLocal是什么

ThreadLocal是java.lang包中实现相同线程数据共享、不同线程数据隔离的工具。

static final ThreadLocal tl = new ThreadLocal<>();
public static void main(String[] args) {
    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":"+tl.get());
    }).start();

    new Thread(()->{
        try {
            TimeUnit.SECONDS.sleep(2);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        tl.set("hello");
    }).start();
    
}

ThreadLocal和锁之间的关系:锁解决的是线程之间数据共享的问题,ThreadLocal出现的场景是线程之间没有数据共享,只涉及到同一个线程不同位置使用数据的问题。

ThreadLocal实现原理

对象实例(对应value)和ThreadLocal对象实例(对应key)是由线程Thread来维护的。对象实例和ThreadLocal实例的映射关系存放在Thread对象中的一个map属性中,这个map的类型为ThreadLocalMap。

由于ThreadLocalMap没有对外暴露方法,想要修改或者获取map中的值只能通过ThreadLocal的api来完成。如果Threadlocal对象销毁了,那么就没有办法获取到map中的值了。

// ThreadLocal的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);
}

// 可以看到map是Thread对象的一个属性
ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

// 创建map,并赋值给Thread对象的一个属性
void createMap(Thread t, T firstValue) {
        t.threadLocals = new ThreadLocalMap(this, firstValue);
    }

所以涉及的值存储的地方都是由ThreadLocalMap来完成的,ThreadLocalMap维护了ThreadLocal对象和实例对象的一个映射关系。

// ThreadLocalMap的set方法
private void set(ThreadLocal key, Object value) {
    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();
}

ThreadLocalMap中使用Entry[]数组来维护键值对的映射关系,其中key为ThreadLocal对象,value为实例对象value。并且这里的key使用了弱引用,如果对应的ThreadLocal对象的强引用不存在了,这里面的ThreadLocal对象也会被清理掉。

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

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

这样设计的目的主要是:如果ThreadLocal对象已经不在被强引用了,那么已经没有办法再访问到map中的值了,因为通过Thread对象的属性访问map,然后map再访问value的路径走不通(map的方法不对外暴露)。这个时候可以认为key已经没有用了。

那么为什么不把value也设置为弱引用呢?因为不清楚这个value除了map的引用是否还存在其他的引用。如果是弱引用,当其他地方value的强引用被清理掉之后,gc时就把value(只有弱引用)直接干掉了。Threadlocal对象作为的key还存在,而value却不存在了,显然是不合适的。

ThreadLocal与内存泄露

当把ThreadLocal对象的强引用设置为null之后,就只剩下TheadLocalMap的key为弱引用类型,那么在下次gc的时候会回收掉ThreadLocal对象占用的空间,key就变成null了,而value还存在没有被回收。而且由于map是Thread对象的一个属性,只要Thread对象不销毁,那么value的强引用就一直存在,这样就导致了内存泄露。如果Thread对象过一段时间就销毁,那么影响不会特别大,如果是在线程池场景下,Thread对象一直不销毁,就会导致value一直存在。

所以当线程中的某个ThreadLocal对象使用完了,需要立即调用remove方法,删除Entry对象。

你可能感兴趣的:(java四种引用方式与ThreadLocal解析)