深入理解Java虚拟机

深入理解Java虚拟机

Java技术体系中所提倡的自动内存管理最终可以归结为自动化地解决了两个问题:给对象分配内存以及回收分配给对象的内存。

1.JVM是什么?JRE是什么?JDK是什么?三者有什么关系?

答:JVM:Java虚拟机

JRE:Java Runtime Environment,Java 运行环境。如果只想运行一个开发好的java程序,只需要安装一个JRE就行。

JDK:Java Development Kit,Java开发工具包。提供给Java开发人员所用,用来开发java程序的。

三者关系:JDK包括JRE,JRE包括JVM。安装一个JDK,万事搞定!

2.在JVM中,类从被加载到虚拟机内存中开始,到卸载出内存为止,它的整个生命周期包括哪几个阶段?

答:7个阶段,分别是:加载、验证、准备、解析、初始化、使用和卸载。

3.java有多少个类加载器?分别的作用是什么?一个Class文件是怎么被加载到JVM里的,描述一下加载流程。

答:java有三个类加载器,分别为:根类加载器扩展类加载器系统类加载器。根类加载器负责java核心类的加载,扩展类加载器负责扩展jar包的加载;系统类加载器负责自定义类的加载。

类加载的全过程包括加载验证准备解析初始化5个阶段。其中,验证、准备、解析三个部分统称为连接。

加载阶段。虚拟机利用类加载器将Class文件加载到内存中,准确的讲,是加载到内存中的方法区,并为这个Class文件生成一个Class文件对象(类的字节码对象),作为方法区中这个Class文件的访问入口。(注意,Class文件存放在方法区,但是Class文件对象并不一定存放在堆中,还可能在方法区中)

验证阶段。这一阶段的目的是为了确保Class文件中包含的信息符合当前虚拟机的要求,并且不会危害虚拟机自身的安全。

准备阶段。这一阶段是正式在方法区内为类变量分配内存,并设置初始值。

这里有两个特别容易混淆的概念需要强调一下。首先,这个时候进行内存分配的类变量指的仅是静态变量,而不是成员变量(实例变量),成员变量将会在对象实例化时随着对象一起分配在堆内存中。其次,这里的初始值指的是数据类型的零值(假设有一个类的静态变量为static int value = 123,那变量value在准备阶段过后的初始值为0而不是123)。

解析阶段。这一阶段是将类文件中的符号引用替换为直接引用。

符号引用与虚拟机的内存布局无关,引用的目标并不一定加载到内存中。在java中,一个java类将会被编译成一个Class文件。在编译时,java类并不知道所引用的类实际地址,因此只能使用符号引用来替代。比如People类引用了Language类,在编译时People类并不知道Language类的实际内存地址,因此只能使用符号org.simple.Language(假设是这个符号,当然,实际不是)来表示Language类的地址。

直接引用是和虚拟机的布局相关的,同一个符号引用在不同的虚拟机实例上翻译出来的直接引用一般不会相同。直接引用就是类的实际内存地址,如果有了直接引用,那引用的目标必定已经被加载入内存中了。

初始化阶段。执行类中定义的java代码,初始化类变量和其它资源(假设有一个类的静态变量为static int value = 123,准备阶段过后,value初始值为0,而初始化阶段,就是执行java代码,将123赋给value,value的初始值为123)。

4.在语言层面上,创建对象(例如克隆、反序列化)通常仅仅是一个new关键字而已,而在虚拟机中,对象(文中讨论的对象限于普通Java对象,不包括数组和Class对象等)的创建又是怎样的一个过程呢?

答:虚拟机遇到一条new指令时,首先检查相应的类文件是否已被加载到内存中,如果没有,就必须先执行相应的类加载过程。

在类加载完毕后,虚拟机将会在堆内存中为新生对象分配内存空间。

内存分配完毕后,虚拟机需要将分配到的内存空间都初始化为零值。这一步操作保证了对象的实例字段在Java代码中可以不赋初始值就直接使用,程序能访问到这些字段的数据类型所对应的零值。

