volatile

volatile是轻量级的synchronized,他在多级处理器开发中保证了共享变量的"可见性"。可见性的意思是当一个线程修改一个共享变量是,另一个线程能读到这个修改值。

定义与原理实现

Java语言提供了volatile,在某些情况下比锁要更加方便。如果一个字段被声明成volatile,java线程内存模型确保所有的线程看到这个变量的值是一致的。

前置知识 CPU术语定义

术语 英文单词 术语描述
内存屏障 memory barriers 是一组处理器指令,用于实现对内存操作的顺序限制
缓存行 cache line 缓存中可以分配的最小存储单位。处理器填写缓存线时会加载整个缓存村线,需要使用多个主内存读周期
原子操作 atomic operations 不可中断的一个或一系列操作
缓存行填充 cache line fill 当处理器识别到从内存中读取操作是可缓存的,处理器读取整个缓存行到适当的缓存(L1,L2,L3的或所有)
缓存命中 cache hit 如果进行高速缓存进填充操作的内存位置仍然是下次处理器访问的地址是,处理从缓存中读取操作书,而不是从内存读取。
写命中 write hit 当处理器将操作书写回到一个内存缓存的区域是,首先会检查这个缓存的内存地址是否在缓存行中,如果存在一个有效的缓存行,则处理器将这个操作书写回到缓存,而不是写回到内存,这个操作被称为写命中
写缺失 write misses the cahce 一个有效的缓存行被写入到不存在的内存区域

通过将java代码转化成汇编代码,会发现被volatile修饰的变量前面添加了lock 前缀的指令。

lock 前缀的指令在多核处理器下会做两件事:

1.将当前处理器缓存行的数据写回到系统内存中。
2.这个写回内存的操作会使在其他CPU里缓存了该内存地址的数据无效。

为了提高处理速度,处理器不直接和内存进行通信,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后在进行操作,但操作完不知道何时会写入内存。如果对声明了volatile的变量进行写操作,JVM就会想处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是,就算写回到内存,如果其他处理器缓存的值还是旧的,再次执行计算操作就会有问题。所以,在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器对这个数据进行操作的时候,会重新从系统内存中把数据读到处理器缓存里。

volatile的使用优化

在volatile修饰的变量前后凑够64字节,这样就可以一个变量占用一个缓存行,减小读取缓存行的次数,提升多线程的效率。

但是在java 7 下不可能生效,java 7 会淘汰或重新排列无用字段。

synchronized

实现原理与应用

synchronized实现同步的基础:Java中的每一个对象都可以作为锁。具体表现为:

对于普通同步方法,锁是当前实例对象。
对于静态同步方法,锁是当前类的Class对象。
对于同步方法块,锁是synchronized括号里配置的对象。

在JVM规范中synchronized的实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但两者的实现细节不一样。代码块同步时使用monitorentermonitorexit指令实现的,而方法同步时使用另一种方式来实现的,但是细节在jvm规范里没有细说。但时方法的同步同样可以使用这两个指令完成。

注意:monitorenter指令时在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束和异常处,JVM要保证每个monitorenter必须有对应的monitorexit与之配对。任何对象都有一个monitor与之关联,当且一个monitor被持有后,它将处于锁定状态。线程执行到monitroenter指令时,将会尝试获取对象对应的monitor的所有权,即尝试获得对象的锁。

Java对象头

synchronized用的锁时存在Java对象头里的。

对象头包括 MrakWord,classpointer,数组长度。三部分。对象的实例数据和对齐填充字节是对象剩余的部分。

Mark Word

这部分主要用来存储对象自身运行时数据,如hashcode,gc分代年龄,锁标记位。

classpointer

存储到对象类型数据的指针

数组长度

如果该对象时数据组的话,存放的时数组长度。

锁升级与对比

在Java SE 1.6 中,锁一共有4中状态,级别从低到高依次时:无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不降级,意味这偏向锁升级成为轻量级锁后不能降级成偏向锁。在synchronized就存在锁升级的问题。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得执行,为了让线程获得锁的代价更低从而引入偏向锁。当一个线程访问同步代码块并获取锁时,会在对象头和栈帧中的锁记录里存储偏向线程ID,以后该线程在进入和推出代码块时不需要进行CAS操作来加锁和解锁,只需要简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要在在测试一下Mark Word中偏向锁标识是否设置为1(表示当前是偏向锁):如果没写设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

1)偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁是,持有偏向锁的线程才会释放锁。偏向锁的撤销,需要等待全局安全点(在这个事件点上没有正在执行的字节码)。它会首先暂停拥有偏向锁的线程,然后检查持有偏向锁的线程是否活着,如果线程不处于活动状态,则将对象头设置成无所状态;如果线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word 要么重新偏向于其他线程,要么恢复到无锁或者标记对象不适合作为偏向锁,最后唤醒暂停线程。

2)关闭偏向锁

