synchronized与锁

Java 中的锁都是基于对象的,每一个对象都可以作为一个锁。

synchronized 关键字

我们可以使用 synchronized 为一段代码或一个方法加上锁。静态方法上加的是类锁。

主要有以下形式。

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    Object o = new Object();
    synchronized (o) {
        // code
    }
}

临界区内的代码同一时间只能由一个获得锁的线程执行。

synchronized 下面写法是等价的。

// 关键字在实例方法上,锁为当前实例
public synchronized void instanceLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this) {
        // code
    }
}

静态方法同理,使用的类锁(class对象)

// 关键字在静态方法上,锁为当前Class对象
public static synchronized void classLock() {
    // code
}

// 关键字在代码块上,锁为括号里面的对象
public void blockLock() {
    synchronized (this.getClass()) {
        // code
    }
}

锁分类

Java6 为了减少获得锁和释放锁带来的性能损耗,引入了 偏向锁 和 轻量锁。Java6 以前,所有的锁都是重量级锁。Java6 后,一个对象有四种锁状态,由低到高分别为:

  • 无锁
  • 偏向锁
  • 轻量锁
  • 重量锁

无锁是没有对资源进行锁定,任何线程都可以尝试修改它。

几种锁会随着竞争逐渐升级,锁容易升级,但降锁条件比较苛刻。锁降级发生在 Stop The World 期间,当 JVM 进入安全点时,会检查是否有闲置的锁,然后进行降级。

Java 对象头

Java 的锁都是基于对象的,我们看一下 对象的 锁 信息放在哪。

每个 Java 对象都有对象头。如果是非数组类型,用2个字宽存储对象头,如果是数组,3个字宽存储对象头。

在32位处理器中,一个字宽是32位;在64位虚拟机中,一个字宽是64位。

长度内容说明
32/64 bitMark Word对象的hashCode或锁信息
32/64 bitClass Metadata Address对象类型数据指针
32/64 bitArray length数组长度

Mark Word的格式

29/61 bit1bit(是否为偏向锁)2bit(锁标志)
无锁001
偏向锁线程ID101
轻量锁指向栈中锁记录的指针00
重量级锁指向互斥量(重量级锁)的指针10
GC11
  • 当对象状态为偏向锁时,Mark Word 存储的是偏向的线程ID;

  • 为轻量级锁时,Mark Word存储的是指向线程栈中Lock Record的指针;

  • 为重量级锁时,Mark Word为指向堆中 monitor 对象的指针

偏向锁

Hotspot的作者经过以往的研究发现大多数情况下大多数情况下 锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

偏向锁会偏向于第一个获得访问锁的线程,如果接下来,该锁没有被其他线程访问,则持有该锁的线程将永远不需要触发同步。

也就是说,偏向锁在资源无竞争情况下消除了同步语句,连CAS操作都不做了,提高了程序的运行性能。

实现原理

一个线程在第一次进入同步块时,会在对象头栈帧中的锁记录存储锁偏向的线程ID,当下次该线程再进入整个同步块,会检查锁的 Mark Word 放的是不是当前线程 ID。

如果是,表明线程已经获得了锁,线程进入和退出同步块时不需要花费 CAS 操作来加/解锁。

如果不是,就代表有另一个线程来竞争这个偏向锁。这时会尝试用CAS来替换 Mark Word里的线程ID为新线程的ID,这时有两种情况:

  • 成功,表示之前的线程不在了,Mark Word 里的线程ID更新为新线程的ID,锁依然为偏向锁。
  • 失败,表示之前的线程依然存在,那么暂停之前的线程,设置偏向锁标识为0,并设置锁标志位为00,升级为轻量级锁,会按照轻量级锁的方式进行竞争锁。

撤销偏向锁

偏向锁升级为轻量锁时,会暂停拥有偏向锁的线程,重置偏向锁标志位。

  • 在一个安全点停止拥有锁的线程
  • 遍历线程栈,如果存在锁记录的话,需要修复锁记录和Mark Word,使其变成无锁状态
  • 唤醒被停止的线程,将当前锁升级成轻量级锁。

如果应用程序里的锁通常处于竞争态,偏向锁是一种累赘,这时可以把偏向锁概念关闭

