一、Java并发机制的底层实现

开篇

1.1 多线程一定快吗

不一定,因为线程的创建和上下文的切换也需要开销。

volatile

volatile可以看成是轻量级的synchronized,保证了共享变量的”可见性“。
如果volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,
因为它不会引起线程上下文的切换和调度。

2.1 volatile如何保证可见性

被volatile声明的变量会被JIT编译器生成一条带有Lock前缀指令的汇编指令。如下:
0x01a3de1d: movb $0×0,0×1104800(%esi);0x01a3de24: lock addl $0×0,(%esp);
Lock前缀的指令在多核处理器下会引发了两件事情

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

2.2 volatile两条实现原则。

接下来介绍一些volatile两条实现原则。

  • Lock前缀指令会引起处理器缓存回写到内存
    Lock前缀指令在指定指定指定期间,声明一个处理器的Lock#信号,这样处理器就可以独占任何共享内存。但是,在最近的处理器中,Lock#信号一般不锁总线,而是锁缓存,这是因为锁总线开销比较大。
    如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反,它会锁定这块内存区 域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁 定”,缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。
  • 一个处理器的缓存回写到内存会导致其他处理器的缓存无效。
    IA-32处理器和Intel 64处 理器使用MESI(修改、独占、共享、无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器进行操作的时候,这两个处理器能够感知到其他的处理器访问内存和它们的内部缓存。处理器使用嗅探技术保证它的内部缓存、系统内存和其他处理器的 缓存的数据在总线上保持一致。如果有一个处理器打算内存地址被检测到了,而这个地址当前处于共享状态,那么正在嗅探的处理 器将使它的缓存行无效,在下次访问相同内存地址时,强制执行缓存行填充。

synchronized的实现原理

  Java SE 1.6之前,被称为重量级锁,但是在1.6之后,经过各种优化就好多了。
  1.6 中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁。

3.1 实现同步的原理

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

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

当一个线程访问同步代码块的时候,会首先获取锁。那么这个锁到底是在哪里呢。
从JVM规范中可以看出,JVM是基于进入和退出Monitor对象来实现方法同步和代码块同步。但是方法和代码块还有一些不一样的地方,就是代码块是使用monitorenter和monitorexit指令实现的,方法同步使用的是另一个方式(JVM规范并没有详细说明),当然也可以使用这个两个指令来实现。
注意: 每个monitorenter必须有一个对应的monitorexit。

3.2 Java对象头

synchronized用的锁是存在Java对象头里的。
Java对象头中的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位。Mark Word的默认存储结构如下所示。
在这里插入图片描述

但是Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变 化为存储以下4种数据,如表下所示。当然这是32位的虚拟机,64位的和这个不一样,这里不在缀叙。
在这里插入图片描述

3.3 偏向锁、轻量级锁、重量级锁对比

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态。
这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级。目的是为了提高获得锁和释放锁的效率。

3.3.1 偏向锁

当一个线程访问同步代码块并获取锁的时候,会在对象头和栈帧中的锁记录存储锁偏向的线程ID,以后该线程进入代码块的时候就需要进行CAS操作来加锁和解锁了。只需简单地测试一下对象头的Mark Word里是否 存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需 要再测试一下Mark Word中偏向锁的标识是否设置成1:如果没有设置,则 使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

3.3.1.1 偏向锁的撤销

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

3.3.1.2 关闭偏向锁

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

3.3.2 轻量级锁

3.3.2.1 加锁

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

3.3.2.2 解锁

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

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级 成重量级锁,就不会再恢复到轻量级锁状态。当锁处于这个状态下,其他线程试图获取锁时, 都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮 的夺锁之争。

3.3.3 优缺点对比

在这里插入图片描述

原子操作的实现原理

原子操作就是“不可被中断的一个或一系列的操作”

4.1 处理器如何实现原子操作

通过对缓存加锁或者总线加锁来实现的。

4.1.1 使用总线锁保证原子性

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

4.1.2 使用缓存锁保证原子性

总线锁把CPU和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,所以总线锁定的开销比较大。

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

但是有两种情况下处理器不会使用缓存锁定。

第一种情况是:当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。

第二种情况是:有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的 内存区域在处理器的缓存行中也会调用总线锁定。

4.2 Java如何实现原子操作

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

4.2.1 使用循环CAS实现原子操作

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

4.2.1.1 CAS实现原子操作的三大问题
  1. ABA问题
    ABA问题就是一个值原来是A,变成了B,又变成了A。解决办法就是使用版本号。在变量前面 追加上版本号,每次变量更新的时候把版本号加1,那么A→B→A就会变成1A→2B→3A。
    Java 1.5开始,JDK的Atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
  2. 循环时间长开销大
    自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。
  3. 只能保证一个共享变量的原子操作

4.3 使用锁机制实现原子操作

锁机制保证了只有获得锁的线程才能够操作锁定的内存区域。有意思的是除了偏向锁,JVM实现锁的方式都用了循环 CAS,即当一个线程想进入同步块的时候使用循环CAS的方式来获取锁,当它退出同步块的时 候使用循环CAS释放锁。

以上内容来源于:《Java并发并发编程的艺术》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值