前言
共享变量一直是并发中比较让人头疼的问题,
每个线程都对它有操作权, 所以线程之间的同步就显得很关键.
前几章说了很多, 大部分解决之道都和“锁”相关!
总儿言之就是对于“共享变量”进行“锁”控制, 确保某一时刻只有一个线程拿着这个变量, 来解决共享变量的争用问题.
可是大家也知道, 加“锁”这个还是比较消耗性能的, 有没有更好的方式呢?
于是, 大佬们开始思考了:
其实没必要对所有场景进行加锁操作, 对某些场景采用生成“副本”的方式来解决显得更加合适且高效!
经过讨论 , 总结, 大佬们将共享变量分为以下两种使用场景
-
需要线程间同步
-
无需线程间同步
为了说明这两种场景, 咱举个不太恰当但是好理解的例子:
假设10个人(线程)在包子铺需要不多不少共吃完100个包子(共享变量), 你如何安排?
-
方式一(需要线程间同步)
10个人, 同时开吃, 每个人吃之前还得与其他人进行沟通, 及时了解还剩多少个包子可以吃(防止多吃或少吃) -
方式二(无需线程间同步)
10个人, 开吃之前明确的被告知只能吃10个, 此时, 只需知道自己的10个有没有吃完, 无需关心其他人吃了多少.
方式一就是我们平时加锁的方式, 而方式二就是我们今天要学习ThreadLocal!
ThreadLocal介绍
通过以上的吃包子案例不难理解ThreadLocal的核心理念: 共享变量私有化.
每个线程都拥有一份共享变量的本地副本, 每个线程对应一个副本, 同时对共享变量的操作也改为对属于自己的副本的操作, 这样每个线程处理自己的本地变量, 形成数据隔离.
ps: 每个人只关心自己有没有吃完10个包子, 而不关心其他人吃了多少个!
ThreadLocal和Synchronized都是为了解决多线程中共享变量的访问冲突问题
不同的是:
-
Synchronized通过线程等待, 牺牲时间来解决访问冲突
-
ThreadLocal通过每个线程单独一份存储空间来存储共享变量的副本, 牺牲空间来解决冲突
相比于Synchronized, ThreadLocal具有线程隔离的效果:
只有在线程内才能获取到对应的值, 线程外则不能访问到想要的值.
ThreadLocal最适合的是共享变量在线程间隔离, 而在本线程内方法或类间共享的场景.
ThreadLocal使用
为了说明ThreadLocal的作用, 我们举个例子
public class UserLocal {
private static ThreadLocal<String> local = new ThreadLocal<String>() {
//初始化个值
@Override
protected String initialValue() {
return "init";
}
};
//private static String threadName;
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
//threadlocal设置个线程隔离的值
local.set("线程值:" + Thread.currentThread().getName());
//普通共享变量
//threadName = Thread.currentThread().getName();
//睡眠一下,让其它线程可以读取本线程的local值
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + local.get());
//System.out.println(Thread.currentThread().getName() + " " + threadName);
}
}).start();
}
}
}
很简单的案例, 声明一个static ThreadLocal变量 local, 用于存放线程名称.
为了说明ThreadLocal的线程隔离特性, 所以Thread.sleep(1)下, (理论上)让其它线程可以读取到当前的local值.
按常规来说, ThreadLocal定义成了全局变量, 值应该是共享的, 任何一个线程更改之后,其他线程应该也会更改.
但从结果上来看, 线程间互不影响, 这就是ThreadLocal的线程隔离特性.
Thread-0 线程值:Thread-0
Thread-1 线程值:Thread-1
Thread-2 线程值:Thread-2
Thread-3 线程值:Thread-3
Thread-4 线程值:Thread-4
Thread-5 线程值:Thread-5
Thread-7 线程值:Thread-7
Thread-8 线程值:Thread-8
Thread-9 线程值:Thread-9
Thread-6 线程值:Thread-6
注: 案例中也写了普通的共享变量用来对比, 大家自行尝试!
ThreadLocal原理
直接打开ThreadLocal类文件, 查看代码, 不难找到以下几个函数
-
set()
set() -
get()
get() -
getMap()
getMap() -
createMap()
createMap() -
setInitialValue()
setInitialValue() -
remove()
remove()
需要注意的就是getMap()这个方法, 它接受一个Thread对象作为参数, 然后直接返回了这个Thread对象中的threadLocals成员变量.
ThreadLocal的set()、get()、remove()等方法都是基于Thread对象中的threadLocals成员变量来进行工作的.
打开Thread类文件,发现, Thread中的确定义了这么个成员变量,并且类型为ThreadLocal.ThreadLocalMap.
image.png不妨再看看ThreadLocalMap类, IDEA直接command单击查看类文件, 居然直接跳到了ThreadLocal类文件
ThreadLocalMap.class原来, ThreadLocalMap是ThreadLocal的一个静态内部类!
不难看出, ThreadLocalMap就是个特殊点的Map结构, 也就是K-V结构, 只不过, 这个Key特殊点, 居然是个ThreadLocal类型的弱引用.
看到此处, 我们理清了Thread、ThreadLocal、ThreadLocalMap的关系:
-
ThreadLocal虽然提供了set()、get()等存取方法, 但是它并不直接存储数据, 它只是作为一个Key来关联Value, 并且这个Key是个弱引用
-
ThreadLocalMap是ThreadLocal的静态内部类, Thread实例会初始化ThreadLocalMap(也就是threadLocals成员变量), 并用它来真正保存K-V数据
为了加深理解, 我们再举个例子
public class UserLocal {
private static ThreadLocal<String> local1 = new ThreadLocal<String>();
private static ThreadLocal<String> local2 = new ThreadLocal<String>();
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new Runnable() {
public void run() {
local1.set("local1:" + Thread.currentThread().getName());
local2.set("local2:" + Thread.currentThread().getName());
try {
Thread.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + " " + local1.get()+ " " +local2.get());
}
}).start();
}
}
}
- 首先实例了两个ThreadLocal对象local1,local2;
- 线程实例执行到local1.set()时, 会先获取当前线程实例的threadLocals(ThreadLocalMap类型)
- 此时当前线程实例的threadLocals为null, 则初始化线程实例的threadLocals
- 将当前ThreadLocal实例local1的弱引用作为key, "local1:" + Thread.currentThread().getName()作为value, 放入threadLocals中
- 线程实例执行到local2.set()时, 会先获取当前线程实例的threadLocals
- 此时当前线程实例的threadLocals不为null, 则直接返回
- 将当前ThreadLocal实例local2的弱引用作为key, "local2:" + Thread.currentThread().getName()作为value, 放入threadLocals中
- ……
看到这, 想必一切都很清楚了.
不过可能有朋友和小白的咸鱼君一样, 对文章中提到的“弱引用”产生了疑惑.
也就是这段代码
image.png
关于“弱引用”, 其实是Java中“引用类型”中的一种, 要是你和咸鱼一样好奇&小白, 不妨继续往下看.
Java中的引用
8种基本类型
我们知道, Java中有8种基本类型
image
除了这8种基本类型外, 还有个引用类型的概念.
引用类型
那什么叫引用类型呢?
简单来说
所有的非基本数据类型都是引用数据类型.
具体点就是:
- 类
- 接口类型
- 数组类型
- 枚举类型
- 注解类型
- 字符串型
- 其它……
基本类型和引用类型的区别
区别其实很简单: 基本数据类型是分配在栈上的, 而引用类型是分配在堆上的(需要java中的栈、堆概念)
举个例子:
int a = 1;
String str = new String("abc");
我们创建了两个变量, 他们都会先在栈中分配一块内存;
对于基本类型a来说, 这块区域包含的是基本类型的内容, 也就是1;
对于引用类型str来说,这块区域包含的是指向真正内容的指针(地址),真正的内容“abc”被保存在堆内存上.
这个差别也体现在我们常用的比对上, 比如“==”和“equals”的区别, 这里就不过多介绍了.
引用类型的类别
在 JDK.1.2 之后, Java 对引用的概念进行了扩充, 将引用按强度分为了以下4种
- 强引用
- 软引用
- 弱引用
- 虚引用
引用就引用, 为什么还要区分出4种类型??
Java中区分这4种引用类型主要有两个目的 :
- 可以让程序员通过代码的方式决定某些对象的生命周期
- 有利于JVM进行垃圾回收。
那什么是强引用、软引用、弱引用以及虚引用呢?
我们举例说明
强引用(Java中默认声明的就是强引用)
把一个对象赋给一个引用变量,这个引用变量就是一个强引用;
当一个对象被强引用变量引用时,它处于可达状态,它是不可能被垃圾回收机制回收的,即使该对象以后永远都不会被用到JVM也不会回收。因此强引用是造成Java内存泄漏的主要原因之一。只要强引用存在, 垃圾回收器将永远不会回收被引用的对象;
哪怕内存不足时, JVM也会直接抛出OutOfMemoryError, 不会去回收;
如果想中断强引用与对象之间的联系, 可以显示的将强引用赋值为null, 这样一来,JVM就可以适时的回收对象了
//new Object()实例化一个对象, 并将引用obj1指向这个对象
Object obj1 = new Object();
//obj2 将和 obj1 指向同一个对象
Object obj2= obj1;
//obj1 断开了引用, 但是obj1指向的对象不会成为垃圾空间被gc回收, 因为 obj2还引用着这个对象
obj1 = null;
System.gc();
System.out.println("obj1:"+obj1);
System.out.println("obj2:"+obj2);
image.png
软引用
软引用是用来描述一些非必需但仍有用的对象.
内存足够时, 软引用对象不会被回收;
只有在内存不足时,系统则会回收软引用对象;
如果回收了软引用对象之后仍然没有足够的内存, 才会抛出内存溢出异常.
这种特性常常被用来实现缓存技术,比如网页缓存、图片缓存等.
Object obj = new Object();
SoftReference softRef = new SoftReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("softRef:"+softRef.get());
image.png
弱引用
弱引用的引用强度比软引用要更弱一些;
无论内存是否足够, 只要 JVM 开始进行GC垃圾回收,那些被弱引用关联的对象都会被回收
Object obj = new Object();
WeakReference weakRef = new WeakReference(obj);
obj = null;
System.gc();
System.out.println("obj:"+obj);
System.out.println("weakRef:"+weakRef.get());
image.png
虚引用
虚引用是最弱的一种引用关系;
虚引用与其它几种引用都不同, 虚引用并不会决定对象的生命周期.
如果一个对象仅持有虚引用, 那么它就和没有任何引用一样,它随时可能会被回收.
虚引用主要用来跟踪对象被垃圾回收器回收的活动, 必须和引用队列(ReferenceQueue)结合使用.
垃圾回收器回收对象时,该对象还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中(程序可以通过判断引用队列中是否已经加入了引用,来判断被引用的对象是否将要被垃圾回收,这样就可以在对象被回收之前采取一些必要的措施.)
ReferenceQueue<Object> queue = new ReferenceQueue<>();
PhantomReference<Object> phantomReference = new PhantomReference<>(new Object(), queue);
System.out.println("phantomReference:"+phantomReference.get());
image.png
一张图的说明ThreadLocal
先来张图总结下上面对ThreadLocal的介绍
image.pngThread运行时,线程的的一些局部变量和引用使用的内存属于Stack(栈)区,而普通的对象是存储在Heap(堆)区.
-
线程运行时,我们定义的TheadLocal对象被初始化,存储在Heap,同时线程运行的栈区保存了指向该实例的引用,也就是图中的ThreadLocalRef
-
当ThreadLocal的set/get被调用时,虚拟机会根据当前线程的引用也就是CurrentThreadRef找到其对应在堆区的实例,然后查看其对用的TheadLocalMap实例是否被创建,如果没有,则创建并初始化
-
Map实例化之后,也就拿到了该ThreadLocalMap的句柄,然后如果将当前ThreadLocal对象作为key,进行存取操作
-
图中的虚线,表示key对ThreadLocal实例的引用是个弱引用
老生常谈的“内存泄漏”问题
ThreadLocalMap是以弱引用的方式引用着ThreadLocal, 换句话说,就是ThreadLocal是被ThreadLocalMap以弱引用的方式关联着.
如果ThreadLocal没有被ThreadLocalMap以外的对象引用, 那么依据弱引用的特性, 在下一次GC的时候, ThreadLocal实例就会被回收.
此时ThreadLocalMap里的一组 K-V 的 K 就是null了, 那么此处的V便不会被外部访问到.
并且只要Thread实例一直存在, Thread实例就强引用着ThreadLocalMap, 因此ThreadLocalMap就不会被回收, 那么这里K为null的V就一直占用着内存.
TheadLocal本身的优化
针对上面的弱引用引起的内存泄漏问题, TheadLocal本身做了如下优化
image.png可以看到, 调用set(), Key为null时, 会执行replaceStaleEntry(key, value, i)方法, 用当前的值替换掉以前的Key为null的值, 重复利用了空间.
另外ThreadLocalMap的set()、get()、remove()方法还有有以下逻辑(这里以remove()为例):
当Key为null时, 在下一次ThreadLocalMap调用remove()方法的时候会被清除value值.
image.pngimage.png
为什么使用弱引用
既然ThreadLocalMap K-V 的 K 使用弱应用关联ThreadLocal会有内存泄漏问题, 那为什么不使用强引用呢??
我们不如换个问法:
key 使用强引用会怎么样?
若ThreadLocalMap的Key为强引用, 那么回收ThreadLocal时, 因为ThreadLocalMap还持有ThreadLocal的强引用, 如果没有手动删除, ThreadLocal不会被回收, 这会导致Entry内存泄漏.
key 使用弱引用会怎么样?
若ThreadLocalMap的key为弱引用, 那么回收ThreadLocal时,由于ThreadLocalMap持有ThreadLocal的弱引用, 即使没有手动删除, ThreadLocal也会被回收;
当Key为null, 在下一次ThreadLocalMap调用set(), get(), remove()方法的时候会被清除value值.
总结
Thread包含ThreadLocalMap, 因此ThreadLocalMap与Thread的生命周期是一样的.
如果没有手动删除对应Key, 都会导致内存泄漏.
不过, 使用弱引用可以多一层保障:
弱引用ThreadLocal不会使得Entry内存泄漏; Key 为 null 时, 对应的value在下一次ThreadLocalMap调用set(),get(),remove()的时候会被清除.
因此, ThreadLocal内存泄漏的根源是:
ThreadLocalMap的生命周期跟Thread一样长, 如果没有手动删除对应Key就会导致内存泄漏; 而不是因为弱引用.
ThreadLocal正确的使用方法
-
每次使用完ThreadLocal都调用它的remove()方法清除数据
-
将ThreadLocal变量定义成private static, 这样就一直存在ThreadLocal的强引用, 也就能保证任何时候都能通过ThreadLocal的弱引用访问到Entry的value值, 进而清除掉.
小结下
原本以为ThreadLocal这么简单, 简简单单就写完这篇了.
没想到, 深扒一下源码, 洋洋洒洒写了近4-5千字, 希望能帮到大家的学习!
欢迎关注我
技术公众号 “CTO技术”
订阅号.png
网友评论