【转载】Java中的锁机制 synchronized & 偏向锁 & 轻量级锁 & 重量级锁 & 各自优缺点及场景 & AtomicReference

参考文章: http://blog.csdn.net/chen77716/article/details/6618779html

目前在Java中存在两种锁机制:synchronized和Lock,Lock接口及其实现类是JDK5增长的内容,其做者是大名鼎鼎的并发专家Doug Lea。本文并不比较synchronized与Lock孰优孰劣,只是介绍两者的实现原理。java

数据同步须要依赖锁,那锁的同步又依赖谁?synchronized给出的答案是在软件层面依赖JVM,而Lock给出的方案是在硬件层面依赖特殊的CPU指令。c++

synrhronized使用普遍。其应用层的语义是能够把任何一个非null对象做为"锁",
当synchronized做用在方法上时,锁住的即是对象实例(this);
看成用在静态方法时锁住的即是对象对应的Class实例,由于Class数据存在于永久带,所以静态方法锁至关于该类的一个全局锁;
当synchronized做用于某一个对象实例时,锁住的即是对应的代码块。

在HotSpot JVM实现中,锁有个专门的名字:对象监视器。

线程状态及状态转换

当多个线程同时请求某个对象监视器时,对象监视器会设置几种状态用来区分请求的线程:
Contention List:全部请求锁的线程将被首先放置到该竞争队列
Entry List:Contention List中那些有资格成为候选人的线程被移到Entry List
Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
OnDeck:任什么时候刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
Owner:得到锁的线程称为Owner
!Owner:释放锁的线程

 

上图和文章中提到了ContentionList,又提到了EntryList。windows

自旋锁

还提到了自旋锁(Spin Lock),在一个线程获取锁的时候,先进行自旋,尝试。虽然对ContentionList中的线程不尽公平,可是效率能够大大提高。数组

那些处于ContetionList、EntryList、WaitSet中的线程均处于阻塞状态,阻塞操做由操做系统完成(在Linxu下经过pthread_mutex_lock函数)。
线程被阻塞后便进入内核(Linux)调度状态,这个会致使系统在用户态与内核态之间来回切换,严重影响锁的性能 缓解上述问题的办法即是自旋,其原理是:
当发生争用时,若Owner线程能在很短的时间内释放锁,则那些正在争用线程能够稍微等一等(自旋),在Owner线程释放锁后,争用线程可能会当即获得锁,
从而避免了系统阻塞。
但Owner运行的时间可能会超出了临界值,争用线程自旋一段时间后仍是没法得到锁,这时争用线程则会中止自旋进入阻塞状态(后退)
基本思路就是自旋,不成功再阻塞,尽可能下降阻塞的可能性,这对那些执行时间很短的代码块来讲有很是重要的性能提升。
自旋锁有个更贴切的名字:自旋
-指数后退锁,也即复合锁。很显然,自旋在多处理器上才有意义。

自旋锁详细介绍:缓存

还有个问题是,线程自旋时作些啥?其实啥都不作,能够执行几回for循环,能够执行几条空的汇编指令,目的是占着CPU不放,等待获取锁的机会。
因此说,自旋是把双刃剑,若是旋的时间过长会影响总体性能,时间太短又达不到延迟阻塞的目的。
显然,自旋的周期选择显得很是重要,但这与操做系统、硬件体系、系统的负载等诸多场景相关,很难选择,
若是选择不当,不但性能得不到提升,可能还会降低,所以你们广泛认为自旋锁不具备扩展性。 对自旋锁周期的选择上,HotSpot认为最佳时间应是一个线程上下文切换的时间,但目前并无作到。
通过调查,目前只是经过汇编暂停了几个CPU周期,除了自旋周期选择,HotSpot还进行许多其余的自旋优化策略,
具体以下:
若是平均负载小于CPUs则一直自旋 若是有超过(CPUs
/2)个线程正在自旋,则后来线程直接阻塞 若是正在自旋的线程发现Owner发生了变化则延迟自旋时间(自旋计数)或进入阻塞 若是CPU处于节电模式则中止自旋 自旋时间的最坏状况是CPU的存储延迟(CPU A存储了一个数据,到CPU B得知这个数据直接的时间差) 自旋时会适当放弃线程优先级之间的差别
synchronized实现什么时候使用了自旋锁
答案是在线程进入ContentionList时,也即第一步操做前
线程在进入等待队列时首先进行自旋尝试得到锁,若是不成功再进入等待队列。这对那些已经在等待队列中的线程来讲,稍微显得不公平。
还有一个不公平的地方是自旋线程可能会抢占了Ready线程的锁。自旋锁由每一个监视对象维护,每一个监视对象一个。

