专项攻克——ThreadLocal全解

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中填充的变量属于当前线程,该变量对其他线程是隔离的,是当前线程独有的变量。怎么做到隔离的呢?

  1. 每个Thread里都有一个变量ThreadLocalMap threadLocals,变量threadLocals能以(key=threadLocal,value=value)的形式存储threadLocal在当前线程内的数据。
  2. ThreadLocalMap threadLocals 在ThreadLocal对象的set方法去插入threadLocal对象和数据,也由ThreadLocal来维护。

其他:

  1. set赋值的时候首先会获取当前线程thread,并获取thread线程中的ThreadLocalMap属性。如果map属性不为空,则直接更新value值,如果map为空,则实例化threadLocalMap,并将value值初始化。
  2. 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内存泄露

补充知识:内存泄露、内存溢出

  1. 内存泄漏(memory leak)
    是指程序在申请内存后,无法释放已申请的内存空间,一次内存泄漏似乎不会有大的影响,但内存泄漏堆积后的后果就是内存溢出
  2. 内存溢出(out of memory)
    指程序申请内存时,没有足够的内存供申请者使用。比如,给了一块存储int类型数据的存储空间,却存储long类型的数据,结果就是内存不够用,会报错OOM,即所谓的内存溢出。

补充知识: java中的四种引用

  1. 强引用: 如果一个对象具有强引用,它就不会被垃圾回收器回收。即使当前内存空间不足,JVM也不会回收它,而是抛出 OutOfMemoryError
    错误,使程序异常终止。如果想中断强引用和某个对象之间的关联,可以显式地将引用赋值为null,这样一来的话,JVM在合适的时间就会回收该对象
  2. 弱引用: 具有弱引用的对象拥有的生命周期更短暂。因为当 JVM 进行垃圾回收,一旦发现弱引用对象,无论当前内存空间是否充足,都会将弱引用回收。不过由于垃圾回收器是一个优先级较低的线程,所以并不一定能迅速发现弱引用对象
  3. 软引用: 在使用软引用时,如果内存的空间足够,软引用就能继续被使用,而不会被垃圾回收器回收,只有在内存不足时,软引用才会被垃圾回收器回收。(软引用可用来实现内存敏感的高速缓存,比如网页缓存、图片缓存等。使用软引用能防止内存泄露,增强程序的健壮性)
  4. 虚引用: 虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用必须和引用队列(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使用强引用/弱引用的区别

  1. key 使用强引用:引用的ThreadLocal的对象被回收了,但是ThreadLocalMap还持有ThreadLocal的强引用,如果没有手动删除,ThreadLocal不会被回收,导致Entry内存泄漏。
  2. key 使用弱引用:引用的ThreadLocal的对象被回收了,由于ThreadLocalMap持有ThreadLocal的弱引用,即使没有手动删除,ThreadLocal也会被回收。value在下一次ThreadLocalMap调用set,get,remove的时候会被清除。

5.4 ThreadLocal如何避免内存泄露

ThreadLocal避免内存泄露的方法:

  1. 将ThreadLocal变量定义成private static的:这样就随时可以根据ThreadLocal访问到Entry的value值,然后remove() 防止内存泄露。

  2. 每次使用完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的改造思路:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

攻城有术

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值