Java并发与Java内存模型 --《Java并发编程的艺术》

我的个人博客对应文章:Java并发与Java内存模型
欢迎访问!

Java 并发机制的底层实现原理

Volatile 的应用

  • 定义:Java变成语言允许线程访问共享变量,为了确保共享变量能被准确和一致地更新,线程应该确保通过排它锁单独获得这个变量。

几个术语:

  • 缓存行(cache line)是CPU缓存中可分配、操作的最小存储单元,就是获取一块内存数据,放入缓存,这块数据成为缓存行。

  • 缓存行填充:当处理器识别到从内存中读取操作数是可缓存的,处理器读取整个高速缓存行到适当的缓存(L1,L2,L3的或所有)

  • 缓存命中“如果进行高速缓存行填充操作的内存位置仍然是下次处理器访问的地址时,处理器从缓存中读取操作数,而不是从内存读取。

  • 写命中:当处理器将操作数写回到一个内存缓存的区域时,它首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作数写回到魂村,而不是写回到内存。

  • 写缺失:一个有效的缓存行被写入到不存在的内存区域。

volatile如何保证可见性:

在X86处理器下通过工具获取JIT编译器生成的汇编指令来查看对volatile进行写操作时 ,有volatile修饰的变量进行写操作会多出后面一行

0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);

关键就在于这个Lock前缀的指令。

Lock前缀的指令在多核处理器下会引发两件事情

1)将当前处理器缓存行的数据写回到系统内存。

Lock前缀指令导致在执行指令期间,声言处理器的LOCK#信号。在多处理器环境中,LOCK#信号确保在声言该信号期间,处理器可以独占任何共享内存。但是,在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销的比较大。

2)这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

处理器如何知道自己缓存值是不是过期了?

每个处理器通过嗅探在总线上传播的数据来检查自己缓存值是否过期,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile的使用优化

这里提到一个JDK7并发包里的一个队列集合类 LinkedTransferQueue

它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性能。

/** 队列中的头部节点 */
private transient f?inal PaddedAtomicReference<QNode> head;
/** 队列中的尾部节点 */
private transient f?inal PaddedAtomicReference<QNode> tail;
static f?inal class PaddedAtomicReference <T> extends AtomicReference T> {
    // 使用很多4个字节的引用追加到64个字节
    Object p0, p1, p2, p3, p4, p5, p6, p7, p8, p9, pa, pb, pc, pd, pe;
    PaddedAtomicReference(T r) {
        super(r);
    }
    }
    public class AtomicReference <V> implements java.io.Serializable {
    private volatile V value;
    // 省略其他代码

原因:

因为有些处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不支持部分填充缓存行,这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存行,使头、尾节点在修改时不会互相锁定。

  • 第一,当然不是在所有情况下volatile变量都要追加到64字节:缓存行非64字节宽的处理器;共享变量不会被频繁写
  • 第二,在JDK8里面已经改了。
  • 第三,这种追加字节的方式在Java7下可能不生效,会淘汰或重新排列无用字段。

synchronized

📈 JDK1,6后优化,增加了偏向锁和轻量级锁。

synchronized在JVM的实现原理:

JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步是使用monitorenter
monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有详细说明。但是,方法的同步同样可以使用这两个指令来实现。

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁。

Java对象头

synchronized用的锁是存在Java对象头里的,普通对象头是2个字宽,是数组则是3个字宽,其中的一个字宽的内容被称为mark word

其中存储了对象的hashcode,分代年龄和锁信息,重点提及这个锁信息数据。

32位虚拟机和64位虚拟机mark word的储存结构不一样,这里不过多赘述,见原书。

锁升级
  • Java SE 1.6 为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”。

  • 一共四种状态:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态。

  • 锁可以升级但不能降级,目的是为了提高获得锁和释放锁的效率。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。 当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销:

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个时间点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无锁状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停的线程。

过程:

关闭偏向锁

偏向锁 1.6和1.7默认启用,可以通过命令关闭偏向锁,如果锁经常处于竞争状态的话。

轻量级锁
  • 加锁:

    线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

  • 解锁:

    轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

优缺点:

  1. 偏向锁解锁解锁不需要额外消耗,如果有竞争则锁撤销有消耗,适用于单线程场景
  2. 轻量级锁竞争线程不会阻塞,使用自旋,如果市始终得不到锁,则自旋消耗CPU,适用于追求响应时间。
  3. 重量级锁不自旋不消耗CPU,直接阻塞,相应时间慢,适用于追求吞吐量场景。

原子操作的实现原理

术语

内存顺序冲突:一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效(缓存一致性协议针对的是最小存取单元:缓存行),当出现这个内存顺序冲突时,CPU必须清空流水线。

总结来说,就是多核多线程并发场景下,多核要操作的不同变量处于同一缓存行,某cpu更新缓存行中数据,并将其写回缓存,同时其他处理器会使该缓存行失效,如需使用,还需从内存中重新加载。这对效率产生了较大的影响。

处理器实现原子操作:

处理器保证从系统内存中读取或写入一个字节是原子的,意思是当一个处理器读取一个字节时,其他的处理器不能访问这个字节的内存地址。如果是复杂内存操作的原子性,处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操作的原子性。

  1. 总线锁定:所谓总线锁就是使用处理器提供的一个LOCK#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
  2. 缓存锁定:所谓“缓存锁定”是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制(对象就是缓存行)来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效 。

不适用缓存行的情况:

  1. 操作数据不能缓存在处理器内部,或操作数据跨多个缓存行,这种情况会锁总线。
  2. 处理器不支持缓存锁定

Java如何实现原子操作

  1. 使用循环CAS实现原子操作

    从Java 1.5开始,JDK的并发包里提供了一些类来支持原子操作,如AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新的int值)和AtomicLong(用原子方式更新的long值)。这些原子包装类还提供了有用的工具方法,比如以原子的方式将当前值自增1和自减1。

  2. CAS的三大问题:

    • ABA问题,JDK里可以通过AtomicStampedReference解决,不仅比较预期和更新后的Reference,还比较预期和更新后的Stamp(标志)。
    • 循环时间开销大,这里提处理器的pause指令能提升效率,1是延迟流水线执行命令,2是解决内存顺序冲突。
    • 只能保证一个共享变量的原子操作,JDK中可以通过AtomicReference,把多个变量放在一个对
      象里来进行CAS操作。
  3. 使用锁机制实现原子操作。(这里提到除了偏向锁,JVM实现锁都使用到循环CAS)