接下来,虚拟机要对对象进行必要的设置(具体做了哪些设置,不需要知道)。

在上面的工作都完成之后,从虚拟机的视角来看,一个新的对象已经产生了,但从Java程序的视角来看,对象的创建才刚刚开始,因为这时所有的字段都还为零。所以,执行new指令之后会接着执行<init>方法,把对象按照程序员的意愿进行初始化,这样一个真正可用的对象才算完全产生出来。

 

5.Java和C/C++,有一点不同之处在于:Java程序员不需要考虑内存管理和垃圾回收,因为这些操作都是由Java虚拟机自动完成的,既然都已经实现“自动化”了,那为什么还要学习JVM的内存机制和垃圾回收机制呢?(为什么要学JVM?)

答:原因很简单,当出现各种内存溢出、内存泄漏问题时,当垃圾收集成为系统达到更高并发量的瓶颈时,如果你对垃圾收集算法、内存分配机制等一窍不通,那你就无法去排查和解决这些问题。

6.简介一下JVM的内存区域(运行时数据区)。

答:为了提高程序的效率,JVM中的内存区域具体划分了如下5个内存空间,分别是虚拟机栈Java堆方法区本地方法栈程序计数器

虚拟机栈:(官方解释)栈内存描述的是Java方法执行的内存模型:每个方法在执行的同时都会创建一个栈帧用于存储局部变量表、操作数栈、动态链接、方法出口等信息,每一个方法从调用直至执行完成的过程,就对应着一个栈帧在虚拟机栈中入栈到出栈的过程。

(通俗解释)虚拟机栈是用来管理线程运行时方法的。一个运行时方法对应一个栈帧,虚拟机栈通过栈帧来管理正在运行的方法,栈帧随着方法的调用而入栈,随着方法的结束而出栈。一个栈帧又被划分了多个区域,如局部变量表、操作数栈、动态链接、方法出口等区域,每个区域用来管理运行时方法里的不同数据,比方局部变量表里存放的是方法里的局部变量,方法出口里面存放的是方法的返回值信息等。

注意:栈内存存在内存溢出问题,一个线程对应一个虚拟机栈,是线程安全的。

Java堆:存放的是所有new出来的东西。包括数组和对象实例(成员变量及类中方法在方法区中的内存地址)。

注意:并不是所有的对象都会放在堆内存中;堆内存是垃圾回收器管理的主要区域;堆内存存在内存溢出问题;Java堆是线程共享的运行时数据区,是线程不安全的。

方法区:类信息(被虚拟机加载的Class文件),常量(自定义常量和字符串常量,放在运行时常量池中),静态变量等。

注意:方法区中有一部分叫运行时常量池,该常量池中存放着常量以及字符串常量;方法区也会被垃圾回收器管理;方法区存在内存溢出问题;方法区是线程共享的运行时数据区,是线程不安全的。

本地方法栈:(官方解释)本地方法栈与栈所发挥的作用非常相似,它们之间的区别不过是栈为虚拟机执行Java方法服务,而本地方法栈则为虚拟机使用到的Native方法服务。

(通俗解释)本地方法栈的内存机制与虚拟机栈类似。Java中有两种方法,一种是自己用java代码写的方法,一种是JDK自带的本地方法,有native关键字修饰,如Date类里面的currentTimeMillis()等,本地方法栈是用来管理运行时的本地方法的。

注意:本地方法栈存在内存溢出问题;一个线程对应一个本地方法栈,是线程安全的。

程序计数器:(官方解释)程序计数器是一块较小的内存空间,是“线程私有”的内存空间。由于java虚拟机的多线程是通过线程轮流切换并分配处理器执行时间的方式来实现的,在任何一个确定的时刻,一个处理器(对于多核处理器来说是一个内核)都只会执行一条线程中的指令。因此,为了线程切换后能恢复到正确的执行位置,每条线程都需要有一个独立的程序计数器,各条线程之间计数器互不影响,独立存储,我们称这类内存区域为“线程私有”的内存。

