P18_锁升级与偏向锁深入解析
- 在 JDK 1.5 之前(不包括1.5),若想实现线程同步,只能使用 synchronized 关键字这一方式来达到;jdk 层面,也是通过 synchronized 关键字来做到数据的原子性维护的;synchronized 关键字时 JVM 实现的一种内置锁,从 jvm 层面角度看,这种锁的获取和释放都是由 JVM 来完成的;
- 在 JDK 1.5 中,引入 JUC 并发包,其中包含很多并发工具和锁,Lock 同步锁是基于 Java 来实现的,因此锁的获取与释放都是通过 Java 代码来实现与控制的;而 synchronized 是基于底层操作系统的
Mutex Lock
来实现的,每次对锁的获取与释放都会带来用户态和内核态之间的切换,这种切换会极大地增加系统负担;在并发量较高时,也就是锁的竞争比较激烈时
,synchronized 锁在性能上的表现会较差。 - 从 JDK 1.6 开始,synchronized 锁的实现发生了很大的变化;JVM 引入了相应的优化手段来提升 synchronized 锁的性能,这种提升涉及到
偏向锁
、轻量级锁
、重量级锁
等,从而减少锁的竞争所带来的用户态和内核态之间的切换;这种锁的优化是通过Java 对象头
中的一些标志位来实现的; - 从 JDK 1.6 开始,对象实例在堆内存中会由三部分组成:
- 对象头
- Mark Word
- 锁标记
- GC 标记
- 等等
- 指向类的指针
- 数组长度
- 实例数据
- 对齐填充(可选)
hotspot/src/share/vm/oops/markOop.hpp
// The markOop describes the header of an object.
//
// Note that the mark is not a real oop but just a word.
// It is placed in the oop hierarchy for historical reasons.
//
// Bit-format of an object header (most significant first, big endian layout below):
//
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
//
// - hash contains the identity hash value: largest value is
// 31 bits, see os::random(). Also, 64-bit vm's require
// a hash value no bigger than 32 bits because they will not
// properly generate a mask larger than that: see library_call.cpp
// and c1_CodePatterns_sparc.cpp.
//
// - the biased lock pattern is used to bias a lock toward a given
// thread. When this pattern is set in the low three bits, the lock
// is either biased toward a given thread or "anonymously" biased,
// indicating that it is possible for it to be biased. When the
// lock is biased toward a given thread, locking and unlocking can
// be performed by that thread without using atomic operations.
// When a lock's bias is revoked, it reverts back to the normal
// locking scheme described below.
//
// Note that we are overloading the meaning of the "unlocked" state
// of the header. Because we steal a bit from the age we can
// guarantee that the bias pattern will never be seen for a truly
// unlocked object.
//
// Note also that the biased state contains the age bits normally
// contained in the object header. Large increases in scavenge
// times were seen when these bits were absent and an arbitrary age
// assigned to all biased objects, because they tended to consume a
// significant fraction of the eden semispaces and were not
// promoted promptly, causing an increase in the amount of copying
// performed. The runtime system aligns all JavaThread* pointers to
// a very large value (currently 128 bytes (32bVM) or 256 bytes (64bVM))
// to make room for the age bits & the epoch bits (used in support of
// biased locking), and for the CMS "freeness" bit in the 64bVM (+COOPs).
//
// [JavaThread* | epoch | age | 1 | 01] lock is biased toward given thread
// [0 | epoch | age | 1 | 01] lock is anonymously biased
//
// - the two lock bits are used to describe three states: locked/unlocked and monitor.
//
// [ptr | 00] locked ptr points to real header on stack
// [header | 0 | 01] unlocked regular object header
// [ptr | 10] monitor inflated lock (header is wapped out)
// [ptr | 11] marked used by markSweep to mark an object
// not valid at any other time
//
// We assume that stack/thread pointers have the lowest two bits cleared.
P19_轻量级锁与重量级锁的变化深入详解
对于锁的演化来说,可能经历如下阶段:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
- 偏向锁
针对于一个线程来说,它的主要作用就是优化同一个线程多次获取同一个锁的情况;如果一个 synchronized 方法被一个线程访问,那么这个方法所在的对象实例就会在其 Mark Word 中使用偏向锁进行标记且存储该线程的ID;当这个线程再次访问同一个对象的同步代码时,它会检查这个对象的 Mark Word 的锁标记(若是偏向锁且线程ID还是自己,那么该线程无需再去进入管程[Monitor]了,而是直接进入到同步代码中)。
如果有其他线程访问此对象的同步代码时,偏向锁会被取消;
- 轻量级锁
若第一个线程已经获取到了当前对象的锁,这时第二个线程又开始尝试争抢该对象的锁,由于该对象的锁已经被第一个线程获取到,此时还是偏性锁;而第二个线程在争抢时,会发现该对象头中的 Mark Word 的锁标记是偏向锁且线程ID不是自己(是第一个线程ID),那么它会以 CAS(Compare and Swap) 操作的方式去请求锁:
- 请求锁成功:将 Mark Word 中的线程ID指向自己,锁标记不变(还是偏向锁)
- 请求锁失败:表名可能会有多个线程同时在尝试争抢该对象的锁,这时偏向锁会升级为轻量级锁
- 此时这个线程会先进行一段时间的自旋(自旋锁),等待第一个线程执行完成;
- 自旋很短一段时间后,重新获取到了锁,锁标记???
- 自旋一段时间后,依然无法获取到锁(第一个线程在同步代码中执行时间较长),锁会继续升级,升级升重量级锁
- 在这种情况下,无法获取到锁的线程都会进入到 Wait Set (Monitor 内核态)
- 自旋锁的特点就是
避免线程从用户态进入到内核态
- 重量级锁
线程最终从用户态进入到内核态
P20_锁粗化与锁消除技术实例演示与分析
package new_package.thread.p20;
public class MyTest {
int i = 0;
public void method() {
Object object = new Object();
synchronized (object) {
i++;
}
System.out.println(i);
}
}
// 其实这个锁是无用的,因为每次进入方法都会重新生成一个新的 object 对象;
锁消除
JIT 编译器(Just In Time)
可以在动态编译同步代码时,使用一种叫做逃逸分析的技术,来通过该技术判别程序中所使用的锁对象是否只被一个线程所使用而没有散步到其他线程中;如果是,那么 JIT 编译器在编译这个同步代码时就不会生成 synchronized 所标识的锁的申请和释放的机器码,从而消除可锁的使用流程。
这也是编译器对于锁的优化措施之一
并不是在字节码层面进行的优化
package new_package.thread.p20;
public class MyTest2 {
Object object = new Object();
public void method() {
synchronized (object) {
System.out.println("hello");
}
synchronized (object) {
System.out.println("hello2");
}
synchronized (object) {
System.out.println("hello3");
}
}
}
// 本来可以放到一起,但我并没有放到一起
锁粗化
JIT 编译器
在执行动态编译时,若发现前后相邻的 synchronized 块使用的是同一个锁对象,那么它就会把这几个 synchronized 块合并为一个较大的同步代码块,这样处理的好处在于线程在执行这些代码时就无须频繁申请和释放锁,而只需要申请和释放一次,从而提升了性能。