偏向锁

偏向锁(Biased Lock)主要解决无竞争下的锁性能问题.安全

首先咱们看下无竞争下锁存在什么问题:
如今几乎全部的锁都是可重入的,也即已经得到锁的线程能够屡次锁住/解锁监视对象,
按照以前的HotSpot设计,每次加锁/解锁都会涉及到一些CAS操做(好比对等待队列的CAS操做),CAS操做会延迟本地调用,
所以偏向锁的想法是一旦线程第一次得到了监视对象,以后让监视对象“偏向”这个线程,以后的屡次调用则能够避免CAS操做,
说白了就是置个变量,若是发现为true则无需再走各类加锁/解锁流程。

以上内容来自 http://blog.csdn.net/chen77716/article/details/6618779 可是没怎么读懂 多线程

在搜索偏向锁的过程当中,又找到下面这篇并发

http://blog.163.com/silver9886@126/blog/static/35971862201472274958280/函数

Java偏向锁(Biased Locking)是Java6引入的一项多线程优化。它经过消除资源无竞争状况下的同步原语,进一步提升了程序的运行性能。

偏向锁,顾名思义,它会偏向于第一个访问锁的线程,若是在接下来的运行过程当中,该锁没有被其余的线程访问,则持有偏向锁的线程将永远不须要触发同步。
若是在运行过程当中,遇到了其余线程抢占锁,则持有偏向锁的线程会被挂起,JVM会尝试消除它身上的偏向锁,将锁恢复到标准的轻量级锁。
(偏向锁只能在单线程下起做用) 所以 流程是这样的 偏向锁
->轻量级锁->重量级锁

其中还提到轻量级锁和重量级锁。那么这还涉及锁膨胀。

经过知乎上的一篇回答 https://www.zhihu.com/question/39009953?sort=created

轻量级锁就是为了在无多线程竞争的环境中使用CAS来代替mutex,一旦发生竞争,两条以上线程争用一个锁就会膨胀

回到前一篇

锁存在Java对象头里。若是对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,若是对象是非数组类型,则用2字宽存储对象头。
在32位虚拟机中,一字宽等于四字节,即32bit。 锁状态包括:轻量级锁定、重量级锁定、GC标记、可偏向
  • 这篇文章 http://blog.csdn.net/wolegequdidiao/article/details/45116141
    • 对象自身的运行时数据 
      如:哈希吗(HashCode)、GC分代年龄(Generational GC Age)等,这部分数据的长度在32位和64位的虚拟机中分别为32bit和64bit,简称“Mark Word”

下面两张图能够先忽略,由于后面有更清楚的

(忽略图,看后面的)32位JVM的Mark Word的默认存储结构以下: 

 (忽略图,看后面的)64位JVM下, Mark Word是64bit大小的,存储结构以下: 

简单的加锁机制:

机制:每一个锁都关联一个请求计数器和一个占有他的线程,当请求计数器为0时,这个锁能够被认为是unhled的,
当一个线程请求一个unheld的锁时,JVM记录锁的拥有者,并把锁的请求计数加1,若是同一个线程再次请求这个锁时,请求计数器就会增长,
当该线程退出syncronized块时,计数器减1,当计数器为0时,锁被释放(这就保证了锁是可重入的,不会发生死锁的状况)。

