——自旋锁与自适应自旋
互斥同步对性能最大的影响是阻塞的实现,挂起线程和恢复线程需要切换到内核态来完成,这些操作给系统的并发性能带来很大的压力。但是在许多应用上,共享数据的锁定状态只会持续一小段时间,为了这段时间去挂起和恢复线程不值得,如果物理机器有一个以上的处理器,能让2个或以上线程同时并行执行,就可以让后面请求锁的线程稍等一下,但不放弃处理器的执行时间,看持有锁的线程是否很快释放锁,为了让线程等待,只需让线程执行一个忙循环(自旋),即所谓的自旋锁
自旋锁从JDK1.4开始引入,不过默认是关闭的,在JDK1.6之后,默认是开启的。自旋等待不是阻塞,自选等待本身虽然可以避免线程切换的开销,但需要占用处理器的时间,那么自旋会白白消耗处理器资源,因此自旋等待的时间需要有一定的限度,且如果自旋超过一定次数仍然没有获得锁,则必须挂起等待,让出处理器,一般默认的次数是10次
JDK1.6引入的自适应自旋意味着自旋的时间不固定,由前一次在同一锁上自旋时间及锁的拥有者状态来决定
- 如果在同一个锁对象上,自旋等待刚刚获得成功,并且持有锁的线程正在执行,那么虚拟机就认为这次自旋有可能成功,进而允许它自旋的时间更长
- 而如果对于一个锁,自旋很少成功过,那么以后获取这个锁时将可能省略掉自旋过程,以避免处理器资源浪费
——锁消除
锁消除是指虚拟机即时编译器在运行时,对一些代码要求同步,但是被检测到不可能存在共享数据竞争的锁进行消除,锁消除的主要判定依据是来源于逃逸分析的数据支持,如果判断一段代码中,堆上的所有数据都不会逃逸被其他线程访问,那就可以把它们当作栈上的数据对待,认为是线程私有的,同步加锁就无需进行
——锁粗化
原则上,编写代码时,总是推荐同步的作用范围尽可能的小,只在共享数据的实际作用域中才进行同步,这样是为了使得需要同步的操作数量尽可能少,如果存在锁竞争,等待的线程也很快获得锁
但如果一系列的连续操作都对同一对象反复加锁和解锁,那即使没有线程竞争,频繁的进行互斥同步操作导致很大的性能开销
如 String.append()方法 虚拟机检测到一些的操作都是对同一个String 对象操作,那么会扩大锁的范围以提高性能
——轻量级锁
在JDK1.6之后引入新型锁机制,轻量级锁本意在没有多线程竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能开销,对象的内存布局,HotSpot虚拟机的对象头 Object Header分为2部分信息,第一部分用于存储对象本身运行时数据,如哈希码 Hashcode GC分代,称为 Mark Word 是实现轻量级锁和偏向锁的关键,对象的内存布局另一部分是存储指向方法区的对象类型数据的指针,当然如果对象是数组的话,还需要记录数组的长度
在32位HotSpot虚拟机对象未被锁定的情况下,MarkWord的32bit空间中25bit用于存储对象哈希码hashcode 4bit用于存储对象分代年龄,2bit用于存储锁定标志位,1bit固定为0 在其它状态下轻量级锁,重量级锁定,GC标记,可偏向存储内容如下
Mark Word 对象头的部分内容
存储内容 | 标志位 | 状态 |
对象hashcode 对象分代信息 | 01 | 未锁定状态 |
指向锁记录的指针 | 00 | 轻量级锁定 |
指向重量级锁指针 | 10 | 重量级锁定 |
空 | 11 | GC标记 |
偏向线程ID 偏向时间戳 | 01 | 可偏向锁定 |
在代码进入同步块的时候,如果此时对象未被锁定(标志位01) 虚拟机首先在当前线程的栈帧建立一个锁记录 Lock Record的空间,用于存放对象目前的Mark Word拷贝(Displaced Mark Word)
虚拟机使用CAS操作 尝试将对象的Mark Word更新为指向Lock Record的指针,如果成功,说明,该线程成功获得了对象的锁,并且将Mark Word标志位设置为00 即表示对象处于轻量级锁定状态
而如果更新失败,虚拟机首先检查对象的Mark Word是否指向当前栈帧,如果是,说明已经获取了该对象的锁,可以下一步执行,否则,说明该对象的锁被其它线程抢占了,那么轻量级锁设置为重量级锁 10 也就是等待锁的线程进入阻塞状态
轻量级锁基于这样一个常识:对于绝大部分锁,在整个同步期间不存在竞争,使用CAS减少互斥的开销。
——偏向锁
JDK1.6之后引入一系列锁技术,目的消除数据在无竞争的情况下同步原语,进一步提高程序的性能,轻量级锁在无竞争的情况下使用CAS操作去消除同步使用的互斥量,那偏向锁在无竞争情况下消除同步过程
所谓偏,是指锁会偏向第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其它线程获取,则持有偏向锁的线程永远不需要同步
偏向锁可以提高带有同步但无竞争的程序性能,但如果程序中的数据会经常被多线程访问,那偏向模式就会显得多余