(通俗解释)线程A执行到一半,CPU执行权被线程B抢走了,当线程B执行完后,线程A抢到CPU执行权,需要从断开的位置继续向下执行,如果来确定断开的位置在哪呢?这就需要程序计数器了,一个线程对应一个程序计数器,程序计数器是用来专门管理线程的内存空间。

注意:程序计数器主要用来管理线程,一个线程对应一个程序计数器,是线程安全的;程序计数器不存在内存溢出问题。

7.方法递归调用时,虚拟机栈中是只有一个栈帧,还是有多个栈帧?

答:多个栈帧。经过测试,当无线递归调用时,会发生内存溢出异常,所有可得出结论,递归调用的方法,在虚拟机栈中会创建多个栈帧。 

8.虚拟机栈中,一个栈帧对应一个运行时方法,栈帧与栈帧之间大小相等吗?

答:不一定相等。因为栈帧中的局部变量表里存放着方法的局部变量,每个方法所包含的局部变量大小不一定相等,所以局部变量表不一定相等,所以栈帧不一定相等。 

9.堆内存是如何为这些对象分配空间的呢?

答:堆内存可分为两部分,分别是:新生代和老年代。假如堆内存大小为20M,那么新生代10M,老年代10M。

新生代又分为3部分,一个Eden和两个Survivor。其中,Eden:Survivor:Survivor = 8M:1M:1M,每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),即1个Eden和1个Survivor,只有10%的内存会被浪费。当有对象进来时,会优先在新生代中的Eden中进行分配,当Eden区没有足够空间进行分配时,虚拟机将发起一次Minor GC(新生代的垃圾回收)。

新生代中是通过复制回收算法来实现垃圾回收操作的,思路如下:当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块空Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。当新生代中存活的对象大于空Survivor的最大容量时,Survivor空间不够用,需要依赖其他内存(这里指老年代)进行分配担保,也就是说要把超出Survivor最大容量的对象存放到老年代中。

有些对象在分配的时候会直接进入老年代。

1. 大对象直接进入老年代。虚拟机可设置一个参数,假如参数值为3M,当一个对象的大小超过3M时,这个对象就不会往新生代中存放,而是直接放到老年代中。

2. 长期存活的对象直接进入老年代。既然虚拟机采用了分代收集的思想来管理内存,那么堆内存在为对象分配内存时,就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这一点,虚拟机为每个对象定义了一个对象年龄计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1。对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),被视为长期存活的对象,就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数进行设置。

当老年代中的内存不够用时,会触发Major GC(老年代的垃圾回收),老年代进行垃圾回收采用的是标记-清除算法或者标记-整理算法

10.什么情况下,对象会存放在新生代?

答:对象会优先存放在新生代的Eden空间中,如果Eden没有足够的连续空间存放该对象,会将该对象存放在新生代其中1个Survivor空间中,如果Eden和1个Survivor空间中均没有足够大的连续空间存放该对象,那么新生代会进行一次新生代的垃圾回收(Minor GC),将Eden和其中1个Survior中存活的对象复制到另一个Survivor空间中(如果另一个Survivor没有足够大的连续空间来接收这些对象,将会把超出部分的对象利用分配担保机制存放到老生代中),然后清空Eden和其中1个Survior的内存空间,再将该对象存放到Eden中。

11.什么情况下,对象会被存放在老年代?

答:1.当新生代中没有足够大的连续空间来存放这个对象时,会通过分配担保机制将该对象存放在老年代中。

2.大对象直接存放在老年代。

3.长期存活的对象将存放在老年代。

12.长期存活的对象指什么对象?

