在java并发程式设计中volatile和synchronized都扮演着重要的角色。两者都起到相同的作用:保证共享变数的执行绪可见性。与synchronized相比volatile可以看做是轻量级的synchronized,没有执行绪的上下文切换和除错,效能比synchronized要好很多。但需要注意的是volatile变数在复合操作的时候并不能保证执行绪安全,相反sychronized能。下面从底层看一下volatile、synchronized到底是怎么实现的。
假设物件有个属性字段i,初始值为1,物件位于堆上。我们通常把堆看作是主内存,此时分别两个两个不同的执行绪访问字段i。现代操作系统,每个执行绪都分配有单独的处理器快取,用这些处理器快取去快取一些资料,就可以不用再次访问主内存去获取相应的资料,这样就可以提高效率。看下图
这样做可以提高效率,但同时也带来的一个问题:修改资料时,各执行绪的资料不一致。
比如执行绪1修改了i = 2,同时更新了主内存。但此时执行绪2还认为i = 1,这就造成了严重的执行绪安全问题。如下图
所以这时候我们需要做到当执行绪1修改一个共享变数时,其它访问该共享变数的执行绪能够感知到变化。而这个功能volatile可以做到。我们来看下volatile到底是怎样工作的。
volatile 只能用于修饰变数。程式码:
当volatile变数i被赋值2时,这时执行绪1会做两件事:
这时监听CPU总线的处理器会收到这个修改讯号后,如果发现修改的资料自己快取了,就把自己快取的资料失效掉。这样其它执行绪访问到这段快取时知道快取资料失效了,需要从主内存中获取。这样所有执行绪中的共享变数i就达到了一致性。
所以volatile也可以看作执行绪间通讯的一种廉价方式。
在多执行绪并发程式设计中synchronized一直扮演着元老级角色,很多人都会称呼它为重量级锁。但是随着Java SE 1.6对synchronized进行各种优化之后,有引起情况下它就并不那重了。下面来详细介绍这方面的内容。
synchronized实现同步的基础是:Java中的每个物件都可作为锁。所以synchronized锁的都物件,只不过不同形式下锁的物件不一样。
当一个执行绪试图访问同步代时,它必须先获得锁,才能执行程式码逻辑,退出的时候又必须释放锁。那锁到底是怎么实现的呢?
在JVM规范中规定了synchronized是通过Monitor物件来实现方法和程式码块的同步,但两者实现细节有点一不样。程式码块同步是使用monitorenter和monitorexit指令,方法同步是使用另外一种方法实现,细节JVM规范并没有详细说明。但是,方法的同步同样可以使用这两指令来实现。
monitorenter指令是编译后插入到同步程式码块的开始位置,而monitorexit指令是插入到方法结束处和异常处。JVM保证了每个monitorenter都有对应的monitorexit。任何一个物件都有一个monitor与之关联,当且一个monitor被持有后,物件将处于锁定状态。执行绪执行到monitorenter指令时,将会尝试获取物件对应monitor的所有权,即尝试获得物件的锁。
那我们一直口口声声说的锁,它到底在哪里呢?远近天边,近在眼前。上面提到物件可以作为锁,其实锁就在物件里面,准确来说是物件头的Mark World结构中。关于物件头的结构具体可参考:
这里简单描述一下:
物件头分为两部分:Mark Word 与 Class Pointer(型别指标)。
Mark Word储存了物件的hashCode、GC资讯、锁资讯三部分,Class Pointer储存了指向类物件资讯的指标。在32位JVM上物件头占用的大小是8字节,64位JVM则是16字节,两种型别的Mark Word 和 Class Pointer各占一半空间大小。
下面的图是32位JVM上面的物件头展示。前面25bit是hashCode, 4bit是GC资讯,后面两位分别是偏向锁标志与锁状态标志。Mark Word在不同的级别锁时,储存内容会发生变化。
上面提到Java SE 1.6对synchronized进行各种优化,这优化指的是什么呢。指的是synchronized不一定就是重量级锁,它根据锁的重量级分成了三种,由低到高:偏向锁、轻量级锁、重量级锁。上面图就展示了每种锁在Mark Word的储存内容。下面来介绍每种锁的应用场景和升级过程。
偏向锁:经过研究发现,在多执行绪竞争不激烈的环境或业务中,一个锁总是由同一个执行绪多次获得。这样的话就没有必要用生成重量级锁和重复加锁了,因为这样代价会很高。所以这时可以通过引入偏向锁来解决这代价过高的问题。具体做法是当一个执行绪尝试去获取锁时,在物件头和栈帧的锁记录里储存指向当前执行绪的偏向锁(CAS 操作),同时设定偏向锁标志位为1(CAS 操作)。之后执行绪再对同一物件加锁,只需要简单测试一下物件头里面是否储存着指向当前执行绪的偏向锁就可以了,不需要真正执行加锁操作。这时其它执行绪来尝试获取锁时,CAS操作是获取不了锁的,这时只能等待原先的执行绪把锁撤销了,才能竞争锁。偏向锁的撤销需要等到全域性安全点才能撤销,这就意味着其它执行绪可能要等很长时间。所以竞争激烈的情况偏向锁就不是很适用。这时应该升级为轻量级锁。
轻量级锁:执行绪在执行同步块之前,JVM会先在当前执行绪的的栈帧中建立用于储存锁记录的空间,并将物件头中的Mark World复制到执行绪栈帧的锁记录空间里面。然后呢,用CAS操作把物件头的Mark World替换为指向锁记录空间的指标。成功就表示获得锁了,失败就暂时自旋一下,等待其它执行绪解锁。如果自旋到了时间发现还不能获得锁,这时只有两种情况:(1)竞争超激烈 (2)同步程式码执行时间太长。这时候如果还自旋是很不划算的,因为不但不能快速获取锁,还会白白浪费了CPU。这种情景轻量级锁就不合适了还不如升级为重量级锁。
重量级锁:所谓的重量级锁,其实就是最原始和最开始java实现的阻塞锁。在JVM中又叫物件监视器。这时锁物件的物件头字段指向的是一个互斥量,所有执行绪竞争重量级锁,竞争失败的执行绪进入阻塞状态(操作系统层面),并且在锁物件的一个等待池中等待被唤醒,被唤醒后的执行绪再次去竞争锁资源。
所以偏向锁、轻量级锁、重量级锁是适用于不同的竞争环境。
关于volatile和synchronized就暂介绍到这了,不知道大家看了之后有没有收获呢。





























