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

前言

代码最终需要转变为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令。
本文将深入底层去理解并发机制的底层实现原理。会介绍:

  1. volatile的实现原理
  2. final的实现原理
  3. synchronized的实现原理
  4. 原子操作的实现原理

1. volatile的定义和实现原理

  • volatile定义
    Java语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量。
    如果一个变量被声明成volatile,Java线程内存模型会确保所有线程看到这个变量的值是一致的。它在多处理器开发中通过插入内存屏障保证了共享变量的可见性,并且禁止指令重排序。

  • CPU术语定义和内存屏障类型


    CPU术语定义

    内存屏障类型
  • volatile是如何保证可见性的呢
    通过查看生成的汇编指令发现,对volatile进行写操作时:

    1. Lock前缀指令将当前处理器缓存行的数据写回到系统内存
      • 为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(线程工作内存)后再进行操作,但操作完后不会立刻写会到主内存。
      • 如果是对volatile变量进行写操作,JVM会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是其他处理器缓存还是旧的,再执行计算操作就会有问题。
    2. 一个处理器的缓存写回到内存的操作会使在其他CPU里缓存了该内存地址的数据无效
      • 所以为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议。
        每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从主内存中把数据读到处理器缓存中。
  • volatile的实现原理:
    在每个volatile写操作前插入一个StoreStore屏障
    在每个volatile写操作后插入一个StoreLoad屏障
    在每个volatile读操作后插入一个LoadLoad屏障
    在每个volatile读操作后插入一个LoadStore屏障
    这是JMM基于保守策略的内存屏障插入策略,在具体实现时,只要不改变volatile写-读的内存语义,编译器可以根据具体情况省略不必要的屏障。

  • volatile只保证了并发的可见性和顺序性,并未保证原子性,在使用时要多多小心。

2. final域的内存语义

  • 对于final域,编译器和处理器要遵守两个重排序规则:

    1. 写final域的重排序规则:在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    2. 读final域的重排序规则:初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序。
    3. 对于引用类型:在构造函数内对一个final引用对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作不能重排序(即会先初始化final引用变量的成员变量)。
  • 写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。

  • 读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。

  • 只要对象是被正确构造的,即被构造对象的引用在构造函数中没有逸出,那么不需要使用同步,就可以保证任意线程都能看到这个final域在构造函数中被初始化的值。

  • final的实现原理:
    写final域的重排序规则要求编译器在final域的写之后,构造函数return之前插入StoreStore屏障;
    读final域的重排序规则要求编译器在读final域的操作前面插入一个LoadLoad屏障。

3. synchronized的实现原理

本节介绍对synchronized的优化,在Java6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

  • 当一个线程试图访问同步代码块时,它首先必须要得到锁,退出或抛出异常必须释放锁;Java中的每一个对象都可以作为锁,具体表现为以下3种形式:
    1. 对于普通同步方法,锁是当前实例对象
    2. 对于静态同步方法,锁是当前类的Class对象
    3. 对于同步方法块,锁是Synchronized括号里配置的对象

从JVM规范中可以看到,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步。但两者实现细节不一样。

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

  2. 方法同步
    synchronized方法在method_info结构AAC_synchronized标记,线程执行时会识别该标记,获取对应的锁,实现方法同步。

两者实现细节不同,但本质上都是对一个对象的监视器(Monitor)的获取。
一个线程在执行到同步代码块或同步方法时,执行方法的线程必须先获得该对象的监视器才能执行;没有获取到监视器的线程将会阻塞,进入同步队列,状态变为Blocked。当成功获取监视器的线程释放锁后,会唤醒阻塞在同步队列中的线程,使其重新尝试对监视器的获取。

下一篇中将介绍锁的种类和升级。

4. 原子操作的实现原理

CPU术语定义

首先处理器会自动保证基本的内存操作原子性。而对复杂操作(跨总线宽度、跨多个缓存行和跨页表访问),处理器提供总线锁定和缓存锁定两个机制来保证它的操作的原子性。

  1. 使用总线锁保证原子性
    使用处理器提供的一个Lock#信号,当一个处理器在总线上输出此信号时,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。
    总线锁定把CPUT和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。
    所以处理器在某些场合下使用缓存锁定来代替总线锁定。

  2. 使用缓存锁保证原子性
    缓存锁定是指内存区域如果被缓存在处理器的缓存行中,并且在Lock操作期间被锁定,那么当它执行锁操作回写到内存时,处理器不在总线上声言LOCK#信号,而是修改内部的内存地址,并允许它的缓存一致性机制来保证操作的原子性,因为缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据,当其他处理器回写已被锁定的缓存行的数据时,会使缓存行无效。

  3. 不能使用缓存锁定的两种情况

    1. 处理器不支持
    2. 当前操作的数据不能被缓存在处理器内部,或者操作的数据跨多个缓存行时,处理器会调用总线锁定。

5. Java如何实现原子操作

通过 循环CAS 的方式来保证操作的原子性。

1.使用CAS实现原子操作
Java中CAS操作利用了处理器的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。

  • CAS实现原子操作的缺点
    1. ABA问题
      如果一个值原来是A,变成了B,又变成了A,那么CAS进行检查时会发现它的值没有发生变化,但实际上却变化了。
      ABA问题的解决思路上是使用版本号,在变量前追加上版本号,每次变量更新时把版本号加1。
      JDK提供了AtomicStampedReference来解决此类问题。
    2. 循环时间长开销大
      自旋CAS如果长时间不成功,会给CPU带来很大的执行开销。
    3. 只能保证一个共享变量的原子操作。
      对多个共享变量时,循环CAS就无法保证其原子性,这个时候可以用锁。
      取巧的办法时将多个共享变量合成一个共享变量来操作,比如i_j。
      JDK提供了AtomicReference类保证引用对象之间的原子性,可以把多个对象放在一个对象里进行CAS操作。
  1. 使用锁机制实现原子操作
    锁机制保证了只有获得锁的线程才能操作锁定的内存区域。
    除了偏向锁,JVM实现锁的方式都用了循环CAS,当一个线程进行同步块的时候使用CAS来获取锁,当线程退出同步块时使用CAS释放锁。

结语

本文介绍了volatile、final,synchronized、原子操作的实现原理,了解这些对后续了解并发框架和容器会更有帮助。

参考:
ABA问题示例

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

推荐阅读更多精彩内容