volatile,synchronized,Java内存模型详解

 什么是volatile

volatile是JVM提供的轻量级的同步机制。volatile 关键字可以保证并发编程三大特性(原子性、可见性、有序性)中的可见性和有序性,但是不能保证原子性。

可见性:当对volatile变量进行写操作的时候,会向处理器发送一条lock前缀的指令,将这个缓存中的变量回写到系统主存中。

所以,如果一个变量被volatile所修饰的话,在每次数据变化之后,值都会被强制写入主存。而其他处理器的缓存由于遵守缓存一致性协议,就会把变量的值从主存读取到自己的工作内存中。这就保证了volatile在并发编程中,其值在多个缓存中是可见的。

有序性:volatile除了可以保证数据的可见性之外,还有一个重要的作用,那就是它可以禁止指令重排优化。volatile是通过内存屏障来禁止指令重排的。

硬件层的内存屏障

Intel硬件提供了一系列的内存屏障,主要有︰

1.lfence,是一种Load Barrier读屏障

2.sfence,是一种Store Barrier 写屏障

3. mfence,是一种全能型的屏障,具备lfence和sfence的能力

4.Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD,ADC, AND,BTC,BTR,BTS,CMPXCHG,CMPXCH8B, DEC,INC,NEG,NOT,OR,SBB,SUB,XOR,XADD,and XCHG等指令。

JVM的内存屏障

不同硬件实现内存屏障的方式不同,Java内存模型屏蔽了这种底层硬件平台的差异,由JVM来为不同的平台生成相应的机器码。JVM中提供了几类内存屏障指令:

重排序规则表

下图是JMM针对编译器制定的volatile重排序规则表:

总结︰

  • volatile 读之后的任何操作不能重排序到volatile 读之前。
  • volatile 写之前的任何操作不能与volatile 写重排序。
  • volatile 写不能与之后的volatile 操作重排序

volatile读指令序列示意图

volatile读的JMM内存屏障插入策略∶

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障。

volatile写指令序列示意图

volatile写的JMM内存屏障插入策略∶

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

volatile读插入内存屏障后生成的指令序列示意图︰

X86处理器

X86处理器仅会对写-读操作做重排序,不会对读-读、读-写和写-写操作做重排序,因此在X86处理器中会省略掉这3种操作类型对应的内存屏障。在X86中,JMM仅需在volatile写后面插入一个StoreLoad屏障即可正确实现volatile写-读的内存语义。这意味着在X86处理器中,volatile写的开销比volatile读的开销会大很多(因为执行StoreLoad屏障开销会比较大)。

什么是synchronized

synchronized是Java提供的一种原子性内置锁。Java中的每个对象都可以把它当作一个同步锁来使用,这些Java内置的使用者看不到的锁被称为内置锁,也叫作隐式锁。synchronized 所添加的锁有以下几个特点:

  • 互斥性

同一个时间点,只有一个线程可以获得锁,获得锁的线程才可以处理被synchronized修饰的方法或者代码段。

  • 阻塞性

只有获得锁的线程才可以执行被synchronized修饰的方法或者代码段。未获得锁的线程只能阻塞,等待锁释放。

  • 可重入性

如果一个线程已经获得锁,在锁未释放之前,再次请求锁的时候,是必然可以获得锁的。

  • 非公平锁

当多个线程同时请求锁时,synchronized 不会按照请求锁的顺序分配锁,而是将锁分配其中一个线程。这个线程可能是最后一个请求锁的线程,也有可能是其他线程。

使用

方法级

同步方法是通过设置常量池中的access_flags为ACC_SYNCHRONIZED标志来实现的。

当某个线程要访问同步方法的时候,会检查是否有设置ACC_SYNCHRONIZED标志,如果有设置,则需要先获得监视器锁,然后开始执行方法,方法执行完毕后在释放监视器锁。这时如果有其他线程请求执行同步方法,会因为无法获得监视器锁而被阻塞。
值得注意的是,如果在方法执行过程中,发生了异常,并且方法内部并没有处理该异常,那么在异常被抛到方法外面之前监视器锁会被自动释放。

Monitor(管程/监视器)

Monitor是Java锁体系的设计思想和理论基础。

无论是同步方法还是同步代码块,其实现都是要依赖对象的监视器Monitor。每个对象都拥有自己的监视器锁Monitor,当我们尝试获得对象的锁的时候,其实是对该对象拥有的Monitor进行操作。

MESA模型

在管程的发展史上,先后出现过三种不同的管程模型,分别是Hasen模型、Hoare模型和MESA模型。现在正在广泛使用的是MESA模型。

