共计 17494 个字符,预计需要花费 44 分钟才能阅读完成。
内存模型有多少种
内存模型通常指计算中的存储器模型,常见的有以下几种:
- Von Neumann 模型:该模型将程序和数据存储在同一块存储器中,应用雷同的总线进行数据传输。
- Harvard 模型:该模型将程序和数据存储在不同的存储器中,应用不同的总线进行数据传输。
- 混合模型:该模型联合了 Von Neumann 和 Harvard 模型的特点,既能够共享存储器,也能够离开存储程序和数据。
- NUMA 模型:该模型应用多个处理器和本地存储器,每个处理器能够拜访本地存储器和共享存储器。
- 分布式模型:该模型应用多台计算机形成的集群,每台计算机都有本人的存储器,通过网络进行通信和协同计算。
不同的内存模型实用于不同的计算机架构和利用场景,抉择适合的内存模型能够进步计算机的性能和可靠性。
java 应用的是哪种
Java 应用的是 Von Neumann 模型。在 Java 中,程序和数据都存储在同一块内存中,被称为 Java 虚拟机内存(JVM Memory),这些内存被划分为不同的区域,包含堆、栈、办法区等,每个区域有不同的作用和生命周期。Java 程序通过指针(援用)来拜访内存中的对象和数据,应用 JVM 外部的垃圾回收机制来治理堆内存的调配和开释,进步了程序的安全性和稳定性。
能够聊一下 JMM
JMM 是 Java 内存模型(Java Memory Model)的缩写,它定义了在多线程环境下,线程之间如何拜访共享内存。JMM 基于 Von Neumann 模型,为 Java 提供了一个形象的内存模型,规定了如何操作内存和如何保障内存操作的可见性和有序性。
JMM 规定了一组规定,称为 happens-before 规定,用于确定一个操作的后果对于其余操作的可见性和有序性。happens-before 规定包含以下几种:
- 程序程序规定(Program Order Rule):在一个线程中,依照程序代码的先后顺序执行的操作,肯定会依照这个程序被其余线程所察看到。
- volatile 变量规定(Volatile Variable Rule):对一个 volatile 变量的写操作先于对同一个变量的读操作,保障了在多线程环境下的可见性。
- 锁规定(Lock Rule):一个 unlock 操作先于后续的 lock 操作,保障了线程间的同步和可见性。
- 传递性规定(Transitive Rule):如果 A happens-before B,且 B happens-before C,则 A happens-before C。
- 线程启动规定(Thread Start Rule):一个线程的启动操作先于该线程执行的任何操作。
- 线程终止规定(Thread Termination Rule):一个线程的所有操作都先于该线程的终止操作。
- 中断规定(Interruption Rule):对线程 interrupt() 办法的调用先于被中断线程的代码检测到中断事件。
JMM 为多线程程序提供了统一的内存拜访规定和保障,防止了因为指令重排、内存可见性等问题导致的线程平安问题。同时,JMM 也对程序员提出了更高的要求,须要正当地应用同步机制、volatile 变量等机制来保障程序的正确性和可靠性。
实用场景有哪些
JMM 次要实用于须要应用多线程编程的场景,例如:
- 服务器端应用程序:服务器端应用程序通常须要同时解决多个申请,应用多线程能够进步并发性能和响应速度。
- 多媒体解决应用程序:多媒体解决应用程序通常须要同时进行多个工作,应用多线程能够实现多任务并行处理。
- 游戏开发:游戏开发须要实现简单的逻辑和交互,应用多线程能够进步游戏性能和用户体验。
- 大数据处理:大数据处理须要对海量数据进行计算和剖析,应用多线程能够进步数据处理的效率和速度。
- 图形界面应用程序:图形界面应用程序须要同时响应多个用户操作,应用多线程能够进步界面的响应速度和晦涩度。
须要留神的是,多线程编程须要审慎应用,须要思考线程平安和死锁等问题,程序员须要正当地设计和应用线程,保障程序的正确性和可靠性。同时,对于单线程程序,也能够通过异步编程、事件驱动等形式实现相似的成果。
JDK17 中如何应用 Unsafe 呢
在 JDK 17 中,Oracle 曾经将 sun.misc.Unsafe 类标记为废除(Deprecated),不再举荐应用该类。这是因为 Unsafe 类具备十分高的危险性和复杂性,容易导致内存透露和 JVM 解体等问题。不过,如果你的确须要应用 Unsafe 类,能够依照以下步骤应用:
-
在 Java 代码中导入 sun.misc.Unsafe 类:
arduinoCopy code import sun.misc.Unsafe;
-
通过反射获取 Unsafe 类的实例,如下所示:
csharpCopy codeField f = Unsafe.class.getDeclaredField("theUnsafe"); f.setAccessible(true); Unsafe unsafe = (Unsafe) f.get(null);
这里应用反射的形式获取 Unsafe 类的实例,须要留神的是,该形式可能会被 JVM 的平安机制拦挡。
-
应用 Unsafe 类的办法进行操作,例如:
csharpCopy codelong address = unsafe.allocateMemory(1024); unsafe.setMemory(address, 1024, (byte) 0);
这里应用 allocateMemory() 办法调配一段内存,而后应用 setMemory() 办法初始化内存内容。
须要留神的是,应用 Unsafe 类须要十分小心和审慎,因为该类具备十分高的危险性和复杂性,容易导致内存透露和 JVM 解体等问题。因而,如果可能的话,应该尽量避免应用 Unsafe 类,应用 Java 规范库提供的办法进行操作。
我是想手动触发 内存屏障 所以应用 unsafe 的 storeFence 办法 还有其余办法吗
在 Java 中,能够应用 Unsafe 类提供的以下办法来手动触发内存屏障:
- storeFence() 办法:该办法用于在写操作后插入一个 Store Barrier,确保该操作之前的所有内存操作都曾经实现。
- loadFence() 办法:该办法用于在读操作前插入一个 Load Barrier,确保该操作之后的所有内存操作都曾经实现。
- fullFence() 办法:该办法用于插入一个 Full Barrier,确保所有内存操作都曾经实现。
以下是示例代码:
csharpCopy codeimport sun.misc.Unsafe;
public class MemoryBarrierExample {private static Unsafe unsafe = Unsafe.getUnsafe();
public static void main(String[] args) {Object obj = new Object();
long offset = unsafe.objectFieldOffset(Object.class.getDeclaredFields()[0]);
// 应用 storeFence 办法插入 Store Barrier
unsafe.storeFence();
unsafe.putObject(obj, offset, "Hello, World!");
// 应用 loadFence 办法插入 Load Barrier
unsafe.loadFence();
String str = (String) unsafe.getObject(obj, offset);
System.out.println(str);
// 应用 fullFence 办法插入 Full Barrier
unsafe.fullFence();
// ...
}
}
须要留神的是,应用内存屏障须要十分小心和审慎,不当的应用可能会导致程序呈现重大的内存问题。因而,倡议在必要时应用内存屏障,并且须要深刻理解内存模型和多线程编程的相干常识。
除了应用 unsafe 类 还有其余办法吗
除了应用 Unsafe 类外,Java 中还有其余形式能够触发内存屏障。
- synchronized 关键字:synchronized 块能够作为内存屏障,确保在 synchronized 块外部的所有写操作都曾经实现后,能力让其余线程看到共享变量的新值。同理,在 synchronized 块内部,所有的读操作都会先读取共享变量的新值,而后再读取其余变量的值。
- volatile 关键字:volatile 变量的读操作和写操作都会作为内存屏障,确保在写操作实现后,其余线程可能立刻看到共享变量的新值。
- Atomic 类:Java 中的 Atomic 类提供了一组原子操作方法,例如 compareAndSet()、getAndAdd() 等。这些办法都会应用内存屏障来确保操作的原子性和可见性。
须要留神的是,尽管 synchronized、volatile 和 Atomic 类等形式能够确保内存可见性和原子性,但并不能齐全代替内存屏障,因为内存屏障能够更准确地管制内存操作的程序和可见性,以及保障内存操作的程序不会被编译器和 CPU 优化。因而,在须要准确管制内存操作的程序和可见性时,还是倡议应用内存屏障。
如何再 java 代码中应用内存屏障呢
在 Java 代码中应用内存屏障通常须要应用 Unsafe 类提供的办法,如上文所述。以下是应用 Unsafe 类中的办法来插入内存屏障的示例代码:
csharpCopy codeimport sun.misc.Unsafe;
public class MemoryBarrierExample {private static Unsafe unsafe = Unsafe.getUnsafe();
public static void main(String[] args) {
// 应用 storeFence 办法插入 Store Barrier
unsafe.storeFence();
// 执行写操作
// ...
// 应用 loadFence 办法插入 Load Barrier
unsafe.loadFence();
// 执行读操作
// ...
// 应用 fullFence 办法插入 Full Barrier
unsafe.fullFence();
// ...
}
}
须要留神的是,应用内存屏障须要十分小心和审慎,不当的应用可能会导致程序呈现重大的内存问题。因而,在必要时应用内存屏障,并且须要深刻理解内存模型和多线程编程的相干常识。同时,应用 Unsafe 类也须要分外小心,因为它提供了间接拜访内存的能力,如果使用不当可能会造成内存透露、越界拜访等问题。
JMM 的个性
JMM(Java Memory Model)是 Java 虚拟机标准定义的一组规定,用于规定多线程访问共享变量时的行为和交互规定。JMM 定义了内存模型中的各种变量、操作和屏障,并且规定了在不同场景下内存操作的程序、可见性和原子性等行为。上面是 JMM 的一些次要个性:
- 可见性:JMM 要求在一个线程中对共享变量的批改必须对其余线程可见,也就是说,当一个线程批改了共享变量的值,其余线程必须可能看到这个批改后的值。
- 原子性:JMM 要求对共享变量的读写操作必须具备原子性,即一个线程对共享变量的读写操作在同一时刻只能被一个线程执行,不会呈现数据不统一的状况。
- 程序性:JMM 要求在不同的线程中,对共享变量的操作必须依照肯定的程序执行,保障多线程访问共享变量时的正确性。JMM 定义了 happens-before 关系来规定操作之间的程序。
- 内存屏障:JMM 定义了一些内存屏障,用于确保内存操作的程序、可见性和原子性等行为。内存屏障能够分为 Load Barrier、Store Barrier 和 Full Barrier 三种类型,每种类型的内存屏障在执行时会影响不同类型的内存操作。
- 重排序:JMM 容许编译器和处理器对指令进行重排序,然而必须保障指令执行的后果不会影响程序的正确性。在必要时,须要应用内存屏障来控制指令重排序。
这些个性独特形成了 JMM 的规定和行为,为多线程编程提供了一套残缺的内存模型。在编写多线程程序时,须要深刻理解 JMM 的规定和个性,遵循 JMM 的要求,以确保程序的正确性和可靠性。
汇编中的 Lock 指令是如何达到内存屏障的成果
在汇编中,lock
指令是一种用于实现原子操作的指令,它能够在多处理器零碎中确保同一时刻只有一个处理器可能拜访某个共享内存变量,从而达到内存屏障的成果。具体来说,lock
指令能够实现以下两个性能:
- 原子操作:当一个处理器执行
lock
指令时,会向总线发送一个信号,告诉其余处理器暂停拜访内存,而后执行指定的操作。在这个操作执行结束后,处理器再向总线发送信号,告诉其余处理器能够持续拜访内存了。这个过程保障了指定操作的原子性,即在同一时刻只有一个处理器可能执行指定操作。 - 内存屏障:当一个处理器执行
lock
指令时,会将其它处理器的缓存数据和总线上的内存数据进行同步,从而达到内存屏障的成果。具体来说,执行lock
指令时,处理器会在执行指定操作之前插入一个 Store Barrier,以确保先将批改后的数据写入内存;在执行指定操作之后,会插入一个 Load Barrier,以确保从内存中读取最新的数据。
须要留神的是,lock
指令是一种比拟底层的汇编指令,间接应用它进行编程比拟艰难,容易出错。在理论的程序中,通常会应用高级语言和库来进行多线程编程,如 Java 中的 synchronized 关键字和 Lock 接口,以及 C ++ 中的 atomic 和 mutex 等。这些语言和库中封装了对 lock
指令的应用和治理,能够更加不便和平安地实现多线程编程。
lock 指令是如何让缓存生效的
在多处理器零碎中,每个处理器都有本人的缓存,处理器拜访内存时,会先从缓存中读取数据,如果缓存中没有数据,则从内存中读取数据,并将数据缓存在本人的缓存中。这样,多个处理器可能会同时缓存同一个内存地址的数据,这就会导致缓存之间的数据不统一问题。
为了解决缓存一致性问题,古代的处理器都反对一种称为 MESI 协定的缓存一致性协定。MESI 协定定义了四种缓存状态:Modified(M)、Exclusive(E)、Shared(S)、Invalid(I)。每个缓存行的状态能够由多个处理器独特保护。
当某个处理器执行 lock
指令时,它会将以后缓存行的状态设置为 Invalid(I),示意该缓存行的数据曾经生效了。这会触发总线嗅探机制,告诉其余处理器以后缓存行的状态曾经产生了变动。当其余处理器拜访该缓存行时,因为缓存行的状态为 Invalid(I),处理器会从内存中从新获取数据,并将数据缓存到本人的缓存中,从而确保所有处理器的缓存数据都是最新的。
须要留神的是,lock
指令是一种比拟底层的汇编指令,间接应用它进行编程比拟艰难,容易出错。在理论的程序中,通常会应用高级语言和库来进行多线程编程,如 Java 中的 synchronized 关键字和 Lock 接口,以及 C ++ 中的 atomic 和 mutex 等。这些语言和库中封装了对 lock
指令的应用和治理,能够更加不便和平安地实现多线程编程。
总线嗅探机制 是如何实现的
在多处理器零碎中,每个处理器都有本人的缓存,处理器拜访内存时,会先从缓存中读取数据,如果缓存中没有数据,则从内存中读取数据,并将数据缓存在本人的缓存中。这样,多个处理器可能会同时缓存同一个内存地址的数据,这就会导致缓存之间的数据不统一问题。
为了解决缓存一致性问题,古代的处理器都反对一种称为 MESI 协定的缓存一致性协定。MESI 协定定义了四种缓存状态:Modified(M)、Exclusive(E)、Shared(S)、Invalid(I)。每个缓存行的状态能够由多个处理器独特保护。
在 MESI 协定中,处理器在执行缓存操作时,须要先将本地缓存的缓存行状态更新为 Invalid(I),而后发送一个缓存生效申请到总线上,申请其余处理器将该地址的缓存行从本人的缓存中删除或更新。这个过程称为“总线嗅探”。
总线嗅探机制是由硬件实现的。在多处理器零碎中,所有处理器都通过总线连贯到共享内存,因而总线能够通过嗅探这些数据来放弃缓存的一致性。当某个处理器执行缓存操作时,总线会将申请播送给所有处理器,其余处理器会通过总线嗅探机制来检测是否存在本人的缓存中缓存了与该地址相干的数据,并更新本人的缓存状态,确保所有处理器的缓存数据都是最新的。
总线嗅探机制的实现须要思考性能和复杂度等因素,因而古代的处理器个别都采纳了一些优化措施,例如应用缓存目录(Cache Directory)或者相似于基于目录的协定(Directory-based Protocol)等技术,来进步总线嗅探的效率和可扩展性。
JMM 是如何通过 final 关键字保障可见性的
在 Java 内存模型中,final 关键字的语义被赋予了非凡的含意,它能够保障被 final 润饰的变量在多线程环境下的可见性和不可变性。
具体来说,当一个线程首次拜访被 final 润饰的变量时,JVM 会确保该变量的值曾经被正确初始化,并将该变量缓存在线程的本地内存中。当其余线程拜访该变量时,它们会从主内存中读取该变量的值,而不是从本人的本地内存中读取。
这种行为保障了被 final 润饰的变量在多线程环境下的可见性,因为它保障了所有线程都看到了雷同的变量值。同时,因为被 final 润饰的变量不能被从新赋值,它也保障了该变量的不可变性。
须要留神的是,final 关键字只能保障被 final 润饰的变量自身的不可变性,而不能保障它所援用的对象的不可变性。如果一个 final 变量援用了一个可变对象,那么该对象的状态依然能够被批改。
另外,须要留神的是,final 关键字只能保障被 final 润饰的变量在构造函数执行结束后能力被其余线程拜访。如果在构造函数中应用了未初始化的 final 变量,那么会导致未定义的行为。因而,应该保障在应用 final 变量之前,其值曾经被正确初始化。
也就是说 如果 final 润饰的是一个援用类型 就无奈保障可见性了 是吗
不完全正确。当一个对象被申明为 final 时,该对象的援用不能被重新分配,然而该对象的属性是能够被批改的。因而,如果一个 final 援用指向一个可变对象,那么该对象的状态依然能够被批改。
在这种状况下,final 关键字不能保障对象的状态不变,但它依然能够保障对象援用的可见性。具体来说,当一个线程首次拜访被 final 润饰的援用时,JVM 会确保该援用指向的对象曾经被正确初始化,并将该援用缓存在线程的本地内存中。当其余线程拜访该援用时,它们会从主内存中读取该援用的值,而不是从本人的本地内存中读取。
因而,final 润饰的援用能够保障在多线程环境下的可见性,但不能保障其指向的对象的状态不变。如果须要保障对象的状态不变,能够思考应用其余技术,例如不可变对象或者加锁等。
对象的属性是指什么 举个例子
在 Java 中,一个类的属性指的是该类的成员变量,也就是定义在类中但不在任何办法或代码块外部的变量。这些成员变量也能够称为实例变量,因为它们属于类的每个实例(对象)。
当一个 final 援用指向一个可变对象时,尽管该援用自身不能被重新分配,然而该对象的属性(成员变量)依然能够被批改,因为 final 关键字只能保障援用的可见性和不可变性,不能保障对象本身的不可变性。
上面是一个简略的例子,阐明 final 润饰援用类型变量时,对象的属性是能够被批改的:
javaCopy codeclass MyClass {
private int x;
public MyClass(int x) {this.x = x;}
public void setX(int x) {this.x = x;}
public int getX() {return x;}
}
public class Main {public static void main(String[] args) {final MyClass obj = new MyClass(10);
System.out.println(obj.getX()); // 输入 10
obj.setX(20); // 批改 obj 援用指向的对象的属性
System.out.println(obj.getX()); // 输入 20
}
}
在这个例子中,MyClass 是一个简略的类,蕴含一个公有的 int 类型成员变量 x,以及一个 setter 和 getter 办法。在 Main 类的 main 办法中,咱们应用 final 关键字将 obj 援用指向一个 MyClass 对象,并且在后续代码中批改了该对象的属性 x 的值。尽管 obj 援用自身不能被重新分配,然而它所指向的 MyClass 对象的属性 x 依然能够被批改。
只是说 final 润饰的对象 援用的地址不会被扭转 然而 这个对象的属性 是能够被更改的
是的,你说得没错。当一个对象被申明为 final 时,该对象的援用不能被重新分配,然而该对象的属性是能够被批改的。因而,如果一个 final 援用指向一个可变对象,那么该对象的状态依然能够被批改。final 关键字只是保障了援用的不可变性,而不是对象自身的不可变性。
在 Java 中,如果须要实现不可变对象,能够采纳一些技术,比方将对象的所有属性都申明为 final,并且不提供任何批改属性的办法。或者将对象的属性申明为 private,并提供 getter 办法,但不提供 setter 办法。这样能够确保对象的状态不变,并且保障在多线程环境下的安全性。
当一个根本类型 是 final 润饰的 为什么这个时候 其余线程拜访该变量会从主内存中读取 而不是从缓存中 底层是如何实现的
当一个根本类型变量被申明为 final 时,因为 final 关键字能够保障该变量的不可变性,Java 编译器会在编译期间进行优化,将该变量的值间接嵌入到程序的字节码中。这样,在运行时期间,不须要从内存中读取该变量的值,而是间接应用该变量的值,因而不会存在缓存一致性问题。
因而,当一个根本类型变量被申明为 final 时,其余线程在拜访该变量时,会间接从主内存中读取该变量的值,而不会从缓存中读取。这是因为 Java 编译器进行了优化,将该变量的值间接嵌入到程序的字节码中,因而不须要从内存中读取该变量的值。
须要留神的是,如果一个 final 变量是一个援用类型,那么其余线程拜访该变量时,依然须要从主内存中读取该变量的值,因为 final 关键字只保障了该变量援用的不可变性,而不保障该变量所援用的对象的状态不可变。因而,在多线程环境下,如果一个 final 变量援用了一个可变的对象,那么依然须要思考线程平安问题。
这里的主内存 和内存不是同一个货色吧
主内存(Main Memory)是指所有的线程共享的内存区域,是多个处理器共享的物理内存。每个线程都能够拜访主内存中的数据,然而不能间接操作主内存中的数据,而是通过将数据拷贝到本人的工作内存中进行操作,而后再将批改后的数据同步回主内存中。
内存(Memory)是指计算机中用来存储数据和指令的硬件设施,包含主内存和辅助存储器(硬盘、光盘等)等。
在 Java 内存模型中,主内存指的是所有线程共享的内存区域,是所有线程从中读取数据和写入数据的中央。而每个线程都有本人的工作内存,也叫本地内存,是指线程所应用的内存区域,存储了该线程读取的主内存中的变量的正本,每个线程的工作内存都独立于其余线程的工作内存。
因而,主内存和内存不是同一个货色,主内存是指多个线程共享的内存区域,而内存是指计算机中用来存储数据和指令的硬件设施。
也就是说 final 润饰的变量 会间接初始化在主内存中
是的,final 润饰的变量在定义时必须进行显式初始化,而且只能赋值一次,赋值后不能再被批改。因为 final 变量的值不能被批改,所以 Java 编译器会将 final 变量的值间接编译进字节码中,而不是在运行时动静计算。这样在程序运行时,final 变量的值就曾经确定下来了,并且在主内存中调配了一块空间用于存储这个变量。
因而,final 润饰的变量会间接在主内存中进行调配和初始化。其余线程在拜访 final 变量时,会间接从主内存中读取其值,而不是从本地内存或缓存中读取,这样能够保障 final 变量的值对所有线程都可见。
既然 final 保障可变量的不可变性 线程间接从本人的工作内存中读取不是更快吗 为什么还要去主内存呢
尽管 final 润饰的变量的值不能被批改,然而 final 变量依然须要保障在多线程之间的可见性。如果一个线程在本地内存中缓存了 final 变量的值,而其余线程更新了该变量的值,如果没有采取措施,该线程将无奈感知到变量值的变动。
因而,为了保障 final 变量的可见性,Java 编程语言标准要求,final 变量的值必须在主内存中调配和初始化,并且所有线程在拜访 final 变量时必须从主内存中读取其最新值,以确保线程之间的数据同步。尽管这样会减少一些额定的开销,然而因为 final 变量的非凡性质,它们通常是在程序中用于存储常量或者不变的配置信息等,因而通常不会对程序的性能产生过大的影响。
对啊 为了保障可见性 在主内存初始化就好了 反正也不会被批改 线程 读取并保留到各自的工作内存中 正是因为不会被批改 所以工作内存 和主内存中始终是同样的 那为什么要去主内存中读取
是的,final 变量的值在主内存中调配和初始化,因而线程第一次读取 final 变量时会从主内存中获取其最新值,并将其保留到线程的工作内存中。之后,线程在对 final 变量进行读取时,能够从本人的工作内存中获取其值,而不用每次都从主内存中获取。
不过,为了保障线程之间的数据同步,Java 编程语言标准要求,每个线程在拜访 final 变量时都必须从主内存中读取其最新值。这是因为,尽管 final 变量的值在初始化后不会被批改,然而 Java 内存模型并不保障不同线程之间的工作内存是同步的。如果某个线程从本人的工作内存中获取 final 变量的值,而该值尚未被其余线程写入到主内存中,那么
对啊 为了保障可见性 在主内存初始化就好了 反正也不会被批改 线程 读取并保留到各自的工作内存中 正是因为不会被批改 所以工作内存 和主内存中始终是同样的 那为什么要去主内存中读取
是的,为了保障可见性,final 变量的值在主内存中调配和初始化,线程在第一次读取 final 变量时会从主内存中获取其最新值,并将其保留到线程的工作内存中。之后,线程在对 final 变量进行读取时,能够从本人的工作内存中获取其值,而不用每次都从主内存中获取。
然而,为了保障不同线程之间的数据同步,Java 编程语言标准要求,每个线程在拜访 final 变量时都必须从主内存中读取其最新值,而不是从本人的工作内存中读取。这是因为,只管 final 变量的值在初始化后不会被批改,然而 Java 内存模型并不保障不同线程之间的工作内存是同步的。如果某个线程从本人的工作内存中获取 final 变量的值,而该值尚未被其余线程写入到主内存中,那么该线程将无奈读取到最新的 final 变量的值,从而导致数据不统一的问题。
因而,即便 final 变量的值不会被批改,每个线程在拜访 final 变量时都必须从主内存中读取其最新值,以保障不同线程之间的数据同步。
如果某个线程从本人的工作内存中获取 final 变量的值,而该值尚未被其余线程写入到主内存中,那么该线程将无奈读取到最新的 final 变量的值,从而导致数据不统一的问题。能够举个例子吗
假如有一个 final 润饰的变量 x 的初始值为 0,并且有两个线程 A 和 B 同时对它进行读写操作。初始状态下,x 的值在主内存中为 0,在 A 和 B 的工作内存中也都是 0。
接下来,线程 A 将 x 的值批改为 1,并且将该值写入主内存。此时,x 在主内存中的值变为 1,但在 B 的工作内存中仍为 0。
而后,线程 B 从本人的工作内存中读取 x 的值,因为 x 的值在主内存中曾经被批改为 1,所以 B 冀望失去的值也应该是 1。然而,因为 B 从本人的工作内存中读取 x 的值,因而 B 实际上读取的是之前从主内存中读取的值 0,导致数据不统一的问题。如果 B 间接从主内存中读取 x 的值,就能失去最新的值 1,防止了数据不统一的问题。
变量 X 是根本类型还是援用类型
变量 X 能够是根本类型或援用类型,final 润饰的规定实用于任何类型的变量。
final 润饰的不是不能够被批改吗
final 润饰的变量不可被批改指的是变量的值不可变,而不是变量自身不可变。即对于根本类型的 final 变量,其值不可被批改;对于援用类型的 final 变量,其援用不可被批改,但其所指向的对象的内容是能够被批改的。
如果 X 是根本类型 X 初始化的值就是 0 前面还能够批改成 1?
如果一个根本类型的变量被申明为 final,则它的值在初始化后不可被批改。在初始化时能够为其赋一个值,然而之后就不能再扭转它的值了。如果 X 是根本类型,申明为 final 并初始化为 0,那么在程序中就不能再批改它的值了。如果程序中尝试将 final 变量的值批改为 1,则会编译谬误。
那方才举的例子 如果变量 X 是根本类型 是不是就不对了
是的,如果变量 X 是根本类型,那么 final 润饰的变量就是不可批改的。在这种状况下,X 的初始值就是 final 变量的值,其余线程无奈批改该值。因而,在多线程环境下,其余线程读取 final 变量的值时,将会读取到最新的值,并且不会存在数据不统一的问题。
如果是援用类型呢
如果是援用类型,final 关键字只会保障被润饰的援用自身不可变,即不能再指向其余对象,然而被援用的对象自身的状态是能够批改的。举个例子:
javaCopy codepublic class MyClass {private final List<Integer> myList = new ArrayList<>();
public void addToList(Integer i) {myList.add(i);
}
public List<Integer> getMyList() {return myList;}
}
在这个例子中,myList
被申明为 final
,这意味着在MyClass
实例被创立后,无奈再将 myList
指向其余的 List 对象。然而,咱们依然能够通过调用 addToList()
办法向 myList
中增加元素,也能够通过调用 getMyList()
办法获取到 myList
并进行批改。因而,final
关键字并不能保障援用的对象状态的不可变性。
明确了 也就是说 每次都从主内存读取 次要还是针对 援用类型的
是的,final 润饰的援用类型变量,其地址不可变,然而它所指向的对象自身是能够扭转的。因而,在读取 final 润饰的援用类型变量时,须要从主内存中读取最新值,以确保读取到最新的对象。而根本类型变量,其在初始化时曾经在主内存中赋值,之后的批改也会同步到主内存中,因而在读取时能够间接从本人的工作内存中读取最新值。
不对吧 根本类型 初始化是在主内存中复制 之后就无奈批改了
是的,你说得没错。如果一个根本类型被申明为 final,那么它的值在初始化后就无奈批改了,并且其余线程能够从主内存中读取该变量的值。因而,如果变量 X 是一个根本类型,那么 final 修饰符确保了其不可变性和可见性。每次拜访 final 变量时,JVM 都会从主内存中读取该变量的最新值,以确保可见性。
再往底层是话是怎么实现的
在底层硬件实现上,CPU 通常会有一个高速缓存,用于存储从主存储器中读取的数据。当 CPU 读取一个变量时,它会首先查看它的高速缓存,如果该变量的值曾经在高速缓存中,则间接返回缓存中的值,否则会从主存储器中读取该变量的值,并将其存储在高速缓存中。
当一个变量被申明为 final
时,编译器会在编译时生成特定的指令,以保障该变量在初始化之后不会被批改。这些指令通常会被转化为底层硬件的指令,例如应用 mov
指令将该变量的值从主存储器中读取到高速缓存中,并应用 mfence
指令刷新 CPU 的写缓冲区,以保障批改后的值可能被其余 CPU 可见。因而,即便是 final
变量,当 CPU 读取该变量时,依然须要从主存储器中读取,以保障多个线程之间的可见性。
须要留神的是,这里提到的底层实现形式可能会因为不同的 CPU 架构和编译器而有所差别。
那底层应该是没有用到内存屏障的吧
实际上,在 JVM 的实现中,应用了相似于内存屏障的机制来保障 final 变量的可见性。
在 Java 虚拟机标准中,对 final 变量的内存模型做了如下规定:final 变量在结构器中初始化后,就不能再被批改。因而,在结构器中,对于 final 变量的写入操作能够间接写入到主内存中,而不须要通过写入到工作内存再写入到主内存的过程。
对于读取 final 变量,虚构机会尝试间接从线程的工作内存中读取,如果发现工作内存中没有该变量的值,就会去主内存中读取最新的值,并将其写入到工作内存中。这个过程就相当于读取 final 变量时的内存屏障。
因而,尽管在底层实现中可能没有间接应用内存屏障,然而 JVM 会通过其余的机制来保障 final 变量的可见性。
其余机制是什么呢 还能够再深刻吗
当线程从主内存中读取 final 变量的值时,会应用一种称为“volatile 读取”的机制。在这种机制下,线程在读取变量值时,会强制刷新本人的本地内存,并从主内存中读取最新的变量值,以确保可见性。
在这个过程中,JVM 底层会应用一些 CPU 指令和硬件机制,如 load-load 屏障和 memory fence,来确保变量的可见性和有序性。具体实现机制可能会因 CPU 和 JVM 的版本而有所不同,但都会保障 final 变量的可见性和有序性。
须要留神的是,只有应用了 volatile 润饰的变量能力保障读写的原子性,final 润饰的变量不能保障原子性。
volatile 读取 底层也是内存屏障啊
是的,volatile 读取底层也是用到了内存屏障来保障可见性和禁止重排序。在 Java 内存模型中,对于 volatile 变量的读操作会插入一个 LoadLoad 屏障和一个 LoadStore 屏障,LoadLoad 屏障保障该操作之前的所有 load 操作都实现了,LoadStore 屏障则保障该操作之前的所有 load 和 store 操作都实现了。这样能够保障在 volatile 变量的读操作之前,所有其余线程对该变量的批改操作都曾经实现了,保障了可见性。同时,也能够保障 volatile 变量的读操作不会被重排序到该操作之后,防止了指令乱序的影响。
那也就是说 final 底层也是内存屏障
不完全正确。final 的底层实现与内存屏障没有间接关联。final 次要是通过 JVM 的编译器优化来实现的,编译器在编译时会对 final 润饰的变量进行非凡解决,使得在应用 final 变量时间接应用该变量的值,而不是从主内存中读取该变量的值。这样能够防止多线程之间的竞争和不一致性问题。
因而,尽管 final 的实现可能会波及到内存屏障,但 final 自身并不是一种内存屏障。
为什么说可能会波及到内存屏障
因为 final
关键字在不同的编译器和不同的 JVM 实现中,具体的实现形式可能不同,有些可能会波及到内存屏障,有些可能不会。在某些状况下,编译器或 JVM 可能会应用内存屏障来确保 final
关键字的语义正确实现。然而,具体实现细节是由编译器和 JVM 厂商决定的,不同的实现可能采纳不同的机制来保障 final
的语义。
如果是 linux x86 呢
在 Linux x86 上,JVM 实现了一种叫做 “StoreStore” 内存屏障的机制来确保 final 变量的可见性。在 StoreStore 内存屏障后的所有指令都必须期待之前的写入操作全副实现后能力执行,从而确保任何线程读取 final 变量时都能看到最新的值。然而,在读取 final 变量时不须要进行额定的内存屏障操作,因为 final 变量的值曾经在主内存中初始化,并且不会被批改,所以线程能够间接从本人的工作内存中读取最新的值。
Thread.yield 是如何保障可见性的
Thread.yield()
办法能够让以后线程让出 CPU,让其余线程有机会执行。它并没有显式地保障可见性,然而能够间接地帮忙实现可见性。
假如有一个线程批改了共享变量的值,而后调用了 Thread.yield()
办法。这将会使以后线程让出 CPU,给其余线程执行的机会。如果另一个线程在此时执行,并读取了批改后的共享变量的值,那么它将会失去最新的值。这样,间接地实现了可见性。
然而,须要留神的是,Thread.yield()
办法并不是保障可见性的正式机制,因为它并不能保障其余线程立刻执行。如果其余线程不立刻执行,那么就可能存在可见性问题。因而,要想保障可见性,倡议应用更为严格的同步机制,例如 synchronized
关键字或者 java.util.concurrent
包中的类。
非凡状况下 如果线程的获取 CPU 优先级高 这个时候批改了共享变量的值 及时调用了 Thread.yield 有可能 下一个获取到 cpu 的还是它 所以就生效了
是的,如果一个线程的优先级较高,它可能会间断屡次获取到 CPU 资源,从而导致 Thread.yield()生效,因为 yield()只是给其余优先级雷同或更高的线程一个执行机会,如果以后线程优先级最高,那么其余线程也就无奈抢夺 CPU 资源,就算执行了 yield()也无奈保障可见性。
因而,Thread.yield()办法并不是一个牢靠的保障可见性的办法,它只是一个帮助线程调度的工具。如果须要保障可见性,应该应用更加牢靠的形式,例如应用 volatile 关键字或者加锁来保障可见性。
为什么 如果其余线程执行 就能保障可见性
如果其余线程执行,会导致 CPU 缓存中的数据被刷新回主内存,从而使得另一个线程在读取该共享变量时可能获取到最新的值,从而保障了可见性。当然,这仅仅是一种可能的状况,也有可能呈现 CPU 缓存始终没有被刷新回主内存的状况,此时依然可能存在可见性问题。因而,在多线程编程中,咱们还须要应用其余机制来保障可见性,例如应用 volatile 关键字或者 synchronized 关键字等。
也就是说 不同线程之间的切换会间接保障可见性?
是的,因为不同线程之间的切换会波及到 CPU 缓存和主存之间的数据同步,从而保障了可见性。当一个线程批改了共享变量的值后,如果它放弃了 CPU 使用权,那么其余线程可能会被调度并开始执行。在开始执行之前,这些线程须要先将本人的工作内存中的数据与主存中的数据进行同步,以确保读取到最新的共享变量的值。因而,即便这些线程没有被动调用同步办法或者同步块,它们在被调度执行时也会进行数据同步,从而保障了可见性。
原来是因为线程在执行前会现将内存数据同步
是的,线程在执行前会将内存中的数据同步到本人的工作内存中,以保障线程获取的变量值是最新的。如果一个变量被多个线程共享,那么当其中一个线程批改了该变量的值,其余线程在执行时会查看该变量的值是否有更新,如果有更新则会将主内存中最新的值同步到本人的工作内存中,以保障可见性。这个同步操作能够通过内存屏障等机制来实现。
也就是说 我想保障线程之间数据同步 只须要告诉其余线程 什么时候去取最新数据就好了
告诉其余线程获取最新数据是一种办法,但具体要怎么实现会根据具体情况而定。在 Java 中,能够应用 synchronized 和 Lock 等机制来保障线程之间的数据同步和可见性。另外,Java 中的 volatile 关键字也能够保障可见性,即当一个线程批改了 volatile 变量的值时,其余线程能够立刻看到这个变量的最新值。当然,在实现上,具体要用哪种机制还须要依据场景和需要进行抉择。
会不会存在 A 线程批改了共享变量 让出工夫片给 B 线程 B 线程去主内存获取最新数据 刚获取完 还没有应用 C 线程又更改了这个共享变量
是的,这种状况是可能产生的,因为线程调度是由操作系统进行治理的,而操作系统在调度线程时是不会思考线程之间的共享变量的,也就是说,A 线程在批改共享变量后,让出 CPU 工夫给 B 线程时,如果此时 C 线程也在批改该共享变量,那么 B 线程在从主内存中获取共享变量的最新值后,可能失去的并不是 A 线程所批改的值,而是 C 线程批改后的值,从而导致数据不统一的问题。
为了解决这个问题,能够采纳 synchronized 或者 Lock 来保障线程间的同步,或者应用 volatile 关键字来保障共享变量的可见性,从而防止线程间数据不统一的问题。
不是 是 A 批改了 而后 B 获取到了 A 批改的数据 同时 C 又批改了
如果 A 线程批改了共享变量,而后让出工夫片给 B 线程,B 线程从主内存获取了最新数据并开始应用。此时,C 线程批改了共享变量,如果 C 线程对该共享变量的批改还没有写入主内存,那么 B 线程是不会看到这个批改的。然而如果 C 线程曾经将批改写入主内存,那么下次 B 线程从主内存获取共享变量时就会失去最新的值。
这个过程中,如果是在共享变量前后插入了内存屏障,那么这个过程中的数据同步会更加牢靠。
也就是说在 B 执行完了 才容许 C 批改 这样就能够保证数据一致性了吗
是的,这样能够保证数据一致性。在 Java 中,线程之间的操作是有程序的,也就是说如果线程 A 先对共享变量进行了批改,那么在其余线程 B、C 等读取该变量之前,JVM 会保障 A 的批改对其余线程可见,即便 B 和 C 是并发执行的。具体来说,在 JVM 中,当一个线程写入一个共享变量时,JVM 会向处理器发送一条 lock 指令,这个指令会把缓存中的变量值写回到主存中,这个写回操作会导致其余线程的缓存有效,即其余线程会从新从主存中读取共享变量的值,这样就保障了共享变量的可见性。所以,在 B 读取共享变量时,它读取到的是 A 批改后的最新值,而不是 C 批改的值。
那如果我想让 B 读取到的是 C 的值呢
如果你想让 B 读取到 C 的值,那么你须要在 B 中减少对共享变量的读取操作,这样在 B 读取共享变量的时候就会失去最新的值。然而须要留神的是,这种做法会减少线程间的竞争,可能会影响程序的性能。
减少读取操作是指什么
减少读取操作是指在 B 读取共享变量之前减少一次读取操作,从而使得 B 可能读取到最新的值。例如,在 B 线程中能够增加一行读取操作,比方再次读取共享变量的值,这样能够保障 B 线程读取到的是最新的值。
也就是说 B 读取到之后开始应用 而后是应用之前再读取一次
是的,您能够在 B 线程开始应用共享变量之前再进行一次读取操作,以获取最新的值。这样能够确保 B 线程应用的是 C 线程批改后的值。例如,在 B 线程中能够进行如下操作:
javaCopy code// 获取共享变量的值
int value = sharedVariable;
// B 线程开始应用共享变量,例如进行一些计算操作
// ...
// 应用之前再读取一次共享变量的值,确保是最新值
value = sharedVariable;
当 B 线程开始应用共享变量时,它会先读取共享变量的值,而后进行一些计算操作。在这之后,B 线程再次读取共享变量的值,以确保它应用的是最新值。这样能够保障 B 线程应用的是 C 线程批改后的值,而不是批改之前的旧值。
这样的话会不会有问题
在某些状况下可能会呈现问题。如果在 B 应用共享变量的过程中,C 也正在批改这个变量,那么 B 在应用完共享变量后再次读取变量时可能曾经不是最新值了,因为 C 的批改可能曾经被笼罩了。这种状况下,可能须要应用一些同步机制,比方锁,来保障 B 应用变量时的安全性。
会不会有两个线程同时失去 CPU
在单个 CPU 的状况下,不会有两个线程同时失去 CPU,只会有线程间的切换。线程间的切换能够通过操作系统的线程调度器来管制,通过工夫片轮转或者优先级调度等算法,保障每个线程都可能被调配到足够的 CPU 工夫片进行执行。在多个 CPU 的状况下,多个线程能够同时被调配到不同的 CPU 进行执行。
然而多个 CPU 是共享一个主内存的 是吧
是的,不同的 CPU 外围共享同一块主内存。所以当一个线程批改共享变量后,其余线程须要通过主内存来获取最新的值。
本文由博客一文多发平台 OpenWrite 公布!