在 Java 中,使用 synchronized
关键字对类加锁时,锁的信息存储在 Java 虚拟机(JVM)内部。具体来说,类锁是与类的 Class
对象相关联的。下面详细介绍类锁的实现原理和存储位置。
1. 锁的类型
在 Java 中,锁可以分为两种类型:
- 实例锁:用于对象实例,锁定的是实例的监视器(monitor)。
- 类锁:用于整个类,锁定的是与该类相关的
Class
对象。
2. 类锁的存储
Class
对象:每个类在 JVM 加载时会生成一个唯一的Class
对象。这个对象包含了该类的结构信息(如字段、方法、超类、接口等)以及与该类相关的监视器。- 监视器(Monitor):当使用
synchronized
关键字锁定一个类时,JVM 会使用该类的Class
对象的监视器进行同步控制。这是一个底层的同步机制,确保只有一个线程可以执行被synchronized
关键字修饰的代码块或方法。
3. 类锁的实现原理
- 当一个线程尝试访问
synchronized
修饰的类方法或块时,它首先需要获取类的监视器锁。 - 如果监视器锁被其他线程持有,当前线程会被阻塞,直到持有锁的线程释放锁。
- 一旦线程获得了锁,它就可以安全地执行被
synchronized
修饰的代码,保证在同一时间只有一个线程能够访问这个代码块。
4. 内存中的结构
在 JVM 内部,类锁的信息是与 Class
对象的监视器关联的。监视器的实现通常是通过一种叫做“监视器锁”的结构来维护的,可能涉及以下信息:
- 锁状态:表示锁的持有者、锁的重入计数等。
- 等待线程列表:存储因未能获取锁而被阻塞的线程的队列。
5. 总结
- 锁信息存储位置:类锁的信息存储在与类相关的
Class
对象中,具体由 JVM 内部的监视器机制管理。 - 监视器的作用:监视器保证了对类级别资源的线程安全访问,确保在同一时间只有一个线程能执行被锁定的代码。
通过这种机制,Java 能够有效地管理多线程环境中的同步问题,提供了一种相对简单而强大的方式来实现线程安全。
1. 对象锁
存储位置
- 对象头:每个对象在 Java 堆中都有一个对象头,包含了对象的元数据,包括对象的哈希码、GC 信息和锁状态等。对象头是在堆内存中分配的。
信息
- 锁信息:对象头中的锁信息用于表示该对象的监视器状态(monitor state)。在对象被锁定时,锁的状态会记录在对象头中,包括:
- 锁的状态(无锁、偏向锁、轻量锁、重锁等)。
- 持有锁的线程 ID。
- 锁的重入计数(在重入的情况下)。
2. 类锁
存储位置
- 类对象:每个类在 JVM 加载时会生成一个
Class
对象,类对象也是在 Java 堆内存中分配的。该对象代表类的结构信息,包括方法、字段、父类信息等。
信息
- 锁信息:类锁的信息存储在类对象的监视器中,通常包括:
- 锁状态(是否被锁定,持有锁的线程等)。
- 等待获取锁的线程队列。
3. 存储位置与信息的区别
存储位置
- 对象锁:存储在特定对象的对象头中,通常与对象的内存布局密切相关。
- 类锁:存储在类对象(
Class
对象)的监视器中,类对象在堆中也是一个对象,但它代表的是类的结构和信息。
信息内容
- 对象锁信息:包含关于具体实例的锁状态(如偏向锁、轻量级锁、重锁等),以及持有锁的线程信息和重入计数。
- 类锁信息:包含关于类的锁状态(是否被锁定)、持有锁的线程以及等待线程的信息。
4. 总结
- 对象锁:存在于每个实例的对象头中,控制对实例的访问,是对象级别的锁。主要用于保护实例变量。
- 类锁:存在于类对象的监视器中,控制对类的访问,是类级别的锁。主要用于保护类变量或静态资源。
两者之间的主要区别在于锁的粒度和控制范围。对象锁针对具体实例,而类锁则针对整个类。这种设计允许开发者在需要时选择合适的锁策略,以实现更高效的并发控制。
对象头是 Java 中每个对象的一个重要组成部分,存储了对象的元数据以及锁的相关信息。下面将详细介绍对象头中的信息内容、锁状态的实现、可重入锁的机制等。
1. 对象头的结构
在 Java 中,对象头通常由以下几个部分组成:
1.1. Mark Word
- Mark Word:用于存储对象的哈希码、GC 状态、锁状态和其他信息。根据不同的锁状态,Mark Word 的内容会有所变化。具体结构可能因 JVM 的实现而略有不同,但通常包含以下信息:
- 哈希码:当对象被调用
hashCode()
方法时生成的值,首次调用时可能会延迟计算并存储。 - GC 状态:用于标识对象的垃圾回收状态。
- 锁状态:用于指示对象的锁状态。锁状态可以是:
- 无锁(0)
- 偏向锁(1)
- 轻量级锁(2)
- 重锁(3)
- 持有锁的线程 ID:在锁被持有时,会记录持有该锁的线程 ID。
- 哈希码:当对象被调用
1.2. 类型指针
- 类型指针:指向对象的类元数据,包含该对象的类型信息。它指向对象的
Class
对象,使得 JVM 能够知道该对象的类是什么。
2. 锁状态的实现
锁状态是通过 Mark Word 中的部分字段来实现的,具体过程如下:
- 无锁状态:在对象头中,Mark Word 表示该对象未被锁定。此时,任何线程都可以访问该对象。
- 偏向锁:当一个线程第一次访问对象时,JVM 会将该对象的 Mark Word 改为偏向锁状态,并将该线程的 ID 存储在 Mark Word 中,以便后续访问同一线程时无需获取锁。如果其他线程尝试获取偏向锁,JVM 会撤销偏向锁,转为轻量级锁。
- 轻量级锁:当偏向锁被其他线程竞争时,JVM 会将对象转换为轻量级锁。在这种状态下,线程在栈帧中保存锁的状态,通过原子操作尝试获取锁。如果成功,Mark Word 将被更新为轻量级锁的状态。如果失败,线程会进入自旋状态,反复尝试获取锁,直到成功或锁升级为重锁。
- 重锁:当轻量级锁无法满足并发访问时,JVM 会将对象转为重锁,此时需要使用操作系统的互斥量(mutex)来实现锁的获取和释放。重锁是最传统的锁机制,确保在同一时间只有一个线程能访问被锁定的代码。
3. 可重入锁的实现
可重入锁是指同一线程可以多次获得同一个锁,而不会导致死锁。Java 中的可重入锁通常通过以下机制实现:
- 重入计数:在对象头的 Mark Word 中,存储了一个重入计数器,当一个线程第一次获得锁时,计数器设为 1;每当该线程再次获得锁时,计数器加 1;当该线程释放锁时,计数器减 1。当计数器减至 0 时,锁才真正被释放。
- 持有线程 ID:在 Mark Word 中,除了锁状态之外,还记录了持有锁的线程 ID。这样,JVM 能够判断当前请求锁的线程是否是持有锁的线程。如果是持有线程,JVM 将允许其再次进入临界区。
4. 总结
- 对象头包含重要的元数据,包括 Mark Word 和类型指针。Mark Word 存储锁状态、哈希码、GC 状态等信息。
- 锁状态的实现通过 Mark Word 中的字段变化来反映对象的锁状态,支持无锁、偏向锁、轻量级锁和重锁的转换。
- 可重入锁通过重入计数和持有线程 ID 实现,允许同一线程多次获得同一锁而不会导致死锁,从而提高了多线程环境中的灵活性和安全性。
这种机制使得 Java 在多线程环境下能够高效地管理资源,并确保线程安全。