文章目录
1. Threadlocal作用
简单的说,一个ThreadLocal在一个线程中是共享的,在不同线程之间又是隔离的(每个线程都只能看到自己线程的值)。如下图:
2. API
ThreadLocal类的API非常的简单,在这里比较重要的就是get()、set()、remove()、initiaValue()。
其中,initialValue方法会在第一次调用时被触发,用于初始化当前变量值,默认返回null。
3. 使用案例
ThreadLocal在一个线程中是共享的,在不同线程之间是隔离的(每个线程都只能看到自己线程的值)。隔离性案例如下,在类中创建了一个静态的 “ThreadLocal变量”,在主线程中创建两个线程,在这两个线程中分别设置ThreadLocal变量为2和3。然后等待一号和二号线程执行完毕后,在主线程中查看ThreadLocal变量的值。
public class ThreadLocalTest_1 {
public static ThreadLocal<Integer> threadLocal_A = new ThreadLocal<Integer>();
public static ThreadLocal<Integer> threadLocal_B = new ThreadLocal<Integer>();
static {
threadLocal_A.set(1);
threadLocal_B.set(11);
}
public static void main(String[] args) {
System.out.println("主线程-A:" + threadLocal_A.get());
System.out.println("主线程-B:" + threadLocal_B.get());
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程2-A前:" + threadLocal_A.get());
System.out.println("线程2-B前:" + threadLocal_B.get());
threadLocal_A.set(2);
threadLocal_B.set(22);
System.out.println("线程2-A后:" + threadLocal_A.get());
System.out.println("线程2-B后:" + threadLocal_B.get());
}
}).start();
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("线程3-A前:" + threadLocal_A.get());
System.out.println("线程3-B前:" + threadLocal_B.get());
threadLocal_A.set(3);
threadLocal_B.set(33);
System.out.println("线程3-A后:" + threadLocal_A.get());
System.out.println("线程3-B后:" + threadLocal_B.get());
}
}).start();
System.out.println("主线程-A结束:" + threadLocal_A.get());
System.out.println("主线程-B结束:" + threadLocal_B.get());
}
}
结果:
4. 源码分析
ThreadLocal 也叫线程变量,意思是ThreadLocal中填充的变量属于当前线程,该变量对其他线程是隔离的,是当前线程独有的变量。怎么做到隔离的呢?
- 每个Thread里都有一个变量ThreadLocalMap threadLocals,变量threadLocals能以(key=threadLocal,value=value)的形式存储threadLocal在当前线程内的数据。
- ThreadLocalMap threadLocals 在ThreadLocal对象的set方法去插入threadLocal对象和数据,也由ThreadLocal来维护。
其他:
- set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
- remove方法,直接将ThrealLocal 对应的值从当前相差Thread中的ThreadLocalMap中删除。为什么要删除,这涉及到内存泄露的问题。
public class ThreadLocal<T> {
private final int threadLocalHashCode = nextHashCode();
private static AtomicInteger nextHashCode = new AtomicInteger();
private static final int HASH_INCREMENT = 0x61c88647;
...
public ThreadLocal() {
}
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();
}
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,设置key为当前threadLocal,值为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);
}
public void remove() {
ThreadLocalMap m = getMap(Thread.currentThread());
if (m != null)
m.remove(this);
}
ThreadLocalMap getMap(Thread t) {
return t.threadLocals;
}
/** 内部类 */
static class ThreadLocalMap {
...
}
}
5. ThreadLocal内存泄露
补充知识:内存泄露、内存溢出
- 内存泄漏(memory leak)
是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出- 内存溢出(out of memory)
指程序申请内存时,没有足够的内存供申请者使用。比如,给了一块存储int类型数据的存储空间,却存储long类型的数据,结果就是内存不够用,会报错OOM,即所谓的内存溢出。
补充知识: java中的四种引用
- 强引用: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError
错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象- 弱引用: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
- 软引用: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
- 虚引用: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(ReferenceQueue)联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之关联的引用队列中。(注意哦,其它引用是被JVM回收后才被传入ReferenceQueue中的。由于这个机制,所以虚引用大多被用于引用销毁前的处理工作。可以使用在对象销毁前的一些操作,比如说资源释放等。)
5.1 ThreadLocal(key)是弱引用
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
-
Entry将ThreadLocal作为Key,值作为value保存,它继承自WeakReference,注意构造函数里的第一行代码super(k),这意味着ThreadLocal对象是一个「弱引用」。
-
ThreadLocalMap的生命周期跟Thread(注意线程池中的Thread)一样长。如果没有手动remove 对应key,value是不会被回收的,一定会导致内存泄漏(解释:线程使用结束归还给线程池了,其中的KV不再被使用但又不会GC回收,可认为是内存泄漏)。
-
弱引用回收,value内存泄露:当弱引用ThreadLocal等于null时,ThreadLocal 会被GC回收,而对应的value在下一次ThreadLocalMap调用set,get,remove方法时才被清除。但是,如果线程是线程池里的核心线程,由于线程的周期特别长,线程一直被重复利用,entry(null,value)的对象越来越多,线程中Entry对象中的value就可能一直得不到回收,发生内存泄露。
原因补充:Java8优化
在ThreadLocal的get()、set()、remove()方法调用时,会清除线程ThreadLocalMap中所有Entry中Key为null的Value,并将整个Entry设置为null,利于下次内存回收。
5.2 为什么不将key设计为强引用
5.2.1 假如key 设计成强引用
为什么ThreadLocalMap的key要设计成弱引用呢?其实很简单,如果key设计成强引用且没有手动remove(),那么key会和value一样伴随线程的整个生命周期。
解释:
假设在业务代码中使用完ThreadLocal, ThreadLocal本该被回收了,但是threadLocalMap的Entry强引用了threadLocal,造成ThreadLocal无法被回收。在没有手动删除Entry以及CurrentThread(当前线程)依然运行的前提下,始终有强引用链CurrentThread Ref → CurrentThread →Map(ThreadLocalMap)-> entry,Entry就不会被回收( Entry中包括了ThreadLocal实例和value),导致Entry内存泄漏。
5.3 为什么 key 要设计成弱引用
事实上,在 ThreadLocalMap 中的set/getEntry 方法中,会对 key 为 null(也即是 ThreadLocal 为 null )进行判断,如果为 null 的话,那么会把 value 置为 null 的.这就意味着使用threadLocal , CurrentThread 依然运行的前提下.就算忘记调用 remove 方法,弱引用比强引用可以多一层保障:弱引用的 ThreadLocal 会被回收.对应value在下一次 ThreadLocaI 调用 get()/set()/remove() 中的任一方法的时候会被清除,从而避免内存泄漏.
key使用强引用/弱引用的区别
- key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
- key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。
5.4 ThreadLocal如何避免内存泄露
ThreadLocal避免内存泄露的方法:
-
将ThreadLocal变量定义成private static的:这样就随时可以根据ThreadLocal访问到Entry的value值,然后remove() 防止内存泄露。
-
每次使用完ThreadLocal,都调用它的remove()方法,清除数据。
6、应用场景
ThreadLocal 适用于如下两种场景:
- 每个线程需要有自己单独的实例
- 实例需要在多个方法中共享,但不希望被多线程共享
应用场景举例:
6.1 存储用户登录session
private static final ThreadLocal threadSession = new ThreadLocal();
public static Session getSession() throws InfrastructureException {
Session s = (Session) threadSession.get();
try {
if (s == null) {
s = getSessionFactory().openSession();
threadSession.set(s);
}
} catch (HibernateException ex) {
throw new InfrastructureException(ex);
}
return s;
}
6.2 数据库连接,处理数据库事务
6.3 数据跨层传递(controller,service, dao)
每个线程内需要保存类似于全局变量的信息(例如在拦截器中获取的用户信息),可以让不同方法直接使用,避免参数传递的麻烦却不想被多线程共享(因为不同线程获取到的用户信息不一样)。
例如,用 ThreadLocal 保存一些业务内容(用户权限信息、从用户系统获取到的用户名、用户ID 等),这些信息在同一个线程内相同,但是不同的线程使用的业务内容是不相同的。
在线程生命周期内,都通过这个静态 ThreadLocal 实例的 get() 方法取得自己 set 过的那个对象,避免了将这个对象(如 user 对象)作为参数传递的麻烦。
比如说我们是一个用户系统,那么当一个请求进来的时候,一个线程会负责执行这个请求,然后这个请求就会依次调用service-1()、service-2()、service-3()、service-4(),这4个方法可能是分布在不同的类中的。这个例子和存储session有些像。
package com.kong.threadlocal;
public class ThreadLocalDemo05 {
public static void main(String[] args) {
User user = new User("jack");
new Service1().service1(user);
}
}
class Service1 {
public void service1(User user){
//给ThreadLocal赋值,后续的服务直接通过ThreadLocal获取就行了。
UserContextHolder.holder.set(user);
new Service2().service2();
}
}
class Service2 {
public void service2(){
User user = UserContextHolder.holder.get();
System.out.println("service2拿到的用户:"+user.name);
new Service3().service3();
}
}
class Service3 {
public void service3(){
User user = UserContextHolder.holder.get();
System.out.println("service3拿到的用户:"+user.name);
//在整个流程执行完毕后,一定要执行remove
UserContextHolder.holder.remove();
}
}
class UserContextHolder {
//创建ThreadLocal保存User对象
public static ThreadLocal<User> holder = new ThreadLocal<>();
}
class User {
String name;
public User(String name){
this.name = name;
}
}
执行的结果:
service2拿到的用户:jack
service3拿到的用户:jack
6.4 Spring使用ThreadLocal解决线程安全问题
我们知道在一般情况下,只有无状态的Bean才可以在多线程环境下共享,在Spring中,绝大部分Bean都可以声明为singleton作用域。就是因为Spring对一些Bean(如RequestContextHolder、TransactionSynchronizationManager、LocaleContextHolder等)中非线程安全的“状态性对象”采用ThreadLocal进行封装,让它们也成为线程安全的“状态性对象”,因此有状态的Bean就能够以singleton的方式在多线程中正常工作了。
一般的Web应用划分为展现层、服务层和持久层三个层次,在不同的层中编写对应的逻辑,下层通过接口向上层开放功能调用。在一般情况下,从接收请求到返回响应所经过的所有程序调用都同属于一个线程。
这样用户就可以根据需要,将一些非线程安全的变量以ThreadLocal存放,在同一次请求响应的调用线程中,所有对象所访问的同一ThreadLocal变量都是当前线程所绑定的。
下面的实例能够体现Spring对有状态Bean的改造思路: