Java的内置锁(synchronized)的本质是对象锁,是因为它的实现完全依赖于JVM中每个对象内置的隐藏数据结构(对象头)和关联的监视器(Monitor)机制。
对象头
Java对象头是每个Java对象在内存中开头的一部分固定结构的数据,它是JVM自动为每个对象添加的元数据,用于存储对象的运行时信息。
内存布局
对象头的主要组成部分:
- Mark Word(标记字)
- 存储对象自身的运行时数据
- 锁状态
- 指向锁(监视器)的指针
- 在32位JVM中占4字节,64位JVM中占8字节
- 内容会随着对象状态变化而变化
- 存储对象自身的运行时数据
- Klass Pointer(类型指针)
- 指向对象类元数据的指针
- 在32位JVM和开启指针压缩的64位JVM中占4字节
- 在未开启指针压缩的64位JVM中占8字节
每一个对象能够通过getClass()来获取类的信息,就是因为Klass pointer指向了方法区对应的类
用处
对象头是JVM高效管理Java对象的基石,它支撑了Java的多个核心特性:
-
垃圾回收
- 通过对象头中的GC年龄(分代年龄)实现分代垃圾回收
- 标记阶段利用对象头记录对象存活状态
-
锁机制
- synchronized关键字的实现基础
- 记录锁状态:无锁、偏向锁、轻量级锁、重量级锁
- 支持锁升级和降级
-
存储Hash码
监视器
监视器(Monitor)是Java中实现线程同步的核心机制与模型。在Java中,每个对象(Object)都天生自带一个监视器,它由三部分组成:
- 锁(Lock/Mutex) :这是房间的门锁。
synchronized关键字就是用来获取这个锁的。一个线程拿到锁,才能进入同步代码块或方法(进入房间)。 - 入口等待队列(Entry Set) :当锁被其他线程占用时,想进来的线程会在这个队列里排队等待。
- 条件等待队列(Wait Set) :已经进入房间(持有锁)的线程,如果发现运行条件不满足(例如,任务队列为空),可以调用
wait()方法主动释放锁,然后进入这个等待队列休眠。直到其他线程调用notify()或notifyAll()将其唤醒,移动到入口等待队列,它才有机会重新去竞争锁。
解决并发访问的共享问题
- 原子性(Atomicity) :通过互斥锁,确保一个线程在执行关键代码段时,不会被其他线程打断,从而保证操作是“一气呵成”的。
- 可见性(Visibility) :线程在释放锁(退出
synchronized块)前,会强制将工作内存中的变量修改刷新到主内存;在获取锁时,会清空工作内存,从主内存重新加载变量。这保证了共享变量的修改对所有线程立即可见。
主内存:可以理解为“共享内存”。它存储了所有线程共享的实例字段、静态字段和构成数组对象的元素。主内存是物理内存的一种抽象。
工作内存:可以理解为线程的“私有缓存”。每个线程都有自己的工作内存,它存储了该线程对主内存共享变量的副本,以及线程私有的局部变量、方法参数等。工作内存是CPU高速缓存和寄存器的抽象。
如果所有线程都直接读写主内存,速度会非常慢。工作内存作为缓存,极大地提升了线程执行速度。由于每个线程都在自己的“工作内存”中操作变量副本,这就导致了一个线程对共享变量的修改,可能无法立即被其他线程看到(可见性问题),并且编译器和处理器可能对指令进行重排序(有序性问题)。JMM通过定义一套规则(如
volatile、synchronized、happens-before规则)来协调工作内存与主内存的交互,从而解决这些问题,为程序员提供清晰的内存可见性保证。
- 有序性(Ordering) :
synchronized块内的代码虽然可能发生指令重排序,但由于“一个线程持有锁”的语义,能确保其他线程观察到的执行结果与顺序执行一致。
Java锁
在Java中,每一个对象(每一个new出来的对象,包括Class对象)都与一个内置的、隐式的锁(称为监视器锁或Monitor Lock )相关联。
当线程进入一个synchronized修饰的方法或者代码中,它实质上是尝试获取这个synchronized关联的那个对象的监视器锁。如果获取成功,就持有锁并执行代码;如果失败,就会被阻塞,直到锁被释放。