序言

闲来无事,记录学习历程,交个朋友。欢迎讨论碰撞,产生好的解决方案!!!👏👏👏

背景

学习一下Java的锁,巩固基础

synchronized 的三种应用方式

1. 同步实例方法

锁是当前实例对象(this)。synchronized修饰的实例方法会公用一把锁,不同对象之间不会影响,非synchronized方法也不会影响。

public synchronized void instanceMethod() {
    // 同步代码块,锁是 this,即调用该方法的对象实例
    // ...
}
2. 同步静态方法

锁是当前类的 Class 对象(例如 MyClass.class)。所有带synchronized的静态方法公用一把锁,不会影响实例方法,非synchronized方法也不会影响。

public static synchronized void staticMethod() {
    // 同步代码块,锁是 MyClass.class
    // ...
}
3. 同步代码块

锁是括号里配置的对象。

public void method() {
    // 前置代码,非线程安全
    
    synchronized (lockObject) { // lockObject 可以是任意对象
        // 临界区代码,线程安全
    }
    
    // 后置代码,非线程安全
}

关键点:

  • 线程在进入同步代码块/方法前必须获得锁,退出时(正常退出或异常退出)必须释放锁。
  • 基于对象的内置锁(Monitor Lock)工作。

字节码分析

package com.gyg.codeing.demo2;

import java.util.concurrent.CountDownLatch;

public class CodeDemo {
    private int count = 0;

    public void add(int num) {
        synchronized (this) {
            count += num;
        }
    }

    public synchronized void add2(int num) {
        count += num;
    }

    public static void main(String[] args) throws InterruptedException {
        CodeDemo codeDemo = new CodeDemo();
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                codeDemo.add(1);
                countDownLatch.countDown();
            }).start();
        }
        countDownLatch.await();
        System.out.println(codeDemo.count);
    }
}

在这里插入图片描述

在这里插入图片描述

synchronized是由monitorenter和 monitorexit一对指令来保障的原子性的,add2因为开启了逃逸分析,导致锁被优化调了。

monitorenter和monitorexit的作用

synchonized 的语义是通过 JVM 中的 monitorenter 和 monitorexit 指令以及对象的 Monitor(监视器锁) 来实现的。

monitorenter   // 尝试获取对象的 Monitor
...            // 临界区代码
monitorexit    // 释放 Monitor

Java对象头结构详解

对象头整体结构

组成部分大小说明
Mark Word64 bits存储对象运行时数据:哈希码、GC年龄、锁状态等
Klass Pointer32 bits指向类元数据的指针(开启指针压缩)
Array Length32 bits仅数组对象存在,存储数组长度

Mark Word详细结构(64位)

通用结构

位范围字段名大小描述
63-25unused25 bits未使用空间
24-22identity_hash31 bits对象标识哈希码
21-18age4 bits对象分代年龄(最大15)
17biased_lock1 bit偏向锁标识位
16-15lock2 bits锁状态标志位
-lock_state-锁状态描述

不同锁状态下的Mark Word结构

无锁状态 (Normal, lock=01)
位范围字段大小值说明
63-39unused25 bits-未使用
38-8identity_hash31 bits-对象哈希码
7-4age4 bits-GC年龄
3biased_lock1 bit0非偏向锁
2-1lock2 bits01无锁状态
-state-Normal无锁
偏向锁状态 (Biased, lock=01)
位范围字段大小值说明
63-10thread | epoch54 bits-线程ID + 时间戳
9-6age4 bits-GC年龄
5biased_lock1 bit1偏向锁
4-3lock2 bits01偏向锁状态
-state-Biased偏向锁
轻量级锁状态 (Lightweight, lock=00)
位范围字段大小值说明
63-2pointer_to_lock_record62 bits-指向栈中锁记录指针
1-0lock2 bits00轻量级锁
-state-Lightweight轻量级锁
重量级锁状态 (Heavyweight, lock=10)
位范围字段大小值说明
63-2pointer_to_monitor62 bits-指向Monitor指针
1-0lock2 bits10重量级锁
-state-Heavyweight重量级锁
GC标记状态 (Marked, lock=11)
位范围字段大小值说明
63-2GC information62 bits-GC相关信息
1-0lock2 bits11GC标记
-state-MarkedGC标记状态

锁状态转换表

当前状态触发条件下一状态
无锁第一个线程访问偏向锁
偏向锁第二个线程竞争轻量级锁
偏向锁重偏向条件满足偏向锁(新线程)
轻量级锁竞争加剧重量级锁
任意状态GC发生GC标记

实战

通过jol可以看待,对象在JVM的一些信息。

<dependency>
    <groupId>org.openjdk.jol</groupId>
    <artifactId>jol-core</artifactId>
    <version>0.16</version>
</dependency>

通过模拟枪锁的力度,来展示对象头锁的变化

package com.gyg.codeing.demo2;

import org.openjdk.jol.info.ClassLayout;
import org.openjdk.jol.vm.VM;