答:既然虚拟机采用了分代收集的思想来管理内存,那么内存回收时就必须能识别哪些对象应放在新生代,哪些对象应放在老年代中。为了做到这点,虚拟机给每个对象定义了一个对象年龄(Age)计数器。如果对象在Eden出生并经过第一次Minor GC后仍然存活,并且能被Survivor容纳的话,将被移动到Survivor空间中,并且对象年龄设为1.对象在Survivor区中每“熬过”一次Minor GC,年龄就增加1岁,当它的年龄增加到一定程度(默认为15岁),就将会被晋升到老年代中。对象晋升老年代的年龄阈值,可以通过参数-XX:MaxTenuringThreshold设置。

13.你知道哪些垃圾回收算法,垃圾回收的时候怎么判断一个对象是否可以被回收?

:常见的垃圾回收算法有标记-清除算法复制回收算法标记-整理算法分代收集算法

标记-清除算法:是最基础的垃圾回收算法。如其名一样,算法分为“标记”和“清除”两个阶段:首先标记出所有需要回收的对象,在标记完成后统一回收所有被标记的对象。之所以说是最基础的垃圾回收算法,是因为后面几个都是基于它的思路并改其不足而得到的。

缺点:一个是效率问题,标记和清除两个过程的效率都不高;另一个是空间问题,标记清除之后会产生大量不连续的内存碎片,空间碎片太多可能会导致以后在程序运行过程中需要分配较大对象时,无法找到足够的连续内存而不得不提前触发另一次垃圾回收动作。

复制回收算法:为了解决效率问题,一种称为“复制”的算法出现了,它将可用内存按容量划分为大小相等的两块,每次只使用其中的一块。当这一块的内存用完了,就将还存活着的对象复制到另外一块上面,然后再把已使用过的内存空间一次清理掉。这样使得每次都是对整个半区进行内存回收,内存分配时也就不用考虑内存碎片等复杂情况,只要移动堆顶指针,按顺序分配内存即可,实现简单,运行高效。只是这种算法的代价是将内存缩小为原来的一半,代价未免太高了一点。

现在的商业虚拟机都采用这种回收算法来回收新生代。但并不是按1:1的比例来划分内存空间。我们发现,新生代中的对象98%都是“朝生夕死”的,所以不需要按1:1的比例来划分内存空间,而是将内存分为一块较大的Eden空间和两块较小的Survivor空间,每次使用Eden和其中一块Survivor。当回收时,将Eden和Survivor中还存活着的对象一次性地复制到另外一块Survivor空间上,最后清理掉Eden和刚才用过的Survivor空间。HotSpot虚拟机默认Eden和Survivor的大小比例是8:1,也就是每次新生代中可用内存空间为整个新生代容量的90%(80%+10%),只有10%的内存会被浪费。当然,98%的对象可回收只是一般场景下的数据,我们没有办法保证每次回收都只有不多于10%的对象存活,当Survivor空间不够用时,需要依赖其他内存(这里指老年代)进行分配担保。

内存的分配担保是指,如果另外一块Survivor空间没有足够空间存放上一次新生代收集下来的存活对象时,这些对象将直接通过分配担保机制进入老年代。

缺点:复制回收算法在对象存活率较高时就要进行较多的复制操作,效率将会变低。更关键的是,如果不想浪费50%的空间,就需要有额外的空间进行分配担保,以应对被使用的内存中所有对象都100%存活的极端情况,所以在老年代一般不能直接选用这种算法。

标记-整理算法:根据老年代的特点,有人提出了另外一种“标记—整理”算法。标记过程仍然与“标记—清除”算法一样,但后续步骤不是直接对可回收对象进行清理,而是让所有存活的对象都向一端移动,然后直接清理掉端边界以外的内存。

分代收集算法:当代商业虚拟机的垃圾收集算法都采用“分代收集”算法,这种算法并没有什么新的思想,只是根据对象存活周期的不同将内存划分为几块。一般是把Java堆内存划分为新生代和老年代,这样就可以根据各个年代的特点采用最适当的收集算法。在新生代中,每次垃圾收集时都发现有大批对象死去,只有少量存活,那就选用复制算法。而老年代中因为对象存活率高、没有额外空间对它进行分配担保,就必须使用“标记—清除”算法或者“标记—整理”算法来进行回收。

