第二章 : Java并发机制的底层实现原理
程序员文章站
2022-05-13 15:10:03
...
Java代码在编译后会变成Java字节码,字节码被类加载器加载到JVM里,JVM执行字节码,最终需要转化为汇编指令在CPU上执行,Java中所使用的并发机制依赖于JVM实现和CPU指令
1. volatile的应用
- volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的"可见性",就是说当一个线程T1修改了一个共享变量,另外一个线程T2能读到这个被修改的值.它不会引起线程的上下文切换和调度,成本比synvhronized低.
- volatile的定义和实现原理
- Java语言规范第三版中对volatile的定义 : Java编程语言允许线程访问共享变量,为了确保共享变量能被准确和一致的更新,线程应该确保通过排他锁单独获得这个变量
- volatile如何保证可见性 :
// java代码
// instance是volatile变量
instance = new Singleton();
// 转变成汇编语言
0x01a3de1d: movb $0x0,0x1104800(%esi);0x01a3de24: lock add1$0x0,(%esp)
有volatile变量修饰的共享变量进行写操作时会多出第二行汇编代码,通过IA-32架构软件开发者手册可知,lock前缀指令在多核处理器下回引发两件事情
1). 将当前处理器缓存行的数据写回到系统内存.
2). 这个写回内存的操作会使在其他cpu里缓存了该内存地址的数据无效.
在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,当处理器发现自己缓存行对应的内存地址被修改,就会将处理器的缓存行设置成无效状态,当处理器对这个数据进行修改操作的时候,会重新从系统内存中吧数据读到处理器缓存里.
- volatile两条实现原则 :
1). Lock前缀指令会引起处理器缓存回写到内存
2). 一个处理器的缓存回写到内存会导致其他处理器的缓存无效 - volatile使用优化
1). LinkedTransferQueue在使用volatile变量时用一种追加字节(64)的方式来优化队列出列和入列的性能
2). 使用追加到64字节的方式来填满高速缓冲区的缓存行,避免头结点和尾节点加载到同一个缓存行,使头,尾节点修改时不会互相锁定
以下场景不可用 :
1). 缓存行非64字节宽的处理器
2). 共享变量不会被频繁的写
2. synchronized的实现原理与应用
- synchronized实现同步的基础 : java中每一个对象都可以作为锁,具体变现
1). 对于普通同步的方法,锁是当前实例对象
2). 对于静态同步方法,锁是当前类的Class对象
3). 对于同步方法快,锁是synchronized括号里配置的对象 - synchronized在JVM实现原理,JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,代码块同步是使用monitorenter和monitorexit指令实现,方法同步细节在JVM规范里没有,但是方法同步也可以使用这两个指令来实现
- monitorenter指令是在编译后插入到同步代码块的开始位置,monitorexit是插入到方法结束处和异常处,JVM要保证每个monitorenter必须有对应的monitorexie与之配对,任何对象都有一个monitor’与之关联,并且一个monitor被持有后,它将处于锁定状态,线程执行到monitorenter指令时,将会尝试获取对象所对应的monitor的所有权,即尝试获得对象的锁
2.1 : Java对象头
- synchronized用的锁是存在java对象头里的,如果对象是数组类型,则虚拟机用3个字宽(Word)存储对象头,如果是非数组类型,则用2个字宽存储对象头,在32位虚拟机中,1字宽等于4字节,即32bit,如果2-1
图: 2-1
长度 | 内容 | 说明 |
---|---|---|
32/64bit | Mark Word | 存储对象的hashCode或锁信息等 |
32/64bit | Class Metadata Address | 存储对象类型数据的指针 |
32/32bit | Array length | 数组长度(如果当前对象是数组) |
Java对象头里的Mark Word默认存储的是HashCode,分代年龄和锁标记位,32位JVM的Mrak Word的默认存储结构如图2-3
图: 2-3
锁状态 | 25bit | 4bit | 1bit是否是偏向锁 | 2bit |
---|---|---|---|---|
无锁状态 | 对象的hashCode | 对象的分代年龄 | 0 | 0 |
2.2 : 锁的升级和对比
- 锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁,这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率.
- 偏向锁
- 大多数情况下,锁不仅不存在多线程竞争,而且总是有同一个线程多次获得,为了让线程获得锁的代码更低而引入了偏向锁.
- 偏向锁撤销
- 关闭偏向锁
- 轻量级锁
- 轻量级锁加锁 : 线程在执行同步快之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针,如果成功当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁
- 轻量级锁解锁 : 轻量级锁解锁,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生,如果失败,表示当前锁在竞争,锁就会膨胀成重量级锁
- 锁的优缺点对比
锁 | 优点 | 缺点 | 使用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的消耗,和执行非同步方法相比仅存在纳秒级的差距 | 如果线程间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于只有一个线程访问同步快的场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,使用自旋会消耗CPU | 追求响应时间同步快执行速度非常快 |
重量级锁 | 线程竞争不适用自旋,不会消耗CPU | 线程阻塞,响应速度缓慢 | 试求吞吐量同步快执行速度较长 |
3. 原子操作的实现原理
- 原子操作是指不可被中断的一个或一系列操作
- 处理器如何实现原子操作
- 通过总线锁保证原子性
- 使用缓存锁保证原子性
- 有两种情况下处理器不会使用缓存锁定
- 当操作的数据不能被缓存在处理器内部或操作的数据跨多个缓存行时,则处理会调用总线锁定
- 有些处理器不支持缓存锁定,就算锁定在内存区域在处理器的缓存行中也会调用总线程锁定
- java如何实现原子操作
java中可以通过所和循坏CAS的方式来实现原子操作
- 使用循环CAS实现原子操作
- CAS实现原子操作三大问题
1). ABA问题
2). 循坏事件长开销大
3). 只能保证一个共享变量的原子操作 - 使用锁机制实现原子操作
1). 锁机制保证了只有获得锁的线程才能够操作锁定的内存区域,JVM内部实现了很多种锁机制,有偏向锁,轻量级锁和互斥锁.出了偏向锁,JVM实现锁的方式都用了循坏CAS,即当一个线程想进入同步快的时候使用循环CAS的方式来获取锁,当他退出同步快的时候使用循环CAS来释放锁
4. 本章小结
volatile,synchronized和原子操作的实现原理
推荐阅读
-
[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的
-
探究并发包中ConcurrentHashMap中的put方法底层实现原理
-
我的并发编程(三):Volatile的底层实现及原理
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 下
-
java Map及其实现类的底层原理
-
Java并发编程深入理解之Synchronized的使用及底层原理详解 上
-
Java多线程高并发进阶篇(三)-原子操作的实现原理
-
JAVA---->HashMap的底层实现原理
-
[五]类加载机制双亲委派机制 底层代码实现原理 源码分析 java类加载双亲委派机制是如何实现的
-
第二章 : Java并发机制的底层实现原理