import java.util.concurrent.CountDownLatch;

public class CodeDemoAsync {
    private static int count = 0;

    public static void main(String[] args) throws InterruptedException {
        // 打印 JVM 详细信息
        printJVM();
        Thread.sleep(5000);
        // 1. 查看无锁状态的对象头
        noLock(new CodeDemoAsync());
        Thread.sleep(5000);
        // 2. 查看偏向锁状态(需要延迟一下让偏向锁生效)
        biasedLock(new CodeDemoAsync());
        Thread.sleep(5000);
        // 3. 查看轻量级锁状态
        lightLock(new CodeDemoAsync());
        Thread.sleep(5000);
        // 4. 查看重量级锁状态
        heavyLock(new CodeDemoAsync());
        Thread.sleep(5000);
    }

    private static void printJVM() {
        System.out.println(VM.current().details());
    }

    private static void noLock(CodeDemoAsync demo) {
        System.out.println("=== 无锁状态 ===");
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
    }

    private static void biasedLock(CodeDemoAsync demo) {
        synchronized (demo) {
            System.out.println("=== 偏向锁状态 ===");
            System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        }
    }

    private static void lightLock(CodeDemoAsync demo) throws InterruptedException {
        Thread t1 = new Thread(() -> {
            synchronized (demo) {
                System.out.println("=== 轻量级锁状态 (线程1) ===");
                System.out.println(ClassLayout.parseInstance(demo).toPrintable());
            }
        });

        Thread t2 = new Thread(() -> {
            synchronized (demo) {
                System.out.println("=== 轻量级锁状态 (线程2) ===");
                System.out.println(ClassLayout.parseInstance(demo).toPrintable());
            }
        });
        t1.start();
        t1.join(); // 等待t1执行完毕
        t2.start();
        t2.join(); // 等待t2执行完毕
    }

    private static void heavyLock(CodeDemoAsync demo) throws InterruptedException {
        System.out.println("=== 重量级锁状态 (多线程竞争) ===");
        CountDownLatch countDownLatch = new CountDownLatch(1000);
        for (int i = 0; i < 1000; i++) {
            new Thread(() -> {
                synchronized (demo) {
                    count += 1;
                }
                countDownLatch.countDown();
            }).start();
        }
        System.out.println(ClassLayout.parseInstance(demo).toPrintable());
        countDownLatch.await();
    }
}

执行分析

E:\java\jdk\jdk-21.0.8\bin\java.exe "-javaagent:E:\software\IntelliJ IDEA 2025.2.1\lib\idea_rt.jar=59249" -Dfile.encoding=UTF-8 -Dsun.stdout.encoding=UTF-8 -Dsun.stderr.encoding=UTF-8 -classpath E:\java\project\demo\codeing\target\classes;E:\java\maven\repository\org\openjdk\jol\jol-core\0.16\jol-core-0.16.jar com.gyg.codeing.demo2.CodeDemoAsync
# WARNING: Unable to get Instrumentation. Dynamic Attach failed. You may add this JAR as -javaagent manually, or supply -Djdk.attach.allowAttachSelf
# WARNING: Unable to attach Serviceability Agent. sun.jvm.hotspot.memory.Universe.getNarrowOopBase()
# Running 64-bit HotSpot VM.
# Using compressed oop with 3-bit shift.
# Using compressed klass with 3-bit shift.
# WARNING | Compressed references base/shifts are guessed by the experiment!
# WARNING | Therefore, computed addresses are just guesses, and ARE NOT RELIABLE.
# WARNING | Make sure to attach Serviceability Agent to get the reliable addresses.
# Objects are 8 bytes aligned.
# Field sizes by type: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]
# Array element sizes: 4, 1, 1, 2, 2, 4, 4, 8, 8 [bytes]

=== 无锁状态 ===
com.gyg.codeing.demo2.CodeDemoAsync object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x0000000000000001 (non-biasable; age: 0)
  8   4        (object header: class)    0x01003800
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=== 偏向锁状态 ===
com.gyg.codeing.demo2.CodeDemoAsync object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000743e2ff5a8 (thin lock: 0x000000743e2ff5a8)
  8   4        (object header: class)    0x01003800
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=== 轻量级锁状态 (线程1) ===
com.gyg.codeing.demo2.CodeDemoAsync object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000743f2feeb8 (thin lock: 0x000000743f2feeb8)
  8   4        (object header: class)    0x01003800
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=== 轻量级锁状态 (线程2) ===
com.gyg.codeing.demo2.CodeDemoAsync object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x000000743f4ff128 (thin lock: 0x000000743f4ff128)
  8   4        (object header: class)    0x01003800
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total

=== 重量级锁状态 (多线程竞争) ===
com.gyg.codeing.demo2.CodeDemoAsync object internals:
OFF  SZ   TYPE DESCRIPTION               VALUE
  0   8        (object header: mark)     0x00000229ce8bb682 (fat lock: 0x00000229ce8bb682)
  8   4        (object header: class)    0x01003800
 12   4        (object alignment gap)    
Instance size: 16 bytes
Space losses: 0 bytes internal + 4 bytes external = 4 bytes total


Process finished with exit code 0

关键观察点

  1. 锁状态位: 查看最后2-3位,标识锁状态
    • 01: 无锁/偏向锁
    • 00: 轻量级锁
    • 10: 重量级锁
  2. 偏向锁位: 查看倒数第三位,标识是否偏向
  3. 线程ID: 在偏向锁状态下,可以看到持有锁的线程ID

观察结果

无锁,最后是 001

(object header: mark) 0x0000000000000001 (non-biasable; age: 0)

偏向锁

因为jdk21已经移除了,直接用轻量级锁了

轻量级锁,最后是 000

(object header: mark) 0x000000743f4ff128 (thin lock: 0x000000743f4ff128)

二进制:0000 0000 0000 0000 0000 0000 0111 0100 0011 1111 0100 1111 1111 0001 0010 1000

重量级锁,最后是 010

0x00000229ce8bb682 (fat lock: 0x00000229ce8bb682)

二进制:0000 0000 0000 0000 0000 0010 0010 1001 1100 1110 1000 1011 1011 0110 1000 0010

底层分析

下面我们分步骤详细说明:

  1. JVM字节码指令:当Java代码中使用了synchronized关键字时,编译后的字节码中会出现monitorenter和monitorexit指令。
  2. 解释执行:在解释执行时,JVM会解释这些指令。例如,在HotSpot VM中,有一个解释器,它针对每个字节码指令都有一个对应的处理代码。当遇到monitorenter时,会调用相应的例程来获取锁。
  3. JIT编译:当方法被JIT编译时,这些字节码指令会被编译成本地机器代码。在编译过程中,monitorenter和monitorexit会被替换成更底层的锁操作。
  4. 锁的实现:在底层,锁的实现通常依赖于操作系统的同步机制(如Linux下的futex)和CPU提供的原子操作(如CAS指令)。

具体来说,在HotSpot VM中,锁的实现有以下几种情况:

  • 偏向锁:在无竞争的情况下,整个同步周期内只有一个线程使用锁,那么会使用偏向锁,避免多次的CAS操作。
  • 轻量级锁:当有多个线程交替使用锁,但不存在同时竞争时,会使用轻量级锁,通过CAS操作来尝试获取锁。
  • 重量级锁:当有多个线程同时竞争锁时,锁会升级为重量级锁,此时会使用操作系统的互斥量(mutex)来实现,导致线程阻塞和唤醒。

在重量级锁的情况下,会使用到操作系统的同步原语,例如在Linux中,会使用pthread_mutex_lock和pthread_mutex_unlock。这些操作最终会通过系统调用进入内核,由内核来调度线程。

  1. CPU的参与:CPU提供了原子指令,如CAS(Compare-And-Swap),用于实现轻量级锁和偏向锁。在x86架构中,CAS指令是cmpxchg,它能够原子地比较并交换某个位置的值。如果多个线程同时执行CAS,只有一个会成功,其他的会失败。失败的线程会通过自旋(循环重试)或者进入阻塞状态(重量级锁)来等待。
  2. 内存屏障:为了保证可见性和有序性,锁操作还会插入内存屏障(memory barrier)。例如,在monitorenter和monitorexit对应的机器指令中,会加入适当的内存屏障,确保临界区内的读写操作不会重排序到临界区之外,并且保证锁释放时,临界区内的修改对下一个获取锁的线程可见。
  3. 具体步骤(以重量级锁为例):
    • 当线程执行monitorenter时,如果锁对象处于未锁定状态,JVM会先尝试使用CAS操作将锁对象的标记字段设置为指向当前线程的锁记录。如果成功,则获取锁。
    • 如果CAS失败,表示有竞争,JVM会膨胀为重量级锁。重量级锁会使用操作系统的互斥量,将线程阻塞,并放入等待队列。
    • 当锁持有线程执行monitorexit时,会唤醒等待队列中的线程,被唤醒的线程会重新尝试获取锁。
  4. 与操作系统的交互:在重量级锁中,线程的阻塞和唤醒是通过操作系统内核的调度来完成的。这涉及到用户态和内核态的切换,成本较高。

总结:monitorenter和monitorexit指令在CPU层面的合作,是通过JVM将它们翻译成包含原子指令(如CAS)和操作系统同步原语(如互斥量)的代码来实现的。同时,为了内存可见性和顺序性,还会使用内存屏障。

注意:不同的JVM实现和不同的硬件平台,其具体实现细节可能会有所不同。上述描述主要针对HotSpot VM和常见的x86架构。

尾声

如果解决有解决你的问题,点点赞,求个关注👍👍👍
天生我材必有用,千金散尽还复来🤞🤞🤞,加油👏👏👏

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