管程中引入了条件变量的概念,而且每个条件变量都对应有一个等待队列。条件变量和等待队列的作用是解决线程之间的同步问题。

wait()的正确使用姿势

对于MESA管程来说,有一个编程范式︰

Java语言的内置管程synchronized

Java 参考了MESA模型,语言内置的管程(synchronized)对MESA模型进行了精简。MESA模型中,条件变量可以有多个,Java 语言内置的管程里只有一个条件变量。模型如下图所示:

Monitor机制在Java中的实现

java.lang.Object类定义了wait(), notify(),notifyAll()方法,这些方法的具体实现,依赖于ObjectMonitor 实现,这是JVM内部基于C++实现的一套机制。

ObjectMonitor

ObjectMonitor其主要数据结构如下(hotspot源码ObjectMonitor.hpp) :

执行流程

在获取锁时,是将当前线程插入到_cxq的头部,而释放锁时,默认策略(QMode=0)是:如果_EntryList为空,则将_cxq中的元素按原有顺序插入到_EntryList,并唤醒第一个线程,也就是当_EntryList为空时,是后来的线程先获取锁。_EntryList不为空,直接从_EntryList中唤醒线程。

synchronized是如何记录锁状态的?

对象的内存布局

Hotspot虚拟机中,对象在内存中存储的布局可以分为三块区域∶对象头(Header)、实例数据(InstanceData)和对齐填充(Padding)

  • 对象头∶比如 hash码,对象所属的年代,对象锁,锁状态标志,偏向锁(线程)ID,偏向时间,数组长度(数组对象才有)等。
  • 实例数据:存放类的属性数据信息,包括父类的属性信息;
  • 对齐填充︰由于虚拟机要求对象起始地址必须是8字节的整数倍。填充数据不是必须存在的,仅仅是为了字节对齐。

对象头详解

HotSpot虚拟机的对象头包括:

  • Mark Word

用于存储对象自身的运行时数据,如哈希码〈HashCode)、GC分代年龄、锁状态标志、线程持有的锁、偏向线程ID、偏向时间戳等,官方称它为“Mark Word”。

数据长度:在32位和64位的虚拟机中分别为4字节(32bit)和8字节(64bit)。.

  • Klass Pointer

对象头的另外一部分是klass类型指针,即对象指向它的类元数据的指针,虚拟机通过这个指针来确定这个对象是哪个类的实例。数据长度:32位4字节,64位开启指针压缩时4字节,否则8字节。jdk1.8默认开启指针压缩后为4字节,当在JVM参数中关闭指针压缩(一XX:-UseCompressedOops)后,长度为8字节。

  • 数组长度(只有数组对象才有)

如果对象是一个数组,那在对象头中还必须有一块数据用于记录数组长度。数据长度:4字节

Mark Word是如何记录锁状态的?

锁状态被记录在每个对象的对象头的Mark Word中。
Hotspot通过markOop类型实现Mark Word,具体实现位于markOop.hpp文件中。由于对象需要存储的运行时数据很多,考虑到虚拟机的内存使用,markOop被设计成一个非固定的数据结构,以便在极小的空间存储尽量多的数据,根据对象的状态复用自己的存储空间。

名词解释.

hash:保存对象的哈希码。运行期间调用System.identityHashCode()来计算,延迟计算,并把结果赋值到这里。

age:保存对象的分代年龄。表示对象被GC的次数,当该次数到达阈值的时候,对象就会转移到老年代。

biased_lock︰偏向锁标识位。。由于无锁和偏向锁的锁标识都是01,没办法区分,这里引入一位的偏向锁标识位。

lock:锁状态标识位。区分锁状态,比如11时表示对象待GC回收状态,只有最后2位锁标识(11)有效。

JavaThread:保存持有偏向锁的线程ID。

偏向模式的时候,当某个线程持有对象的时候,对象这里就会被置为该线程的ID。在后面的操作中,就无需再进行尝试获取锁的动作。

这个线程ID并不是JVM分配的线程ID号,和Java Thread中的ID是两个概念。

  • epoch:保存偏向时间戳。

偏向锁在CAS锁操作过程中,偏向性标识,表示对象更偏向哪个锁。

  • ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针。

当锁获取是无竞争时,JVM使用原子操作而不是OS互斥,这种技术称为轻量级锁定。在轻量级锁定的情况下,JVM通过CAS操作在对象的Mark Word中设置指向锁记录的指针。

  • ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针。