偏向锁流程:

偏向锁,简单的讲,就是在锁对象的对象头中有个ThreaddId字段,这个字段若是是空的,
第一次获取锁的时候,就将自身的ThreadId写入到锁的ThreadId字段内,将锁头内的是否偏向锁的状态位置1.
这样下次获取锁的时候,直接检查ThreadId是否和自身线程Id一致,若是一致,则认为当前线程已经获取了锁,所以不需再次获取锁,
略过了轻量级锁和重量级锁的加锁阶段。提升了效率。
可是偏向锁也有一个问题,就是当锁有竞争关系的时候,须要解除偏向锁,使锁进入竞争的状态。

下面是清晰的流程

上图中只讲了偏向锁的释放,其实还涉及偏向锁的抢占,其实就是两个进程对锁的抢占,在synchrnized锁下表现为轻量锁方式进行抢占

注:也就是说一旦偏向锁冲突,双方都会升级为轻量级锁。(这一点与轻量级->重量级锁不一样,那时候失败一方直接升级,成功一方在释放时候notify,加下文后面详细描述)

以下图。以后会进入到轻量级锁阶段,两个线程进入锁竞争状态(注,我理解仍然会遵照先来后到原则;注2,的确是的,下图中提到了mark word中的lock record指向堆栈中最近的一个线程的lock record),一个具体例子能够参考synchronized锁机制。(图后面有介绍

上面163的文章中,提到了这一篇 http://xly1981.iteye.com/blog/1766224,里面对于synchronized的过程讲的挺好:

每个线程在准备获取共享资源时: 
第一步,检查MarkWord里面是否是放的本身的ThreadId ,若是是,表示当前线程是处于 “偏向锁” 
第二步,若是MarkWord不是本身的ThreadId,锁升级,这时候,用CAS来执行切换,新的线程根据MarkWord里面现有的ThreadId,通知以前线程暂停,
以前线程将Markword的内容置为空。 第三步,两个线程都把对象的HashCode复制到本身新建的用于存储锁的记录空间,接着开始经过CAS操做
把共享对象的MarKword的内容修改成本身新建的记录空间的地址的方式竞争MarkWord, 第四步,第三步中成功执行CAS的得到资源,失败的则进入自旋 第五步,自旋的线程在自旋过程当中,成功得到资源(即以前获的资源的线程执行完成并释放了共享资源),则整个状态依然处于 轻量级锁的状态,若是自旋失败 第六步,进入重量级锁的状态,这个时候,自旋的线程进行阻塞,等待以前线程执行完成并唤醒本身
总结:
偏向锁,实际上是无锁竞争下可重入锁的简单实现。流程是这样的 偏向锁->轻量级锁->重量级锁

 

发现,这篇文章对于synchronized讲得比较清楚 http://www.infoq.com/cn/articles/java-se-16-synchronized

先介绍一下CAS:

Compare and Swap

比较并设置。用于在硬件层面上提供原子性操做。在 Intel 处理器中,比较并交换经过指令cmpxchg实现。
比较是否和给定的数值一致,若是一致则修改,不一致则不修改。

同步的基础

Java中的每个对象均可以做为锁

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

当一个线程试图访问同步代码块时,它首先必须获得锁,退出或抛出异常时必须释放锁。那么锁存在哪里呢?锁里面会存储什么信息呢?

同步的原理

JVM规范规定JVM基于进入和退出Monitor对象来实现方法同步和代码块同步,但二者的实现细节不同。

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

monitorenter指令是在编译后插入到同步代码块的开始位置,而monitorexit是插入到方法结束处和异常处, JVM要保证每一个monitorenter必须有对应的monitorexit与之配对。

任何对象都有一个 monitor 与之关联,当且一个monitor 被持有后,它将处于锁定状态。线程执行到 monitorenter 指令时,将会尝试获取对象所对应的 monitor 的全部权,即尝试得到对象的锁。

Java对象头

锁存在Java对象头里。若是对象是数组类型,则虚拟机用3个Word(字宽)存储对象头,若是对象是非数组类型,则用2字宽存储对象头。在32位虚拟机中,一字宽等于四字节,即32bit。(下面这个表格讲的很清楚)

 

长度

内容

说明

32/64bit

Mark Word

存储对象的hashCode或锁信息等。

32/64bit

Class Metadata Address

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

32/64bit

Array length

数组的长度(若是当前对象是数组)

 

Java对象头里的Mark Word里默认存储对象的HashCode,分代年龄和锁标记位。32位JVM的Mark Word的默认存储结构以下:

 

25 bit

4bit

1bit

是不是偏向锁

2bit

锁标志位

无锁状态

对象的hashCode

对象分代年龄

0

01

在运行期间Mark Word里存储的数据会随着锁标志位的变化而变化。Mark Word可能变化为存储如下4种数据: 

锁状态

25 bit

4bit

1bit

2bit

23bit

2bit

是不是偏向锁

锁标志位

轻量级锁

指向栈中锁记录的指针

00

重量级锁

指向互斥量(重量级锁)的指针

10

GC标记

11

偏向锁

线程ID

Epoch

对象分代年龄

1

01

上图里面的GC标记,为11的话,推断应该是准备GC的意思

在64位虚拟机下,Mark Word是64bit大小的,其存储结构以下:  

 

锁状态

25bit

31bit

1bit

4bit

1bit

2bit

 

 

cms_free

分代年龄

偏向锁

锁标志位

无锁

unused

hashCode

 

 

0

01

偏向锁

ThreadID(54bit) Epoch(2bit)

 

 

1

01

锁的升级

Java SE1.6为了减小得到锁和释放锁所带来的性能消耗,引入了“偏向锁”和“轻量级锁”

因此在Java SE1.6里锁一共有四种状态,无锁状态,偏向锁状态,轻量级锁状态和重量级锁状态,它会随着竞争状况逐渐升级。

锁能够升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。

这种锁升级却不能降级的策略,目的是为了提升得到锁和释放锁的效率,下文会详细分析。

偏向锁

Hotspot的做者通过以往的研究发现大多数状况下锁不只不存在多线程竞争,并且老是由同一线程屡次得到,为了让线程得到锁的代价更低而引入了偏向锁。
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID
之后该线程在进入和退出同步块时不须要花费CAS操做来加锁和解锁,而只需简单的测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁
若是测试成功,表示线程已经得到了锁,若是测试失败,则须要再测试下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁),若是没有设置,
则使用CAS竞争锁,若是设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。 偏向锁的撤销:偏向锁使用了一种等到竞争出现才释放锁的机制,因此当其余线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。
偏向锁的撤销,须要等待全局安全点(在这个时间点上没有字节码正在执行)
它会首先暂停拥有偏向锁的线程,而后检查持有偏向锁的线程是否活着,
若是线程不处于活动状态,则将对象头设置成无锁状态,
若是线程仍然活着,拥有偏向锁的栈会被执行,遍历偏向对象的锁记录,
栈中的锁记录和对象头的Mark Word要么从新偏向于其余线程,要么恢复到无锁或者标记对象不适合做为偏向锁,最后唤醒暂停的线程。

