目录
前言
在多线程的场景中常常使用“锁”或ThreadLocal“线程变量隔离”等方式去实现线程同步,比如互斥同步(Mutual Exclusion & Synchronization) 是一种最常见也是最主要的并发正确性保障手段。
同步是指在多个线程并发访问共享数据时, 保证共享数据在同一个时刻只被一条(或者是一些,当使用信号量的时候) 线程使用。 而互斥是实现同步的一种手段, 临界区(Critical Section) 、 互斥量(Mutex) 和信号量(Semaphore) 都是常见的互斥实现方式。
在 Java 里面, 最基本的互斥同步手段就是 synchronized 关键字, 这是一种块结构(Block Structured) 的同步语法。它解决的是多个线程之间访问资源的同步性,synchronized 关键字可以保证被它修饰的方法或者代码块在任意时刻只能有一个线程执行。
synchronized 关键字可以保证原子性、可见性和有序性,相较于 volatile 关键字功能更加强大,本文将对该关键字进行深入学习。
Synchronized作用
- 原子性:synchronized保证语句块内操作是原子的
- 可见性:synchronized保证可见性(通过“在执行unlock之前,必须先把此变量同步回主内存”实现)
- 有序性:synchronized保证有序性(通过“一个变量在同一时刻只允许一条线程对其进行lock操作”)
Tips:可与【JVM】底层实现(一):浅谈 OOP-Klass 对象模型_RichardGeek的博客-优快云博客
文章关联一块阅读。
从语法上讲,Synchronized可以把任何一个非null对象作为"锁",在HotSpot JVM实现中,锁有个专门的名字:对象监视器(Object Monitor)
synchronized的使用范围
- 修饰实例方法,对当前实例对象加锁
- 修饰静态方法,对当前类的Class对象加锁
- 修饰代码块,对synchronized括号内的对象加锁
解释:
1)当synchronized作用在实例方法时,监视器锁(monitor)便是对象实例(this)
2)当synchronized作用在静态方法时,监视器锁(monitor)便是对象的Class实例,因为Class数据存在于永久代,因此静态方法锁相当于该类的一个全局锁;
3)当synchronized作用在某一个对象实例时,监视器锁(monitor)便是括号括起来的对象实例;
注意,synchronized 内置锁是一种对象锁(锁的是对象而非引用变量),作用粒度是对象 ,可以用来实现对临界资源的同步互斥访问 ,是可重入的。
关联知识点
Java 语言的调度模型 依赖JVM、操作系统、硬件关系

JVM内存模型概览

线程状态转换概述


Synchronized底层原理(较为重要的底层知识点)
在《Happens-Before规则详解》一文中讲解 Happens-Before 规则时,其中有个规则叫做「管程锁定规则」,具体定义为:synchronized 是 Java 对管程的实现,管程中的锁在 Java 里是隐式实现的,隐式加锁、释放锁,对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
再往深处来说,Java 虚拟机中的同步(synchronization)是基于进入和退出管程(Monitor)对象实现的, 无论是显式同步(有明确的 monitorenter 和 monitorexit 指令,即同步代码块),还是隐式同步都是如此。
在 Java 语言中,同步用的最多的地方可能是被 synchronized 修饰的同步方法。同步方法并不是由 monitorenter 和 monitorexit 指令来实现同步的,而是由方法调用指令读取运行时常量池中方法的 ACC_SYNCHRONIZED 标志来隐式实现的,关于这点,稍后详细分析。下面先来了解一个概念 Java对象头,这对深入理解synchronized 实现原理非常关键。
Java对象头与Monitor
synchronized 用的锁是存在 Java 对象头里的表示。在 JVM 中,对象在内存中的布局分为三块区域:对象头、实例数据和对齐填充。
对象头包括两部分信息:标记字段(Mark Word)和类型指针(Class Metadata Address),如果对象是一个数组,还需要一块用于记录数组长度的数据。

其中 Mark Word 在默认情况下存储着对象的 HashCode、分代年龄、锁标记位等,32位 JVM 的 Mark Word 的默认存储结构如下图所示:

