Java四种引用类型以及ThreadLocal
Java中引用类型和ThreadLocal
大厂面试题:
- 1.Java中的引用类型有哪几种? 强软弱虚
- 2.每种引用类型的特点是什么?
- 3.每种引用类型的应用场景是什么?
- 4.ThreadLocal你了解吗
- 5.ThreadLocal应用在什么地方? Spring事务方面应用到了
- 6.ThreadLocal会产生内存泄漏你了解吗?
1、java中引用类型及特点
强 引用: 最普通的引用 Object o = new Object()
软 引用: 垃圾回收器, 内存不够的时候回收 (缓存)
弱 引用: 垃圾回收器看见就会回收 (防止内存泄漏)
虚 引用: 垃圾回收器看见二话不说就回收,跟没有一样 (管理堆外内存) DirectByteBuffer -> 应用到NIO Netty
finalize(): 当对象被回收时, finalize()方法会被调用, 但是不推荐使用去回收一些资源,因为不知道他什么时候会被调用, 有时候不一定会调用
public class C {
@Override
protected void finalize() throws Throwable {
System.out.println("finalize");
}
}
1.1 强引用
正常引用,但没有人指向的时候就会被回收
import java.io.IOException;
/**
* 强引用
*/
public class R1_NormalReference {
public static void main(String[] args) throws IOException {
//正常引用
C c = new C();
c = null;//没人指向
System.gc();//DisableExplicitGC
//阻塞一下,方便看结果
System.in.read();
}
}
1.2 软引用
垃圾回收器, 内存不够的时候回收 (缓存)
import java.io.IOException;
import java.lang.ref.SoftReference;
/**
* 软引用
*/
public class R2_SoftReference {
public static void main(String[] args) {
SoftReference<byte[]> soft = new SoftReference<>(new byte[1024 * 1024 * 10]);//10M
System.out.println(soft.get());
//gc回收
System.gc();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(soft.get());
//再分配一个数组,好heap(堆)放不下, 这个时候系统会回收一次, 如果不够,会把软引用回收
byte[] bytes = new byte[1024 * 1024 * 15];
System.out.println(soft.get());
}
}
结果:
[B@1540e19d
[B@1540e19d
null
前提设置 -Xmx30M 堆内存最大30M 用于测试
1.3 弱引用
遇到GC就会被回收
import java.lang.ref.WeakReference;
/**
* 弱引用
*/
public class R3_WeakReference {
public static void main(String[] args) {
WeakReference<C> weak = new WeakReference<>(new C());
System.out.println(weak.get());
//gc回收
System.gc();
//遇到GC就会被回收
System.out.println(weak.get());
}
}
结果:
com.cz.reference.C@3c679bde
null
finalize
1.4 虚引用
不管三七二十一 遇到直接回收
一个对象是否有虚引用的存在,完全不会对其生命周期构成影响,也无法通过虚引用获得一个对象实例。
虚引用只有一个含有队列的构造函数,也就是说,虚引用必须和队列同时使用,换句话说,虚引用是通过队列来实现它的价值的。
当虚引用所指向的那块内存被回收之后,JVM就会把那个虚引用的变量放到队列中,表示对象被回收。
import java.lang.ref.PhantomReference;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.util.LinkedList;
import java.util.List;
/**
* 虚引用
*/
public class R4_PhantomReference {
private static final List<Object> LIST = new LinkedList<>();
private static final ReferenceQueue QUEUE = new ReferenceQueue();
public static void main(String[] args) {
PhantomReference<C> phantomReference = new PhantomReference<>(new C(),QUEUE);
new Thread(() -> {
while (true){
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<? extends C> poll = QUEUE.poll();
if (poll != null){
System.out.println("-----虚引用对象被JVm回收了--------" + poll);
return;
}
}
}).start();
}
}
结果:
null
null
finalize
null
null
使用虚引用的目的就是为了得知对象被GC的时机,所以可以利用虚引用来进行销毁前的一些操作,比如说资源释放等。这个虚引用对于对象而言完全是无感知的,有没有完全一样,但是对于虚引用的使用者而言,就像是待观察的对象的把脉线,可以通过它来观察对象是否已经被回收,从而进行相应的处理。
事实上,虚引用有一个很重要的用途就是用来做堆外内存的释放,DirectByteBuffer就是通过虚引用来实现堆外内存的释放的。
总结: 强软弱虚
- 强 正常的引用
- 软 内存不够, 进行清除
- 大对象的内存
- 常用对象的缓存
- 弱 遇到GC就会被回收
- 缓存, 没有容器引用指向的时候就需要清除缓存
- ThreadLocal
- WeakReferenceMap
- 虚 看见就回收, 且看不到值
- 管理堆外内存
2 ThreadLocal
从Java官方文档中的描述:ThreadLocal类用来提供线程内部的局部变是。这种变畺在多线程环境下访问(通 过get和set方法访问)时能保证各个线程的变星=相对独立于其他线程内的变垦。ThreadLocal实例通常来说都是 private static类型的,用于关联线程和线程上下文。
2.1 特点
特点 | 内容 |
---|---|
1.线程并发 |
在多线程并发场景下 |
2.传递数据 |
我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量 (保存每个线程的数据,在需要的地方可以直接获取, 避免参数直接传递带来的代码耦合问题) |
3.线程隔离 |
每个线程的变量都是独立的, 不会互相影响.(核心) (各线程之间的数据相互隔离却又具备并发性,避免同步方式带来的性能损失) |
2.2 ThreadLocal vs Synchronized
虽然ThreadLocal模式与Synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同
synchronized | ThreadLocal | |
---|---|---|
原理 | 同步机制采用以时间换空间 的方式,只提供了一份变量, 让不同的线程排队访问 |
ThreadLocal采用以空间换时间 的方式, 为每一个线程都提供了一份变量的副本, 从而实现同访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
2.3 ThreadLocal的内部结构
早期设计
现在让我们看一下ThreadLocal的内部原理, 探究它能实现线程数据隔离的原理
每个ThreadLocal都创建一个Map
, 然后用Thread(线程) 作为Map的key
, 要存储的局部变量作为Map的value
, 这样就能达到各个线程的局部变量隔离的效果, 这是最简单的设计方法. 早期设计
JDK8 优化设计(现在的设计)
JDK8中ThreadLocal的设计是 : 每个Thread维护一个ThreadLocalMap, 这个Map的key是ThreadLocal实例本身,value才是真正要存储的值Object
每个THreadLocal线程内部都有一个Map(ThreadLocalMap)
Map里面存储的ThreadLocal对象(key)和线程变量副本(Value)也就是存储的值
Thread内部的Map是由ThreadLocal维护的, 有THreadLocal负责向map获取和设置线程变量值
对于不同的线程, 每次获取value(也就是副本值),别的线程并不能获取当前线程的副本值, 形成了副本的隔离,互不干扰.
对比一下,发现早期 的设计 ThreadLocal维护了(包含了,对应了)一个ThreadLocalMap,这个map中的Key就是线程
这样的坏处是:
一般多线程的时候map存储的Entry很多,线程越来越大,map越来越大
当一个Thread销毁时,由于ThreadLocalMap不会随之销毁,因为还有其他线程在里面存着。
还有一点,如果拿Thread作为Key,试想一个Thread内,如果我定义了多个ThreadLocal对象,如果我想获取一个线程中存储的变量,那么我该如何获取确定的哪一个变量呢?
现在这种设计,讲Thread与ThreadLocal关系互换,ThreadLocalMap由Thread 对象维护(包含,阅读源码,发现ThreadLocalMap是 Thread的成员变量)这样,一个Thread内可以创建多个ThreadLocal对象,并获取对应的值
2.4 ThreadLocalMap
通过查看源码,发现 对于ThreadLocal对象的增删查改操作,其底层都是调用ThreadLocalMap实现的(具体关系可以看上面的图)
- 一般首先要获取当前线程
- 通过线程对象得到线程实例对象的成员变量ThreadLocalMap,
- 对这个ThreadLocalMap进行增删查改
ThreadLocalMap成员变量
/**
* The initial capacity -- MUST be a power of two.
* 初始化容量,必须是2的整数次幂
*/
private static final int INITIAL_CAPACITY = 16;
/**
* 存放数据的table, 同样数组长度必须是2的整数次幂
* The table, resized as necessary.
* table.length MUST always be a power of two.
*/
private Entry[] table;
/**
* 数组里entrys的个数,可以判断table是否超过阈值 (存储的格式)
* The number of entries in the table.
*/
private int size = 0;
/**
* 阈值 进行扩容的阈值,表使用大于他的时候,进行扩容
* The next size value at which to resize.
*/
private int threshold; // Default to 0
存储结构
/**
* 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.
翻译:
* Entry继承WeakReference, 并且用ThreadLocal作为key
* 如果key为null(entry.get() == null)意味着key不在被引用,因此这时候entry也可以从tab
*中清除(被垃圾回收器回收)
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
下面的一些介绍来自博客threadlocal 内部为什么是一个map,而不是set?
从上图中看出,在每个Thread类中,都有一个ThreadLocalMap的成员变量,该变量包含了一个Entry数组,该数组真正保存了ThreadLocal类set的数据。
Entry是由threadLocal和value组成,其中threadLocal对象是弱引用,在GC的时候,会被自动回收。而value就是ThreadLocal类set的数据。
下面用一张图总结一下引用关系:
上图中除了Entry的key对ThreadLocal对象是弱引用,其他的引用都是强引用。
需要特别说明的是,上图中ThreadLocal对象我画到了堆上,其实在实际的业务场景中不一定在堆上。因为如果ThreadLocal被定义成了static的,ThreadLocal的对象是类共用的,可能出现在方法区。
为什么用ThreadLocal做key?
不知道你有没有思考过这样一个问题:ThreadLocalMap为什么要用ThreadLocal做key,而不是用Thread做key?
一个线程中很有可能不只使用了一个ThreadLocal对象。这时使用Thread做key不就出有问题?
如你的service实现类中
@Service public class ThreadLocalService { private static final ThreadLocal<Integer> threadLocal1 = new ThreadLocal<>(); private static final ThreadLocal<Integer> threadLocal2 = new ThreadLocal<>(); private static final ThreadLocal<Integer> threadLocal3 = new ThreadLocal<>(); }
假如使用Thread做key时,你的代码中定义了3个ThreadLocal对象,那么,通过Thread对象,它怎么知道要获取哪个ThreadLocal对象呢?
因此,不能使用Thread做key,而应该改成用ThreadLocal对象做key,这样才能通过具体ThreadLocal对象的get方法,轻松获取到你想要的ThreadLocal对象。
Entry的key为什么设计成弱引用?
前面说过,Entry的key,传入的是ThreadLocal对象,使用了WeakReference对象,即被设计成了弱引用。
那么,为什么要这样设计呢?
假如key对ThreadLocal对象的弱引用,改为强引用。
我们都知道ThreadLocal变量对ThreadLocal对象是有强引用存在的。
即使ThreadLocal变量生命周期完了,设置成null了,但由于key对ThreadLocal还是强引用。此时,如果执行该代码的线程使用了线程池,一直长期存在,不会被销毁。
就会存在这样的强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> key -> ThreadLocal对象。
那么,ThreadLocal对象和ThreadLocalMap都将不会被GC回收,于是产生了内存泄露问题。
为了解决这个问题,JDK的开发者们把Entry的key设计成了弱引用。
弱引用的对象,在GC做垃圾清理的时候,就会被自动回收了。
如果key是弱引用,当ThreadLocal变量指向null之后,在GC做垃圾清理的时候,key会被自动回收,其值也被设置成null。
如下图所示:
接下来,最关键的地方来了。
由于当前的ThreadLocal变量已经被指向null了,但如果直接调用它的get、set或remove方法,很显然会出现空指针异常。因为它的生命已经结束了,再调用它的方法也没啥意义。
此时,如果系统中还定义了另外一个ThreadLocal变量b,调用了它的get、set或remove,三个方法中的任何一个方法,都会自动触发清理机制,将key为null的value值清空。
如果key和value都是null,那么Entry对象会被GC回收。如果所有的Entry对象都被回收了,ThreadLocalMap也会被回收了。
这样就能最大程度的解决内存泄露问题。
需要特别注意的地方是:
- key为null的条件是,ThreadLocal变量指向null,并且key是弱引用。如果ThreadLocal变量没有断开对ThreadLocal的强引用,即ThreadLocal变量没有指向null,GC就贸然的把弱引用的key回收了,不就会影响正常用户的使用?
- 如果当前ThreadLocal变量指向null了,并且key也为null了,但如果没有其他ThreadLocal变量触发get、set或remove方法,也会造成内存泄露。
- 为了直接回收资源可以使用完ThreadLocal对象之后调用ThreadLocal对象的remove方法
弱引用就能解决内存泄漏?
通过上面的Entry对象中的key设置成弱引用,就能彻底解决内存泄露问题?
答案是否定的。
如下图所示:
假如ThreadLocalMap中存在很多key为null的Entry,但后面的程序,一直都没有调用过有效的ThreadLocal的get、set或remove方法。
那么,Entry的value值一直都没被清空。
所以会存在这样一条强引用链:Thread变量 -> Thread对象 -> ThreadLocalMap -> Entry -> value -> Object。
其结果就是:Entry和ThreadLocalMap将会长期存在下去,会导致内存泄露。
如何解决内存泄露问题?
前面说过的ThreadLocal还是会导致内存泄露的问题,我们有没有解决办法呢?
答:有办法,调用ThreadLocal对象的remove方法。 remove方法中会把对应Entry中的key和value都设置成null
不是在一开始就调用remove方法,而是在使用完ThreadLocal对象之后。列如:
先创建一个,其中包含了ThreadLocal的逻辑。
先创建一个CurrentUser类,其中包含了ThreadLocal的逻辑。
public class CurrentUser { private static final ThreadLocal<UserInfo> THREA_LOCAL = new ThreadLocal(); public static void set(UserInfo userInfo) { THREA_LOCAL.set(userInfo); } public static UserInfo get() { THREA_LOCAL.get(); } public static void remove() { THREA_LOCAL.remove(); } }
然后在业务代码中调用相关方法:
public void doSamething(UserDto userDto) { UserInfo userInfo = convert(userDto); try{ CurrentUser.set(userInfo); ... //业务代码 UserInfo userInfo = CurrentUser.get(); ... } finally { CurrentUser.remove(); } }
需要我们特别注意的地方是:一定要在finally代码块中,调用remove方法清理没用的数据。如果业务代码出现异常,也能及时清理没用的数据。remove方法中会把Entry中的key和value都设置成null,这样就能被GC及时回收,无需触发额外的清理机制,所以它能解决内存泄露问题。
所以
无论 ThreadLocalMap 中的 key 使用哪种类型引用都无法完全避免内存泄漏,跟使用弱引用没有关系。
要避免内存泄漏有两种方式:
1 .使用完 ThreadLocal ,调用其 remove 方法删除对应的 Entry
2 .使用完 ThreadLocal ,当前 Thread 也随之运行结束相对第一种方式,第二种方式显然更不好控制,特别是使用线程池的时候,线程结束是不会销毁的.
细心的同学会发现,在以上两种内存泄漏的情况中.都有两个前提:
1 .没有手动侧除这个 Entry
2 · CurrentThread 依然运行
第一点很好理解,只要在使用完下 ThreadLocal ,调用其 remove 方法翻除对应的 Entry ,就能避免内存泄漏。
第二点稍微复杂一点,由于ThreodLocalMap 是 Threod 的一个属性,被当前线程所引甲丁所以它的生命周期跟 Thread 一样长。那么在使用完 ThreadLocal 的使用,如果当前Thread 也随之执行结束, ThreadLocalMap 自然也会被 gc 回收,从根源上避免了内存泄漏。
综上, ThreadLocal 内存泄漏的根源是:
由于ThreadLocalMap 的生命周期跟 Thread 一样长,如果没有手动删除对应 key 就会导致内存泄漏