ThreadLocal
ThreadLocal
ThreadLocal 是 Java 中的一个线程局部变量,它允许每个线程独立地存储和获取数据,保证线程之间的数据互相独立,避免并发访问带来的竞争条件。
ThreadLocal 不是用来解决共享数据的问题,而是为了实现线程隔离的目的。它在某些场景下非常有用,如 Web 应用中的用户身份信息、数据库连接、事务管理等。
使用
在上面的示例中,通过 UserContext 类和 userThreadLocal 实例,我们可以在不同的方法中共享用户身份信息,无需显式传递。可以通过 UserContext.setUser(user)方法设置用户信息,然后在其他方法中使用 UserContext.getUser()获取该用户信息。最后,可以通过 UserContext.clearUser()方法清除当前线程的用户信息。
作用
提供线程封闭(Thread Confinement)机制:ThreadLocal 允许每个线程拥有自己的数据副本,避免了多线程之间的数据竞争问题。每个线程都可以独立地修改和访问自己的数据副本,而不会影响其他线程。
实现线程安全:通过将数据保存在 ThreadLocal 中,可以在多线程环境下实现线程安全。每个线程访问自己的 ThreadLocal 实例,不会受到其他线程的干扰,避免了使用锁机制的开销和复杂性。
提供线程间上下文传递:ThreadLocal 可以用于在线程之间传递上下文信息,而不需要显式传递参数。在某些场景下,需要在多个方法或组件之间共享数据,但又不希望公开全局变量或传递参数,这时可以使用 ThreadLocal 来存储和获取数据。
解决线程安全问题:在某些情况下,一些对象可能不是线程安全的,无法在多线程环境下直接使用。通过将非线程安全的对象存储在 ThreadLocal 中,每个线程都拥有自己的副本,从而避免了并发访问的问题。
使用场景
Web 应用中的用户身份信息:可以在请求处理链路的各个组件中共享用户身份信息,无需在每个方法中显式传递。
数据库连接、事务管理:ThreadLocal 可以用于存储数据库连接、事务对象等,使每个线程都拥有独立的连接或事务,从而避免并发访问的问题。
线程池中的任务隔离:使用 ThreadLocal 可以为每个任务提供独立的上下文环境,避免数据混乱。
方法
ThreadLocal 类接口很简单,其提供了一下方法:
get(): 用于获取当前线程的 ThreadLocal 实例中存储的值。如果该线程尚未设置过值,则会使用初始值提供者(如果已设置)或返回 null。
set(T value): 将指定的值设置到当前线程的 ThreadLocal 实例中。
remove(): 从当前线程的 ThreadLocal 实例中移除值。这样做可以避免潜在的内存泄漏,因为 ThreadLocal 会持有线程的引用。
initialValue(): 该方法在首次调用 get()方法时被调用,返回 ThreadLocal 的初始值。默认情况下返回 null,可以通过继承 ThreadLocal 并重写该方法来自定义初始值。
setInitialValue(ThreadLocal<?> local, Object value): 设置指定 ThreadLocal 实例的初始值。内部调用了 initialValue()方法来获取初始值,并将其设置到指定的 ThreadLocal 实例中。
createMap(ThreadLocal<?> firstKey, Object firstValue): 在首次调用 set()方法时被调用,用于创建 ThreadLocal 实例的内部映射。
实现
ThreadLocal 的底层实现主要依赖于 Thread 类中的一个成员变量 ThreadLocalMap。
每个 Thread 对象中都有一个 ThreadLocalMap 对象,用于存储线程的 ThreadLocal 实例以及对应的值。ThreadLocalMap 是一个自定义的哈希表结构,用于实现线程局部变量的存储和访问。
在 ThreadLocal 类中,使用 ThreadLocalMap 来管理每个线程的 ThreadLocal 实例及其对应的值。每个 ThreadLocal 实例作为 ThreadLocalMap 的键,而实际存储的值则作为 ThreadLocalMap 的值。
当调用 ThreadLocal 的 set()方法时,实际上是通过 Thread 对象获取当前线程的 ThreadLocalMap,并将 ThreadLocal 实例作为键,要设置的值作为值,存储到 ThreadLocalMap 中。同样,当调用 ThreadLocal 的 get()方法时,也是通过 Thread 对象获取当前线程的 ThreadLocalMap,并根据 ThreadLocal 实例获取对应的值。
为了保证高效的查找和存储,ThreadLocalMap 使用线性探测法来解决哈希冲突。在哈希冲突的情况下,使用开放地址法寻找下一个可用的槽位。
需要注意的是,由于 ThreadLocalMap 是作为 Thread 的成员变量存在的,它是与每个线程绑定的。因此,每个线程都拥有自己的 ThreadLocalMap 和其中的数据,相互之间不会产生干扰。
另外,由于 ThreadLocal 在使用完毕后需要进行及时的清理,避免内存泄漏。Java 中的 ThreadLocal 实现中并没有提供自动清理的机制,因此需要手动调用 remove()方法或使用 try-finally 块来确保在使用完 ThreadLocal 后进行清理操作。
总之,ThreadLocal 通过 Thread 对象中的 ThreadLocalMap 来实现线程局部变量的存储和访问,保证了每个线程拥有自己独立的数据副本,避免了并发访问的竞争条件。
开放地址法
开放地址法(Open Addressing)是一种用于解决哈希冲突的方法,常用于哈希表的实现。当多个键映射到相同的哈希桶位置时,开放地址法使用一定的规则来寻找下一个可用的桶位,以存储冲突的键。
在开放地址法中,哈希表通常是一个固定大小的数组,称为哈希桶(Hash Bucket)。每个桶可以存储一个键值对或者被标记为空。当发生哈希冲突时,即多个键映射到同一个桶位置时,开放地址法会根据一定的规则去寻找下一个可用的桶位置,直到找到一个空桶或者遍历整个哈希表。
常见的开放地址法策略有以下几种:
线性探测(Linear Probing):当发生哈希冲突时,线性探测会逐个查找下一个桶,直到找到一个空桶或者遍历整个哈希表。具体地,下一个桶的位置计算公式为 index = (index + 1) % capacity,其中 index 是当前桶的位置,capacity 是哈希表的容量。
二次探测(Quadratic Probing):二次探测通过使用二次方程来计算下一个桶的位置。具体地,下一个桶的位置计算公式为 index = (index + i * i) % capacity,其中 index 是当前桶的位置,i 是探测次数(从 1 开始),capacity 是哈希表的容量。
双重散列(Double Hashing):双重散列使用两个不同的哈希函数来计算下一个桶的位置。具体地,下一个桶的位置计算公式为 index = (index + i * hash2(key)) % capacity,其中 index 是当前桶的位置,i 是探测次数(从 1 开始),hash2(key)是第二个哈希函数的计算结果,capacity 是哈希表的容量。
在使用开放地址法解决哈希冲突时,需要注意以下几点:
负载因子控制:为了保持较低的哈希冲突率,需要合理选择哈希表的容量,以及根据实际情况调整负载因子。负载因子是哈希表中已存储元素数量与桶位总数的比值,当负载因子较高时,哈希冲突的可能性增加,需要进行扩容操作。
删除元素的处理:在使用开放地址法时,删除元素可能会导致后续查询操作无法正确定位到原有的元素。
内存泄露问题
使用 ThreadLocal 时,需要注意潜在的内存泄漏问题。以下是一些可能引起内存泄漏的情况以及如何避免它们:
未及时调用 remove()方法:在使用完 ThreadLocal 后,应该及时调用 remove()方法清除当前线程的 ThreadLocal 实例中存储的数据。如果没有显式调用 remove()方法,ThreadLocal 实例可能会一直持有对线程的引用,导致线程无法被垃圾回收,从而造成内存泄漏。因此,在使用完 ThreadLocal 后,应该确保在适当的时机调用 remove()方法,例如在线程执行结束或不再需要 ThreadLocal 存储的数据时。
长时间存储大量数据:如果在 ThreadLocal 中存储大量的数据,并且线程的生命周期很长,可能会导致内存占用过高。因为 ThreadLocal 的数据是与线程绑定的,线程的长时间存活会导致 ThreadLocal 中的数据一直存在内存中。在这种情况下,可以考虑合理控制数据的大小或使用完后及时清理数据,避免过多的内存占用。
线程池中未清理 ThreadLocal 数据:在使用线程池时,如果使用了 ThreadLocal,需要特别注意清理 ThreadLocal 数据。因为线程池的线程会被重用,ThreadLocal 中的数据可能会被下一个任务错误地共享。为了避免这种情况,可以在任务执行结束后显式调用 remove()方法清理 ThreadLocal 数据,或使用线程池提供的钩子方法(如 afterExecute())来清理 ThreadLocal 数据。
应用服务器的线程池问题:在使用应用服务器(如 Tomcat)时,如果将 ThreadLocal 放在静态变量中,并且使用了应用服务器的线程池,可能会引发内存泄漏。因为应用服务器的线程池线程是被重用的,ThreadLocal 中的数据可能会在不同请求之间共享。为了避免这种情况,应尽量避免将 ThreadLocal 放在静态变量中,或者在使用完后及时清理 ThreadLocal 数据。
在上述示例中,我们创建了一个 ThreadLocal 实例,并在子线程中将大量的数据存储到 ThreadLocal 中。然而,在子线程结束后,我们没有调用 remove()方法来清理 ThreadLocal 中的数据。 如果运行该示例,主线程结束后,由于没有清理 ThreadLocal 中的数据,可能会导致内存泄漏。每个线程的 ThreadLocal 实例会持有对线程的引用,导致线程无法被垃圾回收。 为了避免内存泄漏,应该在合适的时机调用 remove()方法,清理 ThreadLocal 中的数据。在上述示例中,可以在子线程的末尾添加 threadLocal.remove()来手动清理 ThreadLocal 数据。
为了避免 ThreadLocal 可能引发的内存泄漏问题,可以采取以下措施:
及时调用 remove()方法:在使用完 ThreadLocal 后,应该在合适的时机调用 remove()方法,清理 ThreadLocal 中的数据。可以使用 try-finally 块,确保在使用完后无论是否发生异常都能够调用 remove()方法。
使用线程池时注意清理:如果在使用线程池时使用了 ThreadLocal,需要特别注意清理 ThreadLocal 数据。在任务执行结束后,可以通过线程池提供的钩子方法(如 afterExecute())来清理 ThreadLocal 数据。
使用 InheritableThreadLocal 时小心传递:如果使用 InheritableThreadLocal,它会将数据从父线程传递给子线程。在使用 InheritableThreadLocal 时,需要注意在子线程中是否需要清理或重置 ThreadLocal 数据,以防止不必要的数据泄漏。
避免将 ThreadLocal 放在静态变量中:在某些情况下,将 ThreadLocal 实例放在静态变量中可能导致意外的内存泄漏。尽量避免在静态变量中使用 ThreadLocal,或者在使用完后及时清理数据。
使用弱引用的 ThreadLocal 实现:可以自定义 ThreadLocal 的子类,使用弱引用(WeakReference)来持有线程本地的值。这样,在没有其他强引用指向 ThreadLocal 实例时,ThreadLocal 的键值对将被垃圾回收。
版权声明: 本文为 InfoQ 作者【Yaien】的原创文章。
原文链接:【http://xie.infoq.cn/article/9267f59259d37978963a91459】。文章转载请联系作者。
评论