15-ThreadLocal

本文深入探讨了ThreadLocal的工作原理及其在多线程环境中的作用,例如防止并发问题,如线程安全的SimpleDateFormat。此外,还展示了ThreadLocal在Spring事务管理中的应用,并解释了其内存管理和解决哈希冲突的机制。了解ThreadLocal的使用能帮助开发者更好地处理线程间的变量隔离和数据安全。
摘要由CSDN通过智能技术生成

线程本地变量——ThreadLocal

ThreadLocal,即线程变量,是一个以ThreadLocal对象为键,任意对象为值的存储结构。这个结构被附带在线程上,也就是说一个线程可以根据一个ThreadLocal对象查询到绑定在这个线程上的一个值。

可以通过set(T)方法来设置一个值,在当前线程下再通过get()方法获取到原先设置的值。

ThreadLocal的作用主要是做数据隔离,填充的数据只属于当前线程,变量的数据对别的线程而言是相对隔离的,多线程环境下可以有效防止自己的变量被其他线程篡改。

ThreadLocal的使用场景

SimpleDateFormat线程不安全的场景

我们都知道SimpleDateFormat在多线程场景下是线程不安全的,因为我们在将SimpleDataFormat设置为静态变量时,在多线程场景下,会共用SimpleDateFormat中的Calendar对象:

public abstract class DateFormat extends Format {
    // DateFormat中的Calendar对象
    protected Calendar calendar;

而SimpleDateFormat对日期的转换就是通过这个Calendar对象来操作的,而且这个calendar这个成员变量即被用于format方法也被用于parse方法,下面是parse中的方法片段:

// ...
try {
    parsedDate = calb.establish(calendar).getTime();
    // ...
}
// CalendarBuilder中的establish方法
Calendar establish(Calendar cal) {
	// ...
    cal.clear();
	// ...
}

这里我们可以看到parse()方法中调用了CalendarBuilder中的establish()方法,而该方法中有一步针对calendar的清除操作——calendar.clear()。如果这时有另一个线程进入到parse方法用到了SimpleDateFormat对象中的calendar,就会产生线程安全问题。

同样的,SimpleDateFormat中的format()方法调用了Calendar的setTime()方法,同样可能会在多线程场景下出现问题。

解决方案

这里使用ThreadLocal创建线程独有的SimpleDateFormat,来避免日期工具在多线程环境下的不安全问题:

public class DateUtilSafe {
    
    private static final ThreadLocal<SimpleDateFormat> THREAD_LOCAL = ThreadLocal.withInitial(() ->
            new SimpleDateFormat("yyyy-MM-dd hh:mm:ss")
    );

    private String parse(Date date) {
        return THREAD_LOCAL.get().format(date);
    }
}

Spring中ThreadLocal实现事务隔离

Spring采用ThreadLocal的方式,来保证单个线程中的数据库操作使用的是同一个数据库连接,同时,采用这种方式可以使业务层使用事务时不需要感知并管理connection对象,通过传播级别,巧妙地管理多个事务配置之间的切换,挂起和恢复。代码如下:

public abstract class TransactionSynchronizationManager {

   private static final Log logger = LogFactory.getLog(TransactionSynchronizationManager.class);

   private static final ThreadLocal<Map<Object, Object>> resources =
         new NamedThreadLocal<>("Transactional resources");

   private static final ThreadLocal<Set<TransactionSynchronization>> synchronizations =
         new NamedThreadLocal<>("Transaction synchronizations");

   private static final ThreadLocal<String> currentTransactionName =
         new NamedThreadLocal<>("Current transaction name");

   private static final ThreadLocal<Boolean> currentTransactionReadOnly =
         new NamedThreadLocal<>("Current transaction read-only status");

   private static final ThreadLocal<Integer> currentTransactionIsolationLevel =
         new NamedThreadLocal<>("Current transaction isolation level");

   private static final ThreadLocal<Boolean> actualTransactionActive =
         new NamedThreadLocal<>("Actual transaction active");
    
