ThreadLocal的用途
两大使用场景:
1.典型场景一:每个线程需要一个独享的对象(通常是工具类,典型需要使用的类有SimpleDateFormat和Random)
2.典型场景二:每个线程内需要保存全局变量(例如在拦截器中获取用户信息),可以让不同方法直接使用,避免参数传递的麻烦。
每个线程需要一个独享的对象
每个Thread内有自己的实例副本,不共享
比喻:一个班教材只有一本,一起做笔记有线程安全问题(并发读写),每个人复印一本后没问题。
1.SimpleDateFormat的进化之路
/**
* 2个线程打印日期
*/
public class ThreadLocalNormalUsage00 {
public static void main(String[] args) {
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(10);
System.out.println(date);
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage00().date(1007);
System.out.println(date);
}
}).start();
}
public String date(int seconds) {
//参数的单位是1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
30个线程打印时间
/**
* 、10个线程打印日期
*/
public class ThreadLocalNormalUsage01 {
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 30; i++) {
int finalI =i;
new Thread(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage01().date(finalI);
System.out.println(date);
}
}).start();
Thread.sleep(100);
}
}
public String date(int seconds) {
//参数的单位是1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
return dateFormat.format(date);
}
}
当需求变成了1000个,那么必然要用线程池,否则消耗内存太多

当所有线程都共同用一个SimpleDateFormat对象时发生线程安全问题

**
* 、1000个线程打印日期的任务,用线程池来执行
*/
public class ThreadLocalNormalUsage03 {
private static ExecutorService threadLocal = Executors.newFixedThreadPool(10);
static SimpleDateFormat dateFormat = 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;
threadLocal.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage03().date(finalI);
System.out.println(date);
}
});
}
threadLocal.shutdown();
}
public String date(int seconds) {
//参数的单位是1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
return dateFormat.format(date);
}
}
打印结果出现相同时间

使用加锁解决线程安全问题,直接给format方法加锁
public String date(int seconds) {
//参数的单位是1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
String s = null;
synchronized (ThreadLocalNormalUsage04.class) {
s = dateFormat.format(date);
}
return s;
}
虽然加锁能够解决问题,但在高并发情况下效率低下,这时用ThreadLocal可以解决性能问题
/**
* 描述:利用ThreadLocal,给每个线程分配自己的dateFormat对象,保证了线程安全,高效利用内存
*/
public class ThreadLocalNormalUsage05 {
private static ExecutorService threadLocal = Executors.newFixedThreadPool(10);
public static void main(String[] args) throws InterruptedException {
for (int i = 0; i < 1000; i++) {
int finalI = i;
threadLocal.submit(new Runnable() {
@Override
public void run() {
String date = new ThreadLocalNormalUsage05().date(finalI);
System.out.println(date);
}
});
}
threadLocal.shutdown();
}
public String date(int seconds) {
//参数的单位是1970.1.1 00:00:00 GMT计时
Date date = new Date(1000 * seconds);
// SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
SimpleDateFormat dateFormat = ThreadSafeFormatter.dateFormatThreadLocal.get();
return dateFormat.format(date);
}
}
class ThreadSafeFormatter {
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal = new ThreadLocal<SimpleDateFormat>() {
@Override
protected SimpleDateFormat initialValue() {
return new SimpleDateFormat("yyyy-MM-dd hh:mm:ss");
}
};
//lambda表达式写法
public static ThreadLocal<SimpleDateFormat> dateFormatThreadLocal2 = ThreadLocal.withInitial(
() -> new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
);
}
每个线程内需要保存全局变量
一个比较繁琐的2解决方案是把user作为参数层层传递,从service-1(),传到service-2(),再从service-2()传到service-3(),一次类推,但是这样做会导致代码冗余且不易维护

1.用ThreadLocal保存一些业务内容(用户权限信息、从用户系统获取到的用户名、userID等)
2.这些信息在同一个线程内相同,但是不同的线程使用的业务内容不相同的
3.在线程生命周期内,都通过这个静态ThreadLocal实例的get()方法取得自己set过的哪个对象,避免了将这个对象(例如user对象)作为参数传递的麻烦
4.强调的是同一个请求内(同一个线程内)不同方法间的共享
5.不需要重写initialValue()方法,但是必须手动调用set()方法
实例:当前信息需要被线程内所有方法共享 在之前基础上演进,使用UserMap

