目录
基本使用
底层实现
synchronized锁升级
对象的内存结构
ⅰ. 对象头
1. ① 运行时元数据 (Mark Word) (占64位)
a. 哈希值 (HashCode)
b. GC分代年龄
c. 锁状态标记
2. ② 类型指针: (Klass Point) (占 32位)
ⅱ. 实例数据
ⅲ. 对齐填充
Moniter重量级锁
轻量级锁
偏向锁
基本使用
Java中的synchronized
关键字主要用于实现线程同步,确保在多线程环境下同一时间只有一个线程可以访问被它保护的代码块或方法。以下是synchronized
的基本使用方式:
1.修饰非静态方法:
public class MyClass {
private int count;
// 一个同步实例方法
public synchronized void increment() {
count++;
}
}
在这个例子中,synchronized
关键字修饰了increment()
方法。这意味着任何时刻只有一个线程可以执行该方法,其他试图同时访问此方法的线程将会阻塞,直到当前线程完成方法调用并释放锁。
2.修饰静态方法:
public class MyClass {
private static int staticCount;
// 一个同步静态方法
public static synchronized void incrementStatic() {
staticCount++;
}
}
当synchronized
修饰静态方法时,它锁定的是类对象(即Class级别的锁),因此,在多线程环境中,所有对该类静态方法的访问将被串行化。
3.修饰代码块:
public class MyClass {
private final Object lock = new Object();
private int instanceCount;
public void incrementInstanceCount() {
// 同步代码块
synchronized (lock) {
instanceCount++;
}
}
}
这里synchronized
用于一个代码块,它可以根据需要指定一个特定的对象作为锁对象(这里是lock
对象)。只有获取到这个对象锁的线程才能进入并执行同步代码块内的内容。
底层实现
javap -v xx.class 查看class字节码信息
synchronized
关键字在Java中的底层原理主要涉及操作系统层面的互斥锁(Monitor)机制,以及JVM内部的实现细节
JVM使用monitorenter和monitorexit指令来实现对监视器的操作。
- 当执行到monitorenter指令时,如果对象没有被锁定或者当前线程已经持有该对象的锁,则将锁计数器加1,并允许线程继续执行同步代码块。
- 当执行到monitorexit指令时,锁计数器减1;当计数器为0时,表示锁被释放,其他等待该锁的线程可以有机会竞争获取锁并执行相应代码。
Monitor 被翻译为监视器,是由jvm提供,c++语言实现
Owner:存储当前获取锁的线程的,只能有一个线程可以获取
EntryList:关联没有抢到锁的线程,处于Blocked状态的线程
WaitSet:关联调用了wait方法的线程,处于Waiting状态的线程
synchronized锁升级
Monitor实现的锁属于重量级锁,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低。
synchronized
锁升级是Java 1.6及以后版本引入的一种优化机制,目的是为了在多线程环境下更好地平衡锁的性能和资源消耗。在早期版本中,Java对于synchronized
关键字的实现是直接使用重量级锁(即操作系统互斥量Mutex),这种方式虽然能够确保线程安全,但其开销较大,尤其是在锁竞争不激烈的情况下。
在HotSpot虚拟机中,对象在内存中存储的布局可分为3块区域:对象头(Header)、实例数据(Instance Data)和对齐填充
对象的内存结构
ⅰ. 对象头
1. ① 运行时元数据 (Mark Word) (占64位)
a. 哈希值 (HashCode)
31位的对象标识,采用延迟加载技术,调用 System.identityHashCode() 计算得到,并将结果写到对象头中,用于栈空间中,对对象的引用的指向,不然是无法找到堆中的对象的
b. GC分代年龄
(占用4位) 记录着幸存者区对象被GC过后的age, 一般age为15, 如果对象的gc次数到达了这个值,将进入老年代
c. 锁状态标记
记录加锁的一些信息
- hashcode:25位的对象标识Hash码
- age:对象分代年龄占4位
- biased_lock:偏向锁标识,占1位 ,0表示没有开始偏向锁,1表示开启了偏向锁
- thread:持有偏向锁的线程ID,占23位
- epoch:偏向时间戳,占2位
- ptr_to_lock_record:轻量级锁状态下,指向栈中锁记录的指针,占30位
- ptr_to_heavyweight_monitor:重量级锁状态下,指向对象监视器Monitor的指针,占30位
2. ② 类型指针: (Klass Point) (占 32位)
是对方法区中,类的元信息的引用
ⅱ. 实例数据
真实的记录一个对象包含的数据,比如Cat类对象 ,里面包含 name,age 等等相关的信息,如果有字符串,要引用字符串常量池
ⅲ. 对齐填充
仅起到占位符的作用 ,因为 HotSpot虚拟机要求对象的起始地址必须是8字节的整数,如果达不到,用对齐添充的方式补齐,为什么是8呢,因为64位的机器能被8整除,效率高
Moniter重量级锁
每个 Java 对象都可以关联一个 Monitor 对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word 中就被设置指向 Monitor 对象的指针
轻量级锁
在很多的情况下,在Java程序运行时,同步块中的代码都是不存在竞争的,不同的线程交替的执行同步块中的代码。这种情况下,用重量级锁是没必要的。因此JVM引入了轻量级锁的概念。
加锁流程
1.在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
2.通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
3.如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为null,起到了一个重入计数器的作用。
4.如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
1.遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record。
2.如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
3.如果Lock Record的 Mark Word不为null,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁
轻量级锁在没有竞争时(就自己这个线程),每次重入仍然需要执行 CAS 操作。 Java 6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现 这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有