JAVA各种锁的优劣对比分析( 四 )


偏向锁只有遇到其他线程尝试竞争偏向锁时 , 持有偏向锁的线程才会释放锁 , 线程不会主动释放偏向锁 。偏向锁的撤销 , 需要等待全局安全点(在这个时间点上没有字节码正在执行) , 它会首先暂停拥有偏向锁的线程 , 判断锁对象是否处于被锁定状态 。撤销偏向锁后恢复到无锁(标志位为“01”)或轻量级锁(标志位为“00”)的状态 。
偏向锁在JDK 6及以后的JVM里是默认启用的 。可以通过JVM参数关闭偏向锁:-XX:-UseBiasedLocking=false , 关闭之后程序默认会进入轻量级锁状态 。
轻量级锁
是指当锁是偏向锁的时候 , 被另外的线程所访问 , 偏向锁就会升级为轻量级锁 , 其他线程会通过自旋的形式尝试获取锁 , 不会阻塞 , 从而提高性能 。
在代码进入同步块的时候 , 如果同步对象锁状态为无锁状态(锁标志位为“01”状态 , 是否为偏向锁为“0”) , 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间 , 用于存储锁对象目前的Mark Word的拷贝 , 然后拷贝对象头中的Mark Word复制到锁记录中 。
拷贝成功后 , 虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 , 并将Lock Record里的owner指针指向对象的Mark Word 。
如果这个更新动作成功了 , 那么这个线程就拥有了该对象的锁 , 并且对象Mark Word的锁标志位设置为“00” , 表示此对象处于轻量级锁定状态 。
如果轻量级锁的更新操作失败了 , 虚拟机首先会检查对象的Mark Word是否指向当前线程的栈帧 , 如果是就说明当前线程已经拥有了这个对象的锁 , 那就可以直接进入同步块继续执行 , 否则说明多个线程竞争锁 。
若当前只有一个等待线程 , 则该线程通过自旋进行等待 。但是当自旋超过一定的次数 , 或者一个线程在持有锁 , 一个在自旋 , 又有第三个来访时 , 轻量级锁升级为重量级锁 。
重量级锁
升级为重量级锁时 , 锁标志的状态值变为“10” , 此时Mark Word中存储的是指向重量级锁的指针 , 此时等待锁的线程都会进入阻塞状态 。
整体的锁状态升级流程如下:

JAVA各种锁的优劣对比分析

文章插图
 
综上 , 偏向锁通过对比Mark Word解决加锁问题 , 避免执行CAS操作 。而轻量级锁是通过用CAS操作和自旋来解决加锁问题 , 避免线程阻塞和唤醒而影响性能 。重量级锁是将除了拥有锁的线程以外的线程都阻塞 。
4. 公平锁 VS 非公平锁公平锁是指多个线程按照申请锁的顺序来获取锁 , 线程直接进入队列中排队 , 队列中的第一个线程才能获得锁 。公平锁的优点是等待锁的线程不会饿死 。缺点是整体吞吐效率相对非公平锁要低 , 等待队列中除第一个线程以外的所有线程都会阻塞 , CPU唤醒阻塞线程的开销比非公平锁大 。
非公平锁是多个线程加锁时直接尝试获取锁 , 获取不到才会到等待队列的队尾等待 。但如果此时锁刚好可用 , 那么这个线程可以无需阻塞直接获取到锁 , 所以非公平锁有可能出现后申请锁的线程先获取锁的场景 。非公平锁的优点是可以减少唤起线程的开销 , 整体的吞吐效率高 , 因为线程有几率不阻塞直接获得锁 , CPU不必唤醒所有线程 。缺点是处于等待队列中的线程可能会饿死 , 或者等很久才会获得锁 。
直接用语言描述可能有点抽象 , 这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁 。
JAVA各种锁的优劣对比分析

文章插图
 
如上图所示 , 假设有一口水井 , 有管理员看守 , 管理员有一把锁 , 只有拿到锁的人才能够打水 , 打完水要把锁还给管理员 。每个过来打水的人都要管理员的允许并拿到锁之后才能去打水 , 如果前面有人正在打水 , 那么这个想要打水的人就必须排队 。管理员会查看下一个要去打水的人是不是队伍里排最前面的人 , 如果是的话 , 才会给你锁让你去打水;如果你不是排第一的人 , 就必须去队尾排队 , 这就是公平锁 。


推荐阅读