ThreadLocal简解

本文详细介绍了ThreadLocal的特性和使用方式,包括线程局部变量的原理、如何避免数据错乱和内存泄漏,以及在复杂业务处理和高并发场景下的应用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

ThreadLocal特点

ThreadLocal实现了线程间数据隔离,ThreadLocal的实例代表了一个线程局部的变量,每条线程都只能看到自己的值,并不会意识到其它的线程中也存在该变量。简单来说就是一个公共的Map,map的key是Thread本身,value是线程携带的数据。

ThreadLocal的简单使用

使用方式一

开启三个新的线程,每个线程对数据进行累加。

public class TestThreadLocal {

    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        for (int i = 0; i < 3; i++) {//启动三个线程
            Thread t = new Thread() {
                @Override
                public void run() {
                    add10ByThreadLocal();
                }
            };
            t.start();
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }

}

执行结果

Thread-1 : ThreadLocal num=1
Thread-1 : ThreadLocal num=2
Thread-2 : ThreadLocal num=1
Thread-2 : ThreadLocal num=2
Thread-2 : ThreadLocal num=3
Thread-0 : ThreadLocal num=1
Thread-2 : ThreadLocal num=4
Thread-1 : ThreadLocal num=3
Thread-2 : ThreadLocal num=5
Thread-0 : ThreadLocal num=2
Thread-1 : ThreadLocal num=4
Thread-0 : ThreadLocal num=3
Thread-1 : ThreadLocal num=5
Thread-0 : ThreadLocal num=4
Thread-0 : ThreadLocal num=5

每个线程的值都是1~5,没有出现混加。这就实现了每个线程之间的数据的隔离。

使用方式二

开启一个定长为3的线程池,每个线程对数据进行累加。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class TestThreadLocal2 {

    //线程本地存储变量
    private static final ThreadLocal<Integer> THREAD_LOCAL_NUM = new ThreadLocal<Integer>() {
        @Override
        protected Integer initialValue() {
            return 0;
        }
    };

    public static void main(String[] args) {
        ExecutorService cachedThreadPool = Executors.newFixedThreadPool(3);
        for (int i = 0; i < 10; i++) {
            cachedThreadPool.execute(() -> add10ByThreadLocal());
        }
    }

    /**
     * 线程本地存储变量加 5
     */
    private static void add10ByThreadLocal() {
        for (int i = 0; i < 5; i++) {
            Integer n = THREAD_LOCAL_NUM.get();
            n += 1;
            THREAD_LOCAL_NUM.set(n);
            System.out.println(Thread.currentThread().getName() + " : ThreadLocal num=" + n);
        }
    }

}

执行结果

pool-1-thread-1 : ThreadLocal num=1
pool-1-thread-3 : ThreadLocal num=1
pool-1-thread-1 : ThreadLocal num=2
pool-1-thread-2 : ThreadLocal num=1
pool-1-thread-1 : ThreadLocal num=3
pool-1-thread-3 : ThreadLocal num=2
pool-1-thread-1 : ThreadLocal num=4
pool-1-thread-2 : ThreadLocal num=2
pool-1-thread-1 : ThreadLocal num=5
pool-1-thread-3 : ThreadLocal num=3
pool-1-thread-2 : ThreadLocal num=3
pool-1-thread-1 : ThreadLocal num=6
pool-1-thread-3 : ThreadLocal num=4
pool-1-thread-1 : ThreadLocal num=7
pool-1-thread-2 : ThreadLocal num=4
pool-1-thread-1 : ThreadLocal num=8
pool-1-thread-3 : ThreadLocal num=5
pool-1-thread-1 : ThreadLocal num=9
pool-1-thread-2 : ThreadLocal num=5
pool-1-thread-1 : ThreadLocal num=10
pool-1-thread-3 : ThreadLocal num=6
pool-1-thread-1 : ThreadLocal num=11
pool-1-thread-2 : ThreadLocal num=6
pool-1-thread-2 : ThreadLocal num=7
pool-1-thread-1 : ThreadLocal num=12
pool-1-thread-3 : ThreadLocal num=7
pool-1-thread-3 : ThreadLocal num=8
pool-1-thread-3 : ThreadLocal num=9
pool-1-thread-3 : ThreadLocal num=10
pool-1-thread-1 : ThreadLocal num=13
pool-1-thread-2 : ThreadLocal num=8
pool-1-thread-1 : ThreadLocal num=14
pool-1-thread-3 : ThreadLocal num=11
pool-1-thread-1 : ThreadLocal num=15
pool-1-thread-2 : ThreadLocal num=9
pool-1-thread-1 : ThreadLocal num=16
pool-1-thread-3 : ThreadLocal num=12
pool-1-thread-1 : ThreadLocal num=17
pool-1-thread-2 : ThreadLocal num=10
pool-1-thread-1 : ThreadLocal num=18
pool-1-thread-3 : ThreadLocal num=13
pool-1-thread-1 : ThreadLocal num=19
pool-1-thread-2 : ThreadLocal num=11
pool-1-thread-1 : ThreadLocal num=20
pool-1-thread-3 : ThreadLocal num=14
pool-1-thread-3 : ThreadLocal num=15
pool-1-thread-2 : ThreadLocal num=12
pool-1-thread-2 : ThreadLocal num=13
pool-1-thread-2 : ThreadLocal num=14
pool-1-thread-2 : ThreadLocal num=15

问题就出现了,由于线程池的线程是可以重复使用的,所以就出现了数据错乱的现象。所以在合线程池结合使用时,需要注意及时清理线程的数据。