我们可以在 JVM 源码 (hotspot/share/oops/markOop.hpp) 中看到对象头中存储内容的定义:
public:
// Constants
enum { age_bits = 4,
lock_bits = 2,
biased_lock_bits = 1,
max_hash_bits = BitsPerWord - age_bits - lock_bits - biased_lock_bits,
hash_bits = max_hash_bits > 31 ? 31 : max_hash_bits,
cms_bits = LP64_ONLY(1) NOT_LP64(0),
epoch_bits = 2
};
在该文件中关于标记字段的结构有如下示例:
// 32 bits:
// --------
// hash:25 ------------>| age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:23 epoch:2 age:4 biased_lock:1 lock:2 (biased object)
// size:32 ------------------------------------------>| (CMS free block)
// PromotedObject*:29 ---------->| promo_bits:3 ----->| (CMS promoted object)
//
// 64 bits:
// --------
// unused:25 hash:31 -->| unused:1 age:4 biased_lock:1 lock:2 (normal object)
// JavaThread*:54 epoch:2 unused:1 age:4 biased_lock:1 lock:2 (biased object)
// PromotedObject*:61 --------------------->| promo_bits:3 ----->| (CMS promoted object)
// size:64 ----------------------------------------------------->| (CMS free block)
//
// unused:25 hash:31 -->| cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && normal object)
// JavaThread*:54 epoch:2 cms_free:1 age:4 biased_lock:1 lock:2 (COOPs && biased object)
// narrowOop:32 unused:24 cms_free:1 unused:4 promo_bits:3 ----->| (COOPs && CMS promoted object)
// unused:21 size:35 -->| cms_free:1 unused:7 ------------------>| (COOPs && CMS free block)
字段含义如下:
- hash: 对象的哈希码
- age: 对象的分代年龄
- biased_lock : 偏向锁标识位
- lock: 锁状态标识位
- JavaThread* : 持有偏向锁的线程 ID
- epoch: 偏向时间戳
markOop中不同的锁标识位,代表着不同的锁状态:

不同的锁状态,存储着不同的数据(注意这句话,表示不是所有的锁状态都是如上述标红的Mark Word“默认存储结构”的数据内容):

在 Java 早期版本中,synchronized 属于重量级锁,效率低下,因为监视器锁(monitor)是依赖于底层的操作系统的 Mutex Lock 来实现的,Java 的线程是映射到操作系统的原生线程之上的。重量级锁的锁标识位为10,其中指针指向的是 monitor 对象的起始地址。每个 Java 对象都关联着一个 monitor,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁,或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。在 Java 虚拟机(HotSpot)中,monitor 是由ObjectMonitor 实现的,其主要数据结构如下(位于HotSpot虚拟机源码 ObjectMonitor.hpp文件,C++实现的)
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
_header = NULL;
_count = 0;
_waiters = 0,
_recursions = 0; //记录嵌套(递归)加锁的次数,最外层的锁的_recursions属性为0
_object = NULL;
_owner = NULL; //占用当前锁的线程
_WaitSet = NULL; //等待集合,处于wait状态的线程,会被加入到_WaitSet,配合 wait和Notify/notifyALl 使用
_WaitSetLock = 0 ; //保护等待队列,简单的自旋锁
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //阻塞队列,处于等待锁block状态的线程,会被加入到该列表,配合synchronized锁进行使用
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
_previous_owner_tid = 0;
}
ObjectMonitor 中有两个队列,_WaitSet 和 _EntryList。
ObjectMonitor 对象中有多个属性,这里我们介绍几个重点的字段。
protected:
ObjectWaiter * volatile _WaitSet; // LL of threads wait()ing on the monitor
protected:
ObjectWaiter * volatile _EntryList ; // Threads blocked on entry or reentry.
protected: // protected for jvmtiRawMonitor
void * volatile _owner; // pointer to owning thread OR BasicLock
WaitSet 用来保存 ObjectWaiter 对象列表( 每个 wait 状态的线程都会被封装成 ObjectWaiter对象),EntryList 用来保存处于 block 状态的线程封装的 ObjectWaiter对象,owner 指向持有 ObjectMonitor 对象的线程。
这里简单描述一下 synchronized(重量级锁)的加锁和解锁过程:当多个线程同时访问一段同步代码时,首先会进入 EntryList 集合,当线程获取到对象的 monitor 后,owner 变量会设置为当前线程,同时 monitor 中的计数器 count 加1。若线程调用 wait() 方法,将释放当前持有的 monitor,owner 变量恢复为 null,count 自减1,同时该线程进入 WaitSet集合中等待被唤醒。若当前线程执行完毕也将释放 monitor 复位 count 变量的值,以便其他线程进入获取 monitor。
由此可知,monitor 对象存在于每个 Java 对象的对象头中(存储的指针的指向),synchronized 锁便是通过这种方式获取锁的,这也是为什么 Java 中任意对象可以作为锁的原因。
Synchronized代码块的字节码标识
1.synchronized 同步语句块的情况举例
public class SynchronizedDemo {
public void method(){
synchronized (this){
System.out.println("synchronized code");
}
}
}
通过 JDK 自带的 javap 命令查看 SynchronizedDemo 类的相关字节码信息:首先切换到类的对应目录执行 javac SynchronizedDemo.java 命令生成编译后的 .class 文件,然后执行 javap -verbose SynchronizedDemo.class。

