Home Tags Posts tagged with "偏向锁"

偏向锁

一、volatile的应用

volatile是轻量级的synchronized,它在多处理器开发中保证了共享变量的“可见性”。(还包括64位原子操作、禁止指令重排序)

可见性:当一个线程修改一个共享变量时,另外一个线程能够读到这个共享变量值。

如果volatile变量修饰符使用恰当 的话,它比synchronized的使用和执行成本更低,因为它不会引起线程上下文的切换和调度。

1.1 volatile定义与原理

volatile定义

如果一个字段被声明成volatile,Java线程内存模型确保所有线程看到这个变量的值是一致的

注:有volatile变量修饰的共享变量进行写操作的时候会多出第二行汇编代码(添加 lock),通过查IA-32架 构软件开发者手册可知,Lock前缀的指令在多核处理器下会引发了两件事情。

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

1.2 volatile的两条实现原则

1)Lock前缀指令会引起处理器缓存回写到内存(从总线锁定变为缓存锁定,使用缓存一致性机制保着修改的原子性)

缓存一致性机制会阻止同时修改由两个以上处理器缓存的内存区域数据。

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

2.1 volatile优化使用

著名的Java并发编程大师Doug lea在JDK 7的并发包里新增一个队列集合类LinkedTransferQueue,它在使用volatile变量时,用一种追加字节的方式来优化队列出队和入队的性 能。

为什么追加64字节能够提高并发编程的效率呢?

因为对于英特尔酷睿i7、酷睿、Atom和 NetBurst,以及Core Solo和Pentium M处理器的L1、L2或L3缓存的高速缓存行是64个字节宽,不 支持部分填充缓存行,这意味着,如果队列的头节点和尾节点都不足64字节的话,处理器会将
它们都读到同一个高速缓存行中,在多处理器下每个处理器都会缓存同样的头、尾节点,当一
个处理器试图修改头节点时,会将整个缓存行锁定,那么在缓存一致性机制的作用下,会导致
其他处理器不能访问自己高速缓存中的尾节点,而队列的入队和出队操作则需要不停修改头
节点和尾节点,所以在多处理器的情况下将会严重影响到队列的入队和出队效率。Doug lea使 用追加到64字节的方式来填满高速缓冲区的缓存行,避免头节点和尾节点加载到同一个缓存
行,使头、尾节点在修改时不会互相锁定。

那么是不是在使用volatile变量时都应该追加到64字节呢?不是的。在两种场景下不应该
使用这种方式。
·缓存行非64字节宽的处理器。如P6系列和奔腾处理器,它们的L1和L2高速缓存行是32个
字节宽。
·共享变量不会被频繁地写。因为使用追加字节的方式需要处理器读取更多的字节到高速
缓冲区,这本身就会带来一定的性能消耗,如果共享变量不被频繁写的话,锁的几率也非常
小,就没必要通过追加字节的方式来避免相互锁定。
不过这种追加字节的方式在Java 7下可能不生效,因为Java 7变得更加智慧,它会淘汰或 重新排列无用字段,需要使用其他追加字节的方式。

2.2 synchronized的实现原理与应用

随着Java SE 1.6对sychronized进行了各种优化之后,有些情况下它就并不是重量级锁了。本节将介绍Java SE 1.6中为了减少获得锁和释放锁带来的性能消耗而引入的偏向锁和轻量级锁,以及锁的存储结构和升级过程。

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

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

当一个线程试图访问同步代码块时,它首先必须得到锁,退出或抛出异常时必须释放锁

代码块同步是使用monitorenter 和monitorexit指令实现的,而方法同步是使用另外一种方式实现的,细节在JVM规范里并没有
详细说明。但是,方法的同步同样可以使用这两个指令来实现。

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

2.2.1 Java对象头

synchronized用的锁是存在Java对象头里的

Java对象头里的Mark Word里默认存储对象的HashCode、分代年龄和锁标记位,在运行期间,Mark Word里存储的数据会随着锁标志位的变化而变

2.2.2 锁的升级与对比

Java SE 1.6为了减少获得锁和释放锁带来的性能消耗,引入了“偏向锁”和“轻量级锁”,在 Java SE 1.6中,锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率

1.偏向锁

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

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

(1)偏向锁的撤销(看书)

(2)关闭偏向锁(如果确定应用程序里所有的锁通常情况下处于竞争状态,设置JVM参数关闭偏向锁

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

2.轻量级锁

(1)轻量级锁加锁

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

(2)轻量级锁解锁

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

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

2.3 原子操作的实现原理

2.处理器如何实现原子操作?(处理器层面)

处理器提供总线锁定缓存锁定两个机制来保证复杂内存操作的原子性。

总线锁定:

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

缓存锁定:

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

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

第一种情况是:(不在处理器内部)当操作的数据不能被缓存在处理器内部,或操作的数据跨多个缓存行 (cache line)时,则处理器会调用总线锁定。
第二种情况是:(处理器不支持)有些处理器不支持缓存锁定。对于Intel 486和Pentium处理器,就算锁定的内存区域在处理器的缓存行中也会调用总线锁定

3.Java如何实现原子操作?(Java层面) 建立在处理器层面之上

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

(1)使用循环CAS实现原子操作

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

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

1)ABA问题

2)循环时间开销大

3)只能保证一个共享变量的原子操作(取巧—将多个共享变量合并为一个共享变量来操作,JDK1.5提高AtomicReference)

(3)使用锁机制实现原子操作

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