Java|锁--JAVA成长之路( 五 )


在代码进入同步块的时候 , 如果同步对象锁状态为无锁状态(锁标志位为“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中存储的是指向重量级锁的指针 , 此时等待锁的线程都会进入阻塞状态 。
整体的锁状态升级流程如下:
综上 , 偏向锁通过对比Mark Word解决加锁问题 , 避免执行CAS操作 。 而轻量级锁是通过用CAS操作和自旋来解决加锁问题 , 避免线程阻塞和唤醒而影响性能 。 重量级锁是将除了拥有锁的线程以外的线程都阻塞 。
4. 公平锁 VS 非公平锁
公平锁是指多个线程按照申请锁的顺序来获取锁 , 线程直接进入队列中排队 , 队列中的第一个线程才能获得锁 。 公平锁的优点是等待锁的线程不会饿死 。 缺点是整体吞吐效率相对非公平锁要低 , 等待队列中除第一个线程以外的所有线程都会阻塞 , CPU唤醒阻塞线程的开销比非公平锁大 。
非公平锁是多个线程加锁时直接尝试获取锁 , 获取不到才会到等待队列的队尾等待 。 但如果此时锁刚好可用 , 那么这个线程可以无需阻塞直接获取到锁 , 所以非公平锁有可能出现后申请锁的线程先获取锁的场景 。 非公平锁的优点是可以减少唤起线程的开销 , 整体的吞吐效率高 , 因为线程有几率不阻塞直接获得锁 , CPU不必唤醒所有线程 。 缺点是处于等待队列中的线程可能会饿死 , 或者等很久才会获得锁 。
直接用语言描述可能有点抽象 , 这里作者用从别处看到的一个例子来讲述一下公平锁和非公平锁 。
如上图所示 , 假设有一口水井 , 有管理员看守 , 管理员有一把锁 , 只有拿到锁的人才能够打水 , 打完水要把锁还给管理员 。 每个过来打水的人都要管理员的允许并拿到锁之后才能去打水 , 如果前面有人正在打水 , 那么这个想要打水的人就必须排队 。 管理员会查看下一个要去打水的人是不是队伍里排最前面的人 , 如果是的话 , 才会给你锁让你去打水;如果你不是排第一的人 , 就必须去队尾排队 , 这就是公平锁 。
但是对于非公平锁 , 管理员对打水的人没有要求 。 即使等待队伍里有排队等待的人 , 但如果在上一个人刚打完水把锁还给管理员而且管理员还没有允许等待队伍里下一个人去打水时 , 刚好来了一个插队的人 , 这个插队的人是可以直接从管理员那里拿到锁去打水 , 不需要排队 , 原本排队等待的人只能继续等待 。 如下图所示:
接下来我们通过ReentrantLock的源码来讲解公平锁和非公平锁 。
根据代码可知 , ReentrantLock里面有一个内部类Sync , Sync继承AQS(AbstractQueuedSynchronizer) , 添加锁和释放锁的大部分操作实际上都是在Sync中实现的 。 它有公平锁FairSync和非公平锁NonfairSync两个子类 。 ReentrantLock默认使用非公平锁 , 也可以通过构造器来显示的指定使用公平锁 。


推荐阅读