当多线程同时工作时,我们需要保证线程安全,可以用synchronized,也可以用ConcurrentHashMap,但无论用什么,都会对性能有所影响

更好的办法是使用ThreadLocal,这样无需synchronized,可以在不影响性能的情况下,也无需层层传递参数,就可达到保存当前线程对应的用户信息目的。

/**
* 描述: 演示ThreadLocal用法2:避免传递参数的麻烦
*/
public class ThreadLocalNormalUsage06 {
public static void main(String[] args) {
new Service1().process();
}
}
class Service1 {
public void process() {
User user = new User("超哥");
UserContextHolder.holder.set(user);
new Service2().process();
}
}
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service2拿到用户名: "+user.name);
new Service3().process();
}
}
class Service3 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service3拿到用户名 "+user.name);
}
}
class UserContextHolder {
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name) {
this.name = name;
}
}
ThreadLocal两个作用
1.让某个需要用到的对象在线程间隔离(每个线程都有自己独立的对象)
2,在任何方法中都可以轻松获取到该对象
两种不同的实现:
根据共享对象的生成时机不同,选择initialValue或set来保存对象
1.场景一(initialValue):在ThreadLocal第一次get的时候把对象初始化出来,对象的初始化时机可以由我们来控制
2场景二(set):如果需要保存到ThreadLocal里的对象的生成时机不由我们随意控制,例如拦截器生成的用户信息,用ThreadLocal.set()直接放到我们的ThreadLocal中去,以便后续使用。
使用ThreadLocal带来的好处
1.达到线程安全
2.不需要加锁,提高执行效率
3.更高效地利用内存、节省开销:相比于每个任务都新建立一个SimpleDateFormat,显然用ThreadLocal可以节省内存和开销
4.免去传参的繁琐:无论是场景一的工具类还是场景二的用户名,都可以在任何地方直接通过ThreadLocal拿到,再也不需要每次都传递同样的参数。ThreadLocal使得代码耦合度更低,更优雅。
ThreadLocal原理、源码分析
搞清楚Thread、ThreadLocal以及ThreadLocalMap三者之间的关系
1.每个Thread对象中都持有一个ThreadLocalMap成员变量

Thread中成员变量ThreadLocalMap
ThreadLocal.ThreadLocalMap threadLocals = null;
2.主要方法介绍

