前言
马上就是1024程序员节了,在此小航先祝大家:
public static void main(String[] args) {
System.out.println("节日快乐!");
}
😊你好,我是小航,一个正在变秃、变强的文艺倾年。
🔔本文讲解实战ThreadLocal,欢迎大家多多关注!
🔔每天进步一点点,一起卷起来叭!
一、ThreadLocal简介
ThreadLocal叫做线程本地变量
,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLocal和Synchonized都用于解决多线程并发访问,但是ThreadLocal与synchronized有本质的区别:
synchronized(锁) | ThreadLocal | |
---|---|---|
原理 | 同步机制采用了时间换空间的方式,只提供一份变量,让不同线程排队访问(临界区排队) | 采用空间换时间的方式,为每一个线程都提供一份变量的副本,从而实现同时访问而互不相干扰 |
侧重点 | 多个线程之间访问资源的同步 | 多线程中让每个线程之间的数据相互隔离 |
二、ThreadLocal的简单使用
示例代码:
/**
* @author artboy
*/
public class ThreadLocalDemo {
private static ThreadLocal<String> localVar = new ThreadLocal<String>();
static void print(String str) {
//打印当前线程中本地内存中本地变量的值
System.out.println(str + " :" + localVar.get());
//清除本地内存中的本地变量
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
localVar.remove();
}
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalDemo.localVar.set("local_A");
print("A");
//打印本地变量
System.out.println("after remove : " + localVar.get());
}
},"A").start();
new Thread(new Runnable() {
@Override
public void run() {
ThreadLocalDemo.localVar.set("local_B");
print("B");
System.out.println("after remove : " + localVar.get());
}
},"B").start();
}
}
A :local_A
B :local_B
after remove : null
after remove : null
从示例中我们可以看到:两个线程分表获取了自己线程存放的变量,他们之间变量的获取并不会错乱。
三、ThreadLocal源码剖析
ThreadLocal主要有以下几个方法:
public T get() { } // 用来获取ThreadLocal在当前线程中保存的变量副本
public void set(T value) { } //set()用来设置当前线程中变量的副本
public void remove() { } //remove()用来移除当前线程中变量的副本
protected T initialValue() { } //initialValue()是一个protected方法,一般是用来在使用时进行重写的
ThreadLocal的set()方法:
/**
* 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) {
//1、获取当前线程
Thread t = Thread.currentThread();
//2、获取线程中的属性 threadLocalMap ,如果threadLocalMap 不为空,则直接更新要保存的变量值,否则创建threadLocalMap,并赋值
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
// 初始化thradLocalMap 并赋值
createMap(t, value);
}
从上面的代码可以看出,ThreadLocal set赋值的时候首先会获取当前线程thread
,然后通过getMap(t)
方法获取到一个map,map的类型为ThreadLocalMap(获取thread线程中的ThreadLocalMap属性
)。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
那么ThreadLocalMap又是什么呢,还有createMap又是怎么做的,我们继续往下看。
public class ThreadLocal<T> {
static class ThreadLocalMap {
/**
* 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.
*/
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
}
}
可看出ThreadLocalMap是ThreadLocal的内部静态类,而它的构成主要是用Entry来保存数据 ,而且还是继承的弱引用
。在Entry内部使用ThreadLocal作为key,使用我们设置的value作为value。
Java中的引用类型:
- 强引用(Strong Reference):通常我们通过new来创建一个新对象时返回的引用就是一个强引用,若一个对象通过一系列强引用可到达,它就是强可达的(strongly reachable),那么它就不被回收
- 弱引用(Weak Reference):弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象,不管当前内存空间足够与否,都会回收它的内存
- 软引用(Soft Reference):软引用和弱引用的区别在于,若一个对象是弱引用可达,无论当前内存是否充足它都会被回收,而软引用可达的对象在内存不充足时才会被回收,因此软引用要比弱引用“强”一些
- 虚引用(Phantom Reference):虚引用是Java中最弱的引用,那么它弱到什么程度呢?它是如此脆弱以至于我们通过虚引用甚至无法获取到被引用的对象,虚引用存在的唯一作用就是当它指向的对象被回收后,虚引用本身会被加入到引用队列中,用作记录它指向的对象已被回收。
判断弱引用对象的关键在于只具有弱引用的对象,也就是说,如果一个对象有强引用,那么在系统GC时,是不会回收此对象的,也不会释放弱引用。
这里为什么要使用弱引用
呢?
原因是如果不使用弱引用,那么当持有value的强引用释放掉后,当线程没有回收释放时,threadLocalMap会一直持有ThreadLocal以及value的强应用,导致value不能够被回收,从而造成内存泄漏。
关于弱引用,如果有小伙伴不太懂,请康这个栗子:(弱引用回收测试)
WeakReferenceDemo.java
import java.lang.ref.WeakReference;
/**
* 弱引用回收测试
*
* @author artboy
*/
public class WeakReferenceDemo {
public static WeakReference<String> weakReference;
public static void main(String[] args) {
test();
// 可以输出hello值,此时两个弱引用扔持有对象,而且未进行gc
System.out.println("未进行gc时,只有弱引用指向value内存区域:" + weakReference.get());
// 此时已无强一用执行"value"所在内存区域,gc时会回收弱引用
System.gc();
// 此时输出都为null
System.out.println("进行gc时,只有弱引用指向value内存区域:" + weakReference.get());
}
public static void test() {
String hello = new String("value");
weakReference = new WeakReference<>(hello);
System.gc();
// 此时gc不会回收弱引用,因为字符串"value"仍然被hello对象强引用
System.out.println("进行gc时,强引用与弱引用同时指向value内存区域:" + weakReference.get());
}
}
进行gc时,强引用与弱引用同时指向value内存区域:value
未进行gc时,只有弱引用指向value内存区域:value
进行gc时,只有弱引用指向value内存区域:null
分析输出结果可以看出:
-
当有强引用指向value内存区域时,即使进行gc,弱引用也不会被释放,对象不回被回收。
-
当无强引用指向value内存区域是,此时进行gc,弱引用会被释放,对象将会执行回收流程。
通过使用弱引用,当ThreadLocal的强引用释放掉后,通过一次系统GC检查,发现ThreadLocal对象只有threadLocalMap中Entry的弱引用持有,此时根据弱引用的机制就会回收ThreadLocal对象,从而避免了内存泄露。
示例代码:
/**
* @author artboy
*/
public class ThreadLocalDemo2 {
public static ThreadLocal<String> threadLocal = new ThreadLocal<>();
public static void main(String[] args) {
ThreadLocalDemo2.threadLocal.set("hello world main");
// main方法中ThreadLocal的变量threadLocal
System.out.println(ThreadLocalDemo2.threadLocal.get());
try {
// 在new Thread()中对ThreadLocal的变量threadLocal进行修改
Thread thread = new Thread() {
@Override
public void run() {
ThreadLocalDemo2.threadLocal.set("hello world thread");
System.out.println(ThreadLocalDemo2.threadLocal.get());
};
};
thread.start();
thread.join();
} catch (Exception e) {
e.printStackTrace();
}
// main方法中ThreadLocal的变量threadLocal
System.out.println(ThreadLocalDemo2.threadLocal.get());
// 调用remove方法删除threadLocal副本,以防内存泄露
threadLocal.remove();
}
}
hello world main
hello world thread
hello world main
不难看出,我们在new Thread()中对ThreadLocal的变量threadLocal进行修改后,在main线程中再次输出,其值并没有收到影响,他们修改的分别是各自的副本,不会对其他副本有影响。
ThreadLocal的get()方法:
/**
* Returns the value in the current thread's copy of this
* thread-local variable. If the variable has no value for the
* current thread, it is first initialized to the value returned
* by an invocation of the {@link #initialValue} method.
*
* @return the current thread's value of this thread-local
*/
public T get() {
// 1、获取当前线程
Thread t = Thread.currentThread();
// 2、获取当前线程的ThreadLocalMap
ThreadLocalMap map = getMap(t);
// 3、如果map数据不为空,
if (map != null) {
// 3.1、获取threalLocalMap中存储的值
// 注意这里获取键值对传进去的是 this,而不是当前线程t
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 如果是数据为null,则初始化,初始化的结果,TheralLocalMap中存放key值为threadLocal,值为null
return setInitialValue();
}
setInitialValue方法的实现:
private T setInitialValue() {
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
return value;
}
//ThreadLocalMap 构造方法
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
//这个是threadlocal 的内部方法
void createMap(Thread t, T firstValue) {
t.threadLocals = new ThreadLocalMap(this, firstValue);
}
首先是通过调用initialValue,initialValue是protected方法
,初始化ThreadLocal时可以重写此函数,相当于延迟加载,然后通过getMap创建threadLocals,如果threadLocals不存在时,会调用createMap创建一个初始大小为16的Entry数组table
,并新建一个Entry存入table中。这个threadLocals就是用来存储实际的变量副本的,键值为当前ThreadLocal变量,value为变量副本(即T类型的变量)
我们顺着思路,再康康Entry类:
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
Entry类是集成自WeakReference,然后使用ThreadLocal作为了键,也就是说这里的ThreadLocal是一个弱引用
在GC的时候会被回收。
public T get() {
...
// 3、如果map数据不为空,
if (map != null) {
// 3.1、获取threalLocalMap中存储的值
// 注意这里获取键值对传进去的是 this,而不是当前线程t
ThreadLocalMap.Entry e = map.getEntry(this);
...
}
如果map存在,则会调用map的getEntry方法,getEntry方法实现:
private Entry getEntry(ThreadLocal<?> key) {
// 通过hash算出数组下标
int i = key.threadLocalHashCode & (table.length - 1);
Entry e = table[i];
if (e != null && e.get() == key)
// 如果取出Entry,并且e.get也就是referent与threadLocal相同,则说明是需要的值,返回Entry对象e ,判断e.get() = key 是解决hash碰撞的情况
return e;
else
// 如果下标i的Entry不存在或者 其threadLocal不相同,则执行此
return getEntryAfterMiss(key, i, e);
}
private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
Entry[] tab = table;
int len = tab.length;
while (e != null) {
// 说明有此entry,可能是hash碰撞的结果
ThreadLocal<?> k = e.get();
if (k == key)
return e;
if (k == null)
// 处理已无引用的ThreadLocal变量等,解决内存泄漏的机制之一
expungeStaleEntry(i);
else
// 下标+1
i = nextIndex(i, len);
e = tab[i];
}
// 如果getEntry中获取的entry=null,则说明无此ThreadLocal变量,返回null
return null;
}
expungeStaleEntry 方法:
//删除可以释放的Entry
private int expungeStaleEntry(int staleSlot) {
Entry[] tab = table;
int len = tab.length;
// expunge entry at staleSlot
tab[staleSlot].value = null;
tab[staleSlot] = null;
size--;
// Rehash until we encounter null
Entry e;
int i;
for (i = nextIndex(staleSlot, len);
(e = tab[i]) != null;
i = nextIndex(i, len)) {
ThreadLocal<?> k = e.get();
if (k == null) {
//如果发现ThreadLocal已经被释放掉,则通过这里来释放value的引用,以及删除数组table中的Entry
e.value = null;
tab[i] = null;
size--;
} else {
int h = k.threadLocalHashCode & (len - 1);
if (h != i) {
//重新设置Entry在table中的位置
tab[i] = null;
// Unlike Knuth 6.4 Algorithm R, we must scan until
// null because multiple entries could have been stale.
while (tab[h] != null)
h = nextIndex(h, len);
tab[h] = e;
}
}
}
return i;
}
通过对get方法的大致分析,可以分为几个阶段:
1)判断Map是否存在,如果不存在初始化Map以及table等
2)如果已存在,并且获取到Entry,则返回
3)如果不存在,则调用expungeStaleEntry清除需要释放的ThreadLocal、释放对value的一用,从table中删除相应下标的Entry,以及重新设置元素在table中的位置
ThreadLocal的remove()方法:
// ThreadLocal
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
/**
* ThreadeLocalMap
* Remove the entry for key.
*/
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;
}
}
}
remove方法相对简单,通过hashcode计算出下标,然后判断key与要删除的ThreadLocal是否一致,如果一致,释放掉相应的引用,并调用expungeStaleEntry方法清理其他的可以释放的对象。
四、ThreadLocal场景实战
场景:我们访问方法时常常需要获取用户信息,在并发场景下一个用户对应一个请求线程,因为拦截器与对应拦截方法都是属于同一个线程的,我们每次访问方法之前,通过拦截器获取到用户信息,根据当前线程就自然也就保存对应的用户信息,存取比较方便
示例Demo:
1.初始化Spring项目:
2.引入相关依赖:
<dependency>
<groupId>org.springframework.session</groupId>
<artifactId>spring-session-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.83</version>
</dependency>
3.配置application.yml
spring:
session:
store-type: redis
redis:
# 地址
host: .....
# 端口
port: 6379
# 数据库索引(db0,db1,db2...不同业务可以放在不同数据库中)
database: 0
# 密码
password: '....'
4.创建常量类:MyConstant
package com.example.thread_local_demo.constant;
/**
* @author xh
* @Date 2022/10/21
*/
public class MyConstant {
public static final String TEMP_USER_COOKIE_NAME = "my_cookie";
public static final int TEMP_USER_COOKIE_EXPIRE_TIME = 30;
}
5.配置SessionConfig
package com.example.thread_local_demo.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.example.thread_local_demo.constant.MyConstant;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;
/**
* @author xh
* @Date 2022/10/21
*/
@Configuration
@EnableRedisHttpSession
public class MySessionConfig {
@Bean
public RedisSerializer<Object> springSessionDefaultRedisSerializer() {
return new GenericFastJsonRedisSerializer();
}
@Bean
public CookieSerializer cookieSerializer() {
DefaultCookieSerializer serializer = new DefaultCookieSerializer();
serializer.setCookieName(MyConstant.TEMP_USER_COOKIE_NAME);
return serializer;
}
}
6.创建Entity:UserInfoTo
package com.example.thread_local_demo.entity;
import lombok.Data;
import java.io.Serializable;
/**
* @author xh
* @Date 2022/10/21
*/
@Data
public class UserInfoTo implements Serializable {
private static final long serialVersionUID = 1L;
private Long userId;
private String userKey;
}
7.创建自定义拦截器:MyInterceptor
package com.example.thread_local_demo.interceptor;
import com.example.thread_local_demo.constant.MyConstant;
import com.example.thread_local_demo.entity.UserInfoTo;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import java.util.LinkedHashMap;
import java.util.UUID;
/**
* @author xh
* @Date 2022/10/21
*/
public class MyInterceptor implements HandlerInterceptor {
public static ThreadLocal<UserInfoTo> threadLocal = new ThreadLocal<>();
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 获取用户登录信息
HttpSession session = request.getSession();
LinkedHashMap loginUser = (LinkedHashMap) session.getAttribute("loginUser");
UserInfoTo userInfoTo = new UserInfoTo();
if (loginUser != null) {
userInfoTo.setUserId(Long.parseLong(loginUser.get("id").toString()));
}
// 遍历cookie
Cookie[] cookies = request.getCookies();
if (cookies != null && cookies.length > 0) {
for (int i = 0; i < cookies.length; i++) {
// 判断是否是游客
if (MyConstant.TEMP_USER_COOKIE_NAME.equals(cookies[i].getName())) {
userInfoTo.setUserKey(cookies[i].getValue());
}
}
}
threadLocal.set(userInfoTo);
return true;
}
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
UserInfoTo userInfoTo = threadLocal.get();
if (userInfoTo.getUserKey() == null || "".equals(userInfoTo.getUserKey())) {
userInfoTo.setUserKey(UUID.randomUUID().toString());
}
Cookie cookie = new Cookie(MyConstant.TEMP_USER_COOKIE_NAME, userInfoTo.getUserKey());
cookie.setMaxAge(MyConstant.TEMP_USER_COOKIE_EXPIRE_TIME);
response.addCookie(cookie);
}
}
8.将自定义拦截器注册进来:MyWebMvcConfig
package com.example.thread_local_demo.config;
import com.example.thread_local_demo.interceptor.MyInterceptor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
/**
* @author xh
* @Date 2022/10/21
*/
@Configuration
public class MyWebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new MyInterceptor()).addPathPatterns("/**");
}
}
9.新建接口:
package com.example.thread_local_demo.controller;
import com.example.thread_local_demo.entity.UserInfoTo;
import com.example.thread_local_demo.entity.UserLoginVo;
import com.example.thread_local_demo.interceptor.MyInterceptor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
/**
* @author xh
* @Date 2022/10/21
*/
@RestController
public class ApiController {
@GetMapping("/test")
public String test() {
UserInfoTo userInfoTo = MyInterceptor.threadLocal.get();
System.out.println(userInfoTo);
return userInfoTo.toString();
}
@GetMapping("/login/{id}")
public String login(@PathVariable String id, HttpServletRequest request) {
HttpSession session = request.getSession();
LinkedHashMap<String, Long> loginUser = new LinkedHashMap<>();
loginUser.put("id", 1L);
session.setAttribute("loginUser", loginUser);
return "登录成功:ID=" + id;
}
}
测试登录:
测试获取用户信息:
大功告成!
如果您还有任何代码疑惑,可以私信留言小航!
上述涉及到的代码:https://gitee.com/lovexh666/thread_local_demo