Java内存模型

Java内存模型基础

在命令式编程中,线程之间的通信机制有两种:共享内存 和 消息传递。

Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。

从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。

线程中的通信可以看成:

1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。

表面上是线程之间发送该消息,实际通过了主内存这个中间人。

重排序

重排序分三种(从Java源码到执行的指令序列也经过这三种 ):

  1. 编译器优化的重排序。
  2. 指令级并行的重排序
  3. 内存系统的重排序

2和3属于处理器重排序:Java的处理器重排序规则会要求编译器在生成指令序列时插入内存屏障

  • 现代的处理器使用写缓冲区临时保存向内存写入的数据。
  • 但每个处理器上的写缓冲区,仅仅对它所在的处理器可见。这个特性会对内存操作的执行顺序产生重要的影响:处理器对内存的读/写操作的执行顺序,不一定与内存实际发生的读/写操作顺序一致!
  • 常见的处理器不允许对存在 数据依赖 的操作做重排序。

**为了保证内存可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序。 **

StoreLoad Barriers是一个“全能型”的屏障,它同时具有其他3个屏障的效果。

happens-before简介

从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系。这里提到的两个操作既可以是在一个线程之内,也可以是在不同线程之间。

两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个
操作之前执行! happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一
个操作按顺序排在第二个操作之前(the first is visible to and ordered before the second)。

重排序

  • 如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。
  • 这里所说的数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑,我们所说的如果操作没有数据依赖性,编译器和处理器可以进行重排序,是从单线程角度看;从多线程是从整体角度来看了,是另一回事。

as-if-serial

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

重排序对多线程影响,除了上面所说的数据依赖性以外,还有一个控制依赖性(也是相对于单线程来说的),例如if语句和if下面的语句,在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果(这也是as-if-serial语义允许对存在控制依赖的操作做重排序的原因);但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

顺序一致性

顺序一致性模型特点:

1)一个线程中的所有操作必须按照程序的顺序来执行。
2)(不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序。在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

顺序一致性内存模型是一个理论参考模型,在设计的时候,处理器的内存模型和编程语言的内存模型都会以顺序一致性内存模型作为参照。 当然我认为只是参照而已,JVM没有上面两个特点的保证,这里不展开。

这里提到一点:

JMM不保证对64位的long型和double型变量的写操作具有原子性,而顺序一致性模型保证对所有的内存读/写操作都具有原子性。

原因:

总线的工作机制(提到总线仲裁)可以把所有处理器对内存的访问以串行化的方式来执行。在任意时间点,最多只能有一个处理器可以访问内存。这个特性确保了单个总线事务之中的内存读/写操作具有原子性。 但有一些32位处理器,对64位数据的写操作会拆分成两个32位写操作执行,分配到不同的总线事务中执行,所以没有原子性保证。(也就是可能处理器A写一半,B就看到了写了一半)

注意:在JSR-133之前的旧内存模型中,一个64位long/double型变量的读/写操作可以被拆分为两个32位的读/写操作来执行。从JSR-133内存模型开始(即从JDK5开始),仅仅只允许把一个64位long/double型变量的写操作拆分为两个32位的写操作来执行,任意的读操作在JSR-133中都必须具有原子性(即任意读操作必须要在单个读事务中执行)

volatile的内存语义:

**理解volatile特性的一个好方法是把对volatile变量的单个读/写,看成是使用同一个锁对这些单个读/写操作做了同步。 **

  • 锁的happens-before规则保证释放锁和获取锁的两个线程之间的内存可见性,这意味着对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 锁的语义决定了临界区代码的执行具有原子性。这意味着,即使是64位的long型和double型变量,只要它是volatile变量,对该变量的读/写就具有原子性。如果是多个volatile操作或类似于volatile++这种复合操作,这些操作整体上不具有原子性。

特性如下:

  • 可见性。对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写入。
  • 原子性:对任意单个volatile变量的读/写具有原子性,但类似于volatile++这种复合操作不具有原子性。 (这个原子性是因为有锁的语义)
volatile写-读的内存语义

写的内存语义:当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

读的内存语义:当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

总结就是线程间发送消息。

volatile内存语义的实现
  • 为了实现volatile内存语义,JMM会分别限制这两种类型的重排序类型。
  • 为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数几乎不可能。为此,JMM采取保守策略。(首先保证正确性,再去追求效率)

JSR-133为什么要增强volatile的内存语义

在JSR-133之前的旧Java内存模型中,虽然不允许volatile变量之间重排序,但旧的Java内存模型允许volatile变量与普通变量重排序。

上张图明白,本来是1,2,3, 4,根据volatile规则和程序顺序规则,1的结果对4可见,但:

在旧的内存模型中,volatile的写-读没有锁的释放-获所具有的内存语义 ,所以决定增强,严格限制编译器和处理器对volatile变量与普通变量的重排序 。

可以看出volatile就是一种轻量的锁,在功能上,锁比volatile更强大;在可伸缩性和执行性能上,volatile更有优势。

锁的内存语义

  • 当线程释放锁时,JMM会把该线程对应的本地内存中的共享变量刷新到主内存中。
  • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效。从而使得被监视器保护的临界区代码必须从主内存中读取共享变量。

锁释放与volatile写有相同的内存语义;锁获取与volatile读有相同的内存语义。

总结也是线程间的通信。

锁的内存语义的实现

这里书中用ReentrantLock为例分析,总结为:

  • 公平锁和非公平锁释放时,最后都要写一个volatile变量state。

  • 公平锁获取时,首先会去读volatile变量。

  • 非公平锁获取时,首先会用CAS更新volatile变量,这个操作同时具有volatile读和volatile
    写的内存语义。

对ReentrantLock的分析可以看出,锁释放-获取的内存语义的实现至少有下面两种方式。
1)利用volatile变量的写-读所具有的内存语义。
2)利用CAS所附带的volatile读和volatile写的内存语义。

concurrent 包的实现

Java的CAS会使用现代处理器上提供的高效机器级别的原子指令,同时,volatile变量的读/写和CAS可以实现线程之间的通信。把这些特性整合在一起,就形成了整个concurrent包得以实现的基石。如果我们仔细分析concurrent包的源代码实现,会发现一个通用化的实现模式 :

首先,声明共享变量为volatile。
然后,使用CAS的原子条件更新来实现线程之间的同步(指控制操作顺序的机制)。
同时,配合以volatile的读/写和CAS所具有的volatile读和写的内存语义来实现线程之间的通信。

AQS,非阻塞数据结构和原子变量类(java.util.concurrent.atomic包中的类),这些concurrent包中的基础类都是使用这种模式来实现的,而concurrent包中的高层类又是依赖于这些基础类来实现的。从整体来看,concurrent包的实现示意图如所示。

final域的内存语义

1)在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。写final域的重排序规则禁止把final域的写重排序到构造函数之外。

2)初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

3)对于引用类型:在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序 。

这里提到一个final引用不能从构造函数内”溢出”。

比如构造函数:

public FinalReferenceEscapeExample () {
    i = 1; // 1写final域
    obj = this; // 2 this引用在此"逸出"
}