initialValue():
① 该方法会返回当前线程对应的”初始值”,这是一个延迟加载的方法,只有在调用get()的时候,才会触发。
② 当线程第一次使用get()方法变量时,将调用initialValue()方法,除非线程先前调用了set()方法,在这种情况下,不会为线程调用initialValue()方法
③ 通常,每个线程最多调用一次initialValue()方法,但如果已经调用remove后,再调用get(),则可以再次调用此方法
④ 如果不重写initialValue()方法,这个方法会返回为null,一般使用匿名内部类的方法来重写initialValue()方法,以便在后续使用中可以初始化副本对象
initialValue()方法默认返回null,所以我们重写initialValue()方法
protected T initialValue() {
return null;
}
void set(T t):为这个线程设置一个新值
public void set(T value) {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);
if (map != null)
map.set(this, value);
else
createMap(t, value);
}
T get():得到这个线程对应的value,如果是首次调用get(),则会调用initialize来得到这个值,get()方法是先取出当前线程的ThreadLocalMap,然后调用map.getEntry()方法,把ThreadLocal的引用作为参数传入,取出Map中属于本ThreadLocal的Value
注意:这个map以及map中key和value中都是保存在线程中的,而不是保存在ThreadLocal中的
public T get() {
Thread t = Thread.currentThread();
ThreadLocalMap map = getMap(t);//获取Thread中成员变量ThreadLocalMap
if (map != null) {
ThreadLocalMap.Entry e = map.getEntry(this);
if (e != null) {
@SuppressWarnings("unchecked")
T result = (T)e.value;
return result;
}
}
return setInitialValue();
}
如果map等于null,调用setInitialValue(),setInitialValue()调用initialValue()
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;
}
void remove():删除对应这个线程的值
class Service2 {
public void process() {
User user = UserContextHolder.holder.get();
System.out.println("service2拿到用户名: " + user.name);
UserContextHolder.holder.remove();
user = new User("张三");
UserContextHolder.holder.set(user);
new Service3().process();
}
remove()源码
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap类
1.ThreadLocalMap类,也就是Thread.ThreadLocals
2.ThreadLocalMap类是每个线程Thread类里面的变量,里面最重要的是一个键值对数组Entry[] table,可以认为是一个map, 键值对:
键:这个ThreadLocal
值:实际需要的成员变量,比如user或者simpleDateFormat对象
类似HashMap,但也有不同之处,解决冲突方式:
HashMap拉链法:

ThreadLocalMap采用的是线性探测法,也就是如果发生冲突,就继续找下一个空位置,而不是用链表拉链
两种使用场景的相同之处
1.通过源码分析可看出,setInitialValue和直接set最后都是利用map.set()方法来设置值
2.也就是说,最后都会对应到ThreadLocalMap的一个Entry,只不过起点和入口不一样
ThreadLocal导致内存泄漏
内存泄漏:某个对象不再有用,但是占用的内存却不能被回收
1.Key的泄漏:ThreadLocalMap中的Entry继承自WeakReference,是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2.弱引用的特点:如果这个对象只被弱引用关联(没有任何强引用关联),那么这个对象就可以被回收,弱引用不会阻止GC
Value的泄漏:ThreadLocalMap的每个Entry都是对key的弱引用,同时,每个value都包含了一个对value的强引用

正常情况下,当线程终止,保存在ThreadLocal里的value会被垃圾回收,因为没有任何强引用了
但是,如果线程不终止(比如线程需要保持很久),那么key对应的value就不能被回收,例如用线程池时同一个线程反复使用,因为有以下调用链:

因为value和Thread之间还存在这个强引用链路,所以导致vaLue无法回收,就可能出现OOM
JDK已经考虑到了这个问题,所以在set,remove,rehash方法中会扫描key为null的Entry, 并把对应的value设置为null,这样value对象就可以被回收
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;
}
但是如果一个ThreadLoca不被使用,那么实际上set,remove,rehash方法也不会被调用,如果同时线程又不停止,那么调用链就一直存在,那么就导致了value的内存泄漏
如何避免内存泄漏
调用remove()方法,就会删除对应的Entry对象,可以避免内存泄漏,所以使用完ThreadLocal后,应该调用remove方法
ThreadLocal的空指针问题
设置好的包装类的对象
ThreadLocal<Long> longThreadLocal = new ThreadLocal<>();
如果get返回为基本类型则报错空指针异常

本来get没有赋值前为null,当把对象类型转为基本类型转换不到,导致空指针异常

ThreadLocal注意点
1.在进行get之前,必须先set,否则会报空指针异常?所以并不是,只是装箱、拆箱导致的而不是ThreadLocal的问题
2.共享对象:如果在每个线程中ThreadLocal.set()进去的东西本身就是多线程共享的同一个对象,比如sttic对象,那么多个线程的ThreadLocal.get()取得的还是这个共享对象本身,还是有并发访问问题,所以不应该在ThreadLocal中放置静态的对象
3.如果可以不使用ThreadLocal就解决问题,那么不要强行使用,例如在任务数很少的时候,在局部变量中就可以新建对象就可以解决问题,那么就不需要使用到ThreadLocal
4.优先使用框架的支持,而不是自己创造,例如在Spring中,如果可以使用RequestContextHolder,那么就不需要自己维护ThreadLocal,因为自己可能会忘记调用remove方法等,造成内存泄漏
ThreadLocal实际应用场景
在Spring中的实例分析
1.DateTimeContextHolder类,里面用了ThreadLocal存储时间的上下文

原文始发于微信公众号(itmkyuan):ThreadLocal原理、作用、内存泄漏及使用场景
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/39197.html