在常见的并发案例中,会使用多个线程对一个数值进行自加运算,会得到一个和预计总和不一致的结果,这就是很多面试中遇到的自增是否是线程安全问题。在Java中如何进行线程安全问题的规避,常规方式就是使用锁机制。
Java对象结构
- 对象头
1 | Mark Word(标记字):记录对象的运行时数据。其长度值为JVM中一个Word的大小。 |
对象体
记录了对象的成员变量。
对齐字节
填充补位使用,用于确保对象所占内存字节数为8的整数倍。
Java锁状态
- Java中锁的状态可分为无锁、偏向锁、轻量级锁、重量级锁。
- 锁的状态不可降级,只能从低到高,无法进行降级操作。
- 锁的状态记录在Mark Word中。
锁状态 | 57位 | 4位 | 1位(biased) | 2位(lock) |
---|---|---|---|---|
无锁 | [25位]+【31位:对象的HashCode】+[1位] | 分代年龄 | 0 | 01 |
偏向锁 | 【54位:线程ID】+【2位:epoch】 + [1位] | 分代年龄 | 1 | 01 |
轻量级锁 | 【62位锁记录指针】 | - | - | 00 |
重量级锁 | 【62位的锁监视器指针】 | - | = | 10 |
偏向锁
偏向锁,顾名思义,就是偏向于某一个线程的锁,主要应用在没有线程竞争的场景下。
当一个线程获取了锁,则当前锁会进入偏向状态,在锁对象的Mark Word中,会使用54位记录占用的线程ID,如果这个线程再次请求锁,则会直接获取到锁,无需任何操作。
JVM默认会开启偏向锁,默认延时4秒开启,在4秒内,会进行很多的初始化工作,使用大量的synchronized关键字进行加锁,且很多操作均为多线程竞争。
1 | # +UseBiasedLocking标识开启偏向锁,BiasedLockingStartupDelay用于禁止偏向锁延迟 |
在存在多线程竞争的场景下,偏向锁会发生撤销。
在调用锁对象的**hashCode()或者调用System.identityHashCode()**计算哈希码时,也会发生撤销。
轻量级锁
主要用于在应用层实现锁,而避免使用操作系统底层的互斥锁。
1 | 当一个线程进入临界区前,如果锁对象没有被锁定,则会在这个线程的栈帧中创建一个锁记录(Lock Record)。 |
- 轻量级锁分为普通自旋锁和自适应自旋锁。
- 两者区别在于自旋次数。普通自旋锁的自旋次数是固定的,默认为10次,可通过-XX:PreBlockSpin进行修改。而自适应自旋锁是历史占锁自旋的时间及锁状态来动态调整的。
重量级锁
重量级锁应用了操作系统底层的互斥锁,会发生用户态和内核态之间的切换,所以开销较大。
每一个对象均关联一个监视器,监视器实现类为ObjectMonitor,在其内部存在以下属性:
1 | Owner:记录获取到锁的线程。 |
获取锁的流程如下:
1 | 线程通过CAS自旋获取不到锁时,会进入到Cxq队列中等待。 |
在"等待-通知"中,wait()被调用时,JVM会释放Owner线程的资格,将其移入到WaitSet中,等待notify。notify()调用时,则会将WaitSet中的一个线程移动到EntryList队列中,故以上两个方法均需要在同步块中执行。