如果两个不同的线程同时在同一个对象上竞争,则必须将轻量级锁定升级到Monitor以管理等待的线程在重量级锁定的情况下,JVM在对象的ptr_to_heavyweight_monitor设置指向Monitor的指针

32位结构图

64位结构图

锁标记直观图

使用JOL工具查看内存布局

给大家推荐一个可以查看普通java对象的内部布局工具JOL(JAVA OBJECT LAYOUT),使用此工具可以查看new出来的一个java对象的内部布局,以及一个普通的java对象占用多少字节。

首先引入maven依赖

使用方法

执行结果

根据执行结果,我们可以看到,new Object()对象一共占用了16个字节。.

  • Mark Word 占用8个字节(64位机器)
  • Klass Pointer占用4个字节(默认开启压缩指针)
  • 由于8+4 = 12.不足8字节的整数倍,需要补充4位。

这同时也是一道大厂的面试题,使用JOL工具可以帮助我们很好的理解一些抽象的底层问题。赶紧用起来吧。

说一下synchronized的锁升级过程

在JDK 1.6 之前的版本中,synchronized锁是通过对象内部的监视器锁( Monitor )来实现的。当一个线程请求对象锁时,如果对象没有被锁住,线程就会获得锁并继续执行同步代码。如果该对象已经被锁住,线程就会进入阻塞状态直到锁被释放。这种锁被称为重量级锁,因为获得锁和释放锁都需要在操作系统层面上进行线程的阻塞和唤醒,而这些操作会带来很大的性能开销。

在JDK 1.6 版本中,为了减少获得锁和释放锁带来的性能开销,引入了偏向锁(Biased Locking)、轻量级锁(Lightweight Locking)两种状态,来适应不同场景下的锁竞争情况。

synchronized 是可重入、不公平的重量级锁,所以可以对其进行优化

无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁 // 随着竞争的增加,只能锁升级,不能降级

偏向锁

偏向锁是一种针对加锁操作的优化手段,经过研究发现,在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,因此为了消除数据在无竞争情况下锁重入(CAS操作)的开销而引入偏向锁。对于没有锁竞争的场合,偏向锁有很好的优化效果。

匿名偏向状态

当JVM启用了偏向锁模式(jdk6默认开启),新创建对象的Mark Word中的Thread ld为O,说明此时处于可偏向但未偏向任何线程,也叫做匿名偏向状态(anonymously biased)。

延迟偏向

偏向锁模式存在偏向锁延迟机制:HotSpot 虚拟机在启动后有个4s的延迟才会对每个新建的对象开启偏向锁模式。JVM启动时会进行一系列的复杂活动,比如装载配置,系统类初始化等等。在这个过程中会使用大量synchronized关键字对对象加锁,且这些锁大多数都不是偏向锁。为了减少初始化时间,JVM默认延时加载偏向锁。

接下来让我们看一下这段代码

4s后偏向锁为可偏向或者匿名偏向状态:

偏向锁状态跟踪

  • 偏向锁加锁/释放锁后,依然是偏向锁状态。
  • 匿名偏向状态转化为偏向锁状态时,是通过CAS将当前线程ID设置到锁对象的MarkWord中
  • 持有偏向锁的线程之后进入同步块,JVM不会进行任何同步操作

偏向锁撤销之调用对象HashCode

调用锁对象的obj.hashCode()或System.identityHashCode(obj)方法会导致该对象的偏向锁被撒销。因为对于一个对象,其HashCode只会生成一次并保存,偏向锁是没有地方保存hashcode的。

  • 轻量级锁会在锁记录中记录hashCode
  • 重量级锁会在Monitor中记录hashCode

场景一

当对象是匿名偏向状态时,调用HashCode计算,MarkWord将变成无锁状态,并只能升级成轻量锁;偏向锁撤销:当前对象未锁定,偏向锁转变为无锁状态;

执行结果

场景二

偏向锁撤销:如果当前对象锁定,同步块中调用hashcode()或者wait(),偏向锁转变为重量级锁状态;

执行结果

偏向锁撤销之调用wait/notify

场景一

  • 偏向锁撤销:如果当前对象锁定,同步块中调用notify(),会升级为轻量级锁

执行结果

场景二

偏向锁撤销:如果当前对象锁定,同步块中调用hashcode()或者wait(),偏向锁转变为重量级锁状态;

执行结果

轻量级锁

倘若获取偏向锁失败,虚拟机并不会立即升级为重量级锁,它还会尝试使用一种称为轻量级锁的优化手段,此时Mark Word的结构也变为轻量级锁的结构。

