【Java并发编程】01-内存模型与Synchronized、volatile关键字

并发问题的根源不在乎以下几个原因:可见性、原子性、有序性。Java常用Synchronized、volatile关键字来解决并发问题,在了解这两个关键字之前我们先来看看Java内存模型方便理解并发问题是如何产生的。

Java内存模型(JMM)

物理硬件内存模型和Java内存模型

  • 物理硬件内存模型

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

物理内存模型

  • Java内存模型(JMM)

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

Java内存模型

  • JMM如何保证并发编程?

Java内存模型围绕着并发过程中如何处理原子性、可见性和顺序性这三个特征来设计的
原子性(Automicity): 指一个操作是不可中断的,即使是多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程干扰。
可见性: 指当一个线程修改了某一个共享变量的值,其他线程是否能够立即知道这个修改。(volatile在JMM模型上实现MESI协议)
有序性: 指对于单线程的执行代码,执行是按顺序依次进行的。但在多线程环境中,则可能出现乱序现象,因为在编译过程会出现“指令重排”,重排后的指令与原指令的顺序未必一致。(保证有序性的关键字有volatile和synchronized,volatile禁止了指令重排序,而synchronized则由“一个变量在同一时刻只能被一个线程对其进行lock操作”来保证。)
指令重排: 可以保证串行语义一致,但是没有义务保证多线程间的语义一致,对于提高CPU处理性能是十分重要的
Happen-Before规则: (不能重排的指令)
程序顺序原则、volatile规则、锁规则...

  • JVM结构

Java程序的分配是在JVM虚拟机内存分配下完成的。(JMM是内存模型的规范,JVM则是实现)


JVM

解析 Synchronized 和 volatile 关键字

Synchronized

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

  • Synchronized的用法

1、作用在实例方法:监视器锁(monitor)便是对象实例
2、作用在静态方法:监视器锁(monitor)便是对象的Class实例,Class数据存在方法区(永久代)
3、作用在代码块,监视器锁(monitor)便是括号起来的对象实例

  • 查看字节码

我们先创建下面例子,分别使用了Synchronized的三种用法

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");
    }
}

然后通过javap -v 反编译class文件,可以得到:

1、同步方法:

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

image

image

2、同步代码块

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

image

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

  • synchronized的底层实现

1、Java对象头

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

image

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

1.1 Mark Word怎么存储锁信息?

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

image

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

2、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 ;
  }

2、volatile

volatile保证被修饰的变量具有可见性、有序性。被volatile修饰的变量能够保证每个线程能够获取该变量的最新值,从而避免出现数据脏读的现象。

  • volatile的特性

可见性: volatile变量修改后,修改的值立即写入主存,并将其他线程工作空间该变量的缓存cache line置为无效。
有序性: volatile变量在读写操作时,保证前面的代码已经执行,后面的代码一定未执行。

  • volatile实现原理

观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令

  • volatile的使用场景

使用volatile关键字必须具备2个条件:
1、对变量的写操作不依赖于当前值
2、该变量没有包含在具有其他变量的不变式中

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,772评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,458评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,610评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,640评论 1 276
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,657评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,590评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,962评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,631评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,870评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,611评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,704评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,386评论 4 319
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,969评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,944评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,179评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 44,742评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,440评论 2 342