synchronized的用法
阅读提示
建议先看题目目录,再按“概念 -> 原理 -> 场景 -> 优化”顺序复习。
每题先讲结论,再补关键机制和项目实践,回答会更稳。
1、synchronized的用法
1. 修饰实例方法
当synchronized用于修饰一个实例方法时,该方法被称为同步方法。这意味着在同一时刻,只有一个线程可以进入该方法的代码块。这种同步是基于对象的实例锁,即每个对象都有一个锁,当线程进入一个对象的同步方法时,它必须先获得该对象的锁。如果另一个线程已经持有该对象的锁,则该线程必须等待,直到锁被释放。
public synchronized void syncMethod() {
// method body
}2. 修饰静态方法
修饰静态方法时,同步是基于类对象(Class对象)的锁。这意味着所有实例共享同一把锁。当一个线程进入一个静态的同步方法时,它获得的是该类的锁。如果其他线程已经持有该类的锁,则新来的线程必须等待。
public static synchronized void staticSyncMethod() {
// method body
}3. 修饰代码块
当synchronized用于修饰一个代码块时,它允许你指定一个锁对象。这种方式提供了更细粒度的控制,因为它允许你只同步访问特定资源和数据部分的代码。代码块中的代码在执行时必须持有指定的锁对象。
public void someMethod() {
Object lock = new Object();
synchronized(lock) {
// code block
}
}2、锁升级
(1)、内存布局 (对象的内存布局)

在 Java 中,每个对象的内存布局是由 Java 虚拟机(JVM)的具体实现决定的,例如 Oracle HotSpot JVM。这里,我们可以讨论 HotSpot JVM 中普通(非数组)对象的典型内存布局。对象的内存布局通常包括三部分:对象头(Header)、实例数据(Instance Data)和对齐填充(Padding)。
1. 对象头(Object Header)
对象头包含了对象自身运行时的一些必要信息,包括两部分:标记字(Mark Word)和类型指针。
- Mark Word:这部分通常占用 8 字节,用于存储对象自身的运行时数据,如哈希码、锁信息(锁状态标记、线程持有的锁等)、垃圾收集信息(如年龄、是否存活等)。
- Class Pointer:这部分通常也占用 4 字节(默认情况下)
2. 实例数据(Instance Data)
实例数据部分占用的字节数取决于对象的具体字段。Java 数据类型的字节如下:
boolean和byte:1 字节char和short:2 字节int和float:4 字节long和double:8 字节- 引用类型(如对象引用):在 64 位 JVM 中通常是 4 字节(使用压缩指针时)或 8 字节。
字段的排列可能会由 JVM 进行优化,以减少由于内存对齐引起的空间浪费。
3. 对齐填充(Padding)
为了使对象的总大小是 8 字节的倍数(这是大多数现代计算机系统内存寻址的最小单位),JVM 可能会在实例数据之后添加一些填充字节。这样做是为了确保内存对齐,有助于提高 CPU 缓存的效率。
示例
考虑一个简单的 Java 对象:
public class Example {
int a; // 4 字节
boolean b; // 1 字节
}在这个例子中,对象的内存布局可能如下:
- 对象头:12 字节(压缩类指针)或 16 字节(非压缩)
- 实例数据:
int4 字节 +boolean1 字节 = 5 字节 - 对齐填充:为了使总大小为 8 字节的倍数,可能需要添加 3 字节的填充。
这样,总的大小可能是 20 字节(12+5+3,使用压缩指针的情况)或 24 字节(16+5+3,未使用压缩指针)。
Java 示例:简单的 Person 类
考虑下面的 Java 类 Person,它有两个字段:name 和 age。此示例将帮助我们理解类型指针在不涉及继承的情况下如何工作。
public class Person {
private String name;
private int age;
public Person(String name, int age) {
this.name = name;
this.age = age;
}
public void printInfo() {
System.out.println("Name: " + name + ", Age: " + age);
}
public static void main(String[] args) {
Person person = new Person("John Doe", 30);
person.printInfo(); // 输出: Name: John Doe, Age: 30
}
}类型指针的作用
当我们创建 Person 类的一个实例时:
对象创建:JVM 在堆内存中为
Person对象分配空间。这块空间包括对象头、实例数据(name 和 age),以及可能的一些填充字节来满足内存对齐要求。对象头:对象头包含了几个重要的组成部分,其中关键的部分是类型指针。
类型指针:这个指针指向
Person类的元数据。该元数据包含了有关Person类的各种信息,如字段布局、方法表、类加载器信息等。使用类型指针:
当调用
person.printInfo()方法时,即使这个例子没有显示继承和多态,类型指针仍然起到关键作用。JVM 使用类型指针查找到Person类的元数据,进而定位到printInfo()方法的实际代码。这个查找过程确保了即使在类的代码在内存中移动或通过热部署改变时,方法调用也总是正确的,因为类型指针总是指向当前有效的类定义。
(2)、markword 里面具体存的是什么东西

3、synchronized早期为什么叫做重量级锁
在JDK早期 synchronized被称之为重量级锁,是因为我的JVM也是在用户态,有线程向jvm申请锁的时候,那我们JVM需要向操作系统申请锁,那这个过程涉及到从用户态切换到内核态,再从内核态切换到用户态,所以称synchronized为重量级锁。
比较底层的实现(操作系统层面)
当一个 Java 线程尝试进入一个由 synchronized 关键字保护的同步块或方法时,它首先需要获得锁。如果锁已被另一个线程持有,尝试获取锁的线程需要等待。在 Java 的早期实现中,这种等待是通过操作系统的互斥量(Mutex)实现的。这意味着以下几个步骤:
- 锁请求:当线程无法立即获取锁时,JVM 会在用户态下发出一个系统调用,请求操作系统的互斥量。
- 状态切换:执行系统调用(如请求互斥量)涉及从用户态切换到内核态,因为互斥量的管理是在操作系统内核中处理的。
- 等待和唤醒:线程在内核控制的等待队列中阻塞,直到锁变为可用。当锁释放时,操作系统负责唤醒等待的线程,这通常再次涉及从内核态回到用户态的切换。
4、锁升级过程
随着 Java 平台的发展,特别是从 JDK 6 开始,对 synchronized 锁的实现进行了优化。引入了如轻量级锁和偏向锁等技术,使得许多锁操作现在可以完全在用户态下完成,避免了频繁的模式切换。当锁竞争不激烈时,这些优化可以显著减少锁的开销。
在Java中,synchronized关键字实现的锁机制涉及一个从无锁状态到偏向锁、轻量级锁和最终可能的重量级锁的升级过程。这个机制极大地提高了锁的效率,尤其是在不同的并发场景中。下面,我们将更详细地讨论每个锁状态的内部工作机制和对象头中的标记位。
无锁状态
- 锁状态: 当没有线程尝试获取对象的锁时,对象处于无锁状态。无锁并不意味着对象不能被锁定,而是表明当前没有锁定行为。
- 标记位: 在无锁状态下,对象头的标记位通常设置为01(在开启偏向锁的情况下),这表示这个对象可以进入偏向锁状态。
偏向锁
偏向锁是为了优化单个线程多次获取同一个锁的情况。当第一次有线程获取锁时,JVM会将对象头标记为偏向这个线程,以后该线程进入和退出同步块时不需要进行全面的同步。
- 锁状态: 对象头偏向第一个获得它的线程。
- 标记位: 对象头的标记位设置为偏向锁模式,其中包括偏向线程的ID。状态位是01,偏向标志位设置为1,后面跟着偏向的线程ID。
轻量级锁
当另一个线程尝试获取已经偏向某个线程的锁时,偏向锁会升级为轻量级锁。轻量级锁的工作原理是在每个线程的栈帧中创建一个锁记录(Lock Record),它包含了锁对象指针。
- 锁状态: 当锁对象已被线程锁定,但没有其他线程争用它时,使用轻量级锁。
- 标记位: 轻量级锁的状态位为00,锁指针指向线程栈上的锁记录。
重量级锁
当轻量级锁的自旋锁尝试失败,即有多个线程竞争同一个锁时,轻量级锁会升级为重量级锁。这时,JVM会在操作系统层面挂起其他试图获取这个锁的线程。
- 锁状态: 涉及操作系统帮助管理线程阻塞和唤醒的重量级锁。
- 标记位: 重量级锁的状态位通常为10,对象头的锁指针指向一个监视器对象(Monitor),该对象包含了锁的所有者、等待锁的线程列表等信息。
补充内容:
在Java中,轻量级锁升级到重量级锁的自旋次数并没有一个固定的标准,因为这个值可以根据JVM的具体实现和运行时的调优参数而变化。不过,一般来说,HotSpot JVM中有几个因素决定了自旋的次数:
- 自旋次数的默认设置:在早期的HotSpot JVM版本中,自旋的默认次数大约是10次。这意味着如果一个线程尝试获取锁但失败,并且通过自旋的方式进行了约10次重试仍旧无法成功,那么锁将可能被升级为重量级锁。
- 自适应自旋锁:从Java 6开始,HotSpot JVM引入了自适应自旋锁。在这种机制下,自旋的次数不是固定的,而是由JVM在运行时根据之前在同一锁上的自旋行为和锁的竞争情况动态调整的。如果在某个锁上自旋成功率高,JVM可能会增加自旋的次数;反之,如果成功率低,则减少自旋次数。
- JVM调优参数:在HotSpot JVM中,可以通过一些启动参数来调整自旋的行为,例如
-XX:PreBlockSpin参数在某些版本的JVM中用于设置线程在阻塞前进行自旋的次数。
synchronized在java代码层面表示
package com.javatiaozao;
public class Main {
public static void main(String[] args) {
Object o = new Object();
synchronized (o) {
System.out.println("我已经上锁了");
}
System.out.println("Hello world!");
}
}synchronized在JVM汇编层面表示
0 new #2 <java/lang/Object>
3 dup
4 invokespecial #1 <java/lang/Object.<init> : ()V>
7 astore_1
8 aload_1
9 dup
10 astore_2
11 monitorenter
12 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
15 ldc #4 <我已经上锁了>
17 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
20 aload_2
21 monitorexit
22 goto 30 (+8)
25 astore_3
26 aload_2
27 monitorexit
28 aload_3
29 athrow
30 getstatic #3 <java/lang/System.out : Ljava/io/PrintStream;>
33 ldc #6 <Hello world!>
35 invokevirtual #5 <java/io/PrintStream.println : (Ljava/lang/String;)V>
38 return