-XX:UseBiasedLocking=false

轻量级锁

加锁

JVM 为每个线程再当前线程栈帧中创建用于存储锁记录的空间,我们称为 Displaced Mark Word

如果一个线程获得锁时发现是轻量级锁,会把锁的 Mark Word 复制到自己的 Displaced Mark Word中。

然后线程尝试 CAS 将锁的**Mark Word替换为指向锁记录指针**。

  • 如果成功,当前线程获得锁
  • 如果失败,表示Mark Word已经被替换成了其他线程的锁记录,说明与其他线程竞争锁,当前线程尝试使用自旋获取锁。

自旋:不断尝试获取锁,一般使用循环实现

自旋消耗CPU资源,如果一直获取不到,线程一直处在自旋状态,白白浪费CPU。JDK采取的方式,适应性自旋,简单来说就是线程如果自旋成功了,则下次自旋的次数会更多,如果自旋失败了,则自旋的次数就会减少。

当自旋到一定程度,依然没有获取倒锁,即自旋失败了,这个线程会被阻塞,同时这个锁会升级为重量级锁

释放

释放锁时,当前线程会使用 CAS 将Displaced Mark Word的内容复制回锁的Mark Word里,如果无竞争,那么复制会成功。如果有其他线程因为多次自旋导致锁升级为重量级锁,CAS会失败,此时锁被释放并唤醒被阻塞的线程。

重量级锁

重量级锁依赖于操作系统的互斥量(mutex) 实现的,而操作系统中线程间状态的转换需要相对比较长的时间,所以重量级锁效率很低,但被阻塞的线程不会消耗CPU。

当多个线程同时请求某个对象锁时,对象锁会设置几种状态来区分请求线程:

  • Contention List:请求锁的线程首先放到此竞争队列
  • Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
  • Wait Set:那些调用wait方法被阻塞的线程被放置到Wait Set
  • OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为OnDeck
  • Owner:获得锁的线程称为Owner
  • !Owner:释放锁的线程

当线程尝试获取锁时,如果锁被占用,该线程会被封装成 ObjectWaiter 对象插入到 Contention List 队尾,然后调用 park 挂起当前线程。

线程释放锁时,会从 Contention List 或 Entry List 中挑选一个线程唤醒, 被选中的线程叫做 Heir presumptive即假定继承人,但 synchronized 是非公平的,假定继承人不一定能获取到锁。因为对于重量级锁,线程先尝试自旋获取锁,这样会减少操作系统同步操作带来的开销。如果自旋不成功再进入等待队列。

如果线程获得锁后调用 Object.wait 方法,则会将线程加入到 Wait Set 中,当被 Object.notify 唤醒后,会将线程从 Wait Set 移动到Contention ListEntry List中。

当调用一个锁对象的 waitnotify时,如果锁时偏向锁或轻量锁则先升级为重量锁。

总结

锁升级流程

  1. 偏向锁 -> 轻量级锁

    • 线程检查 markword 锁标记,如果是偏向锁,并且 锁对象的线程ID为自己,获取锁

    • 如果不是自己,执行CAS 替换操作,替换失败,说明有其他线程竞争锁,此时锁升级为轻量级锁,暂停获得锁的线程

  2. 轻量级锁 -> 重量级锁

  • 线程将锁对象的 markword复制到自己线程栈的所记录
  • CAS 替换锁对象的 markdord 为当前线程栈的地址
  • 替换成功说明获取到锁
  • 替换失败,自旋一段时间,如果还是未获取到,升级为重量级锁,自旋线程进入阻塞,等待被唤醒。

锁对比

优点缺点适用场景
偏向锁加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。如果线程间存在锁竞争,会带来额外的锁撤销的消耗。适用于只有一个线程访问同步块场景。
轻量级锁竞争的线程不会阻塞,提高了程序的响应速度。如果始终得不到锁竞争的线程使用自旋会消耗CPU。追求响应时间。同步块执行速度非常快。
重量级锁线程竞争不使用自旋,不会消耗CPU。线程阻塞,响应时间缓慢。追求吞吐量。同步块执行时间较长。
Last Updated:
Contributors: himcs