Java锁的本质

0 阅读4分钟

Java的内置锁(synchronized)的本质是对象锁,是因为它的实现完全依赖于JVM中每个对象内置的隐藏数据结构(对象头)和关联的监视器(Monitor)机制。

对象头

Java对象头是每个Java对象在内存中开头的一部分固定结构的数据,它是JVM自动为每个对象添加的元数据,用于存储对象的运行时信息。

内存布局

image.png

对象头的主要组成部分:

  1. Mark Word(标记字)
    • 存储对象自身的运行时数据
      • 锁状态
      • 指向锁(监视器)的指针
    • 在32位JVM中占4字节,64位JVM中占8字节
    • 内容会随着对象状态变化而变化
  2. Klass Pointer(类型指针)
    • 指向对象类元数据的指针
    • 在32位JVM和开启指针压缩的64位JVM中占4字节
    • 在未开启指针压缩的64位JVM中占8字节

每一个对象能够通过getClass()来获取类的信息,就是因为Klass pointer指向了方法区对应的类

用处

对象头是JVM高效管理Java对象的基石,它支撑了Java的多个核心特性:

  1. 垃圾回收

    • 通过对象头中的GC年龄(分代年龄)实现分代垃圾回收
    • 标记阶段利用对象头记录对象存活状态
  2. 锁机制

    • synchronized关键字的实现基础
    • 记录锁状态:无锁、偏向锁、轻量级锁、重量级锁
    • 支持锁升级和降级
  3. 存储Hash码

监视器

监视器(Monitor)是Java中实现线程同步的核心机制与模型。在Java中,每个对象(Object)都天生自带一个监视器,它由三部分组成:

  1. 锁(Lock/Mutex) :这是房间的门锁。synchronized关键字就是用来获取这个锁的。一个线程拿到锁,才能进入同步代码块或方法(进入房间)。
  2. 入口等待队列(Entry Set) :当锁被其他线程占用时,想进来的线程会在这个队列里排队等待。
  3. 条件等待队列(Wait Set) :已经进入房间(持有锁)的线程,如果发现运行条件不满足(例如,任务队列为空),可以调用wait()方法主动释放锁,然后进入这个等待队列休眠。直到其他线程调用notify()notifyAll()将其唤醒,移动到入口等待队列,它才有机会重新去竞争锁。

解决并发访问的共享问题

  • 原子性(Atomicity) :通过互斥锁,确保一个线程在执行关键代码段时,不会被其他线程打断,从而保证操作是“一气呵成”的。
  • 可见性(Visibility) :线程在释放锁(退出synchronized块)前,会强制将工作内存中的变量修改刷新到主内存;在获取锁时,会清空工作内存,从主内存重新加载变量。这保证了共享变量的修改对所有线程立即可见。

主内存:可以理解为“共享内存”。它存储了所有线程共享的实例字段、静态字段和构成数组对象的元素。主内存是物理内存的一种抽象。

工作内存:可以理解为线程的“私有缓存”。每个线程都有自己的工作内存,它存储了该线程对主内存共享变量的副本,以及线程私有的局部变量、方法参数等。工作内存是CPU高速缓存和寄存器的抽象。

如果所有线程都直接读写主内存,速度会非常慢。工作内存作为缓存,极大地提升了线程执行速度。由于每个线程都在自己的“工作内存”中操作变量副本,这就导致了一个线程对共享变量的修改,可能无法立即被其他线程看到(可见性问题),并且编译器和处理器可能对指令进行重排序(有序性问题)。JMM通过定义一套规则(如volatilesynchronizedhappens-before规则)来协调工作内存与主内存的交互,从而解决这些问题,为程序员提供清晰的内存可见性保证。

  • 有序性(Ordering)synchronized块内的代码虽然可能发生指令重排序,但由于“一个线程持有锁”的语义,能确保其他线程观察到的执行结果与顺序执行一致。

Java锁

在Java中,每一个对象(每一个new出来的对象,包括Class对象)都与一个内置的、隐式的锁(称为监视器锁Monitor Lock )相关联。

当线程进入一个synchronized修饰的方法或者代码中,它实质上是尝试获取这个synchronized关联的那个对象的监视器锁。如果获取成功,就持有锁并执行代码;如果失败,就会被阻塞,直到锁被释放。