ThreadLocal方法简介

主要方法如下

public T get() { }
public void set(T value) { }
public void remove() { }
protected T initialValue() { }
  • get()方法是用来获取ThreadLocal在当前线程中保存的变量副本
  • set()用来设置当前线程中变量的副本
  • remove()用来移除当前线程中变量的副本
  • initialValue()是一个protected方法,一般是用来在使用时进行重写的,如果在没有set的时候就调用get,会调用initialValue方法初始化内容。

源码分析

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);
    }

set方法会获取当前的线程,通过当前线程获取ThreadLocalMap对象。然后把需要存储的值放到这个map里面。如果没有就调用createMap创建对象。

getMap
    ThreadLocalMap getMap(Thread t) {
        return t.threadLocals;
    }

getMap方法直接返回当前Thread的threadLocals变量,这样说明了之所以说ThreadLocal是线程局部变量就是因为它只是通过ThreadLocal把变量存在了Thread本身而已。

createMap
void createMap(Thread t, T firstValue) {
   t.threadLocals = new ThreadLocalMap(this, firstValue);
}

在set的时候如果不存在threadLocals,直接创建对象。由上看出,放入map的key是当前的ThreadLocal,value是需要存放的内容,所以我们设置属性的时候需要注意存放和获取的是一个ThreadLocal。

get
    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();
    }

get方法就比较简单,获取当前线程,尝试获取当前线程里面的threadLocals,如果没有获取到就调用setInitialValue方法,setInitialValue基本和set是一样的,就不累累述了。

remove
    public void remove() {
         ThreadLocalMap m = getMap(Thread.currentThread());
         if (m != null)
             m.remove(this);
     }

使用场景

  1. 比如线程中处理一个非常复杂的业务,可能方法有很多,那么,使用 ThreadLocal 可以代替一些参数的显式传递;
  2. 比如用来存储用户 Session。Session 的特性很适合 ThreadLocal ,因为 Session 之前当前会话周期内有效,会话结束便销毁。
  3. 在一些多线程的情况下,如果用线程同步的方式,当并发比较高的时候会影响性能,可以改为 ThreadLocal 的方式,例如高性能序列化框架 Kyro 就要用 ThreadLocal 来保证高性能和线程安全;
  4. 还有像线程内上线文管理器、数据库连接等可以用到 ThreadLocal;

附加

ThreadLocalMap中使用key为ThreadLocal的弱引用,弱引用的特点是,如果这个对象只存在弱引用,那么在下一次垃圾回收的时候必然会被清理掉。

所以如果 ThreadLocal没有被外部强引用的情况下,在垃圾回收的时候会被清理掉的,这样一来 ThreadLocalMap 中使用这个ThreadLocal的key也会被清理掉。但是,value 是强引用,不会被清理,这样一来就会出现 key 为 null 的 value。

ThreadLocalMap 实现中已经考虑了这种情况,在调用 set()、get()、remove() 方法的时候,会清理掉 key 为 null 的记录。如果说会出现内存泄漏,那只有在出现了 key 为 null 的记录后,没有手动调用 remove() 方法,并且之后也不再调用 get()、set()、remove() 方法的情况下。

以下是我和面试官的面试过程,请你指出不正确的地方:ThreadLocal使用场景 如果我们需要让变量线程间是隔离的,在方法之间和模块之间是共享的,那就很适合使用ThreadLocal,它可以实现多个模块之间的解耦合,避免参数的直接传递。比如登录校验的时候,在拦截器中利用ThreadLocal保存用户id,在其它模块就可以取出来使用。 ThreadLocal原理? 首先ThreadLocal变量为每一个线程都创建了一个变量的副本,每个线程都会访问自己的这一份副本。ThreadLocal对象本身是不保存数据的,数据都是保存在线程对象的ThreadLocalMap中,ThreadLocalMap就是一个Hash表,Key就是ThreadLocal对象,只不过它是通过弱引用包装的,value就是我们设置的值。当我们往ThreadLocal对象中存放元素的时候,它会首先拿到当前对象,然后从中取出对象的ThreadLocalMap,然后里面存放键值对。 为什么要设置成弱引用? 设置为弱引用主要是为了保证ThreadLocal对象本身能够被正常回收。key 如果是强引用那么线程对象中的threadLocalMap对ThreadLocal的强引用会一直存在,这就会导致ThreadLocal对象无法被回收,除非我们手动释放。 ThreadLocal存在内存泄露问题吗? 存在,如果我们使用完ThreadLocal之后没有手动释放,那么虽然ThreadLocal对象只存在弱引用的情况下能够被回收,但是对应的Value无法被回收,这就导致在ThreadLocalMap中会出现Key为null但是Value不为null的Entry。如果我们的线程一直被复用那么这种脏Entry就会一直存在。但是ThreadLocalMap采用了线性探测法解决哈希冲突的,所以在进行get/set的时候如果出现哈希冲突那么它会扫描大量元素,如果扫到了脏Entry,它就会清除掉。 为什么ThreadLocalMap要使用线性探测法解决哈希冲突? 首先,线性探测法有一个好处就是cpu缓存命中率会高一些,因为哈希表就是一个数组,它是连续的内存空间,对cpu缓存更加友好。 然后线性探测法能够让ThreadLocalMap更好的清除key为null的脏Entry,避免内存泄露问题。因为他在get/set/remove的时候,如果出现了哈希冲突,那么它会继续往下扫描,如果这个过程中遇到了脏Entry,那么他就会清除掉脏Entry。
最新发布
07-09
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值