深入学习JVM04 线程与变量
  dBFTbkVLMBge 2023年11月12日 19 0
jvm

18 JVM中的变量

在 food 背后,有一个与它对应的 klass 对象,它记录了 food 的类型信息,就像是有一个标签写着 food 的品类和特点。每个 klass 对象维护着一个虚函数表,记录了类中所有的虚函数以及对应的指针。当 food 调用一个方法时,JVM 根据它的实际类型找到它的 klass 对象,然后在虚函数表中寻找正确的函数指针,使得 food 能够调用正确的方法。这种实现方式让我们具备了多态性,让它的行为可以根据实际情况进行适应。

生存空间

首先,栈上的变量是局部变量,包括方法中定义的变量和代码块中定义的变量。它们的生命周期与方法调用或代码块的执行周期相同。变量在栈上的创建非常快速,而且在方法或代码块执行结束后会自动销毁。在使用栈上的变量之前,必须显式地为其赋值。

堆中的变量是通过 new 关键字创建的对象和数组。它们的生命周期与对象或数组本身相同,当没有引用指向它们时,会被垃圾收集器回收。堆中的变量创建比较耗时,需要为对象或数组分配内存空间,并进行初始化。在对象创建时,成员变量会被赋予默认值。

方法区中的变量主要是静态变量。它们的生命周期与类的生命周期相同,在类被加载时创建,在类被卸载时销毁。静态变量在方法区中创建并初始化,在使用之前已经具有默认值。

深入学习JVM04 线程与变量_JVM

深入学习JVM04 线程与变量_JVM_02

深入学习JVM04 线程与变量_JVM_03

深入学习JVM04 线程与变量_JVM_04

19 线程模型

线程的模型

JVM 采用了一对一的线程模型,即每个 Java 线程都对应一个操作系统的内核线程。这种模型通过调用操作系统的内核线程来完成线程的切换和执行,而且每个内核线程可以看作是操作系统内核的一个分身。这也就是操作系统能够处理多任务的原因。

深入学习JVM04 线程与变量_JVM_05

虚拟线程(Virtual Thread)

Java 虚拟线程是一种轻量级的线程实现,它不需要像传统线程那样占用大量的内存和 CPU 时间。相反,它们是在 JVM 层面上管理的,使用更少的系统资源,并且可以在一个或多个底层操作系统线程上运行,它允许开发人员以更高效和简化的方式处理并发任务

虚拟线程就是为了解决这种问题而产生的。原来 JDK 存在 java.lang.Thread 类,俗称线程。为了更好地区分虚拟线程和原有的线程类,引入了一个全新类 java.lang.VirtualThread,也是 Thread 类的一个子类型,直译过来就是“虚拟线程”

更新的 JDK 文档里也把原来的 Thread 叫做 Platform Thread,可以更清晰地和 Virtual Thread 区分开来。实际上,Platform Thread 这个词语可以被翻译成“平台线程”,它指的是在虚拟线程流行前就已经存在的线程概念。需要注意的是,虚拟线程并不能完全替代平台线程。

虚拟化进程是在网络平台进程之间运作的,单个网络平台进程可能包含数个虚拟化进程。同样,每一个平台线程也都对应着内核线程。由此,我们得到了这样一条结论:VT 代表一条虚拟线程。

public class VirtualThreadTest {
 
    public static void main(String[] args) {
 
        Thread.ofVirtual().start(() -> System.out.println("虚拟线程执行中..."));
 
        Thread.startVirtualThread(() -> System.out.println("虚拟线程执行中..."));
 
        ExecutorService executorService = Executors.newVirtualThreadPerTaskExecutor();
        executorService.execute(() -> System.out.println("虚拟线程执行中..."));
    }
}

代码里提供了两种创建虚拟线程的方法,分别是 Thread.ofVirtual().start() 方法和 Thread.startVirtualThread() 方法。此外线程池也支持虚拟线程的使用,我们通过 ExecutorService 的实现类 Executors.newVirtualThreadPerTaskExecutor() 创建了线程池,然后通过 execute() 方法添加了一个虚拟线程。实现原理

虚拟线程实现原理

总体上来看,虚拟线程由两个关键组件组成:Continuation(续体)和 Scheduler(调度器)。

