JVM——JMM

2018/03/14

Java Memory Model——JMM。

一 概念

  1. 定义了多线程之间共享变量的可见性。
  2. 对共享变量进行同步。
  3. 并发编程的两个问题:通信和同步。

二 线程间通信方式——共享内存模型

  1. 一个线程的共享变量 对另一个线程可见。
  2. 线程之间的共享变量存储在 主内存Main Memory
  3. 每个线程都有一个私有的本地内存 Local Memory,本地内存存储该线程以读/写共享变量的副本。
  4. 本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了写缓冲,寄存器,以及其他硬件和编译器优化。

1 两个线程之间的通信

  1. 线程A把本地内存A中修改过的共享变量同步到主存中。
  2. B线程到主内存中读取A线程修改过的共享变量。

2 JMM的实现

  1. Java内存模型把内存分为2部分:线程栈区ThreadStack,堆区Heap。
  2. 线程栈也称为调用栈,包含了当前线程执行的方法调用相关信息,当前方法的所有局部变量信息,存储基本数据类型的本地变量,局部变量的对象的引用。
  3. 堆包含Java应用创建的所有对象信息,包括基本数据类型的封装类;不管对象是属于成员变量,还是方法中的局部变量,都会被存储在堆中。static变量以及类本身都会存储在堆中。

3 JMM基础原理

  1. 指令重排序
  2. 数据依赖性
  3. 内存屏障

三 硬件内存

  1. CPU寄存器(在CPU内部),CPU多级缓存,RAM主存。
  2. Java内存模型和硬件内存交叉访问。堆和栈都可以访问硬件所有的存储空间。
  3. 当对象和变量存储到计算机的各个区域时,必然会有2个问题:
    • 共享对象对各个线程的可见性
    • 共享对象的竞争现象

四 关于并发编程

  1. 核心理论
  2. 线程间的协作(wait/notify/sleep/yield/join)
  3. 竞争对象Synchronized及其实现原理,底层优化(轻量级锁,偏向锁)
  4. 共享对象的可见性 volatile 的使用及其原理

1 并发核心理论

  1. 共享性
  2. 互斥性:资源互斥,只允许一个访问者对其进行访问,具有唯一性和排他性。将锁分为共享锁和排它锁,也称读锁和写锁。
  3. 原子性:数据的操作是一个独立不可分割的整体(如 i++).
  4. 可见性
  5. 有序性:
    • 编译器优化的重排序。
    • 指令级并行的重排序。
    • 内存系统的重排序。

2 线程间协作

2.1 线程状态

  1. 新建 New: new Thread();
  2. 就绪 Runnable: 调用start后,等待CPU资源。
  3. 运行 Running : 就绪状态获取到CPU执行权后进入运行态。就是进入run方法。
  4. 阻塞 Blocked:线程没有执行完,就让出CPU,自身进入阻塞状态。
  5. 死亡 Dead:线程执行完成或执行过程中产生异常。

2.2 Object类 wait/notify/notifyAll方法的使用

  1. wait()是将当前线程挂起,进入阻塞状态,直到notify或notifyAll方法来唤醒线程,如设定时间后在这个时间范围内没有唤醒,则会自动唤醒。
  2. notify/notifyAll() 唤醒对应的对象的monitor。
  3. wait()后,线程会释放对monitor对象的所有权。但wait()必须依赖于同步。synchronized。
  4. 一个通过wait()阻塞的线程,必须满足:线程需要被唤醒(notify),线程唤醒后需要竞争到锁(monitor)。

2.3 Thread类 sleep/yield/join方法

  1. sleep()让当前线程暂停指定的时间(ms)。只是让出CPU,并不释放锁。
  2. yield()暂停当前线程,以便其他线程有机会执行,不过不能指定暂停时间,也不保证马上就暂停。Running状态变为Runnable就绪状态。一般用来测试调试。
  3. join()父线程等待子线程执行完成后再执行——将异步执行的线程合并为同步的线程
    • 通过wait()来将线程的阻塞,如果join的线程还在执行,则将当前线程阻塞起 来,直到join的线程执行完成,当前线程才能执行。
    • join只调用了wait(),却没有对应的notify(),原因是Thread的start()中做了相应的处理,所以当join的线程执行完成以后,会自动唤醒主线程继续往下执行。

3 竞争对象——互斥 synchronizd

synchronized是Java中解决并发问题最常见的一种方法,也是最简单的。

3.1 Synchronized 作用

  1. 保证共享变量的修改能够及时可见——多线程共享一个对象,同时去修改它,保持可见性。
  2. 确保线程互斥的访问,以及同步代码——即同一时间只能一个线程进入代码竞争区。
  3. 有效解决重排序问题。
  4. 三种用法:修饰普通Method,static Method,代码块。
