*/
public synchronized void methodLock() {
System.out.println(Thread.currentThread().getName());
}
/**
- 在代码块上加关键字,锁住当前实例
*/
public void codeBlockLock() {
synchronized (this) {
System.out.println(Thread.currentThread().getName());
}
}
/**
- 在代码块上加关键字,锁住一个变量
*/
public void codeBlockLock2() {
synchronized (lock) {
System.out.println(Thread.currentThread().getName());
}
}
}
依靠 JVM 中的 monitorenter 和 monitorexit 指令控制。通过 javap -v
命令可以看到前面的实例代码中对 synchronized 关键字在字节码层面的处理,对于在代码块上加 synchronized 关键字的情况,会通过 monitorenter
和monitorexit
指令来表示同步的开始和退出标识。而在方法上加关键字的情况,会用 ACC_SYNCHRONIZED
作为方法标识,这是一种隐式形式,底层原理都是一样的。
public synchronized void methodLock();
descriptor: ()V
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
Code:
stack=2, locals=1, args_size=1
0: getstatic #2 // Field java/lang/System.out:Ljava/io/PrintStream;
3: invokestatic #3 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
6: invokevirtual #4 // Method java/lang/Thread.getName:()Ljava/lang/String;
9: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
12: return
LineNumberTable:
line 12: 0
line 13: 12
public void codeBlockLock();
descriptor: ()V
flags: ACC_PUBLIC
Code:
stack=2, locals=3, args_size=1
0: aload_0
1: dup
2: astore_1
3: monitorenter #
4: getstatic #2 // Field
《一线大厂Java面试题解析+后端开发学习笔记+最新架构讲解视频+实战项目源码讲义》
【docs.qq.com/doc/DSmxTbFJ1cmN1R2dB】 完整内容开源分享
java/lang/System.out:Ljava/io/PrintStream;
7: invokestatic #3 // Method java/lang/Thread.currentThread:()Ljava/lang/Thread;
10: invokevirtual #4 // Method java/lang/Thread.getName:()Ljava/lang/String;
13: invokevirtual #5 // Method java/io/PrintStream.println:(Ljava/lang/String;)V
16: aload_1
17: monitorexit
18: goto 26
21: astore_2
22: aload_1
23: monitorexit
24: aload_2
25: athrow
26: return
对象布局
为什么介绍 synchronized 要说到对象头呢,这和它的锁升级过程有关系,具体的锁升级过程稍后会讲到,作为锁升级过程的数据支撑,必须要掌握对象头的结构才能了解锁升级的完整过程。
在 Java 中,任何的对象实例的内存布局都分为对象头、对象实例数据和对齐填充数据三个部分,其中对象头又包括 MarkWord 和 类型指针。
**对象实例数据:**这部分就是对象的实际数据。
**对齐填充:**因为 HotSpot 虚拟机内存管理要求对象的大小必须是8字节的整数倍,而对象头正好是8个字节的整数倍,但是实例数据不一定,所以需要对齐填充补全。
对象头:
*Klass 指针:*对象头中的 Klass 指针是用来指向对象所属类型的,一个类实例究竟属于哪个类,需要有地方记录,就在这里记。
*MarkWord:*还有一部分就是和 synchronized 紧密相关的 MarkWord 了,主要用来存储对象自身的运行时数据,如hashcode、gc 分代年龄等信息。 MarkWord 的位长度为 JVM 的一个 Word 大小,32位 JVM 的大小为32位,64位JVM的大小为64位。
下图是 64 位虚拟机下的 MarkWord 结构说明,根据对象锁状态不同,某些比特位代表的含义会动态的变化,之所以要这么设计,是因为不想让对象头占用过大的空间,如果为每一个标示都分配固定的空间,那对象头占用的空间将会比较大。
*数组长度:*要说明一下,如果是数组对象的话, 由于数组无法通过本身内容求得自身长度,所以需要在对象头中记录数组的长度。
源码中的定义
追根溯源,对象在 JVM 中是怎么定义的呢?打开 JVM 源码,找到其中对象的定义文件,可以看到关于前面说的对象头的定义。
class oopDesc {
friend class VMStructs;
friend class JVMCIVMStructs;
private:
volatile markOop _mark;
union _metadata {
Klass* _klass;
narrowKlass _compressed_klass;
} _metadata;
}
oop 是对象的基础类定义,也就是或 Java 中的 Object 类的定义其实就是用的 oop,而任何类都由 Object 继承而来。oopDesc 只是 oop 的一个别名而已。
可以看到里面有关于 Klass 的声明,还有 markOop 的声明,这个 markOop 就是对应上面说到的 MarkWord。
class markOopDesc: public oopDesc {
private:
// Conversion
uintptr_t value() const { return (uintptr_t) this; }
public:
// Constants
enum { age_bits = 4, //分代年龄
lock_bits = 2, //锁标志位
biased_lock_bits = 1, //偏向锁标记
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
}
以上代码只是截取了其中一部分,可以看到其中有关于分代年龄、锁标志位、偏向锁的定义。
虽然源码咱也看不太懂,但是当我看到它们的时候,恍惚之间,内心会感叹到,原来如此。有种宇宙之间,已尽在我掌控之中的感觉。过两天才发现,原来只是一种心理安慰。但是,已经不重要了。
提示
如果你有兴趣翻源码看看,这部分的定义在 /src/hotspot/share/oops
目录下,能告诉你的就这么多了。
锁升级
JDK 1.6 之后,对 synchronized 做了优化,主要就是 CAS 自旋、锁消除、锁膨胀、轻量级锁、偏向锁等,这些技术都是为了在线程之间更高效地共享数据及解决竞争问题,从而提高程序的执行效率,进而产生了一套锁升级的规则。
synchronized 的锁升级过程是通过动态改变对象 MarkWord 各个标志位来表示当前的锁状态的,那修改的是哪个对象的 MarkWord 呢,看上面的代码中,synchronized 关键字是加在 lock 变量上的,那就会控制 lock 的 MarkWord。如果是 synchronized(this)
或者在方法上加关键字,那控制的就是当前实例对象的 MarkWord。
synchronized 的核心准则概括起来大概是这个样子。
- 能不加锁就不加锁。
- 能偏向就尽量偏向。
- 能加轻量级锁就不用重量级锁。
无锁转向偏向锁
偏向锁的意思是说,这个锁会偏向于第一个获得它的线程,如果在接下来的执行过程中,该锁一直没有被其他的线程获取,则持有偏向锁的线程将永远不需要再进行同步。
当线程尝试获取锁对象的时候,先检查 MarkWord 中的线程ID 是否为空。如果为空,则虚拟机会将 MarkWord 中的偏向标记设置为 1,锁标记位为 01。同时,使用 CAS 操作尝试将线程ID记录到 MarkWord 中,如果 CAS 操作成功,那之后这个持有偏向锁的线程再次进入相关同步块的时候,将不需要再进行任何的同步操作。
如果检查线程ID不为空,并且不为当前线程ID,或者进行 CAS 操作设置线程ID失败的情况下,都要撤销偏向状态,这时候就要升级为偏向锁了。
偏向锁升级到轻量级锁
当多个线程竞争锁时,偏向锁会向轻量级锁状态升级。
首先,线程尝试获取锁的时候,先检查锁标志为是否为 01 状态,也就是未锁定状态。
如果是未锁定状态,那就在当前线程的栈帧中建立一个锁记录(Lock Record)区域,这个区域存储 MarkWord 的拷贝。
之后,尝试用 CAS 操作将 MarkWord 更新为指向锁记录的指针(就是上一步在线程栈帧中的 MarkWord 拷贝),如果 CAS 更新成功了,那偏向锁正式升级为轻量级锁,锁标志为变为 00。
如果 CAS 更新失败了,那检查 MarkWord 是否已经指向了当前线程的锁记录,如果已经指向自己,那表示已经获取了锁,否则,轻量级锁要膨胀为重量级锁。
的指针(就是上一步在线程栈帧中的 MarkWord 拷贝),如果 CAS 更新成功了,那偏向锁正式升级为轻量级锁,锁标志为变为 00。
[外链图片转存中…(img-jDSQjsrH-1638864271998)]
如果 CAS 更新失败了,那检查 MarkWord 是否已经指向了当前线程的锁记录,如果已经指向自己,那表示已经获取了锁,否则,轻量级锁要膨胀为重量级锁。