虚拟线程会将任务(通常是 java.lang.Runnable 对象)封装到一个 Continuation 实例中。当任务需要阻塞挂起时,会调用 Continuation 的 yield 操作进行阻塞;当任务需要解除阻塞并继续执行时,Continuation 会被恢复执行。

Scheduler 负责将任务提交给一个线程池来执行。它是 java.util.concurrent.Executor 的子类。虚拟线程框架为虚拟线程任务提供了一个默认的 ForkJoinPool 线程池来执行任务。

调度方式

JDK 中的虚拟线程调度机制是通过把虚拟线程分派到对应的平台线程上完成的。这是一种基于虚拟线程和平台线程之间的 M:N 调度方式,也就是说,每个虚拟线程都可以被多条平台线程所执行。一旦某个线程的等待队列没有任何待处理的工作,为避免出现线程挨饿的情况,ForkJoinPool 实现了任务抢夺策略,也就是让处于挨饿状态的线程能够从其他正在运行的线程的等待队列里获取新的工作任务。

深入学习JVM04 线程与变量_JVM_06

21 伪共享

Cache Line 的由来

对于缓存与主存之间的查找和存储,有三种主要的映射方式,分别是直接映射、全相联映射和 N- 路组相联映射。直接映射会将内存的每一个块映射到一个固定的 Cache 行中,全相联映射则可以将任意的 Cache 行与任意的 Block 块进行对应关系映射,而 N- 路组相联映射则是将 Cache 所有行进行分组,从而把主存块映射到 Cache 固定组的任一行中。

CPU 在读取数据时,会先在 L1 缓存中寻找,如果未命中,再从 L2 缓存寻找,然后是 L3 缓存,再其次是内存,最后是磁盘。如果在缓存中找到数据就意味着缓存命中,如果没找到就称为缓存缺失

L1 缓存分为两种,一种是指令缓存,另一种是数据缓存。L2 和 L3 缓存不区分指令和数据,它们所存储的数据可以被所有的 CPU 核心共享。每个 CPU 核心都有自己的 L1 和 L2 缓存,而 L3 则是所有 CPU 核心共享的缓存。

深入学习JVM04 线程与变量_JVM_07

伪共享

伪共享出现的主要原因在于硬件设计。每次缓存操作,CPU 并不是直接与主存交互,而是以缓存行为单位在主存和缓存间传递数据。如果多个线程同时修改一块内存区域的不同部分,并且这部分内存在同一缓存行内,那么即使这些修改本身无需同步,由于保存这些部分的缓存行会频繁地被标记为无效和重新读取,就会引起伪共享。

每一个处理器都有自己的缓存,用来缓存经常使用的数据。然而,缓存行是缓存的一个基本单位,一次缓存操作需要将整个缓存行从内存读到缓存中,或者从缓存写回到内存中

深入学习JVM04 线程与变量_JVM_08

如何解决伪共享问题?

深入学习JVM04 线程与变量_JVM_09

而 VolatileLong 这个类的作用就是增大每个数组元素的大小,让它们达到或超过 Cache Line 的大小,从而避免在同一 Cache Line 中,消除伪共享。

现在 Java 8 已经开始用 sun.misc.Contended 这个注解来解决伪共享问题了。这个注解的作用就是在被注解的字段前后都进行填充,使其占满整个 Cache Line,避免出现伪共享。

@jdk.internal.vm.annotation.Contended
public static class PaddedLong {
        volatile long value = 0L;
}

Disruptor 是一个在高性能场景中广泛使用的开源并发框架,Disruptor 的设计中专门考虑到了避免伪共享的问题。它的关键数据结构 RingBuffer(环形缓冲区),通过一定长度的 padding 将每个数据项间隔开,确保每个数据项独立存在于不同的 Cache Line 中。这样做的好处在于极大地提高了数据的并行访问效率。

在 Disruptor 中,通过特殊的数据结构设计,使每一个真实的、需要处理的数据(如 value)都被一些无用的变量(padding)所包围。这确保了每个需要处理的数据都占据了完整的一个缓存行,有效避免了多个线程操作同一缓存行从而导致的伪共享问题。当然,就像我们前面说的,这样也会浪费掉一些空间。不过这也算是一个用空间换时间思想的应用,所以并不适用于所有的场景。

22 Volatile轻量级同步机制