轻量级锁所适应的场景是线程交替执行同步块的场合,如果存在同一时间多个线程访问同一把锁的场合,就会导致轻量级锁膨胀为重量级锁。

锁升级

轻量级锁升级为重量级锁

说一下synchronized的锁优化

从JDK 1.6开始,HotSpot 虚拟机团队对锁进行了很大的优化,引入了偏向锁(Biased Locking)、轻量级锁(LightweightLocking)、锁粗化(Lock Coarsening)、锁消除〈Lock Elimination)、自适应自旋(Adaptive Spinning)等技术来减少锁操作的开销。

偏向锁批量重偏向&批量撤销

实现原理

以class为单位,为每个class维护一个偏向锁撤销计数器,每一次该class的对象发生偏向撤销操作时,该计数器+1,当这个值达到重偏向阈值(默认20)时,JVM就认为该class的偏向锁有问题,因此会进行批量重偏向。

当达到重偏向阈值(默认20)后,假设该class计数器继续增长,当其达到批量撤销的阈值后(默认40) ,JVM就认为该class的使用场景存在多线程竞争,会标记该class为不可偏向,之后,对于该class的锁,直接走轻量级锁的逻辑。

应用场景

批量重偏向(bulk rebias)机制是为了解决:一个线程创建了大量对象并执行了初始的同步操作,后来另一个线程也来将这些对象作为锁对象进行操作,这样会导致大量的偏向锁撤销操作。

批量撤销(bulk revoke)机制是为了解决:在明显多线程竞争剧烈的场景下使用偏向锁是不合适的。

批量重偏向实验

测试结果:

thread1:创建50个偏向线程thread1的偏向锁1-50偏向锁

thread2: 1-19偏向锁撤销,升级为轻量级锁(thread1释放锁之后为偏向锁状态)

20-40 偏向锁撤销达到阈值(20),执行了批量重偏向

批量撤销实验

当撤销偏向锁阈值超过40 次后,jvm 会认为不该偏向,于是整个类的所有对象都会变为不可偏向的,新建的对象也是不可偏向的。

注意∶时间-XX:BiasedLockingDecayTime=25000ms范围内没有达到40次,撤销次数清为0,重新计时

测试结果:

thread3: 1-19从无锁状态直接获取轻量级锁(thread2释放锁之后变为无锁状态)

20-40偏向锁撤销达到阈值(20),执行了偏向锁撤销,升级为轻量级锁

达到偏向锁撤销的阈值40,ObjectExample 会设置为不可偏向,所以新创建的对象是无锁状态

总结

  • 批量重偏向和批量撤销是针对类的优化,和对象无关。
  • 偏向锁重偏向一次之后不可再次重偏向。
  • 当某个类已经触发批量撤销机制后,JVM会默认当前类产生了严重的问题,剥夺了该类的新实例对象使用偏向锁的权利

自旋优化

重量级锁竞争的时候,还可以使用自旋来进行优化,如果当前线程自旋成功(即这时候持锁线程已经退出了同步块,释放了锁),这时当前线程就可以避免阻塞。

  • 自旋会占用CPU时间,单核CPU自旋就是浪费,多核CPU自旋才能发挥优势。
  • 在Java 6之后自旋是自适应的,比如对象刚刚的一次自旋操作成功过,那么认为这次自旋成功的可能性会高,就多自旋几次;反之,就少自旋甚至不自旋,比较智能。可以使用
  • -XX:+UseSpinning 参数来开启自旋锁,使用-XX:PreBlockSpin参数来设置自旋锁的等待次数。
  • Java 7之后不能控制是否开启自旋功能,自旋锁的参数被取消,自旋锁总是会执行,自旋次数也由虚拟机自行调整。

注意∶自旋的目的是为了减少线程挂起的次数,尽量避免直接挂起线程(挂起操作涉及系统调用,存在用户态和内核态切换,这才是重量级锁最大的开销)

锁粗化

假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。如果JVM检测到有一连串的操作都是对同一对象的加锁,将会扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。

上述代码每次调用buffer.append方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作,即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁。再看个例子︰

会被锁粗化为:

锁消除

锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过对运行上下文的扫描,去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

逃逸分析(Escape Analysis)

逃逸分析,是一种可以有效减少Java程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。

使用逃逸分析,编译器可以对代码做如下优化︰

1.同步省略或锁消除(Synchronization Elimination)。如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

⒉将堆分配转化为栈分配(Stack Allocation)。如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。
3.分离对象或标量替换(Scalar Replacement)。有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

