在常见的并发案例中,会使用多个线程对一个数值进行自加运算,会得到一个和预计总和不一致的结果,这就是很多面试中遇到的自增是否是线程安全问题。在Java中如何进行线程安全问题的规避,常规方式就是使用锁机制。

Java对象结构

  • 对象头
1
2
3
  Mark Word(标记字):记录对象的运行时数据。其长度值为JVM中一个Word的大小。
Class Pointer(类对象指针):记录对象元数据信息。
Array Length(数组长度):可选字段,如果对象为数组,则记录数据长度,如果对象非数组,则该字段不存在。
  • 对象体

    记录了对象的成员变量。

  • 对齐字节

    填充补位使用,用于确保对象所占内存字节数为8的整数倍。

Java锁状态

  • Java中锁的状态可分为无锁偏向锁轻量级锁重量级锁
  • 锁的状态不可降级,只能从低到高,无法进行降级操作。
  • 锁的状态记录在Mark Word中。
锁状态57位4位1位(biased)2位(lock)
无锁[25位]+【31位:对象的HashCode】+[1位]分代年龄001
偏向锁【54位:线程ID】+【2位:epoch】 + [1位]分代年龄101
轻量级锁【62位锁记录指针】--00
重量级锁【62位的锁监视器指针】-=10

偏向锁

偏向锁,顾名思义,就是偏向于某一个线程的锁,主要应用在没有线程竞争的场景下。

当一个线程获取了锁,则当前锁会进入偏向状态,在锁对象的Mark Word中,会使用54位记录占用的线程ID,如果这个线程再次请求锁,则会直接获取到锁,无需任何操作。

JVM默认会开启偏向锁,默认延时4秒开启,在4秒内,会进行很多的初始化工作,使用大量的synchronized关键字进行加锁,且很多操作均为多线程竞争。

1
2
# +UseBiasedLocking标识开启偏向锁,BiasedLockingStartupDelay用于禁止偏向锁延迟
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0

存在多线程竞争的场景下,偏向锁会发生撤销。

在调用锁对象的**hashCode()或者调用System.identityHashCode()**计算哈希码时,也会发生撤销。

轻量级锁

主要用于在应用层实现锁,而避免使用操作系统底层的互斥锁。

1
2
3
4
当一个线程进入临界区前,如果锁对象没有被锁定,则会在这个线程的栈帧中创建一个锁记录(Lock Record)。
CAS自旋抢锁,将锁记录指针指向当前线程中的锁记录地址。
JVM更新锁标记位。
当前线程将锁记录中的owner指针指向锁对象,同时在锁记录中的Displaced Mark Word字段中存储锁对象的Mark Word信息。
  • 轻量级锁分为普通自旋锁和自适应自旋锁。
  • 两者区别在于自旋次数。普通自旋锁的自旋次数是固定的,默认为10次,可通过-XX:PreBlockSpin进行修改。而自适应自旋锁是历史占锁自旋的时间及锁状态来动态调整的。

重量级锁

重量级锁应用了操作系统底层的互斥锁,会发生用户态和内核态之间的切换,所以开销较大。

每一个对象均关联一个监视器,监视器实现类为ObjectMonitor,在其内部存在以下属性:

1
2
3
4
Owner:记录获取到锁的线程。
Cxq:竞争队列,为单向链表,请求锁的线程会被放在其中。
EntrySet:存储候选线程。
WaitSet:存储调用wait()方法的线程。

获取锁的流程如下:

1
2
3
线程通过CAS自旋获取不到锁时,会进入到Cxq队列中等待。
如果Owner线程释放锁,则会将Cxq中的线程转移到EntryList中,并指定Head线程为OnDeck Thread。
OnDeck Thread线程重新竞争锁,获取到锁之后称为Owner Thread,不成功则继续等待。

在"等待-通知"中,wait()被调用时,JVM会释放Owner线程的资格,将其移入到WaitSet中,等待notify。notify()调用时,则会将WaitSet中的一个线程移动到EntryList队列中,故以上两个方法均需要在同步块中执行。