并发编程的艺术
# 整体框架概述
# 本书框架
本文主要围绕着前三点来进行描述。第一个是并发机制的一些底层原理,还有整个 Java 的内存模型是怎样的。还有关于一些并发编程的基础是怎样做的?比如说线程的生命周期,线程的优先级和线程之间的通讯方式等等
- Java并发机制的底层实现原理:这一章详细讲解了Java并发机制的底层实现,包括JVM内存模型、线程、锁等。理解这些底层原理对于编写高效、稳定的并发程序至关重要。比如,你需要了解
JVM
如何管理内存、线程如何调度以及锁的实现方式,才能更好地优化你的并发代码。 - Java内存模型:Java内存模型是理解Java并发编程的关键,它定义了线程之间如何共享变量。掌握了Java内存模型,你就能更准确地预测多线程程序的行为,从而避免一些常见的并发问题。比如,你可以利用volatile关键字来保证变量的可见性,或者使用synchronized关键字来保证原子性和可见性。
- Java并发编程基础:这一章主要介绍了Java并发编程的基本概念,如线程、进程、并发、并行等。这些基础知识是后续章节的基础,也是你理解并发编程的起点。比如,你需要了解线程的生命周期、线程的优先级以及线程间的通信方式等。
- Java中的锁:锁是并发编程中的重要概念,用于保护共享资源不被多个线程同时访问。这一章详细讲解了Java中的各种锁机制,包括内置锁、重入锁、读写锁等。在实际工作中,我经常使用
ReentrantLock
和ReadWriteLock
来提高并发性能。 - Java并发容器和框架:Java提供了一些并发容器和框架,如
ConcurrentHashMap
、ExecutorService
等,用于简化并发编程。这一章详细讲解了这些并发容器和框架的使用方法和原理。比如,我经常使用ConcurrentHashMap
来提高多线程访问Map的性能。 - Java中的线程池:线程池是管理和复用线程的一种技术,可以大大提高并发程序的性能。这一章详细讲解了Java中的线程池实现原理和使用方法。在实际工作中,我经常使用Executors工具类来创建和管理线程池。
# 引言
在《并发编程的艺术》这本书中,关于Java并发机制的底层实现,包括JVM内存模型、线程、锁等的讲解非常深入且实用。这些底层原理不仅帮助我们理解并发编程的本质,还是编写高效、稳定并发程序的基础。
首先,JVM内存模型
是Java并发编程的核心。它定义了线程之间如何共享变量,以及如何在多线程环境中保证数据的一致性和原子性。JVM内存模型包括主内存和工作内存的概念。主内存是所有线程共享的,而工作内存是每个线程私有的。线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。这就涉及到了可见性、原子性和有序性这三个重要特性。
通过volatile关键字和synchronized关键字,我们可以确保变量的可见性和原子性。
其次,线程是并发编程的基本单位。在Java中,线程是由JVM管理的,每个线程都有自己的程序计数器、栈和本地变量。线程的生命周期包括新建、就绪、运行、阻塞和死亡五个状态。了解线程的生命周期和状态转换,有助于我们更好地控制线程的执行和调度。此外,线程间的通信和协作也是非常重要的,可以通过wait/notify机制、信号量、阻塞队列等方式实现。
最后,锁是保护共享资源不被多个线程同时访问的重要机制。在Java中,有多种锁机制可供选择,包括内置锁(synchronized)、重入锁(ReentrantLock)、读写锁(ReadWriteLock
)等。这些锁机制的实现原理和使用方法各不相同,但都是为了解决并发访问共享资源的问题。例如,内置锁是通过对象头中的标记位和监视器锁来实现的;重入锁则通过内部维护一个计数器和一个等待队列来实现;读写锁则允许多个线程同时读取共享资源,但只允许一个线程写入。
了解这些底层原理后,我们就可以更好地优化并发代码。例如,通过合理地使用锁机制,可以减少线程间的竞争和等待时间;通过优化线程池的大小和配置,可以提高系统的吞吐量和响应速度;通过合理地利用缓存和批量处理技术,可以减少对共享资源的访问次数和频率。总之,只有深入理解Java并发机制的底层实现原理,我们才能编写出高效、稳定的并发程序。
# 第一章 线程安全
# 上下文切换
上下文切换指的是操作系统在多任务处理的时候,上下文切换虽然是必要的,但是会带来一定的开销,尤其是在高并发的场景下会影响系统性能。
【第一步】将当前任务的状态(也就是CPU寄存器中的内容和内存指针)存储起来,以便之后可以恢复执行。【第二步】当其他任务执行完成之后,再回来,他就要读取之前保存的这个任务的运行状态,这个过程叫做加载。
解决方案:
减少线程数量:在设计并发系统时,可以通过合理设计减少线程数量,避免不必要的上下文切换。 使用线程池:合理利用线程池技术,避免不必要的线程频繁创建和销毁,从而减少上下文切换的开销。
# 死锁
死锁是指两个或者多个线程 因为争夺资源而导致大家都处于相互等待的一种状态,导致程序无法继续向下执行
解决方案
保证各个线程之间能够按照特定的顺序去请求资源,避免出现循环等待的情况
我们还可以为每个线程设置等待时间。当某个线程超过了一定的等待时间之后,它会自动放弃资源,从而避免长时间占用资源,导致整个系统阻塞
避免一个线程同时去获取多个锁,最佳实践是一个线程,只拿一个锁
# 池技术——资源限制
资源限制是指我们在并发的时候,程序的执行速度会被限制
我们一般是采用资源池的方式,对一些有限的资源进行管理。比如说数据库的连接,还有网络连接等等
# 什么是线程安全?
在多线程环境下,线程安全是指当多个线程同时访问共享资源或变量时,避免因编译器和 CPU 的重排序导致的不符合预期的结果。为了应对这个问题,Java Memory Model (JMM) 和 happens-before 原则被用来规范编译器和 CPU 的重排序过程,以确保在运算结果正确的情况下,尽可能提高并发度。JMM 和 happens-before 原则的引入,使得程序员在编写多线程程序时能够依赖这一规范来确保数据的可见性和有序性,而不需过多关注底层的优化和重排序。
因此,线程安全的概念结合了对并发环境下可能出现的结果的关注,同时利用编译器和 CPU 的规范化来保证多线程环境下程序的正确性、可靠性和性能。
# 线程安全
线程安全指的是一个线程去修改了一个共享变量,其他线程都可以马上感知到这个修改的变化 在 Java 中,这种共享变量的可见性是通过内存屏障和缓存一致性来完成的
具体而言,一个线程(比如线程A)从主存中获取共享变量的值,并加载到自己的工作内存中,完成修改后,通过Store过程将修改后的值刷新回主存。在这个过程中,内存屏障确保了操作的顺序性和可见性。随后,由于缓存一致性原则,其他线程中的该共享变量将会失效。因此,其他线程在读取这个共享变量时,必须重新从主存中获取最新的值,从而保证了各个线程之间对共享变量的最新值的感知,从而保证了可见性和一致性。
# 第二章 Java 内存模型
# JMM内存模型
Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种抽象模型,用于描述多线程环境中共享变量的可见性以及线程之间的同步行为。JMM是Java并发编程的基础,它确保了Java程序在多处理器架构上的正确执行。
共享变量的可见性
在Java中,多个线程可以访问共享变量。共享变量的值可以被一个线程修改,然后被另一个线程读取。JMM规定了线程对共享变量的读写操作必须满足一定的规则,以确保所有线程都能看到一致的变量值。
主内存与工作内存
JMM将内存分为主内存和工作内存。主内存是所有线程共享的,它存储了Java实例对象、静态字段和JRE类结构等信息。工作内存是每个线程私有的,它保存了线程使用到的变量的主内存副本拷贝。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的变量。
内存间的交互操作
JMM定义了8种操作来完成主内存和工作内存之间的交互:
- lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
- unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
- read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
- load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
- use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
- store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。
- write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。
先行发生原则(Happens-Before)
为了简化开发者的理解和编程,JMM还提供了一套先行发生原则,用于确定操作之间的偏序关系。如果操作A先行发生于操作B,那么操作A的结果将对操作B可见,并且操作B的执行将建立在操作A的基础之上。
一些重要的先行发生关系包括:
- 程序次序规则:在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。
- 锁定规则:一个unlock操作先行发生于后面对同一个锁的lock操作。
- volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
- 传递性:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C。
- 线程启动规则:Thread对象的start()方法先行发生于此线程的每一个动作。
- 线程终止规则:线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。
- 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测到是否有中断发生。
- 对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。
总结
Java内存模型是一个复杂的规范,它确保了Java程序在多线程环境中的正确性和可见性。通过理解JMM的抽象概念,如主内存、工作内存、先行发生原则等,开发者可以更加自信地编写并发程序,避免常见的并发问题,如数据竞争和死锁。
# volatile
在Java中,volatile
是一种轻量级的同步机制,用于在多线程环境下保证共享变量的可见性。
【保证可见性】当使用volatile
修饰一个变量时,它会保证对该变量的写操作能够立即刷新到主存中,并且对该变量的读操作会直接从主存中获取最新值。这是通过在编译后的字节码中添加lock前缀
来实现的。这种机制确保了在多线程环境下,对volatile
变量的修改对其他线程是可见的。这种机制确保了这个变量的现程安全
【无法保证原子性】当有多个线程同时修改volatile
变量时,可能会因为竞争而导致数据不一致的结果。在这种情况下,推荐使用synchronized
关键字或者使用java.util.concurrent
包下的工具类来确保数据的一致性。
【内存屏障以及缓存一致性的具体过程】
具体而言,一个线程(比如线程A)从主存中获取共享变量的值,并加载到自己的工作内存中,完成修改后,通过Store过程将修改后的值刷新回主存。在这个过程中,内存屏障确保了操作的顺序性和可见性。随后,由于缓存一致性原则,其他线程中的该共享变量将会失效。因此,其他线程在读取这个共享变量时,必须重新从主存中获取最新的值,从而保证了各个线程之间对共享变量的最新值的感知,从而保证了可见性和一致性。
# synchronized
在Java中,synchronized保证了同步块中的代码执行顺序,并且在底层实现中使用了monitor enter和monitor exit指令来实现。当需要进入同步代码块时,线程首先要争夺对应对象的monitor锁。监视器锁的具体实现是通过Java对象头中的mark word来记录一些信息,包括线程ID、锁状态(无锁、偏向锁、轻量级锁等)等标识信息。
# synchronized锁升级过程
在Java中,synchronized
关键字用于确保多线程环境中的线程安全。当多个线程尝试访问同一个同步块时,锁机制会发挥作用。Java中的synchronized
锁经历了几个不同的升级过程,包括偏向锁、轻量级锁和重量级锁,这些升级策略旨在提高不同场景下的性能。
偏向锁(Biased Locking):
- 初始状态:无锁。
- 当第一个线程访问同步块时,它会在Java对象头的mark word中记录下当前线程的ID,这样下次该线程再次访问时,无需进行锁操作,因为系统已经“偏向”于认为这个线程会再次访问。
- 偏向锁的设计基于观察,即同一个线程往往会多次访问同一个同步块。通过消除不必要的锁竞争,偏向锁提高了性能。
轻量级锁(Lightweight Locking):
- 当有多个线程竞争同一个同步块时,偏向锁会升级为轻量级锁。
- 轻量级锁依然只允许一个线程进入同步块,但其他线程会尝试通过自旋(忙等待)来获取锁,而不是立即阻塞。
- 如果同步块的执行时间很短,自旋等待的开销可能比线程上下文切换的开销要小,因此轻量级锁在这种情况下是高效的。
- 然而,如果同步块的执行时间较长,多个线程长时间自旋等待会浪费CPU资源。
重量级锁(Heavyweight Locking):
- 当轻量级锁的自旋等待超过一定次数或时间后,锁会升级为重量级锁。
- 在重量级锁状态下,未获得锁的线程会被阻塞,而不是自旋等待。
- 阻塞的线程不会消耗CPU资源,它们会进入等待状态,直到持有锁的线程释放锁并发出通知。
- 虽然重量级锁的响应时间可能较长,但它有效地防止了CPU资源的浪费,特别是在同步块执行时间较长的情况下。
通过这种逐步升级的锁策略,Java的synchronized
关键字能够在不同的多线程访问模式下提供相对最优的性能表现。从偏向锁到轻量级锁,再到重量级锁,锁的升级过程反映了Java虚拟机(JVM)对多线程并发控制的精细管理。
# CAS问题
- ABA 问题:在并发环境下,如果一个共享变量原来的值是A,后来变成了B,然后又变回A,那么使用CAS进行检查时可能无法捕获到这个过程中的变化。这可能会导致一些意想不到的结果。为了解决ABA问题,可以使用带有版本号、时间戳等信息的CAS操作来检测和处理这种情况。
- 锁竞争影响系统吞吐量:在高并发的情况下,锁的竞争可能导致某些线程长时间占用CPU时间片,而其他线程则处于等待状态。这会影响系统的整体吞吐量。为了解决这一问题,可以调整锁的粒度,优化代码逻辑,或者使用非阻塞的算法来减少对锁的依赖,从而提高系统的并发能力和吞吐量。
final 域的重排序。首先,它是保证了 final 域完成变量的初始化之后,再完成构造方法的这样子,使得其他线程获取对象的引用后,final 域是必然已经被完成初始化的
并且禁止了指令重排序。final 域的初始化必须放在构造函数的第一行
这些方式保证了final域的可见性,以及禁止重排区的规则
# final 域的重排序
- 完成初始化:final域确保在构造方法完成之前已经完成了变量的初始化。这意味着其他线程在获取对象的引用后,final域已经被成功初始化,从而避免了在未完全初始化的对象上操作的情况。
- 禁止指令重排序:在构造函数中,对final域的写入必须在构造函数的结尾处(在构造函数正常结束之前)进行。这实际上防止了JVM对构造函数内部的指令进行重排序,从而确保了final域的可见性并遵循了“禁止重排规则”。
总的来说,final域的这些特性确保了其可见性,并且通过指令序列的顺序执行来避免了重排序,这对于多线程环境下保证对象状态的一致性至关重要。
# 第三章线程状态
# 线程的状态
- NEW(新建)状态:当线程对象被创建时,它处于新建状态。
- RUNNABLE(可运行)状态:当调用start()方法后,线程处于就绪状态,等待CPU分配时间片以便执行。
- RUNNING(运行)状态:当线程获取了CPU时间片,它处于运行状态,执行相应的任务。
- BLOCKED(阻塞)状态:线程被阻塞等待一个监视器锁(synchronized同步块或方法)时,处于阻塞状态。
- WAITING(等待)状态:线程无限期等待另一个线程执行特定操作时,处于等待状态。
- TIMED_WAITING(计时等待)状态:线程在等待另一个线程执行特定操作时,等待一段时间后会自动恢复。
- TERMINATED(终止)状态:线程执行完它的任务后,或者因为异常退出了run()方法,线程将进入终止状态。
# 状态切换
NEW→RUNNABLE: 当创建一个新的线程对象时,初始状态是NEW。当调用start()方法启动线程时,线程进入RUNNABLE状态。此时线程是就绪状态,等待CPU调度执行。
RUNNABLE→RUNNING: 当线程获得CPU时间片,操作系统会将该线程调度到处理器上执行,此时线程进入RUNNING状态,执行相应的任务。
RUNNING→RUNNABLE: 当线程的时间片用完、或者调用了yield()方法主动让出CPU时间片时,线程由RUNNING状态切换回RUNNABLE状态,等待下一次的调度。
RUNNING→BLOCKED: 当线程在执行过程中,需要获取对象的锁而当前锁被其他线程占用时,线程进入BLOCKED状态。这通常发生在同步代码块或方法的锁竞争情况下。
RUNNING→WAITING/TIMED_WAITING: 当线程调用wait()方法(或join()方法、sleep()方法)后,线程会进入WAITING状态(或是TIMED_WAITING状态),以等待其他线程的通知或者等待一定的时间。
RUNNING→TERMINATED: 当线程执行完毕或者因异常而退出时,线程进入TERMINATED状态,线程的执行结束。
# 控制线程比较常用的几个方法
- join方法: 用于等待其他线程完成后,当前线程再继续执行。调用join方法会使当前线程进入阻塞状态,直到被等待的线程执行完毕。
- sleep方法: 类似于timewaiting的状态,它会使当前线程进入睡眠状态,等待指定的时间后继续执行。在这段时间内,线程不会占用CPU时间片,从而实现休眠的效果。
- yield方法: 它是一种线程让步的方法,将CPU让出给同等优先级的其他线程。这并不是一个很可控的行为,因为在某些情况下,yield方法调用后,线程可能会立即重新获得CPU时间片,也可能会被其他线程抢占。在实际开发中用到的情况相对较少。
- interrupt方法: 用来中断线程的执行。在某些情况下,我们需要通过中断来使线程从阻塞状态中恢复,或者通过响应中断来终止线程的执行。这在线程池、并发框架等场景中经常会用到,能够有效且优雅地终止线程的执行。
# 第四章,并发编程基础
# ThreadLocal
线程本地变量是指线程的本地存储空间,用于存储每个线程独有的信息。
- 通常用于存储用户的认证 token、会话信息等。
- 优化历程
- 在 Java 1.8 之前,线程本地变量是通过一个全局共享的 map 来实现的,每个线程只能指向一个数值。这导致了一些限制,比如每个线程只能存储一个数值。
- 从 Java 1.8 开始,线程本地变量的实现进行了升级,每个线程现在都有自己的 map。这样的设计优势在于可以存储多个变量。在这里,还有一个实现细节,就是它使用了弱引用。这意味着在这个 map 中,每个元素都是弱引用的。这个设计是为了在垃圾回收时进行清理操作,防止内存泄漏的发生。
- 【内存泄露问题】内存泄漏是指对象仍然被引用,却不再被程序所需要。举个例子,如果这个 entry 里的元素使用的是强引用,那么在线程池中有很多线程不会被销毁的情况下,就有可能发生内存泄漏。比如,在一些线程池的 worker 中,它们会随着线程池的生命周期而存在。如果这些线程中有一些强引用的 entry 的话,下一次线程进入处理其他任务时,可能会需要用到其他的 entry。如果这些 entry 一直被强引用关联着,它们既没有被使用,也不会被垃圾回收清理,就会导致内存泄漏的问题。通过使用弱引用,可以避免这样的内存泄漏问题。
- 使用ThreadLocal的好处:ThreadLocal 被设计出来主要是为了解决多线程环境下的数据共享与数据隔离问题。
- 提供了线程级别的变量隔离,其中的数据只属于当前线程,不可被其他线程访问或修改。这种特性是多线程环境下处理线程相关数据的重要工具,可以避免因为共享数据而引起的线程安全问题。另外,由于每个线程都拥有自己独立的ThreadLocal实例,它们之间的数据相互隔离,避免了对共享变量进行同步加锁而带来的性能损失,从而提升了并发性能。
# 使用场景
通过将动态表名存储在 ThreadLocal
中,可以实现对不同项目和线程的表名进行有效的隔离和缓存。这种方式可以用于解决在多线程环境下动态切换数据库表名的需求,并且确保每个线程都可以独立地获取到自己需要的表名而不会相互干扰。
举例来说,假设有一个需要查询不同项目数据的系统。项目的数据被存储在不同的数据库表中,表名的获取是根据项目的 id 来确定的。使用 ThreadLocal
可以很好地满足这一需求,具体场景如下:
- 线程隔离: 每个线程都可以单独存储和访问自己需要的表名,而不会影响其他线程。这样可以确保在多线程环境下每个线程都能独立地访问到正确的表名。
- 缓存表名: 通过将表名存储在
ThreadLocal
中,可以在需要时直接从ThreadLocal
获取,而不必反复查询数据库或使用其他机制获取表名,从而提高了访问效率。 - 动态切换表名: 如果需要动态切换表名(例如根据项目 id 进行切换),使用
ThreadLocal
可以轻松实现这一切换过程。 - 避免参数传递: 使用
ThreadLocal
可以避免反复将表名作为参数传递给需要访问数据的方法,提高了代码的简洁性和可维护性。
# 内部结构演进
ThreadLocal 是线程变量,是一个以
线程变量对象为Key,任何对象为值
的一个存储结构。是区分每个线程存储数据的一个区域
在 Java 8 中,ThreadLocal的内部结构仍然是基于Map的,但是其拥有者变为了Thread线程对象,每个Thread实例都拥有一个ThreadLocalMap对象,而Map的键(Key)为ThreadLocal实例。
相较于早期版本的ThreadLocalMap实现,在Java 8中的主要变化有如下几点:
- 拥有者的变化:新版本的ThreadLocalMap的拥有者为Thread,而早期版本的ThreadLocalMap的拥有者为ThreadLocal。
- Key的变化:新版本的ThreadLocalMap中的Key为ThreadLocal实例,而早期版本的Key为Thread实例。
与早期版本相比,Java 8中的ThreadLocalMap实现的主要优势在于:
- 存储的键值对数量变少
- Thread实例销毁后,ThreadLocalMap也会随之销毁:这一改变有助于在一定程度上减少内存的消耗,因为ThreadLocalMap也会随着Thread实例的销毁而释放。这有助于避免可能导致的内存泄漏问题。
# 底层实现
这里使用了一个弱引用的方式来实现Entry。
ThreadLocal 设置为弱引用,可以方便 Java 程序员大多数情况下不需要手动的对里面的 key 进行 remove 操作
在 Java 中,尤其是在使用 ThreadLocal 进行线程封闭的数据存储时,开发人员需要非常小心,确保在合适的时机清理 ThreadLocal 中的数据。尤其是在使用线程池、Web 容器等容器环境中,更容易因为线程的复用而造成 ThreadLocal 的数据被意外保留。
即使使用了弱引用,当 ThreadLocal 对应的线程结束或被回收时,与之关联的数据并不会立即被清理,而是会等到下一次线程使用该 ThreadLocal 时,才会触发数据的清理。这可能会导致一些意外的内存泄漏情况。
static class Entry extends WeakReference<ThreadLocal<?>> {
/** The value associated with this ThreadLocal. */
Object value;
Entry(ThreadLocal<?> k, Object v) {
super(k);
value = v;
}
}
2
3
4
5
6
7
8
9
# 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);
}
}
【1】 设置的时候,走到创建方法里面来了
/**
* The initial capacity -- MUST be a power of two.
*/
private static final int INITIAL_CAPACITY = 16;
/**
* Construct a new map initially containing (firstKey, firstValue).
* ThreadLocalMaps are constructed lazily, so we only create
* one when we have at least one entry to put in it.
*/
ThreadLocalMap(ThreadLocal<?> firstKey, Object firstValue) {
table = new Entry[INITIAL_CAPACITY];
int i = firstKey.threadLocalHashCode & (INITIAL_CAPACITY - 1);
table[i] = new Entry(firstKey, firstValue);
size = 1;
setThreshold(INITIAL_CAPACITY);
}
// 16是二的四次方,所以是只取当前变量哈希code的最后四位。与操作就是只取最后四位
// 这就是 table 的index位置,向指定的位置上添加一个Entry
【2】已经创建过,到一些扩容里面的方法了
// 如果我们预测大多数set()操作是用来替换现有条目的,并为这种情况实现了快速路径,但实际上创建新条目与替换条目一样频繁,那么这个快速路径不仅没有提供预期的性能提升,反而会在遇到创建新条目的情况时导致额外的性能开销(因为需要频繁地跳出快速路径去执行更一般的代码逻辑)。
// 因此,设计师们可能选择不使用针对特定情况(如替换条目)的快速路径,而是为set()实现一个统一的、适用于所有情况的代码路径,以平衡创建新条目和替换现有条目的性能。这样的设计选择能够确保在无法预测set()操作的用途时(即是用来创建新条目还是替换现有条目),都能有合理的性能表现。
private void set(ThreadLocal<?> key, Object value) {
Entry[] tab = table;
int len = tab.length;
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();
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
在Java中,ThreadLocalMap
是ThreadLocal
类的一个内部私有类,用于存储线程本地变量的实际值。replaceStaleEntry(key, value, i)
方法通常被用于替换ThreadLocalMap
中的陈旧(过时)条目。
具体来说,replaceStaleEntry(key, value, i)
方法用于在ThreadLocalMap
中替换特定索引i
处的条目。这个方法通常用于在ThreadLocal
实例中设置新的值时,首先检查ThreadLocalMap
中是否存在过时的条目(特定索引处的条目),如果有的话,就进行替换。这通常发生在ThreadLocal
实例调用set()
方法来存储新的值时,为了确保存储的值能正确地与ThreadLocal
关联。
总之,replaceStaleEntry(key, value, i)
方法在ThreadLocalMap
中用于替换指定索引处的旧条目,以确保线程本地变量的一致性和正确性。