14.Java中,垃圾回收的时候怎么判断一个对象是否可以被回收?

答:有人会说,用引用计数算法来实现,思路如下:给对象中添加一个引用计数器,每当有一个地方引用它时,计数器值就加1;当引用失效时,计数器值就减1;任何时刻计数器为0的对象就是不可能再被使用的。

这种回答是错误的,引用计数算法实现简单,判定效率也很高,也有一些比较著名的应用案例,例如微软的COM技术、Python语言和在游戏脚本领域被广泛应用的Squirrel中都使用了引用计数算法进行内存管理。但是java虚拟机中没有选用引用计数算法来管理内存,其中最主要的原因是它很难解决对象之间相互循环引用的问题。

Java(C#)等主流商用程序语言都是使用可达性分析算法来判定对象是否存活的。这个算法的基本思路如下:通过一系列称为“GC Roots”的对象作为起始点,从这些节点开始向下搜索,搜索所走过的路径称为引用链,当一个对象到GC Roots没有任何引用链相连(即GC Roots到这个对象不可达)时,则证明此对象是不可用的。

在Java语言中,可作为GC Roots的对象包括下面几种:

1. 虚拟机栈中引用的对象。

2. 方法区中类静态属性引用的变量。

3. 方法区中常量引用的对象

4. 本地方法栈中JNI(即一般中的Native方法)引用的对象。

15.Java中,如果垃圾回收器判断一个对象是可以被回收的,是不是就会立即对其进行回收呢?

答:不是。即使在可达性分析算法中不可达的对象,也并非是“非死不可”的,这时候它们暂时处于“缓刑”阶段,要真正宣告一个对象死亡,至少要经历两次标记过程,也就是说要通过可达性分析算法对其进行两次判断,如果两次判断的结果都是可回收,那么就会回收。

16.JVM内存模型指的是什么?

答:“内存模型”可以理解为在特定的操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。

内存模型是用来解决多线程间通信问题的。我们可把内存模型分为两部分:主内存和工作内存。

17.JVM运行时数据区(内存区域)和JVM内存模型有什么区别?

答:大家千万不要把运行时数据区和内存模型搞混了。

运行时数据区分为5部分:虚拟栈、java堆、方法区、本地方法栈、程序计数器。

内存模型分为2个部分:主内存和工作内存。用来解决线程间通信问题。

运行时数据区和内存模型不是一回事,这是两个层次的东西,没有必然联系。如果非要把两者联系起来,那么,主内存中存放的是多线程的共享数据,应该属于java堆或者方法区。工作内存是每个线程独立拥有的内存空间,是线程安全的,应该属于虚拟机栈或者本地方法栈部分。

 

18. 对于volatile型变量的特殊规则

答:当一个变量定义为volatile之后,它将具备两种特性,

第一是保证此变量对所有线程的可见性。这里的“可见性”是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得到的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。

volatile变量在各个线程的工作内存中不存在一致性问题(在各个线程的工作内存中,volatile变量也可以存在不一致的情况,但由于每次使用之前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性问题),但是Java里面的运算并非原子操作,导致volatile变量的运算在并发下一样是不安全。

由于volatile变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用sychronized或java.util.concurrent中的原子类)来保证原子性。

£运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。

£变量不需要与其他的状态变量共同参与不变约束。

使用volatile变量的第二个语义是禁止指令重排序优化,普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。因为在一个线程的方法执行过程中无法感知到这点,这也就是Java内存中描述的所谓的“线程内表现为串行的语义”。

上面的描述仍然不太容易理解,我们举一个具体的例子来说明:

public class Single{

     //构造函数私有化

     private Single(){}

   

    //定义静态成员变量

     private static Single s = null;

    

    //对外提供公共的访问方法

     public static Single getInstance(){

          if(s == null){

               synchronized(Single.class){

                    if(s == null){

                         s = new Single();

                    }

               }

          }

 

         return s;

   }

}

 

