synchronized 关键字底层原理
目录
- synchronized字节码解析
- synchronized与管程的关系
- synchronized与JMM的关系
- synchronized与java对象头解析
- synchronized-cpu的实现
- synchronized-应用场景和实现方式
本文将深入介绍synchronized
的工作原理以及与管程、Java内存模型、对象头和CPU层面的关系。包括synchronized
的字节码解析、与管程的关系、与JMM的关系、对象头的解析和与CPU的实现。接着,会描述synchronized
的应用场景和实现方式。
深入理解Java的synchronized
Java中的**synchronized
关键字是并发编程中的核心元素,用于实现线程间的同步。本文将深入分析synchronized
**的实现原理和应用,通过解析字节码、探讨与管程(Monitor)的关系、Java内存模型(JMM)、对象头以及CPU层面的实现来全面了解其工作机制。
synchronized字节码解析
在Java中,**synchronized
关键字用于实现线程间的同步。当使用这个关键字时,Java虚拟机(JVM)会通过一系列特定的字节码指令来控制锁的获取与释放。我们可以通过查看synchronized
**在方法和同步块中的字节码表现,更深入地理解它的工作机制。
1. 同步方法的字节码
对于一个同步方法,JVM在编译时会在方法的访问标志中添加**ACC_SYNCHRONIZED
**标志。这个标志指示JVM该方法在调用时需要获取在调用对象(对于实例方法)或类对象(对于静态方法)上的锁。
例如,考虑以下Java同步方法:
public synchronized void syncMethod() {
// 方法体
}
在字节码中,这个方法将被标记为**ACC_SYNCHRONIZED
**,如下所示:
public synchronized void syncMethod();
flags: ACC_PUBLIC, ACC_SYNCHRONIZED
在运行时,当一个线程调用这个方法时,它首先必须成功获取与对象(或类对象)关联的锁。只有在获取锁后,线程才能执行方法体。方法执行完成后,无论是正常返回还是通过抛出异常退出,锁都会被释放。
2. 同步块的字节码
对于**synchronized
代码块,JVM使用monitorenter
和monitorexit
指令来显式控制锁的获取和释放。每个synchronized
块的开始处插入一个monitorenter
指令,在结束处插入一个monitorexit
**指令。
考虑以下Java代码段中的同步块:
public void syncBlock() {
Object lock = new Object();
synchronized (lock) {
// 同步块体
}
}
对应的字节码大致如下:
0: new #2 // 创建一个新的Object实例
3: dup
4: invokespecial #1 // 调用Object的构造函数
7: astore_1 // 将引用存储到局部变量1(lock)
8: aload_1 // 将局部变量1(lock)加载到操作数栈
9: monitorenter // 进入monitor
10: ... // 同步块体的字节码
: aload_1
: monitorexit // 退出monitor
: ...
- **
monitorenter
**指令尝试获取锁。如果指定的对象锁不可用(即被其他线程持有),则当前线程将阻塞直到锁可用。 - **
monitorexit
指令用于释放锁。每个monitorenter
必须匹配一个monitorexit
**指令,以确保在方法退出时(无论是正常还是异常退出),持有的锁都能被释放。
通过这种方式,JVM确保了在任何时间点,只有一个线程可以执行同步代码块内的代码,从而实现线程安全。
synchronized与管程的关系
理解**synchronized
关键字涉及到理解管程(Monitor),因为Java中的每个对象都与一个Monitor相关联。Monitor是用于实现同步的基本机制,而synchronized
**关键字就是基于Monitor实现的。让我们深入了解一下这两者之间的关系。
1. 管程(Monitor)的概念
管程是一种用于实现线程间同步的抽象概念,它包含互斥(Mutex,即锁)和条件变量(Condition Variable)。互斥用于保护临界区(Critical Section),防止多个线程同时访问共享资源。条件变量用于在线程间进行通信,以实现线程的等待和唤醒。
管程提供了以下几种基本操作:
- 进入临界区(Enter):获取互斥锁,进入临界区执行操作。
- 退出临界区(Exit):释放互斥锁,退出临界区。
- 等待(Wait):线程在某个条件变量上等待,同时释放互斥锁。
- 通知(Notify):唤醒一个等待在条件变量上的线程。
- 广播(NotifyAll):唤醒所有等待在条件变量上的线程。
2. synchronized
与管程的关系
在Java中,每个对象都与一个Monitor相关联。当使用**synchronized
关键字修饰方法或代码块时,实际上是使用了对象的Monitor来实现线程间的同步。具体来说, synchronized
**关键字封装了Enter、Exit、Wait、Notify和NotifyAll等操作,使得Java开发者可以方便地使用管程模型进行同步编程,而无需显式地使用管程操作。
当一个线程进入同步代码块时,它会尝试获取对象的Monitor(即进入临界区),如果Monitor已被其他线程占用,则线程会被阻塞。当线程退出同步代码块时,它会释放Monitor(即退出临界区),这样其他线程就有机会获取Monitor并进入临界区。
在同步方法中,Java编译器会自动将**ACC_SYNCHRONIZED
标志添加到方法的访问标志中,从而指示该方法需要获取对象的Monitor。在同步代码块中,JVM通过插入monitorenter
和monitorexit
**指令来控制锁的获取和释放。
因此,通过将**synchronized
**关键字与对象的Monitor结合使用,Java实现了管程模型,简化了并发编程中的同步操作,提高了开发效率并减少了死锁等问题的发生。
综上所述,**synchronized
**关键字和管程密切相关,它通过封装管程的操作,为开发者提供了一种方便的方式来实现线程间的同步。
synchronized与JMM的关系
Java内存模型(Java Memory Model,JMM)规定了多线程之间共享变量的访问方式,确保多线程的可见性、有序性和一致性。同步关键字(synchronized)和JMM密切相关,因为同步关键字可以确保JMM中定义的多线程内存可见性和原子性。
JMM的特性
- 内存可见性:多个线程访问共享变量时,一个线程对共享变量的修改能够及时被其他线程观察到。
- 原子性:一个操作是不可中断的。即使在多线程环境下,一个操作不会被其他线程的调度机制打断。
- 有序性:程序中的代码按照我们指定的顺序执行。当程序执行指令的顺序与代码的顺序不一致时,会根据JMM的规则进行指令重排序。
synchronized的实现方式与JMM的特性
同步关键字可以保证多线程中的原子性和可见性。具体来说,同步在实现上包含:
- 进入同步块时,清空工作内存中的共享变量值,并从主内存中重新读取共享变量的值。
- 执行同步块中的代码,修改共享变量的值。
- 退出同步块时,将共享变量的最新值刷新回主内存。
这样,同步能够确保共享变量的可见性和原子性,从而保证了JMM中定义的内存可见性和原子性的特性。
synchronized字节码指令与JMM指令的关系
在字节码中,同步关键字使用了monitorenter和monitorexit指令来控制锁的获取和释放。这些指令与JMM中的内存屏障指令具有对应关系:
- monitorenter指令可以看作是一个前向内存屏障。在获取锁的时候,它会强制线程将共享变量从主内存中读取到线程的工作内存中,确保线程在进入同步块之前获得了最新值。
- monitorexit指令可以看作是一个后向内存屏障。在释放锁的时候,它会将线程工作内存中共享变量的最新值刷新到主内存中,确保了所有其他线程在获取锁之后能够读取到最新的值。
因此,同步关键字的字节码指令与JMM中的内存屏障指令相对应,确保了共享变量的可见性和原子性。
综上所述,同步关键字和JMM密切相关,同步通过实现JMM的特性来确保共享变量的可见性和原子性,而其字节码指令与JMM中的内存屏障指令具有对应关系,保证了多线程间的内存访问顺序和可见性。
synchronized与java对象头解析
在 Java 中,synchronized
关键字的实现是依赖于 JVM 内部的一个重要结构——对象头(Object Header)。对象头是 Java 对象在内存中的一部分,它存储了对象的元数据,包括用于锁定和线程同步的信息。了解对象头对于深入理解 synchronized
的工作原理至关重要。
对象头的组成
Java 对象头主要由两部分组成:Mark Word 和 Class Pointer。
-
Mark Word:
- 这部分存储了对象自身的运行时数据,如哈希码、GC 年龄段、锁状态标志、线程持有的锁等。其具体内容会根据对象状态的改变而变化。
- 锁信息是在这一部分进行编码的,它记录了锁的状态以及到底是哪个线程持有了锁。
-
Class Pointer:
- 指向类元数据的指针,表示该对象是哪个类的实例。这部分信息帮助 JVM 确定该对象的类定义信息。
锁的状态
Mark Word 中关于锁的信息表明了对象可能处于以下几种状态之一:
-
无锁状态:
- 对象未被锁定。任何线程都可以尝试通过 CAS 操作(比较并交换)来获取锁,并将 Mark Word 的值更新为指向锁记录(Lock Record)的指针。
-
偏向锁:
- 一旦一个线程获取了对象的偏向锁,对象头的 Mark Word 会被标记为偏向模式,并且记录下获取它的线程 ID。在此之后,只要没有竞争出现,持有偏向锁的线程可以无锁地进入同步块。
-
轻量级锁:
- 当偏向锁被另一个线程访问时,偏向锁会升级为轻量级锁。此时,对象头的 Mark Word 指向栈帧中的锁记录。
-
重量级锁:
- 如果轻量级锁的自旋失败(即线程尝试获取锁时,锁已被其他线程持有,并且自旋(忙等)没有成功),则锁会升级为重量级锁。此时,Mark Word 指向一个重量级锁(如操作系统的互斥锁),并且所有进一步的锁获取都需要阻塞。
synchronized
的工作原理
当一个线程尝试进入一个由 synchronized
修饰的方法或代码块时,它需要先获取对象的锁。这涉及到检查对象头的 Mark Word:
- 如果锁是无锁状态,JVM 将尝试通过 CAS 设置线程的锁记录。
- 如果对象已经处于偏向锁状态,且当前线程是偏向锁的持有者,线程将直接进入同步块。
- 如果锁升级(偏向锁升级为轻量级锁,或轻量级锁升级为重量级锁),JVM 将采取相应的锁策略来处理线程的进入。
当线程退出同步块时,它会将锁释放,这通常涉及到将对象头的 Mark Word 状态回复到原始状态或更新到新的锁状态。
synchronized-cpu的实现
在CPU层面,synchronized
的实现通常依赖于特定的指令集架构和硬件支持,例如在x86架构上是通过锁定总线来实现。下面简要介绍一下在x86架构上synchronized
的实现原理:
-
锁定总线(Locking the Bus) :
- 在x86架构上,使用了一条名为
LOCK
的指令前缀,可以将总线锁定,防止其他处理器访问内存,从而实现原子操作和同步。 - 当一个线程尝试进入同步块时,它会向处理器发送一个带有
LOCK
前缀的指令,以获取锁。这会导致总线被锁定,其他处理器无法访问内存。 - 当线程离开同步块时,处理器会释放锁,解除总线的锁定状态,使得其他线程可以再次访问内存。
- 在x86架构上,使用了一条名为
-
缓存一致性协议:
- 多处理器系统中,为了保持主内存和各个处理器的缓存一致,通常需要使用缓存一致性协议(如MESI协议)。
- 当一个处理器通过锁定总线获取了锁时,它会发出缓存失效的信号,导致其他处理器上的缓存中的共享数据无效,从而保证数据的一致性。
-
指令重排序:
- 处理器为了提高执行效率可能会对指令进行乱序执行或重排序。在
synchronized
块内部,使用了内存屏障(Memory Barriers)来防止指令重排序,保证了锁的获取和释放顺序。
- 处理器为了提高执行效率可能会对指令进行乱序执行或重排序。在
总的来说,在x86架构上,synchronized
的实现依赖于锁定总线的方式,通过硬件级别的支持来实现原子操作和内存同步,以保证线程间的正确同步。在其他架构上,可能会使用CAS指令(比较并交换)或其他方式来实现类似的原子操作,但无论哪种实现方式,都需要考虑到多处理器系统中的缓存一致性和指令重排序等问题。
synchronized-应用场景和实现方式
synchronized是Java中实现线程间同步的重要机制,通常适用于以下场景和方式:
应用场景
- 共享资源访问控制:当多个线程需要访问共享资源(如变量、对象等)时,可以使用synchronized来确保线程间互斥访问,避免出现数据竞争和不一致性问题。
- 线程间通信:synchronized可以用于实现线程间的通信和协调,例如使用wait()和notify()/notifyAll()方法实现线程的等待和唤醒。
- 实现线程安全的类:可以使用synchronized来实现线程安全的类,确保多线程环境下对共享对象的安全访问。
实现方式
-
同步代码块:通过在方法内部使用synchronized关键字修饰的代码块来实现对指定对象的同步操作。
public void synchronizedMethod() { synchronized (this) { // 同步代码块 } }
-
同步方法:通过在方法声明中使用synchronized关键字来修饰整个方法,实现对方法内部的同步操作。
public synchronized void synchronizedMethod() { // 同步方法体 }
-
静态同步方法:通过在静态方法声明中使用synchronized关键字来实现对整个类的同步操作。
public static synchronized void synchronizedStaticMethod() { // 静态同步方法体 }
-
Lock接口:除了使用synchronized关键字外,还可以使用Lock接口及其实现类,如ReentrantLock,来实现显式的锁定和解锁操作。
Lock lock = new ReentrantLock(); public void synchronizedMethod() { lock.lock(); try { // 同步代码块 } finally { lock.unlock(); } }
总之,synchronized关键字是一种简单而有效的确保线程安全的机制,适用于需要通过互斥访问共享资源或实现线程间通信的情况。除此之外,也可以根据具体场景选择其他同步机制,如Lock接口,来实现线程间同步操作。
转载自:https://juejin.cn/post/7362506905673613327