面试题:由ThreadLocal引发的惨案
作者:互联网
ThreadLocal在面试中经常被问到,下面我们就ThreadLocal进行一下认识和了解。
从以下几个问题去了解ThreadLocal:
1、ThreadLocal是什么?
2、ThreadLocal应用场景?
3、ThreadLocal怎么用,以及ThreadLocal和Synchronized关键词有啥区别?
4、ThreadLocal源码分析?
5、ThreadLocal内存泄漏问题?
问题1:什么是ThreadLocal?
官方介绍:ThreadLocal类用来提供线程内部的局部变量。这种变量在多线程环境下访问(通过get和set方法访问)时能保证各个线程的变量相对独立于其他线程内的变量。ThreadLocal实例通常来说都是private static类型的,用于关联线程和线程上下文。
简单来说:ThreadLocal就是线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
从上述中我们可以得出ThreadLocal的作用:提供线程内的局部变量,不同线程之间互不干扰。这种变量在线程的生命周期内起作用,减少同一线程内多个函数或组件之间一些公共变量传递的复杂度。
总结:
- 线程并发: 在多线程并发的场景下
- 传递数据: 我们可以通过ThreadLocal在同一线程,不同组件中传递公共变量
- 线程隔离: 每个线程的变量都是独立的,不会互相影响
问题2、ThreadLocal应用场景?
- 1、在进行对象跨层传递的时候,使用ThreadLocal可以避免多次传递,打破层次间的约束。
- 2、线程间数据隔离
- 3、进行事务操作,用于存储线程事务信息。
- 4、数据库连接,Session会话管理。
问题3:ThreadLocal怎么用?
- 3.1 ThreadLocal的常用方法
在使用之前,我们先来认识几个ThreadLocal的常用方法
方法声明 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
- 3.2 应该示例
package com.threadlocal;
import java.util.stream.IntStream;
/**
* 需求线程隔离:
* 再多线程并发场景下,每个线程中的变量都是隔离的
* 线程0,设置变量(0),获取变量0;
* 线程1,设置变量(1),获取变量1;
* 线程2,设置变量(2),获取变量2;
* ......
*/
public class MyDemo {
private String content;
public MyDemo() {
}
public MyDemo(String content) {
this.content = content;
}
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo myDemo = new MyDemo();
//使用java8的lambda stream新建5个线程
IntStream.range(0, 5).forEach(
thread -> new Thread(() -> {
//每个线程存一个变量,然后再取这个变量
myDemo.setContent(Thread.currentThread().getName() + "的数据");
System.out.println(Thread.currentThread().getName() + "--->" + myDemo.getContent());
}).start()
);
}
}
运行结果:
执行结果是很明显线程之间没有实现数据的隔离,线程0获取到了线程1的数据。这时我们考虑到的就是使用ThreadLocal实现并发情况下数据的相互隔离。
其中上面讲的ThreadLocal中的set()方法就是‘将变量绑定到当前线程中’;而get()方法就是‘获取当前线程绑定的变量’
将上面的示例改写成ThreadLocal的形式:
class MyDemo02 {
private static ThreadLocal<String> threadLocal = new ThreadLocal<>();
private String content;
public String getContent() {
return threadLocal.get();
}
public void setContent(String content) {
threadLocal.set(content);
}
public static void main(String[] args) {
MyDemo02 myDemo02 = new MyDemo02();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
myDemo02.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-------------------------");
System.out.println(Thread.currentThread().getName() + "--->" + myDemo02.getContent());
}
}).start();
}
}
}
运行结果:
从结果我们可以看到,每一个线程都有各自的local值,这就是TheadLocal的基本使用,是不是非常的简单。
那么为什么会在数据库连接的时候使用的比较多呢?
上面是一个数据库连接的管理类,我们使用数据库的时候首先就是建立数据库连接,然后用完了之后关闭就好了,这样做有一个很严重的问题,如果有1个客户端频繁的使用数据库,那么就需要建立多次链接和关闭,我们的服务器可能会吃不消,怎么办呢?如果有一万个客户端,那么服务器压力更大。
这时候最好使用ThreadLocal,因为ThreadLocal在每个线程中对连接会创建一个副本,且在线程内部任何地方都可以使用,线程之间互不影响,这样一来就不存在线程安全问题,也不会严重影响程序执行性能。是不是很好用。
- 3.3 ThreadLocal Synchronized的区别
上面的程序可以改写成synchronized实现的方式,不过使用synchronized相当于加锁,在高并发情况下,只有获取锁的线程才能继续执行,未获取锁的线程只能循环等待尝试获取锁,导致程序失去了并发性
/**
* 使用synchronized
*/
class MyDemo03 {
private String content;
public String getContent() {
return content;
}
public void setContent(String content) {
this.content = content;
}
public static void main(String[] args) {
MyDemo03 myDemo03 = new MyDemo03();
for (int i = 0; i < 5; i++) {
new Thread(new Runnable() {
@Override
public void run() {
synchronized (MyDemo03.class) {
myDemo03.setContent(Thread.currentThread().getName() + "的数据");
System.out.println("-------------------------");
System.out.println(Thread.currentThread().getName() + "--->" + myDemo03.getContent());
}
}
}).start();
}
}
}
执行结果:
从结果可以看出, 加锁确实可以解决这个问题,但是在这里我们强调的是多线程数据隔离的问题,并不是多线程共享数据的问题, 在这个案例中使用synchronized关键字是不合适的。
说白了,synchronized可以实现隔离线程的需求,由于处理中需要加锁才能实现资源的共享,导致线程只能排队执行,降低了程序在高并发环境下的执行效率。所以使用ThreadLocal在多线程环境下实现数据的隔离性具有更高的并发性能
总结:
虽然ThreadLocal模式与synchronized关键字都用于处理多线程并发访问变量的问题, 不过两者处理问题的角度和思路不同。
synchronized | ThreadLocal | |
原理 | 同步机制采用“以时间换空间”的方式,只提供了一份变量,让不同的线程排队访问 | ThreadLocal采用’以空间换时间’的方式, 为每一个线程都提供了一份变量的副本,从而实现同时访问而相不干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
以时间换空间 -> 即加锁方式,某个区域代码或变量只有一份节省了内存,但是会形成很多线程等待现象,因此浪费了时间而节省了空间。(牺牲时间访问冲突)
以空间换时间 -> 为每一个线程提供一份变量,多开销一些内存,但是线程不用等待,可以一起执行而相互之间没有影响。(牺牲空间来解决冲突)
总之,ThreadLocal是解决线程安全问题一个很好的思路,它通过为每个线程提供一个独立的变量副本解决了变量并发访问的冲突问题。在很多情况下,ThreadLocal比直接使用synchronized同步机制解决线程安全问题更简单,更方便,且结果程序拥有更高的并发性。
问题4、ThreadLocal源码分析?
截止到现在相信ThreadLocal对于大家来说已经不再陌生了,接下来就让我深究ThreadLocal的源码是如何设计的。
在JDK8中 ThreadLocal
的设计是:每个Thread
维护一个ThreadLocalMap
,这个Map的key
是ThreadLocal
实例本身,value
才是真正要存储的值Object
。
具体的过程是这样的:
- 每个Thread线程内部都有一个Map (ThreadLocalMap)
- Map里面存储ThreadLocal对象(key)和线程的变量副本(value)
- Thread内部的Map是由ThreadLocal维护的,由ThreadLocal负责向map获取和设置线程的变量值。
- 对于不同的线程,每次获取副本值时,别的线程并不能获取到当前线程的副本值,形成了副本的隔离,互不干扰。
结构如下图所示:
这样设计有如下两个优势:
- 这样设计之后每个Map存储的Entry数量就会变少。因为之前的存储数量由Thread的数量决定,现在是由ThreadLocal的数量决定。在实际运用当中,往往ThreadLocal的数量要少于Thread的数量。
- 当Thread销毁之后,对应的ThreadLocalMap也会随之销毁,能减少内存的使用。
☆ ThreadLocal常用的方法有这几个:
方法声明 | 描述 |
ThreadLocal() | 创建ThreadLocal对象 |
public void set( T value) | 设置当前线程绑定的局部变量 |
public T get() | 获取当前线程绑定的局部变量 |
public void remove() | 移除当前线程绑定的局部变量 |
- set()方法源码
/**
* 设置当前线程对应的ThreadLocal的值
*
* @param value 将要保存在当前线程对应的ThreadLocal的值
*/
public void set(T value) {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
}
/**
* 获取当前线程Thread对应维护的ThreadLocalMap
*
* @param t the current thread 当前线程
* @return the map 对应维护的ThreadLocalMap
*/
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/**
*创建当前线程Thread对应维护的ThreadLocalMap
*
* @param t 当前线程
* @param firstValue 存放到map中第一个entry的值
*/
void createMap(Thread t, T firstValue) {
//这里的this是调用此方法的threadLocal
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
剖析:从源码中我们可以看到 首先获取当前线程t,然后调用getMap(t) 方法传入当前线程t 获取ThreadLocalMap;然后再判断ThreadLocalMap 存在,则将当前线程对象t作为key,要存储的对象作为value存到ThreadLocalMap里面;如果不存在就使用createMap(Thread t, T firstValue)方法初始化一个。
到这相信你会有几个疑惑了,ThreadLocalMap是什么,getMap方法又是如何实现的。带着这些问题,继续往下看。先来看ThreadLocalMap。
static class ThreadLocalMap {
static class Entry extends WeakReference<ThreadLocal<?>> {//这个弱引用是关键点
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
.....//省略大部分代码
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
我们可以看到ThreadLocalMap其实就是ThreadLocal的一个静态内部类,里面定义了一个Entry来保存数据,而且还是继承的弱引用。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
调用当期线程t,返回当前线程t中的成员变量threadLocals。而threadLocals其实就是ThreadLocalMap。
2. get()方法源码:
/**
* 返回当前线程中保存ThreadLocal的值
* 如果当前线程没有此ThreadLocal变量,
* 则它会通过调用{@link #initialValue} 方法进行初始化值
*
* @return 返回当前线程对应此ThreadLocal的值
*/
public T get() {
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 如果此map存在
if (map != null) {
// 以当前的ThreadLocal 为 key,调用getEntry获取对应的存储实体e
ThreadLocalMap.Entry e = map.getEntry(this);
// 对e进行判空
if (e != null) {
@SuppressWarnings("unchecked")
// 获取存储实体 e 对应的 value值
// 即为我们想要的当前线程对应此ThreadLocal的值
T result = (T)e.value;
return result;
}
}
/*
初始化 : 有两种情况有执行当前代码
第一种情况: map不存在,表示此线程没有维护的ThreadLocalMap对象
第二种情况: map存在, 但是没有与当前ThreadLocal关联的entry
*/
return setInitialValue();
}
源码剖析:首先获取当前线程t,然后调用getMap()方法获取一个ThreadLocalMap,如果map不为null,则在Map中以ThreadLocal的引用作为key来在Map中获取对应的Entry e,然后在判断e 是否为空,如果e不为null,则返回e.value;
如果Map为空或者e为空,则通过initialValue() 函数获取初始值value,然后用ThreadLocal的引用和value作为firstKey和firstValue创建一个新的Map。
那么初始化setInitialValue()是什么样的呢?
/**
* 初始化
*
* @return the initial value 初始化后的值
*/
private T setInitialValue() {
// 调用initialValue获取初始化的值
// 此方法可以被子类重写, 如果不重写默认返回null
T value = initialValue();
// 获取当前线程对象
Thread t = Thread.currentThread();
// 获取此线程对象中维护的ThreadLocalMap对象
ThreadLocalMap map = getMap(t);
// 判断map是否存在
if (map != null)
// 存在则调用map.set设置此实体entry
map.set(this, value);
else
// 1)当前线程Thread 不存在ThreadLocalMap对象
// 2)则调用createMap进行ThreadLocalMap对象的初始化
// 3)并将 t(当前线程)和value(t对应的值)作为第一个entry存放至ThreadLocalMap中
createMap(t, value);
// 返回设置的值value
return value;
}
源码剖析:首先是对Value初始化,然后获取当前线程t, 然后再调用getMap()方法获取ThreadLocalMap,如果map不为空,则将当前线程t作为key,初始化的value为值存储ThreadLocalMap中;如果为空则使用createMap()方法初始化一个。
在setInitialValue()方法中,有一个initialValue()方法进行值得初始化,那么initialValue()源码是什么样的呢?我们来看一下
/**
* 返回当前线程对应的ThreadLocal的初始值
* 此方法的第一次调用发生在,当线程通过get方法访问此线程的ThreadLocal值时
* 除非线程先调用了set方法,在这种情况下,initialValue 才不会被这个线程调用。
* 通常情况下,每个线程最多调用一次这个方法。
*
* <p>这个方法仅仅简单的返回null {@code null};
* 如果程序员想ThreadLocal线程局部变量有一个除null以外的初始值,
* 必须通过子类继承{@code ThreadLocal} 的方式去重写此方法
* 通常, 可以通过匿名内部类的方式实现
*
* @return 当前ThreadLocal的初始值
*/
protected T initialValue() {
return null;
}
其实很简单哈哈哈哈。此方法的作用是 返回该线程局部变量的初始值。
(1) 这个方法是一个延迟调用方法,从上面的代码我们得知,在set方法还未调用而先调用了get方法时才执行,并且仅执行1次。
(2)这个方法缺省实现直接返回一个null。
(3)如果想要一个除null之外的初始值,可以重写此方法。(备注: 该方法是一个protected的方法,显然是为了让子类覆盖而设计的)
3.remove()方法
/**
* 删除当前线程中保存的ThreadLocal对应的实体entry
*/
public void remove() {
// 获取当前线程对象中维护的ThreadLocalMap对象
ThreadLocalMap m = getMap(Thread.currentThread());
// 如果此map存在
if (m != null)
// 存在则调用map.remove
// 以当前ThreadLocal为key删除对应的实体entry
m.remove(this);
}
剖析:方法很简单就是获取当前线程t, 然后调用getMap()方法获取ThreadLocalMap,并且判断map是否为空,如果不是空就直接调用ThreadLocalMap的remove()方法直接移除当前ThreadLocal对象对应的entry。
那么 下面看一下ThreadLocalMap中remove()方法的源码:
private void remove(ThreadLocal<?> key) {
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)]) {
if (e.get() == key) {
e.clear();
expungeStaleEntry(i);
return;
}
}
}
源码就介绍到这里。
问题5、ThreadLocal内存泄漏问题?
这个问题是ThreadLocal最需要注意的问题,稍后会专门设计一篇博客讲述ThreadLocal内存泄漏的问题。
文章如有错误之处欢迎指正,如果该文章对你有帮助,麻烦给予关注,点赞!!!
标签:map,面试题,Thread,ThreadLocalMap,ThreadLocal,value,惨案,线程 来源: https://blog.csdn.net/zuihongyan518/article/details/114794079