读书笔记 ------ 高效并发之轻量级锁

我们知道, Java 虚拟机堆上的对象,除了存储实体数据外,还有个对象头的概念。它存储了诸如 hashCode 、 GC 分代年龄、指向类的指针等信息。对象头中的数据大部分都不是必须的,但却能帮助虚拟机更方便、快捷地完成某些功能。比如“指向类的指针”,就可以很方便地实现反射(即使没有这个指针,虚拟机也能完成反射,但可能就会麻烦很多)。

正因为“不重要”,对象头的数据结构非常灵活,在对象处于不同状态时,根据需要存储不同的东西。比如对象未被锁定时,可以存储哈希码、 GC 分代年龄等,此时把自己的状态标志设为1;对象被轻量级锁定时,存储指向轻量级锁的指针,此时把自己的状态标志设为0;对象处于重量级锁定时,存储指向操作系统互斥量的指针,把自己的状态标志设为2,等等。

那轻量级锁又是什么呢?我们知道,为了保证共享数据的安全,我们不得不使用 synchronized 同步块之类的手段来保证共享数据的安全。但是同步通常意味着线程的阻塞,涉及到线程的挂起和唤醒,而 Java 中的线程是映射到操作系统的物理线程上的,这些挂起和唤醒操作需要程序不停地在用户态和内核态之间切换,这是一笔不小的开销。为了保证并发正确,给共享数据加锁的操作是不可避免的,但是在实际情况下,又有多少机会说好几条线程真的会在同一时间进入到相同的同步块呢?好像很少。。悲哀的是,即便只有一条线程,虚拟机还不得不执行加锁、解锁、维护锁计数器、用户态-内核态切换等等操作(好吧,只有一条线程的时候,不存在阻塞的问题,但加锁和解锁还是必须的,如果不做任何优化的话,加锁和解锁要使用操作系统的互斥量,所以还是得切换到内核态)。所以对于这种情况有没有优化的余地?轻量级锁就是为了解决这个问题被发明出来的。

轻量级锁的原理和执行过程,这里就不再抄书了。可以参考 虚拟机中的锁优化简介(适应性自旋/锁粗化/锁削除/轻量级锁/偏向锁) 这篇文章,作者是周志明大大本人。

这里面有这么一句话: 如果这个更新操作失败了,虚拟机首先会检查对象的 Mark Word 是否指向当前线程的栈帧,如果是就说明当前线程已经拥有了这个对象的锁,那就可以直接进入同步块继续执行,否则说明这个锁对象已经被其他线程抢占了。开始的时候很疑惑,既然 CAS 操作都失败了, Mark Word 怎么可能有指向 Lock Record 的指针呢?其实这句话是针对第二次、第三次、第 N 次加锁过程来说的,第一次加锁过程确实不会有这种情况。还有一点, CAS 操作比较的是什么?加锁的时候,比较的是对象的 Mark Word 和栈上的 Mark Word ;解锁的时候,估计是预先把刚刚加锁后的 Mark Word 记下来了,然后和当前的 Mark Word 比较。

最后再抄一点书,关于偏向锁。
  偏向锁也是 JDK 1.6 中引入的一项锁优化,它的目的是消除数据在无竞争情况下的同步原语,进一步提高程序的运行性能。如果说轻量级锁是在无竞争的情况下使用 CAS 操作去消除同步使用的互斥量,那偏向锁就是在无竞争的情况下把整个同步都消除掉,连 CAS 操作都不做了。
  偏向锁的“偏”,就是偏心的“偏”、偏袒的“偏”。它的意思是这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
    偏向锁在对象头中记录了获取到该锁的线程的 ID 。



补:关于 CAS 过程的修正

CAS 的语义是: CAS 有3个操作数,内存值 V ,旧的预期值 A ,要修改的新值 B 。当且仅当预期值 A 和内存值 V 相同时,将内存值 V 修改为 B ,否则什么都不做。所以,我的猜测是:

加锁过程:在 CAS 之前, Lock Record 的 owner 指针已经指向对象的 Mark Word 了。 CAS 操作判断对象当前的 Mark Word 和栈上刚刚复制过来的 Mark Word 是否相等,若相等修改对象的 Mark Word 为指向 Lock Record 的指针。若该 CAS 操作成功,则本线程成功获得锁,接着把 Object 锁标志位标记为“00”(轻量级锁定);否则检查对象 Mark Word 的指针是否指向当前线程的 Lock Record ,若是说明当前线程已经获得了锁,本次加锁是一个重入过程,可以进入同步块直接执行;否则说明该锁被其它线程抢占了(就在复制 Mark Word 和设置 owner 指针期间。可能我先开始,但别人更快),于是修改对象的锁标志为重量级锁定,并把 Mark Word 的锁指针修改为指向操作系统互斥量,对象膨胀为重量级锁,本线程进入阻塞状态。

解锁过程:和加锁类似。稍有不同的是,这次比较的是栈上 Mark Word 的地址和对象 Mark Word 指针所指向的地址是否一样。若不一样只有一种解释,别的线程在这段时间之内申请过该锁,对象膨胀成了重量锁,这时候需要唤醒被阻塞的线程。

if (header.flag == 00) {
    assign space on stack frame: Lock_Record;
	Lock_Record.header <== object.header;
	Lock_Record.pointer --> object.header;
	
	success <== CAS(object.header, Lock_Record.header, object.header.pointer --> Lock_Record and object.header.flag <== 01);
	
	if (success) {
	    OK;
	} else {
	    reenter <== object.header.pointer --> Lock_Record ??
		if (reenter) {
		    OK;
		} else {
		    object.header.flag <== 02;
			blocked;
		}
	}
} else if (header.flag == 01) {
    object.header.flag <== 02;
    blocked;
} else if (header.flag == 02) {
    blocked;
}

猜你喜欢

转载自dsxwjhf.iteye.com/blog/2204874
今日推荐