一、ThreadLocal简介
ThreadLocal叫做线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程而言是隔离的,也就是说该变量是当前线程独有的变量。ThreadLocal为变量在每个线程中都创建了一个副本,那么每个线程可以访问自己内部的副本变量。
ThreadLoal 变量,线程局部变量,同一个 ThreadLocal 所包含的对象,在不同的 Thread 中有不同的副本。这里有几点需要注意:
- 因为每个 Thread 内有自己的实例副本,且该副本只能由当前 Thread 使用。这是也是 ThreadLocal 命名的由来。
- 既然每个 Thread 有自己的实例副本,且其它 Thread 不可访问,那就不存在多线程间共享的问题。
ThreadLocal 提供了线程本地的实例。它与普通变量的区别在于,每个使用该变量的线程都会初始化一个完全独立的实例副本。ThreadLocal 变量通常被private static修饰。当一个线程结束时,它所使用的所有 ThreadLocal 相对的实例副本都可被回收。
总的来说,ThreadLocal 适用于每个线程需要自己独立的实例且该实例需要在多个方法中被使用,也即变量在线程间隔离而在方法或类间共享的场景。
注意事项:
如何正确的使用ThreadLocal:
1、将ThreadLocal变量定义成private static的,这样的话ThreadLocal的生命周期就更长,由于一直存在ThreadLocal的强引用,所以ThreadLocal也就不会被回收,也就能保证任何时候都能根据ThreadLocal的弱引用访问到Entry的value值,然后remove它,防止内存泄露;
2、每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
使用场景:
原理:一般在请求后台接口的时候,我们会根据前端传过来的 Token ,解析出当前的用户信息,然后放置在当前请求线程的 ThreadLocal 中,当调用 getIndex() 方法时,会从当前线程的 ThreadLocal 中获取出用户的编号(索引后缀),然后拼接为一个完整的索引返回。
我这里为了方便测试,提供了 setSuffix()、remove() 等方法,用于手动设置或移除当前索引后缀。
/**
* 注解/索引
*
* @author c
* @date 2021/05/04 14:52
*/
@Component
public class ElasticConfig {
private static final ThreadLocal<String> THREAD_LOCAL = new ThreadLocal<>();
/**
* 获取索引名称后缀
*
* @return
*/
public String getSuffix() {
//可以在该方法中赋值,也可以在使用前赋值
return THREAD_LOCAL.get();
}
/**
* 设置索引名称后缀
*
* @param suffix
*/
public void setSuffix(String suffix) {
THREAD_LOCAL.set(suffix);
}
/**
* 移除当前索引
*/
public void remove() {
THREAD_LOCAL.remove();
}
/**
* 获取当前索引
*
* @return
*/
public String getIndex() {
if (StrUtil.isBlank(getSuffix())) {
return null;
}
return ElasticConstant.OPERATION + getSuffix();
}
/**
* 获取当前索引
*
* @return
*/
public String getDIndex() {
if (StrUtil.isBlank(getSuffix())) {
return null;
}
return ElasticConstant.OPERATION_DOWN + getSuffix();
}
}
将
indexName
设置为#{@dynamicIndex.getIndex()}
,这是一个SpEL
表达式,dynamicIndex
就是我们上面创建的动态获取索引类的对象,当需要获取索引名称的时候,getIndex()
方法就会被调用。
createIndex
一定要设置为false
,避免当项目启动时索引被自动创建。
// createIndex 一定要设置为 false,避免当项目启动时索引被自动创建
@Data
@Builder
@ApiModel("数据日志")
@Document(indexName = "#{@elasticConfig.getIndex()}", createIndex = false)
public static class EduLiveUp implements Serializable {
@Id
private String id;
@ApiModelProperty("教室Id")
@Field(type = FieldType.Long)
private Long classroomId;
@ApiModelProperty("用户id")
@Field(type = FieldType.Long)
private Long appUserId;
@ApiModelProperty("姓名")
@Field(type = FieldType.Keyword)
private String userName;
}
执行时,先在线程中赋值:
@Autowired
private ElasticConfig esConfig;
// 以下是方法中操作:
// 赋值方法
esConfig.setSuffix(BRInfo.getIndexFlag());
// 如:ES插入前,(以上字段)赋值即可指定索引,无需直接在save方法中添加索引,MQ队列和交换机也是同样逻辑
elasticsearchRestTemplate.save(upInfo);
//elasticsearchRestTemplate.save(upInfo, IndexCoordinates.of(ElasticConstant.OPERATION+BRInfo.getIndexFlag()));
// 移除后缀,防止内存泄漏
esConfig.remove();
Java8中已经做了一些优化如,在ThreadLocal的get()、set()、remove()方法调用的时候会清除掉线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
java中的四种引用:
强引用📍: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError 错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
软引用📍: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
弱引用📍: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
虚引用📍: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)
通常ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长,如果没有手动删除对应key(线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏),一定会导致内存泄漏,但是使用弱引用可以多一层保障:弱引用ThreadLocal会被GC回收,不会内存泄漏,对应的value在下一次ThreadLocalMap调用set,get,remove的时候会被清除,Java8已经做了上面的代码优化。
参考资料:链接1, ThreadLocal链接2, ThreadLocal 链接3
其他:初始化索引配置
import java.io.Serializable;
import java.util.Date;
import lombok.Data;
import lombok.ToString;
import org.springframework.data.annotation.Id;
import org.springframework.data.elasticsearch.annotations.Document;
import org.springframework.data.elasticsearch.annotations.Field;
import org.springframework.data.elasticsearch.annotations.FieldType;
/**
* @author nandao 2021/7/27
*/
@Data
@Document(indexName = "#{esAttribute.indexArticleType}", type = "#{esAttribute.indexArticleType}")
@ToString
public class ArticleEsDto implements Serializable {
private static final long serialVersionUID = 7118858963867439527L;
/**
* es id
*/
@Id
private String id;
/**
* 原始ID
*/
@Field(type = FieldType.Long)
private Long jid;
/**
* 文章ID
*/
@Field(type = FieldType.Long)
private Long articleId;
/**
* 文章标题
*/
@Field(type = FieldType.Text)
private String title;
}
import lombok.Data;
import lombok.ToString;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
/**
* @author nandao
* @version 1.0
* @description 初始化索引配置
* @date 2021/07/27
**/
@Component
@Data
@ToString
public class EsAttribute {
/**
* 文章索引名字
*/
@Value("${spring.data.index.articleType}")//对应配置文件中的articleType
private String indexArticleType;
}
如果本篇文章对你有帮助的话,很高兴能够帮助上你。
当然,如果你觉得文章有什么让你觉得不合理、或者有更简单的实现方法又或者有理解不来的地方,希望你在看到之后能够在评论里指出来,我会在看到之后尽快的回复你。