偏向锁在Java 6和Java7里面都是默认启动,但是在程序启动几秒后才会激活。如有必要可以使用JVM参数来关闭延迟:-XX:BiasedLockingStartupDelay=0。如果你确定应用程序里所有的锁通常情况下处于竞争状态,可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false,那么程序默认会进入轻量级锁状态。

轻量级锁

1)轻量级锁加锁

线程在执行同步代码块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word 复制到锁记录中,官方成为Dispaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word 替换为执行锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

2)轻量级锁解锁

轻量级解锁是,会使用原子的CAS操作将Displaced Mark Word 替换回对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

因为自旋会消耗CPU(自旋10次升级),为了避免无用的自旋(比如获得锁的线程被阻塞了),一旦锁升级成为重量级锁,就不会在恢复到轻量级锁。

重量级锁

重量级锁就是去操作系统申请资源。通过对象内部的监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实 现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。线程竞争不使用自旋,不会消耗CPU。但是线程会进入阻塞等待被其他线程被唤醒,响应时间缓慢。

锁对比

优点 缺点 使用场景
偏向锁 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 适用于只有一个线程访问同步块的场景
轻量级锁 竞争的线程不会阻塞,提高了程序的响应速度 如果始终得不到锁竞争的线程,使用自旋会消耗CPU 追求相应时间,同步块执行速度非常快
重量级锁 线程竞争不使用自旋,不会消耗CPU 线程阻塞,响应时间缓慢 追求吞吐量,同步块执行速度较长

原子操作

原子(atomic)本意是“不能被进一步分割的最小粒子”,而原子操作(atomic operation)意为“不可被中断的一个或一系列操作”。

前置知识

术语名称 英文 解释
缓存行 Cache line 缓存的最小操作单位
比较并交换 Compare and Swap CAS的操作需要输入两个数值,一个旧值(期望操作前的值)和一个新值,在操作期间先比较旧值有没有发生变化,如果没有发生变化,才交换成新值,发生了变化则不交换
CPU流水线 CPU pipeline CPU流水先的工作方式就像工业生产上的装配流水先,在CPU中有56个不同功能的电路单元组成一条指令处理流水线,然后将一条指令分成56步后在由这些电路单元分别执行,这样就能实现在一个CPU时钟周期完成一条指令,因此提高CPU的运算速度。
内存顺序冲突 Menory order violation 内存顺序冲突一般是由假共享引起的,假共享是指多个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU同时修改同一个缓存行的不同部分而引起其中一个CPU的操作无效,当出现这个内存顺序冲突是,CPU必须清空流水线。

处理器实现

使用总线锁保证原子行

如果多个处理器同时对共享变量进行读改写操作,那么共享变量就会被多个处理器同时进行操作,这样读改写操作就不是原子的,操作完之后,共享变量的值会和期望的不一致。

原因可能是多个处理器同时从各自的缓存中读取变量,分别进行操作,然后分别写入系统内存中.为了保障写共享,变量的操作是原子的,就必须保证CPU1读写改共享变量的时候,CPU2不能操作缓存了该共享变量内存地址的缓存

处理器使用总线锁来解决这个问题的。所谓总线锁就是使用处理器提供的一个LOCK #信号,当一个处理器在总线上输出此信号是,其他处理器的请求将被阻塞住,那么该处理器可以独占共享内存。

使用缓存锁保证原子性

在同一时刻,只需保证对某个内存地址的操作是原子性即可,但总线锁定把cpu和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大,目前处理器在某些场合下使用缓存锁代替总线锁定来进行优化。

频繁使用内存会缓存在处理器的L1,L2和L3高速缓存里,所以原子操作直接在处理器内部缓存中进行,并不需要声明总线锁。

可以使用"缓存锁定"的方式来实现复杂的原子性。

缓存锁定

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

特殊情况处理器不会使用缓存锁定

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

Java实现

在Java中可以通过锁和循环CAS的方式来实现原子操作。

循环CAS实现原子操作

JVM中的CAS操作正式利用了处理器提供的CMPXCHG指令实现的。自旋CAS实现的基本思路就是循环进行CAS操作知道成功为止。

从Java1.5 开始,JDK的并发包提供了一些类来支持原子操作,如AtomicBoolean,AtomicInteger和AtomicLong.

CAS实现原子操作的三大问题

1)ABA问题 因为CAS需要在操作值的时候,检查值有没有发生变化,如果没有发生变化则更新,但是如果一个值原来是A,变成了B,又变成了A,那么使用cas进行检查时会发现它的值没有发生变化,但实际上却变化了。ABA问题的解决思路时使用版本号。jdk的Atomic包里提供了一个类AtomicStampedReference来解决问题。
2)循环时间长开销大。自旋CAS如果长时间不成功,会给cpu带来非常大的执行开销。如果JVM能支持处理器提供的pause指令,那么效率会有一定的提升。pause指令有两个作用:(1)可以延迟流水执行指令,使cpu不会消耗过多的执行资源,延迟的时间取决与具体实现的版本。(2)可以避免在退出循环的时候因为内存顺序冲突从而引起cpu流水线被清空。从而提高CPU的执行效率。
3)只能保证一个共享变量的原子操作。当对一个共享变量执行操作时,可以使用循环CAS的方式来保证原子操作,但对多个共享变量操作时,循环CAS就无法保证操作的原子性,这个时候就可以用锁。

锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。

参考书籍:
《Java并发编程的艺术》

Java并发机制的底层实现原理(Java并发编程的艺术整理)相关推荐

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

    Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令.本章我们将 ...

  2. 《Java并发编程的艺术》一一第2章Java并发机制的底层实现原理

    第2章Java并发机制的底层实现原理 2.1 volatile的应用 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行, ...

  3. 《Java并发编程的艺术》:第2章 Java并发机制的底层实现原理

    前言 Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节 码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和 CPU的指令. ...

  4. java并发机制的底层实现原理(volatile,synchronized,原子操作)

    目录 volatile的应用 volatile的定义与实现原理 volatile的使用优化 synchronized的实现原理与应用 Java对象头 锁的升级与对比 偏向锁 轻量级锁 锁的优缺点对比 ...

  5. 【java并发编程艺术学习】(四)第二章 java并发机制的底层实现原理 学习记录(二) synchronized...

    章节介绍 本章节主要学习 Java SE 1.6 中为了减少获得锁 和 释放锁 时带来的性能消耗 而引入的偏向锁 和 轻量级锁,以及锁的存储结构 和 升级过程. synchronized实现同步的基础 ...

  6. RocketMQ(九):rocketMQ设计的全链路消息零丢失方案?+rocketmq消息中间件事务消息机制的底层实现原理?+half是什么?+half消息是如何对消费者不可见的?

    前言: 目前rocketmq更新已经更新了11篇博客了,预计接下来的2-3篇是暂时的更新进度了,准备更新一下springboot或者是jvm,mysql相关的专题出来,后续更新完事后,再分享一些实战性 ...

  7. java语言中 负责并发编程的机制是_Java并发编程艺术-并发机制的底层原理实现...

    Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量. volatile借助Java内存模型保证所有线程能够看到最新的值.(内存可见性) ...

  8. Java高并发编程(二):Java并发机制的底层实现机制

    Java代码在编译后会变成Java字节码,字节码在之后被类加载机制加载到JVM中,JVM执行字节码,最终需要转换为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM的实现和CPU的指令. ...

  9. java反射机制的概念及原理

    java反射机制 什么是反射? 在java开发中有一个非常重要的概念就是java反射机制,也是java的重要特征之一.反射的概念是由Smith在1982年首次提出的,主要是指程序可以访问.检测和修改它 ...

  10. Java反射自定义注解底层设计原理

    文章目录 一.反射 1. 反射概念 2. 反射机制的优缺点 3. 反射的用途 4. 反射技术的使用 5. 反射常用的Api 6. 反射执行构造函数 7. 反射执行给属性赋值 8. 反射执行调用方法 二 ...

最新文章

  1. Java编程时部分快捷键
  2. 【Matlab 图像】边缘检测算法及效果演示
  3. CreateThread
  4. java access dbq_Java-Access汇总
  5. [js] ajax如何接收后台传来的图片?
  6. (转)基于Metronic的Bootstrap开发框架经验总结(6)--对话框及提示框的处理和优化...
  7. linux+读取初始化文件,Linux 初始化系统 SystemV Upstart
  8. [Android Pro] app_process command in Android
  9. C# ToString()日期格式
  10. 计算机驱动程序检测,检测到计算机制造商图形驱动程序对于显卡驱动程序
  11. c++11:nlohmann::json进阶使用(二)应用adl_serializer解决第三方数据类型(such as uri)的序列化和反序列化
  12. Ubiquitous Religions(并查集)
  13. 扩展欧几里得算法 求解 丢番图方程
  14. JSON——Json对象扁平化
  15. 探究 | Elasticsearch如何物理删除给定期限的历史数据?
  16. 由于其配置信息(注册表中的)不完整或已损坏,Windows 无法启动这个硬件设备。 (代码 19)怎么办?
  17. python 调用接口
  18. 中文期刊模板的页面格式,以《电力系统自动化》为例
  19. 使用EF配合Linq语句进行查询
  20. Linux 查看某个端口的连接数

热门文章

  1. CREO产品柔性建模 参数化 模具 TOP DOWN设计
  2. 阿里云短信验证码发送类
  3. CAD随机圆形颗粒插件
  4. BUUCTF WEB easyweb
  5. MATLAB 去除图例legend外边框
  6. 西门子200smart与8台v90伺服驱动器Profinet通讯,控制8台伺服电机
  7. 程序员的算法趣题 python3 - (4)
  8. 《极客与团队》读书记录
  9. 林期苏曼属性标签编辑_标签制作软件如何制作商品标签模板
  10. MATLAB关系运算符和逻辑运算符