电子商务网站建设资料,小程序怎么赚钱的,温州网站建设方案服务,服务器出租简介
在并发编程中#xff0c;导致并发bug的问题都会归结于对共享变量的操作不当。多个线程同时读写同一共享变量存在并发问题#xff0c;我们可以利用写时复制、不变性来突破对原数据的写操作#xff0c;没有写就没有并发问题#xff0c;而本篇文章所介绍的技术是突破共享…简介
在并发编程中导致并发bug的问题都会归结于对共享变量的操作不当。多个线程同时读写同一共享变量存在并发问题我们可以利用写时复制、不变性来突破对原数据的写操作没有写就没有并发问题而本篇文章所介绍的技术是突破共享变量没有共享变量也不会有并发问题。
Java中避免线程共享的一大利器就是ThreadLocal我们本篇文章重点讲述它的底层原理、常见的一些用途、创建和使用等。
首先介绍一下它是什么
ThreadLocal 是 Java 的一个类位于 java.lang 包下。它为每一个线程提供了一个独立的变量副本使得每个线程可以独立地改变自己的副本而不会影响其他线程所对应的副本。
它的主要作用就是避免线程间共享除此之外还能避免无必要的同步以及降低变量使用的复杂度
线程隔离在多线程环境下ThreadLocal 可以为每一个线程提供一个独立的变量实例。因此每个线程都可以独立地操作该变量而不需要考虑并发问题。这种特性使得 ThreadLocal 成为一个非常方便的保存线程状态或线程局部变量的工具。避免无必要的同步由于每个线程都有其自己的变量副本所以它们可以无需任何同步措施就可以访问这些变量从而提高了程序的执行效率。降低复杂度在某些场景下如果需要通过参数传递线程局部变量可能会使得方法签名和调用变得复杂。使用 ThreadLocal 可以避免这种复杂性因为它允许我们在任何地方获取到线程相关的数据。
工作原理
我们在学习ThreadLocal的时候如果不了解其底层实现的话通常会进入一个误区ThreadLocal类里面维护了一个Map这个Map的key为线程引用值为自己设置的值。其实Java真正的实现是ThreadLocal仅是一个工具类提供线程对数据的访问实际真正拥有这个数据的角色是线程自身(这也解释了简介中提到的每个线程都有针对某一变量的副本)如下图所示 相信大家会有这样一个疑问为什么Java没有按照我们预想的那样实现呢其主要原因就是不容易造成内存泄漏。假设Java按照我们预想的方案实现我们分析下会造成什么样的问题ThreadLocal 持有的 Map 会持有 Thread 对象的引用这就意味着只要 ThreadLocal 对象存在那么 Map 中的 Thread 对象就永远不会被回收。ThreadLocal 的生命周期往往都比线程要长所以这种设计方案很容易导致内存泄露。而 Java 的实现中 Thread 持有 ThreadLocalMap而且 ThreadLocalMap 里对 ThreadLocal 的引用还是弱引用WeakReference所以只要 Thread 对象可以被回收那么 ThreadLocalMap 就能被回收。Java 的这种实现方案虽然看上去复杂一些但是更加安全。
另外这里面的关于Entry[]大小的问题有个讨论点为什么ThreadLocalMap中的Entry[]大小一定是2的幂次方呢 其主要原因如下
优化索引计算当 table 大小为 2 的次幂时计算索引位置可以通过简单的位操作实现而不需要执行昂贵的除法操作。例如如果 table 大小为 16那么为了得到一个 key 对应的索引位置只需要计算 key的hashCode (table.length - 1)。这种位操作比标准的模运算%速度快得多。均匀分布2 的次幂大小确保了哈希值在 table 中均匀分布减少了哈希冲突的可能性。这是因为当我们使用 hashCode (table.length - 1) 这种方式时hash值的低位将被有效地使用来计算索引这在大多数情况下可以确保数据在 table 中均匀分布。容易扩容当 table 需要扩容时例如当存储的元素太多达到了负载因子的限制新的大小可以简单地翻倍仍然保持为 2 的次幂。历史与一致性Java 中的其他哈希结构如 HashMap也使用了类似的策略因此保持这种做法在某种程度上也是为了一致性。
总的来说选择 2 的次幂作为 table 大小是一个优化决策旨在提高性能、减少哈希冲突并简化内部操作。
常见用途
数据库连接使用 ThreadLocal 保存每个线程独立的数据库连接确保在同一线程中共享同一个数据库连接而不是频繁地创建和销毁。日期格式化SimpleDateFormat 不是线程安全的但我们可以用 ThreadLocal 来为每个线程创建独立的 SimpleDateFormat 实例避免并发问题。Web 会话管理在 Web 应用中可以使用 ThreadLocal 来保存当前请求的用户会话或其他会话相关的信息。框架中的上下文信息许多框架使用 ThreadLocal 来保存线程级别的上下文数据例如 Spring 中的事务管理。
创建和使用
下面使用ThreadLocal演示一下为每一个线程分配一个线程ID的代码不同线程拥有不同的线程ID。
package com.markus.concurrent;import java.util.concurrent.atomic.AtomicLong;/*** author: markus* date: 2023/8/20 4:16 PM* Description:* Blog: https://markuszhang.com* Its my honor to share what Ive learned with you!*/
public class ThreadLocalDemo {static class ThreadID {static final AtomicLong nextId new AtomicLong(0);static final ThreadLocalLong tl ThreadLocal.withInitial(nextId::incrementAndGet);public static Long get() {return tl.get();}}static class GetThreadId implements Runnable {Overridepublic void run() {for (int i 0; i 3; i) {System.out.println(Thread [ Thread.currentThread().getName() ],its id is ThreadID.get());}}}public static void main(String[] args) {Thread t1 new Thread(new GetThreadId(), t1);Thread t2 new Thread(new GetThreadId(), t2);t1.start();t2.start();}
}控制台打印 优势、缺点与陷阱
在多线程编程中ThreadLocal 是一个不可或缺的工具。其主要优势在于为每个线程提供了私有的、独立的变量副本从而消除了多线程并发访问的问题。这种独立性确保了每个线程都可以无需同步就能访问其局部变量大大提高了程序的并发性和效率。此外它还能简化代码结构避免了复杂的参数传递为线程关联上下文信息提供了便捷手段如日志记录中的会话ID。
但是与其带来的好处相伴的是一些潜在的陷阱。最为严重的问题是与内存泄漏相关。因为ThreadLocal 使用弱引用来引用键但其对应的值则是强引用。如果不适时地清理尤其是在线程生命周期结束前不调用 remove() 方法可能会造成内存泄漏。此外过度或不适当地使用 ThreadLocal 可能导致代码结构混乱并降低代码的可读性和维护性。
总之当我们在项目中使用 ThreadLocal 时应该始终注意其潜在的陷阱和限制。正确、合理地使用 ThreadLocal 可以为我们带来很多好处但不恰当的使用可能会引发一系列问题。在使用前充分了解其工作机制并持续关注是非常必要的。
InheritableThreadLocal
顾名思义可继承的ThreadLocal。我们知道ThreadLocalMap是线程持有的每个变量都在各自的线程中保存一个副本每个线程都能拿到自己的值但是如果父线程创建了一个子线程那么这个子线程是无法拿到父线程中的ThreadLocalMap中的信息的。InheritableThreadLocal的出现就是解决这个问题的。它继承自ThreadLocal相关用法与ThreadLocal一致这里就不介绍了。
最佳实践 限制使用范围仅在确实需要线程局部变量来解决问题时使用 ThreadLocal。不应该过度使用或在不必要的场合使用它。 及时清理 一定要在不再需要线程局部变量时调用 ThreadLocal 的 remove() 方法以释放资源并避免潜在的内存泄漏。在 Web 容器中例如 Tomcat确保在请求结束后清除 ThreadLocal因为容器可能会重用线程。 避免长时间存活的线程局部变量如果一个线程预计会长时间存活而不释放例如线程池中的线程那么这种线程中的 ThreadLocal 变量也会长时间存活。在这种情况下应该特别注意清理这些变量。 不要存储大对象为了避免内存消耗不应该在 ThreadLocal 中存储大对象或那些你不打算很快释放的对象。、 考虑使用 InheritableThreadLocal当需要在一个线程及其所有子线程之间共享一个变量时可以考虑使用 InheritableThreadLocal。但要注意如果子线程修改了这个变量它不会影响到父线程中的值。 考虑线程安全性虽然 ThreadLocal 变量不需要同步来保证线程安全但存储在其中的对象仍然可能需要同步尤其是当多个线程可能访问同一个 ThreadLocal 变量中的同一个实例时例如使用 InheritableThreadLocal。 优先使用库提供的线程局部功能有些库或框架可能已经提供了其自己的线程局部功能例如 Java EE 和 Spring。在这种情况下优先使用库或框架提供的功能除非有特定的需求使得必须使用原生的 ThreadLocal。 不要将 ThreadLocal 作为全局变量尽量避免将 ThreadLocal 作为静态变量除非你确实需要这样做。静态的 ThreadLocal 变量可能会导致预料之外的问题尤其是当类被卸载时。 不建议你在线程池中使用 InheritableThreadLocal不仅仅是因为它具有 ThreadLocal 相同的缺点——可能导致内存泄露更重要的原因是线程池中线程的创建是动态的很容易导致继承关系错乱如果你的业务逻辑依赖 InheritableThreadLocal那么很可能导致业务逻辑计算错误而这个错误往往比内存泄露更要命
总之ThreadLocal 是一个有用但需要小心使用的工具。确保了解其工作原理并始终遵循上述的最佳实践这样可以最大化其价值并避免潜在的问题。
总结
好了简单总结一下我们本篇讲述的内容首先讲述了ThreadLocal是什么以及它的作用是什么并通过图片展示ThreadLocal模型知道了ThreadLocal其实就是一个工具类内部不保存数据真正保存数据的地方是在Thread本身也就是我们常说的每个Thread都有一个变量的副本。也介绍了ThreadLocalMap中的Entry[]数组为什么大小一定要限制为2的幂次方后面通过一段代码简单演示了ThreadLocal的使用并再次声明了它的优势和其缺点以及使用不到出现的陷阱。讲述过程中提到了父子线程不共享变量副本的问题而Java提供了InheritableThreadLocal解决这一问题。最终我们罗列了ThreadLocal的最佳实践。