上面的意思是,先暂停持有偏向锁的线程,尝试直接切换。若是不成功,就继续运行,而且标记对象不适合偏向锁,锁膨胀(锁升级)。
详见,上面有张图中的“偏向锁抢占模式”:
其中提到了mark word中的lock record指向堆栈最近的一个线程的lock record,其实就是按照先来后到模式进行了轻量级的加锁。

上文提到全局安全点:在这个时间点上没有字节码正在执行

关闭偏向锁:偏向锁在Java 6和Java 7里是默认启用的,可是它在应用程序启动几秒钟以后才激活,

若有必要可使用JVM参数来关闭延迟-XX:BiasedLockingStartupDelay = 0。

若是你肯定本身应用程序里全部的锁一般状况下处于竞争状态,能够经过JVM参数关闭偏向锁-XX:-UseBiasedLocking=false,那么默认会进入轻量级锁状态。

轻量级锁

轻量级锁加锁:线程在执行同步块以前,JVM会先在当前线程的栈桢中建立用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为Displaced Mark Word。

而后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。若是成功,当前线程得到锁,若是失败,表示其余线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级锁解锁:轻量级解锁时,会使用原子的CAS操做来将Displaced Mark Word替换回到对象头,若是成功,则表示没有竞争发生。

若是失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。

