目录
1.3使用线程池创建1000个打印线程分别用自己的SimpleDateFormat
1.3.2根据共享对象的生成时机不同,选择initialValue或者set来保存对象
ThreadLocal是本地线程,不是公用的线程。
1.ThreadLocal的用途两个使用场景
1.1场景1
每个线程对象需要一个共享对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Radmon)
每个Thread内有自己在实例副本,不共享。
举个例子:教材只有一本,30个同学都抢着看。一起做笔记有线程安全问题,并发的读写会带来数据不一致,用了ThreadLocal后相当于把这本教材复印了30份教材,每个同学都使用自己的教材,这里的每一本书每一个实例,都只能当前同学当前的线程可以访问到,并且使用。
1.2两个线程分别用自己的SimpleDateFormat
package cn.butool;
import java.text.SimpleDateFormat;
import java.util.Date;
/**
* 用两个线程分别打印日期信息
*/
public class ThreadLocalSimpleDateFormatDemo {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10));
}
);
thread1.start();
Thread thread2 = new Thread(() -> {
System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(212104277));
}
);
thread2.start();
}
/**
* 将秒转换成日期字符串
*
* @param seconds
* @return
*/
public String dataToString(int seconds) {
//参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
Date date = new Date(seconds * 1000);
SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return simpleDateFormat.format(date);
}
}
并没有问题
1970-01-20 10:34:39 1970-01-01 08:00:10
1.3使用线程池创建1000个打印线程分别用自己的SimpleDateFormat
package cn.butool;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 用线程池创建分别打印日期信息
*/
public class ThreadLocalSimpleDateFormatDemo {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
//dateFormat 不需要每次都新建
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.submit(() -> {
System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10+ finalI));
});
}
executorService.shutdown();
}
/**
* 将秒转换成日期字符串
*
* @param seconds
* @return
*/
public String dataToString(int seconds) {
//参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
Date date = new Date(seconds * 1000);
return simpleDateFormat.format(date);
}
}
打印可以看到有很多处两个重复的日期
1970-01-01 08:15:51
1970-01-01 08:16:14
1970-01-01 08:16:13
1970-01-01 08:16:12
1970-01-01 08:16:17
1970-01-01 08:16:18
1970-01-01 08:16:11
1970-01-01 08:16:11
1.4加锁解决线程安全问题
package cn.butool.threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 用线程池创建分别打印日期信息
*/
public class ThreadLocalSimpleDateFormatDemo {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
//dateFormat 不需要每次都新建
private static SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.submit(() -> {
System.out.println(new ThreadLocalSimpleDateFormatDemo().dataToString(10+ finalI));
});
}
executorService.shutdown();
}
/**
* 将秒转换成日期字符串
*
* @param seconds
* @return
*/
public String dataToString(int seconds) {
//参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
Date date = new Date(seconds * 1000);
String format=null;
synchronized (ThreadLocalSimpleDateFormatDemo.class){
format = simpleDateFormat.format(date);
}
return format;
}
}
1.5SimpleDateFormat小结
- 两个线程分别用自己的SimpleDateFormat
- 1000个线程要用到线程池了,否则消耗内存太多
- 优化代码,所有线程共用一个SimpleDateFormat对象
- 加锁解决重复对象,但是降低效率
1.5更好的解决方案是使用ThreadLoacl
利用ThreadLocal给每个线程分配自己的DateFormat对象,同时保证了线程安全。高效利用了内存
package cn.butool.threadlocal;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
/**
* 利用ThreadLocal给每个线程分配自己的DateFormat对象,
* 同时保证了线程安全。高效利用了内存
*/
public class ThreadDateFormatResult {
private static ExecutorService executorService = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
executorService.submit(() -> {
System.out.println(new ThreadDateFormatResult().dataToString(10+ finalI));
});
}
executorService.shutdown();
}
/**
* 将秒转换成日期字符串
*
* @param seconds
* @return
*/
public String dataToString(int seconds) {
//参数的单位是毫秒 是从1970.1.1 00:00:00 GMT计时
Date date = new Date(seconds * 1000);
SimpleDateFormat simpleDateFormat = ThreadSafeDateFormat.dateFormatThreadLocalLambda.get();
return simpleDateFormat.format(date);
}
static class ThreadSafeDateFormat{
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>(){
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
/**
* Lambda表达式写法和dateFormatThreadLocal效果相同
*/
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocalLambda = ThreadLocal.withInitial(()->new SimpleDateFormat("yyyy-MM-dd hh:mm:ss"));
}
}
1.6总结
- 两个线程分别用自己的SimpleDateFormat没有问题
- 延深1000个需要使用线程池,否则消耗内存过大
- 使用线程池后发现,最好使用同一个SimpleDateFormat对象,否则创建1000个任务也是创建1000对象
- 发现是线程不安全的,出现并发安全问题
- 选择加锁,枷锁结果正常,但是效率低
- 使用更好的解决方案ThreadLocal,线程安全的,每个线程内都有一个自己的独享的SimpleDateFormat对象
1.2场景2
每个线程内需要保存,全局变量(在拦截器中获取用户信息)可以让不同方法直接使用,避免参数传递的麻烦
service调用,每个都需要user对象,层层传递,代码冗余且不易维护。
每个线程保存全局变量,可以让不同的方法直接使用。避免参数传递的麻烦。
1.2.1方法
- 用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名,userId等)
- 这些信息在同一个线程内相同,但不同的线程使用的业务内容是不同的
- 使用ThreadLocal,可以在不影响性能的情况下,无需层层传递,就可以保存当前线程内的用户信息
- 强调是同一个请求内,同一个线程内,不同方法间的共享
- 不需要重写initialValue方法
package cn.butool.threadlocal;
/**
* 演示threadLocal避免传递参数的代码冗余
*/
public class UseThreadLocalSaveUser {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
// 读取到用户信息
public void process() {
User user = new User("busl");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
// 读取到用户信息
public void process() {
System.out.println("service2"+UserContextHolder.holder.get().getName());
new Service3().process();
}
}
class Service3 {
// 读取到用户信息
public void process() {
System.out.println("service3"+UserContextHolder.holder.get().getName());
}
}
/**
* 用户上下文持有者
*/
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
service2busl
service3busl
1.3总结
1.3.1两个作用
- 让某个需要用的对象在线程间隔离(每个线程都有自己独立的对象)
- 在任何方法中轻松的获取到对象
1.3.2根据共享对象的生成时机不同,选择initialValue或者set来保存对象
- 在ThreadLocal第一次get的时候把对象给初始化出来,对象的初始化时机可以由我们控制
- 如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set直接放到我们的ThreadLocal中去,以便后续使用。
1.3.3带来的好处
- 线程安全
- 不需要加锁,提高执行效率
- 更高效的利用内存、节省开销:想对于每个任务都新建一个SimpleDateFormat
- 免去传参的繁琐,使得代码更优雅
2.ThreadLocal原理
- Thread、ThreadLocal和ThreadLocalMap三者之间的关系
- 每个Thread对象中都持有一个ThreadLocalMap成员变量
- ThreadLocalMap可以存储多个ThreadLocal
2.1主要方法
1.T initialValue()
- 该方法会返回当前线程对应的初始值,这是一个延迟加载的方法,只有在调用get的时候,才会触发。
- 当线程第一次使用get方法访问变量时,将调用此方法,除非线程先前调用了set方法,在这种情况下,不会为线程调用本initialValue方法
- 通常,每个线程最多调用一次此方法,但如果已经调用了remove0后,再调用get0,则可以再次调用此方法
- 如果不重写本方法,这个方法会返回null。一般使用匿名内部类的方法来重写initialValue0方法,以便在后续使用中可以初始化副本对象。
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
// 1.使用前调用了set方法
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
// 2.调用setInitialValue方法
return setInitialValue();
}
// set()的变量,用于建立initialValue。在用户重写了set()方法的情况下使用,而不是set()
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;
}
2.set(T value) 为这个线程设一个新值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
3.get() 得到这个线程对应的value
get方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry方法,把本ThreadLocal的引用作为参数传入,取出map中属于本ThreadLocal的value
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
4.remove() 删除对应这个线程的值
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
演示:
package cn.butool.threadlocal;
/**
* 演示threadLocal避免传递参数的代码冗余
*/
public class UseThreadLocalSaveUser {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
// 读取到用户信息
public void process() {
User user = new User("busl");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
// 读取到用户信息
public void process() {
System.out.println("service2"+UserContextHolder.holder.get().getName());
UserContextHolder.holder.remove();
new Service3().process();
}
}
class Service3 {
// 读取到用户信息
public void process() {
User user = new User("busl2");
UserContextHolder.holder.set(user);
System.out.println("service3"+UserContextHolder.holder.get().getName());
}
}
/**
* 用户上下文持有者
*/
class UserContextHolder{
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
private String name;
public User(String name) {
this.name = name;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
打印:
service2busl
service3busl2
2.2ThreadLocalMap
- ThreadLocalMap类也就是ThreadLocal.threadLocals
ThreadLocal.ThreadLocalMap threadLocals = null;
- ThreadLocalMap是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认定为一个map键值对
键:这个ThreadLocal 值实际的成员变量,比如User或者simpleDateFormat对象 - setInitialValue和直接set最后都是调用map.set()方法来设置值,最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样
3.ThreadLocal使用注意点
3.1内存泄露
某个对象不在有用了,但是占用的内存却不能回收,会导致这一部分始终被占用,如果这种情况有很多。越来越多的情况会导致内存不够用了,超限制了。
3.2Key的泄露
ThreadLocalMap 中的内部类 Entry继承自WeakReference,是弱引用
弱引用的特点:如果这个对象只被弱引用关联,那么这个对象就可以被回收,弱引用不会阻止GC。
强引用:通常 一个对象等于什么,比如 下面的 value = v;
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
// ThreadLocal Object
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
public WeakReference(T referent) {
super(referent);
}
ThreadLocalMap的每个Entry都是一个对key的弱引用,和一个对value的value的强引用。
正常情况下,当线程终止,保存在Threadlocal里面的Value会被垃圾回收,因此没有任何强引用了。
但是,如果线程不终止,保持很久,那么key对应的value就不能被回收,因此 有以下调用链:
Thread -> ThreadLocalMap -> Entry(key为null) -> value
因为value 和Thread之间还存在强引用链路,所以导致value无法回收,就可能出现oom
JDK已经考虑到这个问题,所以set,remove,rehash方法中会扫描key为null的Entry,并且把对应的value设置为null,这样value就可以被对象回收,下面的考虑,把强引用链给断掉。
if (k == null) {
e.value = null; // Help the GC
}
private void resize() {
Entry[] oldTab = table;
int oldLen = oldTab.length;
int newLen = oldLen * 2;
Entry[] newTab = new Entry[newLen];
int count = 0;
for (int j = 0; j < oldLen; ++j) {
Entry e = oldTab[j];
if (e != null) {
ThreadLocal<?> k = e.get();
if (k == null) {
e.value = null; // Help the GC
} else {
int h = k.threadLocalHashCode & (newLen - 1);
while (newTab[h] != null)
h = nextIndex(h, newLen);
newTab[h] = e;
count++;
}
}
}
setThreshold(newLen);
size = count;
table = newTab;
}
但是如果一个ThreadLocal不被使用,那么实际上set, remove,rehash方法也不会被调用,如果同时线程又不停止,那么用链就一直存在,那么就导致了value的内存泄漏
3.3如何避免内存泄露
调用remove方法,就会删除对应的Entry对象,可以避免内存泄漏。
所以使用完ThreadLocal之后,应该调用remove方法。
在实际开发中,如果使用拦截器的方法拦截保存用户信息,应该在线程请求完成前拦住,调用remove方法。
class Service3 {
// 读取到用户信息
public void process() {
User user = new User("busl2");
UserContextHolder.holder.set(user);
System.out.println("service3"+UserContextHolder.holder.get().getName());
// 在最后的代码使用完去remove
UserContextHolder.holder.remove();
}
}
3.2ThreadLocal带来的空指针异常
package cn.butool.threadlocal;
/**
*
*/
public class ThreadLocalNPE {
ThreadLocal<Long> longThreadLocal = new ThreadLocal<Long>();
public void set() {
longThreadLocal.set(Thread.currentThread().getId());
}
public long get() {
return longThreadLocal.get();
}
public static void main(String[] args) {
ThreadLocalNPE threadLocalNPE = new ThreadLocalNPE();
System.out.println(threadLocalNPE.get());
Thread thread = new Thread(() -> {
threadLocalNPE.set();
System.out.println(threadLocalNPE.get());
});
thread.start();
}
}
//改为Long即可,编码问题
public Long get() {
return longThreadLocal.get();
}
3.3共享对象
如果在每个线程中ThreadLocal.set0进去的东西本来就是多线程共享的同一个对象,比如static对象,那么多个线程的ThreadLocal.get0取得的还是这个共享对象本身,还是有并发访问问题
3.4注意点
1.如果可以不适用ThreadLoca就解决问题,那么不要强行使用
例如任务很少的时候,在局部变量中可以新建对象就可以解决问题,那么就不需要使用到ThreadLoca
2.优先使用框架的支持,而不是自己的创造
例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove()方法等,造成内存泄漏
3.5实际应用场景
DateTimeContextHolder
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//
package org.springframework.format.datetime.standard;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
import org.springframework.core.NamedThreadLocal;
public final class DateTimeContextHolder {
private static final ThreadLocal<DateTimeContext> dateTimeContextHolder = new NamedThreadLocal("DateTime Context");
public DateTimeContextHolder() {
}
public static void resetDateTimeContext() {
dateTimeContextHolder.remove();
}
public static void setDateTimeContext(DateTimeContext dateTimeContext) {
if (dateTimeContext == null) {
resetDateTimeContext();
} else {
dateTimeContextHolder.set(dateTimeContext);
}
}
public static DateTimeContext getDateTimeContext() {
return (DateTimeContext)dateTimeContextHolder.get();
}
public static DateTimeFormatter getFormatter(DateTimeFormatter formatter, Locale locale) {
DateTimeFormatter formatterToUse = locale != null ? formatter.withLocale(locale) : formatter;
DateTimeContext context = getDateTimeContext();
return context != null ? context.getFormatter(formatterToUse) : formatterToUse;
}
}