    //...
    
}

其他场景

比如项目中存在一个线程横跨多个方法调用,时常需要传递一些对象,如用户身份信息等,就会存在过渡传参的问题。这时,就可以使用ThreadLocal去改造,即调用前ThreadLocal设置参数,其他地方使用get()方法获取。

ThreadLocal实现原理

ThreadLocal主要使用set(T value)方法设置线程本地变量,然后通过get()方法获取,最后通过remove()方法清除,接下来我们来一起看一下这几个方法:

设置变量——set(T value)

我们先来看一下ThreadLocal的set(T value)方法:

public void set(T value) {
    Thread t = Thread.currentThread();// 当前线程
    ThreadLocalMap map = getMap(t);// 获取ThreadLocalMap对象
    if (map != null)// 校验对象是否为空
        map.set(this, value); // 不为空设置值
    else
        createMap(t, value);// 为空创建一个map
}
// 返回线程独有的ThreadLocalMap
ThreadLocalMap getMap(Thread t) {
    return t.threadLocals;
}

public class Thread implements Runnable {
    ...
    // Thread中的ThreadLocalMap
    ThreadLocal.ThreadLocalMap threadLocals = null;
    ...
}

set()方法中需要注意的是通过getMap(t)获取的Map其实是Thread中的threadLocals,也就是线程独有的一个ThreadLocalMap,也就是说,每个线程自己维护了自己的ThreadLocalMap

ThreadLocalMap

ThreadLocalMap是ThreadLocal中的一个静态内部类,其中维护了一个Entry:

static class ThreadLocalMap {
    static class Entry extends WeakReference<ThreadLocal<?>> {
       	// Entry的值
        Object value;
        
        Entry(ThreadLocal<?> k, Object v) {
            super(k);
            value = v;
        }
    }
    ...
    private Entry[] table;
    ...
}

这里我们可以看到ThreadLocalMap内部的Entry继承了WeakReference(弱引用),目的是使得该Entry在仅有弱引用关联时,方便被垃圾回收器回收。

通过观察ThreadLocalMap的结构我们可以发现,ThreadLocalMap本质上是一个Entry数组,每一个Entry的key就是ThreadLocal的弱引用,value就是我们要设置的变量值。结构如下

在这里插入图片描述

ThreadLocalMap如何解决hash冲突?

我们一起来看一下ThreadLocalMap中的set方法:

private void set(ThreadLocal<?> key, Object value) {
    Entry[] tab = table;
    int len = tab.length;// 获取当前Entry数组的长度
    // 长度-1与运算
    int i = key.threadLocalHashCode & (len-1);

    for (Entry e = tab[i];
         e != null;
         e = tab[i = nextIndex(i, len)]) {
        ThreadLocal<?> k = e.get();

        if (k == key) {
            e.value = value;
            return;
        }
		
        if (k == null) {
            replaceStaleEntry(key, value, i);
            return;
        }
    }

    tab[i] = new Entry(key, value);
    int sz = ++size;
    if (!cleanSomeSlots(i, sz) && sz >= threshold)
        rehash();
}

// ThreadLocal中的threadLocalHashCode
private final int threadLocalHashCode = nextHashCode();
// ThreadLocal中的nextHashCode()方法
private static int nextHashCode() {
    return nextHashCode.getAndAdd(HASH_INCREMENT);
}

从源码中可以看到,在set方法中:

  1. 首先会使用ThreadLocal对象的hash值,定位table的索引位置i,int i = key.threadLocalHashCode & (len-1)。

  2. 接着进入一个for循环,判断i索引位置的Entry是否为null,若e不为null,则进入循环

  3. 在for循环中会判断该Entry的key和当前ThreadLocal是否相等,相等则直接修改value

    if (k == key) {
        e.value = value;
        return;
    }
    
