文章目录
前言
上一篇博客梳理了线程池的一些常用知识点,这一篇博客开始絮叨一下ThreadLocal,其实从整体上来说,ThreadLocal有两大使用场景:**1、每个线程之间为了避免出现线程安全,需要独享一个对象。2、每个线程内,不同的业务方法之前需要做到数据共享。**这两个使用场景将是我们总结ThreadLocal的切入点。
ThreadLocal——线程间独享一个对象
从SimpleDateFormat开始
如果要问SimpleDateFormat是不是线程安全的,可能大部分程序员都知道,其是线程不安全的。这个类通常用于我们进行日期格式的转换。其内部的parser函数,由于用到calender对象进行日期转换,相关源码并不是线程安全的,这个可以参考相关大牛的总结。我们这篇博客从SimpleDateFormat的使用开始,总结一下ThreadLocal的第一种使用场景
1、基础使用实例
/**
* autor:liman
* createtime:2021/11/7
* comment:简单的多线程下的SimpleDateFormat实例
* 两个线程打印,并没有出现问题
*/
@Slf4j
public class SimpleDateFormatDemo {
public static void main(String[] args) {
new Thread(()->{
String date = new SimpleDateFormatDemo().dateFormat(10);
System.out.println(date);
}).start();
new Thread(()->{
String date = new SimpleDateFormatDemo().dateFormat(1000);
System.out.println(date);
}).start();
}
public String dateFormat(int seconds){
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
两个线程打印日期格式,并没有啥问题。
如果这个时候,有10个线程都想用这个SimpleDateFormat的格式,指定日期输出,那我们该如何做?
2、十个线程使用同一个格式的SimpleDateFormat
/**
* autor:liman
* createtime:2021/11/8
* comment:多个线程打印SimpleDateFormat转换的日期格式
*/
@Slf4j
public class SimpleDateFormatMultiThread {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
int finalI = i;
new Thread(()->{
String date = new SimpleDateFormatMultiThread().dateFormat(finalI);
System.out.println(date);
}).start();
}
}
public String dateFormat(int seconds) {
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
运行一下,好像也没有什么问题,因为每个线程都有自己的SimpleDateFormat对象
3、如果有1000个线程?
这个时候我们就要利用线程池了
/**
* autor:liman
* createtime:2021/11/8
* comment:线程池运行SimpleDateFormat的内容
*/
@Slf4j
public class SimpleDateFormatThreadPool {
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
public static void main(String[] args) {
for (int i = 0; i < 1000; i++) {
int finalI = i;
//每个线程都会创建SimpleDateFormat,相当于创建了1000个SimpleDateFormat对象
threadPool.submit(new Thread(() -> {
String date = new SimpleDateFormatThreadPool().dateFormat(finalI);
System.out.println(date);
}));
}
threadPool.shutdown();
}
}
public String dateFormat(int seconds) {
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
运行一下,好像依旧没有问题,但是,1000个线程,创建了1000个SimpleDateFormat对象,这……也太暴力了。
共享的SimpleDateFormat
上面几个实例下来,每个线程创建SimpleDateFormat对象,并不会出现一些异常,但是SimpleDateFormat并没有必要每个线程都去创建,指定的格式都是一样,为什么不通过静态变量来实现这个逻辑?
/**
* autor:liman
* createtime:2021/11/8
* comment:线程池共享 SimpleDateFormat
*/
@Slf4j
public class SimpleDateFormatStaticThreadPool {
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//共享SimpleDateFormat
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//记录重复数据
private static Set<String> resultSet = new HashSet<>();
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
int finalI = i;
threadPool.submit(new Thread(() -> {
String date = new SimpleDateFormatStaticThreadPool().dateFormat(finalI);
System.out.println(date);
}));
}
threadPool.shutdown();
}
public String dateFormat(int seconds) {
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
String result = dateFormat.format(date);
if(resultSet.contains(result)){
log.warn("{},出现重复数据",result);
}
resultSet.add(result);
return result;
}
}
可以看到,程序中我加入了一个集合,用于记录是否出现重复数据,运行结果如下
一堆重复数据,因为多个线程共享SimpleDateFormat,出现了线程安全问题。
加锁?
/**
* autor:liman
* createtime:2021/11/8
* comment:加锁解决SimpleDateFormat的线程安全问题
*/
@Slf4j
public class SimpleDateFormatLockSlove {
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//共享SimpleDateFormat
private static SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
//记录重复数据
private static Set<String> resultSet = new HashSet<>();
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 100; i++) {
int finalI = i;
threadPool.submit(new Thread(() -> {
String date = new SimpleDateFormatLockSlove().dateFormat(finalI);
System.out.println(date);
}));
}
}
public String dateFormat(int seconds) {
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
String result = "";
//加锁,锁住关键代码
synchronized (SimpleDateFormatLockSlove.class) {
result = dateFormat.format(date);
resultSet.add(result);
}
return result;
}
}
运行结果就正常了,很丝滑
用ThreadLocal优化一下
synchronized自然能解决,但是……这种排队运行的方式,是不是太慢了?如果高并发情况下,ThreadLocal就可以更好的解决这类问题,如果通过线程共享会有线程安全的问题,则ThreadLocal是一个不错的容器,可以保证每个线程内部有一个指定对象的副本。线程间的修改和操作就是隔离的。
/**
* autor:liman
* createtime:2021/11/8
* comment:用ThreadLocal解决SimpleDateFormat的线程安全问题
*/
@Slf4j
public class SimpleDateFormatThreadLocal {
private static ExecutorService threadPool = Executors.newFixedThreadPool(10);
//记录重复数据
private static Set<String> resultSet = new HashSet<>();
public static void main(String[] args) {
for (int i = 0; i < 10000; i++) {
int finalI = i;
threadPool.submit(new Thread(() -> {
String date = dateFormat(finalI);
System.out.println(date);
}));
}
threadPool.shutdown();
}
public String dateFormat(int seconds) {
//从1970-01-01 00:00:00开始计算
Date date = new Date(1000 * seconds);
//直接调用ThreadLocal的get方法,获得其中的SimpleDateFormat对象
String result = ThreadSafeSimpleDateFormat.dateFormatThreadLocal.get().format(date);
if(resultSet.contains(result)){
log.warn("{},出现重复数据",result);
}
resultSet.add(result);
return result;
}
}
//用一个内部类构造ThreadLocal
class ThreadSafeSimpleDateFormat{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
//复写ThreadLocal中的initialValue的方法,放入SimpleDateFormat对象
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
}
丝滑,没有线程安全问题。
支持lambda表达式
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal
= ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
ThreadLocal——同一个线程内部数据共享
在实际的web开发中,其实很多如下图的场景,当前请求有个很长的请求链,这个请求链上的方法,偶尔都会去操作用户数据,则需要一个公共的地方共享数据,并能做到同步,这个时候ThreadLocal就能完美的满足这个需求。
无需synchronized,无需ConcurrentHashMap,效率还很高,ThreadLocal就是这种场景的最优解。
实例代码
/**
* autor:liman
* createtime:2021/11/8
* comment: 不断传递用户信息的问题
*/
@Slf4j
public class UserInfoProblem {
public static void main(String[] args) {
log.info("开始调用");
ServiceOne serviceOne = new ServiceOne();
serviceOne.process();
log.info("调用结束");
}
}
class ServiceOne{
public void process(){
User user = new User("牛哥");
//这里手动setThreadLocal中的内容,没有调用initValue方法
UserContextHolder.holder.set(user);
System.out.println("service one 设置用户"+user);
ServiceTwo serviceTwo = new ServiceTwo();
serviceTwo.process();
}
}
class ServiceTwo{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("server two 中拿到的对象"+user);
ServiceThree serviceThree = new ServiceThree();
serviceThree.process();
}
}
class ServiceThree{
public void process(){
User user = UserContextHolder.holder.get();
System.out.println("server three 中拿到的对象"+user);
//不用了要记得remove
UserContextHolder.holder.remove();
}
}
//Holder是持有者的意思,一些源码中经常看到Holder结尾的类,就是持有者的意思
class UserContextHolder{
public static ThreadLocal<User> holder =
new ThreadLocal<>();
}
class User{
String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
return "User{" +
"name='" + name + '\'' +
'}';
}
}
走到这里,ThreadLocal的两个使用场景,我们已经总结完毕。但是……这不是结束
ThreadLocal的好处
1、可以达到线程安全目的
2、不需要加锁,执行效率比较高
3、更高效地利用内存,节省开销。至少不用每个线程都new一个SimpleDateFormat对象了
4、可以避免线程间共享数据的麻烦。
相关源码
其实在Thread类中,有一个threadLocals的属性,这个属性是ThreadLocalMap类型的
ThreadLocalMap中其实是一个Map类型的对象,这个Map的key值是ThreadLocal,value就是ThreadLocal中的值。也就是说,同一个线程中,其实允许存在多个ThreadLocal,如下图所示
ThreadLocal的get方法和setInitialValue方法
public T get() {
//获取当前线程
Thread t = Thread.currentThread();
//获取线程中的ThreadLocalMap
ThreadLocalMap map = getMap(t);
if (map != null) {
//如果ThreadLocalMap不为空,则取出调用这个方法的ThreadLocal,并返回其value值
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
//如果ThreadLocalMap为空,则调用setInitialValue方法
return setInitialValue();
}
private T setInitialValue() {
//这里就会去调用initialValue方法,这方法源码中直接返回null,因此子类要复写这个方法,达到初始化ThreadLocal中的值的目的。
T value = initialValue();
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
//如果当前ThreadLocalMap为空,则创建一个ThreadLocalMap
createMap(t, value);
}
if (this instanceof TerminatingThreadLocal) {
TerminatingThreadLocal.register((TerminatingThreadLocal<?>) this);
}
return value;
}
initialValue不重写,则直接返回null
ThreadLocal的set方法
//这个就简单了
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
map.set(this, value);
} else {
createMap(t, value);
}
}
ThreadLocal的remove方法
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null) {
m.remove(this);
}
}
最后说一下ThreadLocalMap,ThreadLocalMap是Thread类中的一个内部类,其实通过数组维护了ThreadLocal的Map表。相关源码如下
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.
*/
//这里用的是弱引用 Key是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
private Entry[] table;
}
使用ThreadLocal的问题
无法回收的value
java开发者应该都听过内存泄漏的问题,内存泄漏,其实就是某个对象不再被使用,但是GC依旧无法回收这个对象占用的内存,日积月累,这些对象就会超过我们的内存限制,出现OOM的问题。如果ThreadLocal使用不当,这可能会出现这个问题
上面介绍的ThreadLocalMap的类中,Entry中key的构造,用到了弱引用。key用弱引用的构造函数给赋值了,用弱引用关联,说明其可以被垃圾回收器回收,如果某个时候,某个对象只被弱引用关联,则GC(垃圾回收器)会回收这个对象。但是……ThreadLocalMap的Entry的value构造的时候,用的可是强引用。
在正常情况下,线程运行终止,Thread对象被回收,自然其内部的ThreadLocalMap会被回收,同时ThreadLocalMap中的Entry对应的key和value都会被回收。但是,如果线程不终止,那么Entry中对应的value可能就很难被回收了,这个时候存在如下引用链
Thread->ThreadLocalMap->Entry(key为弱引用,可能被回收)->value
这样就导致value始终无法收回,GC只是收回了key。日积月累,就OOM了。
JDK其实已经考虑了这个问题,在set,remove,rehash方法中会扫描key为null的Entry,并将对应的Entry的value置为null,这样value就能正常被回收了。
但是,还有但是,我们如果忘了触发这几个方法,OOM依旧有可能发生,这也是阿里开发手册中,针对ThreadLocal的使用有一个强制的要求,要求ThreadLocal在不使用的时候,要手动remove。
NPE问题
ThreadLocal中initialValue方法的源码如下
protected T initialValue() {
return null;
}
由于这里方法声明的是一个泛型,因此需要注意装箱拆箱的NPE问题。
总结
ThreadLocal,两种场景,不用的时候务必手动remove,且需要注意装箱拆箱操作。同时需要说明的是在Spring中很多类似于ContextHolder结尾的,都或多或多和ThreadLocal有关系。