深入学习JVM04 线程与变量_JVM_10

可见性

JMM 中有一个主内存和工作内存的概念。主内存是所有线程共享的内存区域,工作内存是每个线程独立的内存区域。当一个线程修改了主内存中的变量时,这个修改对其他线程是立即可见的。Volatile 关键字正是利用了 JMM 的这一特性,确保变量的可见性。

禁止指令重排序

JMM 对指令重排序做了限制,要求处理器按照顺序执行指令。但是为了提高执行效率,编译器和处理器可能会对指令进行重排序。在这种情况下,Volatile 关键字就派上用场了。它会禁止编译器和处理器对代码进行指令重排序优化,确保代码有序执行。

不保证原子性

JMM 定义了原子性操作的概念,要求对于多线程并发修改的变量,必须使用同步机制来保证原子性。Volatile 关键字虽然不能保证原子性,但它提供了一种简单的方式来保证可见性,下面工作原理部分我会为你详细说明。在某些场景下,Volatile 关键字可以与其他同步机制结合使用,实现更好的性能和线程安全。

在 Java 中,原子性是指一个操作是不可中断的,即使在多线程环境下也是如此。但是,volatile 变量的写操作和读操作之间是可以被中断的,这意味着在读取或者修改 volatile 变量的过程中,其他线程可能会对这个变量进行修改。因此,使用 volatile 变量并不能保证对变量的操作是原子性的。

如果想要保证原子性,可以使用 Java 并发包中的 AtomicXXX 类,这些类都提供了原子操作的方法。例如,AtomicInteger 提供了对整型变量的原子操作,AtomicLong 提供了对长整型变量的原子操作等。

Volatile 的工作原理

深入学习JVM04 线程与变量_JVM_11

深入学习JVM04 线程与变量_JVM_12

深入学习JVM04 线程与变量_JVM_13

深入学习JVM04 线程与变量_JVM_14

在实际应用中,Volatile 关键字还可以用于实现基于消息传递的框架中的可见性,例如 Netty 框架中的 EventLoop。在一些用于高性能计算的框架中,例如 Disruptor 和 JCTools,也可以利用 Volatile 关键字进行线程间的数据通信和控制。

24 Synchronized同步锁的原理

锁的状态与类型

Synchronized 是 Java 中最具代表性的互斥同步手段之一,它在底层实现上并不依赖于 Lock 接口及其实现类。Synchronized 所依赖的是 JVM 内部的监视器锁(monitor)。在竞争程度较低的场景下,Synchronized 可以提供较高的性能。在 JVM 对 Synchronized 进行优化后,如使用偏向锁、轻量级锁等,能使其在无竞争和轻度竞争情况下避免重量级锁使用操作系统互斥量带来的性能消耗。

Synchronized 属于 JVM 的内置锁,Synchronized 方法或代码块在编译后,会在字节码层面有一对 monitorenter 和 monitorexit 指令,分别表示获取锁和释放锁。

当一个线程试图获取某个对象的监视器(也叫做监控锁或同步锁)时,它会执行 monitorenter 指令。这个指令会把对象引用加载到操作数栈中,然后尝试获取这个对象所指向的对象的锁。如果获取成功,那么这个线程将成为该对象的所有者,其他线程必须等待锁被释放才能获取。当线程退出同步代码块或调用 wait() 方法时,monitorexit 指令负责释放锁。

深入学习JVM04 线程与变量_JVM_15

深入学习JVM04 线程与变量_JVM_16

深入学习JVM04 线程与变量_JVM_17

深入学习JVM04 线程与变量_JVM_18

深入学习JVM04 线程与变量_JVM_19

深入学习JVM04 线程与变量_JVM_20






【版权声明】本文内容来自摩杜云社区用户原创、第三方投稿、转载,内容版权归原作者所有。本网站的目的在于传递更多信息,不拥有版权,亦不承担相应法律责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@moduyun.com

  1. 分享:
最后一次编辑于 2023年11月12日 0

暂无评论

推荐阅读
  zzJeWaZlVwfH   2023年11月02日   45   0   0 常量池方法区jvm
  dBFTbkVLMBge   2023年11月02日   69   0   0 javajvm
  dBFTbkVLMBge   2023年11月12日   20   0   0 jvm
dBFTbkVLMBge