说一下Java内存模型

什么是Java内存模型?

Java内存模型(Java Memory Model,JMM)是Java虚拟机规范中定义的一种用来屏蔽各种硬件和操作系统的内存访问差异,以实现让Java程序在各种平台下都能达到一致性的内存访问效果的一种内存访问模型。从JDK 5开始JMM才真正成熟,完善起来。

Java内存模型的主要目的是定义程序中各种变量(Java中的实例字段,静态字段和构成数组中的元素,不包括线程私有的局部变量和方法参数)的访问规则。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)。每条线程都有自己的工作内存(Working Memory) ,用来保存被该线程使用的变量的主内存副本。线程对变量的所有操作(读取、赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不同线程之间无法之间访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

线程、主内存、工作内存三者的交互关系如下图:

多级缓存和一致性问题

单线程环境下。缓存只会被一个线程访问,不会出现访问冲突等问题。

单核CPU,多线程环境下。不同的线程在访问共享数据时,会访问到相同的缓存位置。由于在任意时刻,只能有一个线程在执行,因此也不会出现缓存访问冲突等问题。

多核CPU,多线程环境下。每个内核都至少会有一个L1缓存,多个线程访问进程中的某个共享内存,且这些线程是在不同的内核上执行,则每个内核都会在自己的缓存中保留一份共享内存变量的副本。由于多核是可以并行的,可能会出现多个线程同时写各自的缓存的情况,而各自的缓存之间的数据就有可能不同。

在CPU和主存之间增加缓存,在多线程场景下就可能会存在缓存一致性问题。也就是说,在多核CPU中,每个核自己的缓存中,关于同一个数据的缓存内容可能不一致。

并发编程的原子性,可见性与有序性问题

原子性

原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会被其他线程影响。在java中,对基本数据类型的变量的读取和赋值操作是原子性操作。

比如经典的i++,就不是原子性操作。完成i++一共有三个步骤: load、add、save.多线程环境下,共享变量会被多个线程同时进行操作,操作完之后共享变量的值就会和期望的可能不一致。举个例子︰如果i=1,我们进行2次i++操作,我们期望的结果是3,但是结果有可能是2.

可见性

可见性指的是当一个线程修改了某个共享变量的值,其他线程是可以能够马上得知这个修改的值。

对于串行程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。

但是多线程环境下,由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题

有序性

有序性是指对于多线程环境,代码的执行可能会出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。

JMM如何解决原子性,可见性及有序性问题

Java中提供了一系列和并发处理相关的关键字,比如volatile、synchronized、final、concurrent包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的关键字。

原子性

除了JVM自身提供的对基本数据类型读写操作的原子性外,可以通过synchronized和Lock 实现原子性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

可见性

volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值。另外synchronized和Lock也可以保证可见性,因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中。

有序性

有序性是指对于多线程环境,代码的执行可能会出现乱序现象,因为程序编译成机器码指令后可能会出现指令重排现象,重排后的指令与原指令的顺序未必一致。

在Java里面,可以通过volatile关键字来保证有序性。另外可以通过synchronized和Lock来保证有序性,很显然,

synchronized和Lock保证每个时刻是有一个线程执行同步代码,相当于是让线程顺序执行同步代码,自然就保证了有序性。

拓展知识

内存交互

关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成。

数据同步八大原子操作

  • lock(锁定)︰作用于主内存的变量,把一个变量标记为一条线程独占状态。
  • unlock(解锁)︰作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
  • read(读取)︰作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用。
  • load(载入)︰作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用)︰作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎。
  • assign(赋值)︰作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量。
  • store(存储)︰作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
  • write(写入)︰作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传送到主内存的变量


如果要把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。

同步规则分析

1)不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中

2) 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。

3) 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。

4) 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。

5)如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。

6)对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

as-if-serial语义

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

为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被编译器和处理器重排序。

先行发生原则(happens-before)

只靠sychronized和volatile关键字来保证原子性、可见性以及有序性,那么编写并发程序可能会显得十分麻烦,幸运的是,从JDK 5开始,Java使用新的JSR-133内存模型,提供了happens-before原则来辅助保证程序执行的原子性,可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据.

happens-before 原则内容
  1. 程序顺序原则,即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。
  2. 锁规则解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说,如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。
  3. volatile规则 volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。
  4. 线程启动规则线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见
  5. 传递性A先于B,B先于C那么A必然先于C
  6. 线程终止规则线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程绛止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见
  • 39
    点赞
  • 46
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值