在讲ThreadLocal的之前必须要了解一个概念:弱引用,如果这个概念不理解那就是瞎子摸路,最终还是不知所以。
关于引用的介绍可以看我的另一篇文章——再谈引用
其次是一定要耐心,我也是付出了一定时间去学习这些东西的,如果连一两个小时的时间都拿不出,我想对ThreadLocal的理解不会很深并且遗忘得会很快。
这个地方稍微一看就行,反正看了也会先一脸懵逼
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* 这个类支持线程内存储变量。这些变量与一般的变量不同之处在于每一个线程都有自己的独一份
* 已有的变量,这些变量通过get和set方法获得。ThreadLocal在类中的实例一般是私有的静态变量,
* 这是为了与一个线程的生命周期建立联系。
*
* For example, the class below generates unique identifiers local to each
* thread.
* 这个类为每一个线程提供本地唯一的标识符。
* A thread's id is assigned the first time it invokes {@code ThreadId.get()}
* and remains unchanged on subsequent calls.
* 一个线程的id(前面提到的唯一标识符)会在第一次调用get方法的时候确定,并且以后再调用也不会改变了。
*
*
* import java.util.concurrent.atomic.AtomicInteger;
* //原子变量类(这个类是线程安全的,感兴趣的可以去看一下这个类的使用)
* public class ThreadId {
* // Atomic integer containing the next thread ID to be assigned
* private static final AtomicInteger nextId = new AtomicInteger(0);
* //使用一个原子变量类来实现一个线程id,就是为了这个id的变化是具有线程安全性的,说白了就是为了一个线程与一个id是一一对应
* // Thread local variable containing each thread's ID
* private static final ThreadLocal<Integer> threadId =
* new ThreadLocal<Integer>() {
* @Override protected Integer initialValue() {
* return nextId.getAndIncrement();
* }
* };
*
* // Returns the current thread's unique ID, assigning it if necessary
* public static int get() {
* return threadId.get();
* }
* }
*
* Each thread holds an implicit reference to its copy of a thread-local
* variable as long as the thread is alive and the {@code ThreadLocal}
* instance is accessible; after a thread goes away, all of its copies of
* thread-local instances are subject to garbage collection (unless other
* references to these copies exist).
* 每一个线程,只要这个线程活着并且ThreadLocal对象没有消亡,那么这个线程就持有一个独一的引用来指向
* 变量的副本。当线程结束了,这些副本会去进行gc,除非他们的引用还存在。
*
*
* @author Josh Bloch and Doug Lea and zhn
* @since 1.2
*/
在这里大多是对英文的直译,尽量往原意上靠齐,原谅我这蹩脚的英文水平和理解能力。
一个Thread线程都可以由ThreadLocal类产生一个特有的实例,不同的线程之间不会相互影响,这个实例在一个线程的生命周期中起作用。在单线程的情境下是不需要ThreadLocal的。在多线程场景下,就可以使用ThreadLocal在同一线程下,不同组件传递公共变量,它使每个线程的变量都是独立的。在《Java编程并发实践》一书中就谈到ThreadLocal就是维持线程封闭性的一种更规范的方法。
ThreadLocal和Synchronized都是为了解决多线程中相同变量的访问冲突问题:
我知道,你还是不明白,没事,往下看,看完第二节你就明白个差不多了。
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值。 大多数子类将无需重写此方法,仅依靠initialValue()方法设置线程本地值的值。 |
public T get() | 返回当前线程的此线程局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue()方法返回的值。 |
public void remove() | 删除此线程局部变量的当前线程的值。 如果此线程本地变量随后是当前线程的read ,则其值将通过调用其initialValue()方法重新初始化 ,除非其当前线程的值为set 。 这可能导致当前线程中的initialValue方法的多次调用。 |
我们先用一个没有使用ThreadLocal的多线程的例子。由此出发看看Thread Local到底能用来干啥。
我写实现五个线程,分别在五个线程中设置一个独属于自己得内容,并获得它。
package ThreadLocal;
public class Demo1 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
Demo1 demo1 = new Demo1();
for (int i=0;i<5;i++){
//启动五个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//每一个线程存一个变量,紧接着把它取出来
demo1.setContent("得到"+Thread.currentThread().getName()+"的数据");
System.out.println("====================");
System.out.println(Thread.currentThread().getName()+"--->"+demo1.getContent());
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-KTLL49ai-1586815323634)(http://49.234.125.145:8090/upload/2020/04/image-3fd6ba9bd5e94657a9a9126602fa92fd.png)]
很明显这不是我们想要的结果。我们想要的得到的content值是为每个线程自己设置的content值。
分析原因:因为for循环太快了,一次for循环启动一个线程,这个线程可能还没来得及执行完run另一个线程就又紧接着开始执行了,这就造成了对一个变量的存读不安全。这就是线程不隔离问题。
解决方法:当然,最简单的方法就是让这五个线程依次执行run中的代码呗。
thread.setName("线程"+i);
thread.start();
Thread.sleep(500);
直接在线程启动的后面加个延时等待前面线程的完成。它也成功给出了我们想要的结果:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-dDBlF3cw-1586815323636)(http://49.234.125.145:8090/upload/2020/04/image-1f7aa70eebdb4cc5b96d1dfe3d2afc29.png)]
但是!很明显这不是一个好办法。
现在五个线程都想获得一份自己设置的content值,这时候就可以用到ThreadLocal类了。
停停停!我学过synchronized修饰,好像可以实现你这个方法啊。
确实可以用锁机制实现。
在run()方法的三行代码中加锁
package ThreadLocal;
/*锁机制实现*/
public class Demo2 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) throws InterruptedException {
Demo1 demo1 = new Demo1();
Object object="ob";
for (int i=0;i<5;i++){
//启动五个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
synchronized (object) {
//每一个线程存一个变量,紧接着把它取出来
demo1.setContent("得到" + Thread.currentThread().getName() + "的数据");
System.out.println("====================");
System.out.println(Thread.currentThread().getName() + "--->" + demo1.getContent());
}
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-7RJK8SlI-1586815323639)(http://49.234.125.145:8090/upload/2020/04/image-1f7aa70eebdb4cc5b96d1dfe3d2afc29.png)]
确实可以!线程排队去执行run()中的代码,这就造成了时间上的浪费。在1.3中提到了synchronized是牺牲时间换空间,ThreadLocal则是通过空间换时间。所以俩个方法的侧重点是不一样的,解决场景也不同,接下来就来看看ThreadLocal到底是怎么来使用的。
/*
* 需求:线程隔离
* 在多线程并发的场景下,要求每个线程中的变量都是独立的。
* 线程A:设置(变量1) 获取(变量1)
* 线程B:设置(变量2) 获取(变量2)
* ThreadLocal:
* 1.set():将变量绑定到当前线程中
* 2.get():获取当前线程中绑定的变量
* */
这时候我们再去看文章一开始po出的那段源码注释:
/**
* This class provides thread-local variables. These variables differ from
* their normal counterparts in that each thread that accesses one (via its
* {@code get} or {@code set} method) has its own, independently initialized
* copy of the variable. {@code ThreadLocal} instances are typically private
* static fields in classes that wish to associate state with a thread (e.g.,
* a user ID or Transaction ID).
*
* 这个类支持线程内存储变量。这些变量与一般的变量不同之处在于每一个线程都有自己的独一份
* 已有的变量,这些变量通过get和set方法获得。ThreadLocal在类中的实例一般是私有的静态变量,
* 这是为了与一个线程的生命周期建立联系。
*/
实际上就是做给每个线程做一个变量的副本,然后每个线程分别管理自己的副本,通过get、set方法去管理,现在是不是豁然开朗了。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-DH2mUnhI-1586815323641)(http://49.234.125.145:8090/upload/2020/04/image-cdb093e2017f492e8cd7e559e0a3c8b6.png)]
package ThreadLocal;
public class Demo3 {
//与当前线程绑定
ThreadLocal<String> tl=new ThreadLocal<>();
private String content;
public String getContent() {
return tl.get();
}
public void setContent(String content) {
tl.set(content);
}
public static void main(String[] args) throws InterruptedException {
Demo3 demo3 = new Demo3();
for (int i=0;i<5;i++){
//启动五个线程
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//每一个线程存一个变量,紧接着把它取出来
demo3.setContent("得到"+Thread.currentThread().getName()+"的数据");
System.out.println("====================");
System.out.println(Thread.currentThread().getName()+"--->"+demo3.getContent());
}
});
thread.setName("线程"+i);
thread.start();
}
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ytkcUR1y-1586815323643)(http://49.234.125.145:8090/upload/2020/04/image-8f4211616b0f408baf27156811a1f378.png)]
实现了我们想要的效果。也通过这个例子了解了ThreadLocal的创建和set、get的使用。
JDK最早期的设计是这样的:每个ThreadLocal都创建一个Map集合来存储各个线程的副本,然后用线程作为Map的key,要存储的局部变量作为Map的value,这样就能达到各个线程的局部变量隔离的效果。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-E4BB25L3-1586815323645)(http://49.234.125.145:8090/upload/2020/04/image-cb6e9774f9f243ae89e12d36ccb24f51.png)]
巨大的缺陷:就算所有Thread都结束销毁了,但是ThreadLocalMap是由ThreadLocal维护的,它并不能随着Thread的消亡而消亡。
在JDK8中对ThreadLocal优化了设计方案。
每个Thread去维护一个ThreadLocalMap,这个Map的key是ThreadLocal实例本身,value才是真正要存储的值。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-gDppOrmk-1586815323647)(http://49.234.125.145:8090/upload/2020/04/image-7690c2d87e034f4ab40bad4f442eae63.png)]
(1)每个Thread线程内部都有一个Map(ThreadLocalMap)
(2)Map里面存储的ThreadLocal对象(作为key)和线程的变量副本(value)
(3) Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
(4) 对于不同的线程,每次获取副本值时,别的线程并不能获得到当前线程的副本值,形成了副本的隔离。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-91h2jB27-1586815323648)(http://49.234.125.145:8090/upload/2020/04/image-d4d12a736e884847bfff1a51501bf62e.png)]
客官别急,慢慢来~
方法声明 | 描述 |
---|---|
ThreadLocal() | 创建ThreadLocal对象 |
public void set(T value) | 将当前线程的此线程局部变量的副本设置为指定的值。 大多数子类将无需重写此方法,仅依靠initialValue()方法设置线程本地值的值。 |
public T get() | 返回当前线程的此线程局部变量的副本中的值。 如果变量没有当前线程的值,则首先将其初始化为调用initialValue()方法返回的值。 |
public void remove() | 删除此线程局部变量的当前线程的值。 如果此线程本地变量随后是当前线程的read ,则其值将通过调用其initialValue()方法重新初始化 ,除非其当前线程的值为set 。 这可能导致当前线程中的initialValue方法的多次调用。 |
Protect T initialValue() | 返回当前线程局部变量的初始值 |
/**
* Sets the current thread's copy of this thread-local variable
* to the specified value. Most subclasses will have no need to
* override this method, relying solely on the {@link #initialValue}
* method to set the values of thread-locals.
* 将此线程局部变量的当前线程副本设置为指定值……
* @param value the value to be stored in the current thread's copy of
* this thread-local.
*/
public void set(T value) {
//传入数据value
Thread t = Thread.currentThread();//获取当前线程对象
ThreadLocalMap map = getMap(t);//利用getMao(Thread t)拿到当前线程的ThreadLocalMap对象
if (map != null)//ThreadLocalMap存在,调用map.set赋值
map.set(this, value);//这里的this是调用此方法的threadLocal对象
else
//不存在就调用createMap(Thread t,T value)进行初始化,这时候ThreadLocalMap中就有第一个Entry了。
createMap(t, value);
}
/**
* Get the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
* 获取与ThreadLocal有关的map,就是ThreadLocalMap
* @param t the current thread 当前线程
* @return the map
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
上面这个方法的返回值threadLocals是在Thread类中定义的:
/* ThreadLocal values pertaining to this thread. This map is maintained
* by the ThreadLocal class. */
ThreadLocal.ThreadLocalMap threadLocals = null;
所以在这里就可以看出,实实在在地是Thread里面实现(维护)了一个ThreadLocalMap。
/**
* Create the map associated with a ThreadLocal. Overridden in
* InheritableThreadLocal.
* 创建一个ThreadLocalMap
* @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);
//这里的this是调用此方法的threadLocal对象,这个时候一个ThreadLocal就已经和Thread绑定上了
}
ThreadLocal就是在上面这个方法中第一次和Thread绑定的。 注意这个方法的权限,只能在ThreadLocal内部调用,说明是不能通过这个方法直接绑定的。
我们现在再回头去看demo3.java中的实现的时候,我们就知道是怎么回事了。
现在有个线程在执行run方法中的
demo3.setContent("得到"+Thread.currentThread().getName()+"的数据");
这行代码了,在setContent(String content)方法中,这样实现:
public void setContent(String content) {
tl.set(content);
}
这时候这个线程调用tl(一个ThreadLocal的对象)的set方法。此时这个线程(不妨叫做t)中的ThreadLocalMap threadLocals字段还是空值,进入判断为空的代码块,执行createMap方法对t的threadLocals字段实例化,并且设置第一个Entry。
public T get() {
Thread t = Thread.currentThread();//获取当前线程对象
ThreadLocalMap map = getMap(t);//拿到线程的Map
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);//获取这个ThreadLocal对应得Entry实体
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;//不为空取出value返回
return result;
}
}
/*两种情况执行下面这个语句:
* map不存在,表示此线程没有维护的ThreadLocalMap对象
* 虽然map存在,但map中没有当前ThreadLocal关联的entry
* */
return setInitialValue();//设置初始化的值
}
private T setInitialValue() {
T value = initialValue();//该方法可以被重写,如果不重写默认返回null
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
protected T initialValue() {
return null;
}
所以get方法不存在找不到而报异常,大不了返回null嘛。看下图
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-ZybhsZ06-1586815323650)(http://49.234.125.145:8090/upload/2020/04/image-ea69d69c29e344e4a05f8f3bfca2da99.png)]
总结:先获取当前线程的ThreadLocalMap变量,如果存在返回值,不存在则创建并返回初始值。
/**
* 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.
*删除当前线程中保存的ThreadLocal对应的实体entry
* @since 1.5
*/
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
//map存在就调用ThreadLocalMap.remove,删除ThreadLocal对应的Entry
m.remove(this);
}
前面讲get方法时,当ThreadLocal对应得Entry不存在的时候就会去调用setInitial Value,再调用initialValue得到value值。
protected T initialValue() {
return null;
}
(1)这个方法时一个延迟调用的方法,只有在没调用set方法之前先调用了get方法时(ThreadLocal对应的Entry还不存在时)才被调用,且仅调用一次。
(2)这个方法默认直接返回null。
(3)这个方法的修饰符是protect,说明可以被子类覆盖。当想要一个除null之外的初始值的时候,可以重写这个方法。
实现demo.java
package ThreadLocal;
public class Demo4 {
ThreadLocal<String> tl=new ThreadLocal_Content();
private String content;
public String getContent() {
return tl.get();
}
public void setContent(String content) {
//tl.set(content);不执行set了
}
public static void main(String[] args) {
Demo4 demo4=new Demo4();
Thread thread = new Thread(new Runnable() {
@Override
public void run() {
//每一个线程存一个变量,紧接着把它取出来
demo4.setContent("得到"+Thread.currentThread().getName()+"的数据");
System.out.println("====================");
System.out.println(Thread.currentThread().getName()+"--->"+demo4.getContent());
}
});
thread.start();
}
}
class ThreadLocal_Content extends ThreadLocal{
@Override
protected Object initialValue() {
return "我要改掉初始化,不让它为null";
}
public ThreadLocal_Content() {
super();
}
}
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-PEQbB7PJ-1586815323652)(http://49.234.125.145:8090/upload/2020/04/image-4d2ed61975bf4597a0f93f4279217065.png)]
就是这么简单!
转载@KingJack博客中的一张图:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2UsyEehM-1586815323653)(http://49.234.125.145:8090/upload/2020/04/image-e4c03a31ab2b47558320afc5b7fd32ba.png)]
在第四节的ThreadLocal源码分析中,始终离不开ThreadLocalMap的操作,它的方法和HaspMap很像,都是设置、获取key-value值啥的,这可以帮助我们理解。
ThreadLocalMap虽然和Map很像但是却不是Map接口的实现类,而是ThreadLocal的内部实现类,用独立的方式实现了Map的功能,其内部的Entry也是独立实现。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IEtzs967-1586815323656)(http://49.234.125.145:8090/upload/2020/04/image-cb098a5c86f44d96afc355641757854b.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-z9n6Imqo-1586815323658)(http://49.234.125.145:8090/upload/2020/04/image-b849399c09e443ebb4f5c2099b16fa98.png)]
/**
* The initial capacity -- MUST be a power of two.
* 2的次方
*/
private static final int INITIAL_CAPACITY = 16;
/**
* The table, resized as necessary.
* table.length MUST always be a power of two.
* 核心存储容器,存储键值对
*/
private Entry[] table;
/**
* The number of entries in the table.
* entries数量
*/
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继承弱引用,并且用ThreadLocal作为key
* 如果key为null(entry.get()==null)表示key不再被引用。
* 因此这个时候entry可以从map(表table)中删除。
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
//ThreadLocal用了弱引用修饰
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
和HashMap一样,ThreadLocalMap中,也是用Entry来保存K-V数据结构的,不过Entry中的key只能是ThreadLocal对象,这点在构造方法中已经限定死了。
另外Entry继承了WeakReference,也就是key(ThreadLocal)是弱引用,其目的是将ThreadLocal对象生命周期和线程生命周期解绑。
Memory leak:内存泄漏是指程序中已动态分配的堆内存由于某种原因程序未释放或者无法释放,造成内存的浪费,导致程序运行速度减慢甚至系统崩溃等严重后果。内存泄漏的堆积终将导致内存溢出(OOM)。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-6t42orcX-1586815323660)(http://49.234.125.145:8090/upload/2020/04/image-39fc3c6a68ba46759f6fe4d4f907a9a9.png)]
强引用无法避免内存泄漏。
是的会有改变,最起码可以回收ThreadLocal(Entry中的Key)了————因为只有gc开启,弱引用对象就会被回收。但是!看下图的分析。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Bq7AmZmI-1586815323662)(http://49.234.125.145:8090/upload/2020/04/image-47853a4f16ea4ca8918a7db062884500.png)]
那条导致内存泄漏的链还是存在,在这里顺便插一嘴,为啥这个链存在就不会被回收呢,这个判定是由垃圾回收的可达性算法判定的,这条链上的引用都是强引用,所以不会被回收。
无论强引用还是弱引用都会导致内存泄漏,真正的罪魁祸首并不是引用的类型。
内存泄漏的俩个前提:
到此停笔!希望这篇文章能给前来的你带来帮助,来看就是对我创作最大的支持!欢迎大家来我自己博客踩踩,可以留言,必回!妖刀的个人博客