就如上面所示,这个代码看起来很完美,理由如下:

•如果检查第一个singleton不为null,则不需要执行下面的加锁动作,极大提高了程序的性能。

•如果第一个singleton为null,即使有多个线程同一时间判断,但是由于synchronized的存在,只会有一个线程能够创建对象。

•当第一个获取锁的线程创建完成singleton对象后,其他的在第二次判断singleton一定不会为null,则直接返回已经创建好的singleton对象。

通过上面的分析,DCL看起来确实是非常完美,但是可以明确地告诉你,这个错误的。上面的逻辑确实是没有问题,分析也对,但是就是有问题,那么问题出在哪里呢?

在回答这个问题之前,我们先来复习一下创建对象过程,实例化一个对象要分为三个步骤:

1. 分配内存空间;

2. 初始化对象;

3. 将内存空间的地址赋值给对应的引用;

但是由于重排序的缘故,步骤2、3可能会发生重排序,其过程如下:

1. 分配内存空间;

2. 将内存空间的地址赋值给对应的引用;

3. 初始化对象;

如果2、3发生了重排序就会导致第二个判断会出错,singleton != null,但是它其实仅仅只是一个地址而已,此时对象还没有被初始化,所以return的singleton对象是一个没有被初始化的对象。假如有两个线程A、B,线程A先执行,但是发生重排序了,所以线程A返回的singleton对象是一个没有被初始化的对象,仅仅只是一个地址而已。当线程B访问的时候,singleton != null,但是线程B拿到的仅仅只是一个地址而已,是一个没有被初始化的对象。

通过上面的阐述,我们可以判断DCL的错误根源在于步骤4:

Singleton = new Singleton();

知道问题根源所在,那么怎么解决呢?有两个解决办法:

1. 不允许初始化阶段步骤2、3发生重排序。

2. 允许初始化阶段步骤2、3发生重排序,但是不允许其他线程“看到”这个重排序。

这里我们讲第一种解决方案,很简单:将变量singleton声明为volatile即可:

public class Single{

     //构造函数私有化

     private Single(){}

   

    //定义静态成员变量

     private volatile static Single s = null;

    

    //对外提供公共的访问方法

     public static Single getInstance(){

          if(s == null){

               synchronized(Single.class){

                    if(s == null){

                         s = new Single();

                    }

               }

          }

 

         return s;

   }

}

当singleton声明为volatile后,步骤2、步骤3就不会被重排序了,也就可以解决上面那问题了。

19.原子性、可见性与有序性

答:Java内存模型是围绕着在并发过程中如果处理原子性、可见性和有序性这3个特征来建立的,我们逐个来看一下哪些操作实现了这3个特性。

原子性(Atomicity)。由Java内存模型来直接保证的原子性变量操作包括read、load、assign、use、store和write,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和double的非原子性协定,读者只要知道这件事情就可以了,无须太过在意这些几乎不会发生的例外情况)。

如果应用场景需要一个更大范围的原子性保证(经常会遇到),Java内存模型还提供了lock和unlock操作来满足这种需求,尽管虚拟机未把lock和unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令monitorenter和monitorexit来隐士地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块——synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性(Visibility)。可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。上文在讲解volatile变量的时候我们已详细讨论过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式实现可见性的,无论是普通变量还是volatile变量都是如此,普通变量与volatile变量的区别是,volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了volatile之外,Java还有两个关键字能实现可见性,即sychronized和final。同步块的可见性是由“对一个变量执行unlock操作之前,必须先把此变量同步回主内存中(执行store、write操作)”这条规则获得的,而final关键字的可见性是指:被final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见final字段的值。

有序性(Ordering)。Java内存模型的有序性在前面讲解volatile时也详细地讨论过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”(Within-Thread As-If-Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java语言提供了volatile和sychronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排序的语义,而sychronized则是由“一个变量在同一个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

 

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值