详解java 多线程之对象锁及锁升级过程

1,016 阅读5分钟

3.2 对象锁

锁为共享数据的访问提供了原子性,可见性保证。JVM层面提供的锁机制实现为monitorenter,monitorexit,通过对底层系统封装,提供一个统一的基于监视器对象的锁机制。

3.2.1 对象内存布局

在hotspot VM中,对象分为对象头(Mark word/类型指针Klass),实例数据(hotspot中使用直接指针),对齐填充三部分,数组略有不同,但mark word和klass是一样的,mark word存储着对象运行时的信息。

上图为在32位平台上,mard word的表示,占4个字节,部分比特随表示含义的不同大小是可变的,但最后固定2bit表示标志位tag,如00表示轻量级锁,10表示重量级锁,01时,倒数第三位是偏向锁标志,101表示偏向锁,001则表示没有锁。mard word包含了对象的hash,GC信息,还有锁状态,同步块就是基于此实现的。

3.2.2 重量级锁

重量级锁(10)是由底层操作系统支持的,前文讲过,在支持posix线程标准的os里,通过pthread库提供:

可见,此时mark word中的指针即mutex,操作系统会保证重量级锁的访问是互斥的,所以synchronized是互斥锁,请求锁的线程会排队等待,在锁被释放时被唤醒,重新竞争锁。由于依赖操作系统调度,加锁/解锁需要在用户态和内核态之间切换,所以性能消耗是很大的,这块消耗掉的CPU时间片甚至可能超过用户代码执行所需时间片。

这也是以前(jdk1.6之前),说synchronized执行效率低的原因。在jdk1.6,jvm对synchronized的实现进行了优化,引入了偏向锁和轻量级锁,使得其执行性能有了大幅提高。

3.2.3 轻量级锁

重量级锁之所以性能不好是策略的问题,这种策略假定一定会发生竞争,属于悲观锁。轻量级锁(00)则是建立在一般不会有多线程竞争的假设下,大多数情况的确如此。基于这点,我们很容易想到使用CAS代替互斥量。

轻量级锁是指VM首先在栈帧中分配一个锁记录(Lock Record)内存空间,用于存储锁对象当前的Mark Word的拷贝,然后将对象头中Mark word拷贝到锁记录中(Displaced Mark Word),接着通过CAS操作,VM尝试将锁对象的mark word设置为指向Lock Record的指针,并将Lock Record里的owner指针指向锁对象的mark word。

1)如果更新成功,那么当前线程就获得了该对象的锁,此时对象处于轻量级锁定状态(tag标记为00)

2)否则,VM检查当前对象的mark word是否指向当前线程的栈帧,如果是,说明当前线程已经拥有了该对象的锁,直接进入同步块继续执行

3)否则,说明有多个线程发生了锁竞争。此时,如果只有一个线程等待,则该线程通过自旋等待。如果有更多线程来竞争锁,或自旋达到一定次数,轻量级锁就会膨胀为重量级锁。

轻量级锁解锁时通过CAS将锁记录中mark word替换回对象头,如果成功,则释放了锁。失败说明有其他线程尝试获取过该对象的锁,需要唤醒被挂起的线程。

可以看出,轻量级锁相比重量级锁,通过自旋+CAS来获取锁,而不是阻塞线程。

3.2.4 偏向锁

偏向锁则进一步假设同步代码同时只会被一个线程执行,不会发生竞争。那么就可以进一步取消拷贝Lock record及通过CAS设置指针的操作。所以,在不会发生竞争时,偏向锁能够通过避免轻量级锁加锁和解锁带来的性能开销,进一步提高效率。

加锁过程:

1)在加锁时,如果支持偏向锁,JVM通过CAS将当前线程ID记录在锁对象的mark word里,并将偏向标志位为1(101)

2)如果失败,说明有锁竞争且另一线程已经获得偏向锁,此时会挂起当前线程,在安全点(safe point)撤销偏向锁并膨胀为轻量级锁,拥有锁的那个线程继续运行。

偏向锁在加锁时同样通过CAS,但不会自旋,只尝试一次,另外,偏向锁没有解锁过程,只会撤销或膨胀为轻量级锁。

3.2.5 CAS

CAS更近一步,是一种无锁策略,并不会同步任何代码块,而是在更新对象值时采用比较并切换(Compare And Swap)的方式,属于乐观锁,假定条件也是不会有线程并发。CAS只能保证原子性,无法保证可见性,一般与volatile(可见性)和循环一起使用:

//AbstractQueuedSynchronizer.java
private transient volatile Node tail;

private Node enq(final Node node) {
    for (;;) {
        Node t = tail;
        if (t == null) { // Must initialize
            if (compareAndSetHead(new Node()))
                tail = head;
        } else {
            node.prev = t;
            if (compareAndSetTail(t, node)) {
                t.next = node;
                return t;
            }
        }
    }
}

关于CAS底层原理,以后单独介绍

3.2.6 整体流程

重量级锁,轻量级锁,偏向锁都是JVM层次提供的锁支持,其接口是java语言中的synchronized关键字(字节码中表示为monitorenter和monitorexit),获取对象锁的整体流程为:

更多文章,见公-众-号