自旋锁和自适应自旋:
互斥同步对性能最大的影响是阻塞的实现:
挂起和恢复线程的操作都需要内核态去完成,会给虚拟机的并发性带来很大的压力,
同时共享数据的锁定时间实际只会持续很短的一段时间,为了这段时间去挂起和恢复线程不值得
如果锁被占用的时间很短的话,自旋效果好,但是如果长时间都获得不到锁的话,那就会造成资源的白白浪费,因此自旋一般是有一定限度,一般的10次,如果10次之后还没获得锁的话,就挂起
JDK6对自旋锁优化,引入自适应自旋锁。自旋的时间不再是固定的,是由前一次在同一个锁上的自旋时间以及锁的拥有者的状态来决定的
如果对于同一个锁,自旋等待刚刚成功获得过锁,并且持有锁的线程正在运行 中,那么虚拟机就会认为这次自旋也很有可能再次成功,进而允许自旋等待持续相对更 长的时间,
如果对于某个锁,自旋很少成功获得过锁,那在以后要获取这个锁时将有可能直接省略掉自旋过程
锁消除:
虚拟机即时编译器在运行时,对一些代码要求同步,但是对被检测到不可能存在共享数据竞争的锁进行消除
锁消除的主要判定依据来源于逃逸分析的数据支持,如果判断到一段代码中,在堆上的所有数据都不会逃逸出去被其他线程访问到,那就可以把它们当作栈上数据对待,认为它们是线程私有的,同步加锁自然就无须再进行
补充:逃逸分析是比较复杂的,但是程序员为什么会在没有并发冲突的情况下去加锁呢,是因为有些程序员写的代码没有共享数据,但是不代表没有同步操作,例如String字符串的+法,是会转成StringBuffer的append,JDK5之后改成StringBuilder,StringBuffer的append是有锁的,但是实际上这部分代码是不存在数据竞争的情况的,所以进行锁消除
锁粗化:
在编写代码的时候,同步块的作用范围应该尽量限制的小(只有在共享数据的实际作用域中才同步),但是如果一系列连续操作都对同一个锁对象反复加锁解锁,即使没有线程竞争,这种频繁地进行互斥同步操作也会导致不必要的性能损耗
例如StringBuffer.append()连续执行的话,虚拟机就会把加锁的范围扩展到整个append操作系列的外面
//加锁
sb.append();
sb.append();
sb.append();
//释放锁
对象头的内存布局:
HotSpot 虚拟机的对象头分为两部分:
第一部分用于存储对象自身的运行时数据,如哈希码、GC 分代年龄等。这部分数据的长度在 32 位和 64 位的 Java 虚拟机中分别会占用 32 个或 64 个比特,官方称它为“Mark Word”。这部分 是实现轻量级锁和偏向锁的关键。
另外一部分用于存储指向方法区对象类型数据的指针,如果是数组对象,还会有一个额外的部分用于存储数组长度。
对象头信息实际跟对象自身定义的数据无关的额外存储成本,考虑到java虚拟机的空间使用效率,mark word被设计成了一个非固定的动态数据结构,根据不同的对象状态存不同的内容:
轻量级锁:
在代码即将进入同步块的时候,如果此同步对象没有被锁定(锁标志位为“01”状态),虚拟机首先将在当前线程的栈帧中建立一个名为【锁记录】(Lock Record)的空间,用于存储锁对象目前的 Mark Word 的拷贝
然后,虚拟机将使用 CAS 操作尝试把对象的 Mark Word 更新为指向【锁记录】的指针。如果这个更新动作成功了,即代表该线程拥有了这个对象的锁,并且对象 Mark Word 的锁标志位将转变为“00”,
如果这个更新操作失败了,那就意味着至少存在一条线程与当前线程竞争获取该对象的锁。虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了这个对象的锁,那直接进入同步块继续执行就可以了,否则就说明这个锁对象已经被其他线程抢占了。
如果出现两条以上的线程争用同一个锁的情况,或者线程自旋过久没获得锁,那轻量级锁就不再有效,必须要膨胀为重量级锁,锁标志的状态值变为“10”,此时 Mark Word 中存储的就是指向重量级锁(互斥量:虚拟机为该对象创建一个Monitor对象)的指针,后面等待锁的线程也必须进入阻塞状态。
轻量级锁提升的性能的依据是:大部分的锁在整个同步周期中都不存在竞争
如果不存在竞争,轻量级锁通过CAS避免了互斥量的开销
但是如果存在竞争的话,额外还发生了CAS操作的开销,所以在有竞争的情况下,轻量级锁反而会更慢
偏向锁:
实际是对轻量级锁的一个优化,在没有竞争的时候轻量级锁通过CAS降低了创建互斥量的开销,偏向锁优化的是同一个线程多次请求同一个锁
在轻量级锁的情况下,如果线程要获得锁的话,不论自己有没有获得,都会去先创建锁记录的空间,然后尝试去CAS锁对象的MW,如果CAS失败的话再去对比是不是指向自己的栈空间
偏向锁就是第一次获得锁的时候把偏向量设置为1,然后CAS将MW换成线程的id,后续再获得的话只需要去对比线程id是否一致就行,如果一致就获得锁,不一致的话就把锁升级
此时MW里的对象的哈希值就没了,存的是线程的id,那要是如果还需要用到哈希值的时候,他的偏向锁就会被撤销,升级为重量级锁
所以偏向锁也不是一定会带来性能优化的