从上面我们可以看出:
synchronized 同步语句块的实现使用的是 monitorenter 和 monitorexit 指令,其中 monitorenter 指令指向同步代码块的开始位置,monitorexit 指令则指明同步代码块的结束位置。 当执行 monitorenter 指令时,线程试图获取锁也就是获取 monitor 的持有权。当计数器为0则可以成功获取,获取后将锁计数器设为1也就是加1。相应的在执行 monitorexit 指令后,将锁计数器设为0,表明锁被释放。如果获取对象锁失败,那当前线程就要阻塞等待,直到锁被另外一个线程释放为止。这里提到的锁计数器,即上文提到的 count 变量。另外还有锁重入的情况,当线程获取该对象的锁后,在未释放锁之前,可以直接进行代码调用,不需要等待。具体到代码实现,就是重入时重入计数器会加1,这块逻辑在 enter()方法中有描述。
值得注意的是编译器将会确保无论方法通过何种方式完成,方法中调用过的每条 monitorenter 指令都要执行其对应的 monitorexit 指令,而无论这个方法是正常结束还是异常结束。为了保证在方法异常完成时 monitorenter 和 monitorexit 指令依然可以正确配对执行,编译器会自动产生一个异常处理器,这个异常处理器声明可处理所有的异常,它的目的就是用来执行 monitorexit 指令。从字节码中也可以看出多了一个monitorexit指令,它就是异常结束时被执行的释放monitor 的指令。
2.synchronized 同步方法的情况举例
方法级的同步是隐式,即无需通过字节码指令来控制的,它实现在方法调用和返回操作之中。JVM可以从方法常量池中的方法表结构(method_info Structure) 中的 ACC_SYNCHRONIZED 访问标志区分一个方法是否同步方法。当方法调用时,调用指令将会检查方法的 ACC_SYNCHRONIZED 访问标志是否被设置,如果设置了,执行线程将先持有 monitor , 然后再执行方法,最后再方法完成(无论是正常完成还是非正常完成)时释放 monitor。
public class SynchronizedDemo {
public synchronized void foo(){
System.out.println("synchronized method");
}
}

synchronized 修饰的方法并没有 monitorenter 指令和 monitorexit 指令,取得代之的确实是 ACC_SYNCHRONIZED 标识,该标识指明了该方法是一个同步方法,JVM 通过该 ACC_SYNCHRONIZED 访问标志来辨别一个方法是否声明为同步方法,从而执行相应的同步调用。
JVM对synchronized的优化
JDK1.6 对锁的实现引入了大量的优化,如偏向锁、轻量级锁、自旋锁、适应性自旋锁、锁消除、锁粗化等技术来减少锁操作的开销。【优化的相关细节略,不在本文的讨论范围内】
本文详细探讨了Java中synchronized关键字的底层工作原理,包括对象头中的Monitor、synchronized代码块的字节码实现、以及JVM对synchronized的优化策略。通过实例解析,阐述了原子性、可见性和有序性的保证机制,并结合JVM内存模型和线程状态转换进行讲解。
1277

被折叠的 条评论
为什么被折叠?