public synchroinzed void method() {}
public static synchroinzed void staticMethod() {}
public void codeBlockMethod() {
	synchroinzed (this) {
		thread.doSomethind();
	}
}

3.2 Synchroinzed 原理

  1. 对于方法块来说,使用的是monitorenter, monitorexit
  2. 关于monitorenter:
    • 如果monitor entry count = 0,则该线程进入monitor,然后reset 进入数=1,该线程为monitor的所有者。
    • 如果线程已经占用该monitor,只是重新进入,reset monitor的进入数=1;
    • 如果其他线程占用了monitor,则该线程进入阻塞,知道monitor进入数为0时,重新尝试获取monitor的所有权 ownership。
  3. 关于monitorexit:
    • 执行monitorexit的线程必须是Objectref所对应的monitor的所有者。
    • 指令执行时,monitor进入数-1,线程退出monitor,不再是这个monitor的owner。其他阻塞线程可尝试获取monitor的ownership。
  4. wait/notify等方法也依赖monitor对象,这就是为何只有在同步的块或方法中才可调用wait/notif,否则会抛出IllegalMonitorStateException.

  5. 对于方法的同步,常量池中有一个ACC_SYNCHRONIZED标识符。JVM根据此符号实现方法调用。
    • 方法调用时,调用指令会检查这个标识符是否被设置。如果设置了,执行线程将先获取monitor,获取成功过后执行方法体,执行完后再释放。
    • 在执行期间,任何线程都无法再获得同一个monitor对象。
    • 本质上都是要用monitor实现同步,但方法的同步是一种隐式的实现,无需通过字节码完成。

3.2 Synchronized 执行结果

  1. 1个对象的2个普通方法的同步:竞争对象上的锁(monitor),只能互斥获取到锁。所以就会顺序执行两方法。
  2. 同一个类的2个对象的2个static 方法的同步:获取同一个类上的锁monitor,也只能顺序执行。
  3. 2个代码块的同步:竞争1个对象this的monitor,(sychronized(this)),顺序执行。

3.3 Synchronized 底层优化——偏向锁,轻量级

3.3.1 重量级锁
  1. Synchronized 是通过对象内部的一个叫做monitor锁来实现的。
  2. 但监视器锁本身依赖于底层OS的Mutex Lock来实现的,OS实现线程间的切换需要从用户态->核心态。状态之间的切换导致Synchronized低效的原因。
  3. Mutex Lock称为重量级锁。Java1.6后为了减少获得锁和释放锁带来的性能消耗,引入了轻量级锁,偏向锁
3.3.2 轻量级锁
  1. 锁的四种状态:无锁状态,偏向锁,轻量级锁和重量级锁。锁的升级是单向的,只能从低向高升级。
3.3.3 几种 锁的比较

lock

4 共享对象的可见性 volatile

4.1 volatile 作用

  1. 防止重排序。在并发环境下的单例模式中,一般采用双重检查锁DDL的方式实现。
    • 实例化一个对象的步骤:1 分配内存空间 2 初始化对象 3 将内存空间的地址赋值给对应的引用。
    • 使用volatile修饰singleton,就是为了防止在多线程环境下这个过程的重排序,将一个未初始化的对象引用暴露出来。
     public static volatile Singleton singletion;
    	
     public static Singleton getInstance() {
         if (singleton == null) {
             synchroinzed (this) {
                 singleton = new Singleton();
             }
         }	
     }
    
  2. 实现可见性:一个线程修改了共享变量值,保证另一个线程可见。保证变量直接从主存读取,对变量的更新也会直接写到主存中。
  3. 保证原子性:对volatile变量的单次读写操作可以保证原子性,如long和double类型变量,但不能保证i++这种操作的原子性,因为本质上i++是读写两次操作。若要保证这类操作的原子性可通过AtomicInteger 或 Synchronized

4.2 volatile 原理

  1. 有序性的实现。
    • a happen-before b规则: a所做的任何操作对b可见。单线程内按代码顺序执行。
    • monitor解锁操作 happen-before 加锁操作 (Sychronized规则)。
    • 对volatile变量的写操作 happen-before 读操作 (volatile规则)。
    • 线程start()方法 happen-before 该线程所有 后续操作 (线程启动规则)。
    • 线程所有的操作 happen-before 其他线程在该线程调用 join返回成功后的操作;
    • 如果 a happen-before b, b happen-before c, 则 a happen-before c (传递性)。
  2. 可见性的实现。
    • 线程的工作内存和主内存进行数据交互。
    • volatile变量修改后强制更新到主内存中。
    • volatile变量修改后其他线程工作内存的对应值失效,使用时需要从主内存中读取。
  3. 内存屏障 —— 内存栅栏
    • 为了实现volatile可见性和happen-before的语义,JVM使用 内存屏障来完成。
    • 是一组处理器指令,用于实现对内存操作的顺序限制。

参考文献 Java并发编程

文内导航