java并发编程的艺术笔记1之Java并发机制的底层实现原理

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

1volatile

Figure 1‑1 CPU的术语定义

java并发编程的艺术笔记1之Java并发机制的底层实现原理

1volatile的两条实现原则:

(1)  Lock前缀指令会引起处理器缓存回写到内存

多处理器中,Lock#信号在声言期间会锁住总线,导致其他CPU用总线,也就无法用内存,此时该处理器可以独占所有内存。有些处理器锁缓存。

缓存锁定:如果已经缓存在处理器内部,则不会声言Lock#信号,它会锁定这块内存区域的缓存并回写到内存,使用缓存一致性机制来确保修改的原子性。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

(2)一个处理器的缓存回写到内存会导致其他处理器的缓存无效。

期间,处理器通过嗅探在总线上传播的数据来检测自己缓存值是否过期。

使用MESI(修改,独占,共享,无效)的控制协议去维护内部缓存和其他处理器缓存的一致性。

volatile的优化

1.2追加字节优化性能

LinkedTransferQueue类中:在内部类追加共享变量到64字节。

因为处理器的高速缓存行是64字节,不支持部分填充缓存行。意味着如果队列的头节点和尾节点都不足64字节,处理器会将他们填入同一个高速缓存行,在多处理器下每个处理器都会缓存同样的头尾节点,当一个处理器进行修改时,其他的进行锁定,但队列的出入队操作会不断的修改头尾节点,多处理器情况下严重影响效率。

两种情况下例外:

  1. 缓存行非64字节宽的处理器
  2. 共享变量不会被频繁的写。因为本身追加字节也会带来一定的消耗。

Java 7中可能不生效,因为更加智慧。

 

2.synchronized

主要介绍为了减小获得锁和释放锁带来的性能消耗而引入的轻量级锁和偏向锁,以及锁的存储结构和升级过程。

java并发编程的艺术笔记1之Java并发机制的底层实现原理

Java基于进入和退出Monitor对象来实现方法同步和代码块同步:monitorenter和monitorexit。monitorenter是在编译后插入到同步代码块的开始位置,moniotrexit插入到方法结束处和异常处。每个enter必须有一个exit对应,同时有一个monitor与之关联。

2.1Java对象头

synchronized用的锁存储在Java对象头中,若对象是数组类型,虚拟机用3个字宽度存储对象头,非数组类型为2个字。32位虚拟机一字宽等于4字节,即32bit。

Java对象头的组成:Mark Word(存对象的HashCode,分代年龄,锁标记位等),Class Metadata Address,Array Length。

Mark Word 通过锁标志位(最后2bit)区别轻重量级锁,GC标记,偏向锁

JavaSE1.6中,锁有四种状态,级别从低到高:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。锁可以升级但不能降级,为了提高获得锁和释放锁的效率。

锁的升级和对比

  • CAS操作

乐观锁和悲观锁

独占锁是一种悲观锁,如synchronized,会导致其他所有需要锁的线程被挂起,等待持有锁的线程释放锁。

乐观锁:每次不加锁在假设没有冲突的情况下去完成某项操作,因为冲突失败了就重试,直至成功为止。

乐观锁用的就是CAS,Compare and Swap。CAS有3个操作数,内存值V,旧的预期值A,要修改的新值B。当且仅当V=A时,将V修改成B。

  1. 偏向锁

大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程获得,为了降低代价,引入偏向锁。

当一个线程访问同步块并获取锁的时候,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,之后该线程再进出同步块时,不需要进行CAS操作来加解锁,只要简单测试一下对象头的Mark Word里面是否存储着指向当前线程的偏向锁。如果没有存储,需要再测试Mark Word中偏向锁的标识是否设置成1(表明当前是偏向锁),如果没有设置,则使用CAS竞争锁,如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

  • 撤销:采用等到竞争状态出现才释放锁的机制。具体细节:需要等到全局安全点(此时没有正在执行的字节码)先暂停拥有锁的线程,检查其是否活跃,如果线程不处于活动状态,则将对象头设置成无锁状态;如果活着,拥有偏向锁的栈会遍历偏向对象的锁记录,栈中的锁记录和对象头的Mark Word会重新偏向其他线程,要么恢复无锁,或标记对象不适合作为偏向锁,最后唤醒暂停的线程。

java并发编程的艺术笔记1之Java并发机制的底层实现原理

  • 关闭:偏向锁在Java 6,7里默认程序启动后几秒后自动启动,可以使用JVM参数关闭延迟,或关闭锁。

(2)轻量级锁

  • 加锁:JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中(官方成为Displaced Mark Word)。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁。如果失败,其他线程获得锁,当前线程尝试使用自旋来获取锁。
  • 解锁:使用原子CAS操作将锁记录中的Displaced Mark Word替换回对象头,如果成功,则表示没有竞争发生;如果失败,表示其他线程同时竞争锁,锁会膨胀成重量级锁。释放锁并唤醒等待的线程。

自旋会消耗CPU,所以重量级无法退回到轻量级,以此来避免无用的自旋。

(3)重量级锁

该状态下,其他线程试图获取锁失败后,会直接堵塞,不会自旋,等待当前线程释放锁并进行唤醒堵塞线程后,会开启新一轮的争夺。

(4)锁的优缺点对比

java并发编程的艺术笔记1之Java并发机制的底层实现原理

2.3 原子操作的原理和实现

原子操作:不可被中断的一个或一系列操作

(1)术语

缓存行,CAS,CPU流水线,内存顺序冲突

(2)原子操作的实现

处理器能够保存基本的内存操作的原子性(当处理器处理一个字节时,其他处理器不能访问)使用总线锁定和缓存锁定保证复杂内存操作的原子性。

  • 总线锁定:也叫总线锁,就是之前的声言Lock#信号时,只有当前处理器可以访问总线。
  • 缓存锁定:内存区域如果被缓存在处理器的缓冲行,并在Lock期间(注意:不是声言Lock#)被锁定。那么当它执行操作回写到内存时,处理器不在总线上声言Lock#信号。而是修改内部的内存地址,并通过缓存一致性机制来保持操作的原子性。

处理器在某些场合会使用比总线锁定的开销小的缓存锁定进行优化。频繁使用的内存会缓存在处理器L1,L2和L3高速缓存里,此时原子操作会在缓存中进行。缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

(跟volatile的两条实现原则一毛一样!)

例外:有些处理器不支持缓存锁定。或者操作的数据无法放入缓存或者跨越多个缓存行时,只能调用总线锁定。

(3)Java实现原子操作

  • 循环CAS 利用处理器提供的CMPXCHG指令实现。自旋CAS就是循环进行CAS操作直到成功。
  • 但是还存在三大问题:

(1)ABA问题。解决办法是在变量前面追加版本号,每次更新变量时版本号加1。(Java1.5使用AutoStampedReference解决问题)

(2)循环时间开销大 JVM如果允许Pause指令,可以提升一定程度的效率,(延迟流水线指令,避免内存顺序冲突引起的CPU流水线清空)

(3)只能保证一个共享变量的原子操作。多共享变量时,可以用锁或者取巧将多个变为1个。Java1.5可以通过AtomicReference保证引用对象的原子性。