  4. 若当前位置Entry的key为null,就初始化一个Entry放在当前索引位置。由replaceStaleEntry(key, value, i)中的逻辑我们可以看到,该方法会通过tab[staleSlot].value = null将原value值置为null,并设置新的Entry。其实平时在使用结束后,由于Key是WeakReference的,会被优先回收,若不及时remove(),则可能会造成内存泄漏的问题,所以在开发中我们要尽量避免这种情况。

    if (k == null) {
        replaceStaleEntry(key, value, i);
        return;
    }
    
    private void replaceStaleEntry(ThreadLocal<?> key, Object value, int staleSlot) {
        ...
        tab[staleSlot].value = null;
        tab[staleSlot] = new Entry(key, value);
        ...
    }
    
获取变量——get()

这里我们可以看到get()方法实际上是调用了ThreadLocalMap的getEntry方法:

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

​ 有了前面set源码的经验,再看这里就很好懂了,大致流程是:根据ThreadLocal对象的Hash值,定位table的索引位置,若key和get的key一致,就返回该位置的value,如果不一致就进入getEntryAfterMiss()方法判断后续位置。

private Entry getEntry(ThreadLocal<?> key) {
    int i = key.threadLocalHashCode & (table.length - 1);
    Entry e = table[i];
    if (e != null && e.get() == key)
        return e;
    else
        return getEntryAfterMiss(key, i, e);
}

private Entry getEntryAfterMiss(ThreadLocal<?> key, int i, Entry e) {
    Entry[] tab = table;
    int len = tab.length;

    while (e != null) {
        ThreadLocal<?> k = e.get();
        if (k == key)
            return e;
        if (k == null)
            expungeStaleEntry(i);
        else
            i = nextIndex(i, len);
        e = tab[i];
    }
    return null;
}
ThreadLocal内存模型是指通过ThreadLocal类来实现的一种特殊的线程封闭机制,它可以让每个线程都拥有自己的变量副本,从而实现线程间的数据隔离。 在使用ThreadLocal时,每个线程都有自己独立的ThreadLocalMap实例,该实例存储了线程自身的ThreadLocal变量。实际上,数据是保存在当前的Thread对象上,而不是ThreadLocal对象上。ThreadLocal只是提供了一个操作的框架,用于将数据存储在当前线程ThreadLocalMap中。 ThreadLocal内存泄漏是指当ThreadLocal对象被回收时,由于ThreadLocalMap中的Entry仍然持有对ThreadLocal对象的强引用,导致ThreadLocal对象无法被垃圾回收,从而造成内存泄漏。要避免内存泄漏,需要在不再使用ThreadLocal对象时手动调用其remove方法来清除对应的Entry。 ThreadLocal内存模型可以通过以下代码示例来观察: ```java import java.util.ArrayList; public class Main { static class ValueObject { private long[] data = new long[131072]; // 需要 1M 空间 (1024 * 1024 / 8) } public static void main(String[] args) throws InterruptedException { int threadNumber = 10; while (threadNumber-- > 0) { Thread worker = new Thread(() -> { int localCount = 15; var locals = new ArrayList<>(localCount); while (localCount-- > 0) { ThreadLocal<ValueObject> newLocal = new ThreadLocal<>(); newLocal.set(new ValueObject()); locals.add(newLocal); // newLocal.remove(); } locals = null; System.gc(); }, "工作线程"); worker.start(); worker.join(); } System.out.println("运行结束"); } } ``` 在上述代码中,每个工作线程创建了15ThreadLocal实例,并将其添加到一个ArrayList中。在每个ThreadLocal实例中,我们设置了一个ValueObject对象作为。如果没有手动调用remove方法来清除Entry,那么在垃圾回收时,这些Entry将持续引用ThreadLocal对象,导致内存泄漏。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [java(8)--线程ThreadLocal详解](https://blog.csdn.net/hguisu/article/details/8024799)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] - *2* *3* [ThreadLocal 内存模型、内存泄漏原因、现象观测、解决](https://blog.csdn.net/the_first_snow/article/details/105743395)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v93^chatsearchT3_2"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值