Java 内存模型与 Synchronized 原理

本贴最后更新于 1450 天前,其中的信息可能已经事过境迁

并发问题的根源不在乎以下几个原因:可见性、原子性、有序性。

Java 常用 Synchronized、volatile 关键字来解决并发问题,在了解这两个关键字之前我们先来看看 Java 内存模型方便理解并发问题是如何产生的。

Java 内存模型(JMM)

硬件内存模型

目前基于高速缓存的存储交互很好的解决了 cpu 和内存等其他硬件之间的速度矛盾,多核情况下各个处理器(核)都要遵循一定的诸如 MSI、MESI 等协议来保证内存的各个处理器高速缓存和主内存的数据的一致性。

硬件内存模型.png

Java 内存模式(JMM)

Java 内存模型来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各个平台下都能达到一致的并发效果。
主内存: Java 虚拟机规定所有的变量(不是程序中的变量)都必须在主内存中产生,为了方便理解,可以认为是堆区。
工作内存: Java 虚拟机中每个线程都有自己的工作内存,该内存是线程私有的为了方便理解,可以认为是虚拟机栈。
主内存、工作内存与 java 内存区域中的 java 堆、虚拟机栈、方法区并不是一个层次的内存划分。这两者是基本上是没有关系的,上文只是为了便于理解,做的类比

Java 内存模型.png

JMM 如何保证并发编程

Java 内存模型围绕着并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的

  • 原子性(Automicity): 指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
  • 可见性: 指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。(volatile 在 JMM 模型上实现 MESI 协议)
  • 有序性: 指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。(保证有序性的关键字有 volatile 和 synchronized,volatile 禁止了指令重排序,而 synchronized 则由“一个变量在同一时刻只能被一个线程对其进行 lock 操作”来保证。

指令重排: 可以保证串行语义一致,但是没有义务保证多线程间的语义一致,对于提高 CPU 处理性能是十分重要的

Happen-Before 规则: (不能重排的指令) 程序顺序原则、volatile 规则、锁规则...

Synchronized

Synchronized 除了原子性、可见性、有序性之外还有可重入性(一个线程可以重复申请锁)

Synchronized 的用法

  1. 作用在实例方法:监视器锁(monitor)便是对象实例
  2. 作用在静态方法:监视器锁(monitor)便是对象的 Class 实例,Class 数据存在方法区(永久代)
  3. 作用在代码块,监视器锁(monitor)便是括号起来的对象实例
public void test0(){
    System.out.println("test0");
}

public synchronized void test1(){
    System.out.println("test1");
}

public static synchronized void test2(){
    System.out.println("test2");
}

public void test3() {
    synchronized (this) {
        System.out.println("test3");
    }
}

查看 Synchronized 字节码

使用 javap -v 命令反编译 class 文件即可得到字节码文件,下面我们分别查看上述三种用法的字节码文件:

  • 同步方法

作用在方法上可以看到在 flags 中多了一个 ACC_SYNCHRONIZED 的标志,这标志用来告诉 JVM 这是一个同步方法,在进入该方法之前先获取相应的锁,锁的计数器 +1,方法结束后计数器 -1,如果获取失败就阻塞住,直到该锁被释放。

test1():

同步方法.png

static test2():

test2.png

  • 同步代码块

从反编译的同步代码块可以看到同步块是由 monitorenter 指令进入,然后 monitorexit 释放锁,在执行 monitorenter 之前需要尝试获取锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,那么就把锁的计数器 +1。当执行 monitorexit 指令时,锁的计数器也会 -1。当获取锁失败时会被阻塞,一直等待锁被释放。

image.png

但是为什么会有两个 monitorexit 呢?其实第二个 monitorexit 是来处理异常的,仔细看反编译的字节码,正常情况下第一个 monitorexit 之后会执行 goto 指令,而该指令转向的就是 22 行的 return,也就是说正常情况下只会执行第一个 monitorexit 释放锁,然后返回。而如果在执行中发生了异常,第二个 monitorexit 就起作用了,它是由编译器自动生成的,在发生异常时处理异常然后释放掉锁。

Synchronized 的底层实现

在理解底层实现之前先了解一下 Java 对象头和 Monitor,在 JVM 中,对象分为三部分存在的:对象头、实例数据、对齐填充

image.png

  • 实例数据: 存放类的属性数据信息,包括父类的属性信息,如果是数组的实例部分还包括数组的长度,这部分内存按 4 字节对齐
  • 对其填充: 不是必须部分,由于虚拟机要求对象起始地址必须是 8 字节的整数倍,对齐填充仅仅是为了使字节对齐
  • 对象头: 在 Hotshot 虚拟机的对象头主要由 Mark WordClass Metadata Address 组成。其中 Mark Word 存储对象的 hashCode锁信息分代年龄GC标志信息Class Metadata Address 是类型指针 指向对象的类元数据,JVM 通过该指针确定该对象是那个类的实例。

Mark Word 怎么存储锁信息

JDK6 之前只有两个状态:无锁有锁(重量级锁),而在 JDK6 之后对 synchronized 进行了优化,新增了两种状态,总共就是四个状态:无锁状态偏向锁轻量级锁重量级锁,其中无锁就是一种状态了。考虑到存储成本,Mark Word 被设计成一个非固定的数据结构,它会根据对象的状态复用自己的存储空间,它可能随着运行状态变成下面 4 中数据:

image.png

最后两位存储锁的标志位,01 是初始状态,未加锁。偏向锁存储的是当前占用此对象的线程 ID;而轻量级则存储指向线程栈中锁记录的指针

Monitor 对象

每一个锁都对应一个 monitor对象,在 HotSpot 虚拟机中它是由 ObjectMonitor 实现的(C++ 实现)。每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

ObjectMonitor() {
    _header       = NULL;
    _count        = 0;  //锁计数器
    _waiters      = 0,
    _recursions   = 0;
    _object       = NULL;
    _owner        = NULL;
    _WaitSet      = NULL; //处于wait状态的线程,会被加入到_WaitSet
    _WaitSetLock  = 0 ;
    _Responsible  = NULL ;
    _succ         = NULL ;
    _cxq          = NULL ;
    FreeNext      = NULL ;
    _EntryList    = NULL ; //处于等待锁block状态的线程,会被加入到该列表
    _SpinFreq     = 0 ;
    _SpinClock    = 0 ;
    OwnerIsThread = 0 ;
  }
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3187 引用 • 8213 回帖
  • JVM

    JVM(Java Virtual Machine)Java 虚拟机是一个微型操作系统,有自己的硬件构架体系,还有相应的指令系统。能够识别 Java 独特的 .class 文件(字节码),能够将这些文件中的信息读取出来,使得 Java 程序只需要生成 Java 虚拟机上的字节码后就能在不同操作系统平台上进行运行。

    180 引用 • 120 回帖
  • synchronized
    3 引用 • 6 回帖

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...
  • Tooi6
    作者

    👍