要保证在引用变量为任意线程可见之前,该引用变量指向的对象的final域已经在构造函数中被正确初始化过了,2操作是不能出现的。

final域在处理器中的实现

简单说就是插入屏障,但也取决于处理器。

**JSR-133为什么要增强final的语义 **

在旧的Java内存模型中,一个最严重的缺陷就是线程可能看到final域的值会改变。

为了修补这个漏洞,JSR-133专家组增强了final的语义。通过为final域增加写和读重排序规则,可以为Java程序员提供初始化安全保证:只要对象是正确构造的(被构造对象的引用在构造函数中没有“逸出”),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能看到这个final域在构造函数中被初始化之后的值。

happens-before

JMM把happens-before要求禁止的重排序分为下面两类

  • 会改变程序执行结果的重排序。
  • 不会改变程序执行结果的重排序。

JMM对这两种不同性质的重排序,采取了不同的策略,如下。

  • 对于会改变程序执行结果的重排序,JMM要求编译器和处理器必须禁止这种重排序。
  • 对于不会改变程序执行结果的重排序,JMM对编译器和处理器不做要求(JMM允许这种
    重排序)。
image-20210608125841719

JMM向程序员提供的happens-before规则能够满足程序员的需求。JMM的happens-before规则不但简单易懂,而且也向程序员提供了足够强的内存可见性保证 。

上面这张图很好地展示了happens-before存在的意义还有和JVM,程序员三者之间的关系关联。👍

happens-before的定义

《JSR-133:Java Memory Model and Thread Specification》对happens-before关系的定义如下

1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。

上面的1)是JMM对程序员的承诺。

上面的2)是JMM对编译器和处理器重排序的约束原则。

会发现 happens-before关系本质上和as-if-serial语义是一回事。

happens-before规则

《JSR-133:Java Memory Model and Thread Specification》定义了如下happens-before规则。

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 传递性:如果A happens-before B,且B happens-before C,那么A happens-before C。
  5. start()规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  6. join()规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回。

双重检查锁定与延迟初始化

在Java多线程程序中,有时候需要采用延迟初始化来降低初始化类和创建对象的开销。

双重检查锁定是常见的延迟初始化技术,但它是一个错误的用法。

书中用了单例模式的代码作为例子讲解,这里不展开。

基于volatile的解决方案
public class SafeDoubleCheckedLocking {
    private volatile static Instance instance;
    public static Instance getInstance() {
        if (instance == null) {
            synchronized (SafeDoubleCheckedLocking.class) {
                if (instance == null)
                	instance = new Instance(); // instance为volatile,现在没问题了
            }
        }
        return instance;
    }
}

其实就加了个volatile,学过单例的都清楚

这个方案本质上是通过禁止指令(instance = new Instance()涉及的三个指令)之间的重排序,来保证线程安全的延迟初始化。

支持需要JDK5以上版本,因为JSR-133后来增强了volatile的语义。

基于类初始化的解决方案
public class InstanceFactory {
	private static class InstanceHolder {
		public static Instance instance = new Instance();
	}
    public static Instance getInstance() {
		return InstanceHolder.instance ; // 这里将导致InstanceHolder类被初始化
	}
}

原因:类的初始化在JVM中会做同步处理

通过对比基于volatile的双重检查锁定的方案和基于类初始化的方案,我们会发现基于类初始化的方案的实现代码更简洁。

但基于volatile的双重检查锁定的方案有一个额外的优势:除了可以对静态字段实现延迟初始化外,还可以对实例字段实现延迟初始化。

Java内存模型综述

这里书中介绍了几种处理器的内存模型,如TSO,PSO,PMO。

而JMM屏蔽了不同处理器内存模型的差异,它在不同的处理器平台之上为Java程序员呈现了一个一致的内存模型。

各种内存模型之间的关系

JMM是一个语言级的内存模型,处理器内存模型是硬件级的内存模型,顺序一致性内存模型是一个理论参考模型。

**JSR-133对旧内存模型的修补 **

JSR-133对JDK 5之前的旧内存模型的修补主要有两个。

  • 增强volatile的内存语义。旧内存模型允许volatile变量与普通变量重排序。JSR-133严格限制volatile变量与普通变量的重排序,使volatile的写-读和锁的释放-获取具有相同的内存语义。
  • 增强final的内存语义。在旧内存模型中,多次读取同一个final变量的值可能会不相同。为此,JSR-133为final增加了两个重排序规则。在保证final引用不会从构造函数内逸出的情况下,final具有了初始化安全性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值