注:轻量级锁会一直保持,唤醒老是发生在轻量级锁解锁的时候,由于加锁的时候已经成功CAS操做;而CAS失败的线程,会当即锁膨胀,并阻塞等待唤醒。(详见下图)

下图是两个线程同时争夺锁,致使锁膨胀的流程图。

锁不会降级

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

锁的优缺点对比 

优势

缺点

适用场景

偏向锁

加锁和解锁不须要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

若是线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提升了程序的响应速度。

若是始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。

同步块执行速度很是快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。

同步块执行速度较长。

 上面这张表格好好看,总结的很是好!

6 参考源码

对象头源码markOop.hpp。偏向锁源码biasedLocking.cpp。以及其余源码ObjectMonitor.cpp和BasicLock.cpp。

 

上面这篇文章(http://www.infoq.com/cn/articles/java-se-16-synchronized)讲得很是清晰了。下面还有几篇文章以前打开了,看是否有补充。

 

http://blog.csdn.net/wolegequdidiao/article/details/45116141

这篇文章提到:

轻量级锁加锁进行的CAS操做中,是先更新Lock Record指针,而后再更新最后2bit的锁标记位(也不必定,没有明确说。可是各类锁的顺序要一致;注:涉及偏向锁,极可能是先改锁标记位的)。

下面这张图,和上面那张图是一致的:

总结

轻量级锁能提升程序同步性能的依据是“对于绝大部分的锁,在整个同步周期内都是不存在竞争的”,这是一个经验数据。若是没有竞争,轻量级锁使用CAS操做避免了使用互斥量的开销,但若是存在锁竞争,除了互斥量的开销外,还额外发生了CAS操做,所以在有竞争的状况下,轻量级锁会比传统的重量级锁更慢。

偏向锁

JDK1.6引入

    • 优势:消除数据在无竞争状况下的同步原语,提升性能。
    • 偏向锁与轻量级锁理念上的区别: 
      • 轻量级锁:在无竞争的状况下使用CAS操做去消除同步使用的互斥量
      • 偏向锁:在无竞争的状况下把整个同步都消除掉,连CAS操做都不作了
    • 意义:锁偏向于第一个得到它的线程。若是在接下来的执行过程当中,该锁没有被其余的线程获取,则持有偏向锁的线程将永远不须要再进行同步。
    • 相关参数: 
      • 默认-XX:+UseBiasedLocking=true
      • -XX:-UseBiasedLocking=false关闭偏向锁
      • 应用程序启动几秒钟以后才激活
      • -XX:BiasedLockingStartupDelay = 0关闭延迟

注意:偏向锁的锁标记位和无锁是同样的,都是01,可是有单独一位偏向标记设置是否偏向锁。

再复习一下,轻量级锁00,重量级锁10,GC标记11,无锁 01.  

下面这张图作一个复习:

总结

偏向锁能够提升带有同步但无竞争的程序性能。若是程序中大多数的锁老是被多个不一样的线程访问,那偏向模式就是多余的。

在具体情形分析下,禁止偏向锁优反而可能提高性能。

 

看这篇文章的一些笔记 http://www.cnblogs.com/javaminer/p/3889023.html

在jdk1.6中对锁的实现引入了大量的优化,如锁粗化(Lock Coarsening)、锁消除(Lock Elimination)、轻量级锁(Lightweight Locking)、
偏向锁(Biased Locking)、适应性自旋(Adaptive Spinning)等技术来减小锁操做的开销。 锁粗化(Lock Coarsening):也就是减小没必要要的紧连在一块儿的unlock,lock操做,将多个连续的锁扩展成一个范围更大的锁。 锁消除(Lock Elimination):经过运行时JIT编译器的逃逸分析来消除一些没有在当前同步块之外被其余线程共享的数据的锁保护,
经过逃逸分析也能够在线程本地Stack上进行对象空间的分配(同时还能够减小Heap上的垃圾收集开销)。 轻量级锁(Lightweight Locking):这种锁实现的背后基于这样一种假设,即在真实的状况下咱们程序中的大部分同步代码通常都处于无锁竞争状态
(即单线程执行环境),在无锁竞争的状况下彻底能够避免调用操做系统层面的重量级互斥锁,
取而代之的是在monitorenter和monitorexit中只须要依靠一条CAS原子指令就能够完成锁的获取及释放。
当存在锁竞争的状况下,执行CAS指令失败的线程将调用操做系统互斥锁进入到阻塞状态,当锁被释放的时候被唤醒(具体处理步骤下面详细讨论)。 偏向锁(Biased Locking):是为了在无锁竞争的状况下避免在锁获取过程当中执行没必要要的CAS原子指令,
由于CAS原子指令虽然相对于重量级锁来讲开销比较小但仍是存在很是可观的本地延迟(可参考这篇文章)。 适应性自旋(Adaptive Spinning):当线程在获取轻量级锁的过程当中执行CAS操做失败时,在进入与monitor相关联的操做系统重量级锁
(mutex semaphore)前会进入忙等待(Spinning)而后再次尝试,当尝试必定的次数后若是仍然没有成功则调用与该monitor关联的semaphore(即互斥锁),
进入到阻塞状态。

注:(适应性)自旋锁,是在从轻量级锁向重量级锁膨胀的过程当中使用的,是在进入重量级锁以前进行的。

轻量级锁具体实现:

一个线程可以经过两种方式锁住一个对象:一、经过膨胀一个处于无锁状态(状态位001)的对象得到该对象的锁;
2、对象已经处于膨胀状态(状态位00)但LockWord指向的monitor record的Owner字段为NULL,
则能够直接经过CAS原子指令尝试将Owner设置为本身的标识来得到锁。 从中能够看出,是先检查锁的标识位。

看下面这篇文章的记录:

 http://www.cnblogs.com/javaminer/p/3892288.html?utm_source=tuicool&utm_medium=referral 

偏向锁只须要在置换ThreadID的时候依赖一次CAS原子指令。

其余,这篇文章也没什么须要注意的内容了。

这时候,我忽然想到,为何CAS就不能对标识位和数据一块儿操做呢,一次操做完成是否能够呢?

CAS应用

CAS有3个操做数,内存值V,旧的预期值A,要修改的新值B。当且仅当预期值A和内存值V相同时,将内存值V修改成B,不然什么都不作。

下面从分析比较经常使用的CPU(intel x86)来解释CAS的实现原理。

 下面是sun.misc.Unsafe类的compareAndSwapInt()方法的源代码:

public final native boolean compareAndSwapInt(Object o, long offset,
                                              int expected,
                                              int x);
能够看到这是个本地方法调用。这个本地方法在openjdk中依次调用的c++代码为:unsafe.cpp,atomic.cpp和atomicwindowsx86.inline.hpp。

对于32位/64位的操做应该是原子的:

奔腾6和最新的处理器能自动保证单处理器对同一个缓存行里进行16/32/64位的操做是原子的,可是复杂的内存操做处理器不能自动保证其原子性,
好比跨总线宽度,跨多个缓存行,跨页表的访问。可是处理器提供总线锁定和缓存锁定两个机制来保证复杂内存操做的原子性。

CAS的缺点

CAS虽然很高效的解决原子操做,可是CAS仍然存在三大问题。ABA问题,循环时间长开销大和只能保证一个共享变量的原子操做

1.  ABA问题。由于CAS须要在操做值的时候检查下值有没有发生变化,若是没有发生变化则更新,可是若是一个值原来是A,变成了B,又变成了A,
那么使用CAS进行检查时会发现它的值没有发生变化,可是实际上却变化了。ABA问题的解决思路就是使用版本号。
在变量前面追加上版本号,每次变量更新的时候把版本号加一,那么A-B-A 就会变成1A-2B-3A。 从Java1.5开始JDK的atomic包里提供了一个类AtomicStampedReference来解决ABA问题。
这个类的compareAndSet方法做用是首先检查当前引用是否等于预期引用,而且当前标志是否等于预期标志,若是所有相等,
则以原子方式将该引用和该标志的值设置为给定的更新值。 关于ABA问题参考文档: http:
//blog.hesey.net/2011/09/resolve-aba-by-atomicstampedreference.html 2. 循环时间长开销大。自旋CAS若是长时间不成功,会给CPU带来很是大的执行开销。若是JVM能支持处理器提供的pause指令那么效率会有必定的提高,
pause指令有两个做用,第一它能够延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源
延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。
第二它能够避免在退出循环的时候因内存顺序冲突(memory order violation)而引发CPU流水线被清空(CPU pipeline flush),从而提升CPU的执行效率。
3. 只能保证一个共享变量的原子操做。当对一个共享变量执行操做时,咱们可使用循环CAS的方式来保证原子操做,
可是对多个共享变量操做时,循环CAS就没法保证操做的原子性,这个时候就能够用锁,
或者有一个取巧的办法,就是把多个共享变量合并成一个共享变量来操做。好比有两个共享变量i=2,j=a,合并一下ij=2a,而后用CAS来操做ij。
从Java1.5开始JDK提供了AtomicReference类来保证引用对象之间的原子性,你能够把多个变量放在一个对象里来进行CAS操做。

AtomicReference类的学习能够参考如下

http://www.cnblogs.com/skywang12345/p/3514623.html

简单源码示例以下(已经实际实验验证):

// AtomicReferenceTest.java的源码
import java.util.concurrent.atomic.AtomicReference;

public class AtomicReferenceTest {
    
    public static void main(String[] args){

        // 建立两个Person对象,它们的id分别是101和102。
        Person p1 = new Person(101);
        Person p2 = new Person(102);
        // 新建AtomicReference对象,初始化它的值为p1对象
        AtomicReference ar = new AtomicReference(p1);
        // 经过CAS设置ar。若是ar的值为p1的话,则将其设置为p2。
        ar.compareAndSet(p1, p2);

        Person p3 = (Person)ar.get();
        System.out.println("p3 is "+p3);
        System.out.println("p3.equals(p1)="+p3.equals(p1));
    }
}

class Person {
    volatile long id;
    public Person(long id) {
        this.id = id;
    }
    public String toString() {
        return "id:"+id;
    }
}

运行结果:

p3 is id:102
p3.equals(p1)=false

 

以上,是关于synchronized, 偏向锁,轻量级锁,重量级锁,自旋锁,CAS等的一些内容和笔记。

后续还会再对JVM以及Java的一些机制和实现作更多学习。

好比会新开一篇关于JVM的文章,

http://www.cnblogs.com/charlesblc/p/5996268.html

相关文章
相关标签/搜索