前言
synchronized 是 Java 中的一个关键字,它是一个重量级锁,用于保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,同时也可以保证可见性,即一个线程的变化可以被其他线程所见。
应用
synchronized 关键字最主要有以下 3 种应用方式,下面分别介绍
修饰实例方法
修饰实例方法即为为当前实例加锁。当一个线程正在访问一个对象的 synchronized 实例方法时,其他线程不能访问该对象的其他 synchronized 方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized 实例方法。
但是其他线程还是可以访问该实例对象的其他非 synchronized 方法。
修饰静态方法
需要注意的是如果一个线程 A 调用一个实例对象的非 static synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的 class 对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。
修饰代码块
如 Synchronized(obj) 这里的 obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized 方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。
特性
原子性
被 synchronized 修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的 stop()方法),确保了同一时刻只有一个线程操作类和对象。
可见性
synchronized 对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性。
有序性
synchronized 保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。
可重入性
synchronized 和 ReentrantLock 都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。
对象头
在 JVM 中,对象是分成三部分存在的:对象头、实例数据、对其填充。
实例数据和对其填充与 synchronized 无关。
对象头是我们需要关注的重点,它是 synchronized 实现锁的基础,因为 synchronized 申请锁、上锁、释放锁都与对象头有关。
对象头主要结构是由 Mark Word
和 Class Metadata Address
组成,其中 Mark Word
存储对象的 hashCode
、锁信息或分代年龄或 GC 标志等信息, Class Metadata Address
是类型指针指向对象的类元数据,JVM 通过该指针确定该对象是哪个类的实例。
- 锁状态
JDK6 之前只有两个状态:无锁、有锁(重量级锁),而在 JDK6 之后对 synchronized 进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁。
锁的类型和状态在对象头 Mark Word
中都有记录,在申请锁、锁升级等过程中 JVM 都需要读取对象的 Mark Word
数据。
- Monitor
每一个锁都对应一个 monitor 对象,在 HotSpot 虚拟机中它是由 ObjectMonitor 实现的(C++实现)。
每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。
1 | ObjectMonitor() { |
这里有几个比较重要的字段:
_owner 指向持有 ObjectMonitor 对象的线程地址。
_WaitSet 存放调用 wait 方法,而进入等待状态的线程的队列。
_EntryList 这里是等待锁 block 状态的线程的队列。
_recursions 锁的重入次数。
_count 线程获取锁的次数。
对象监视器会设置几种状态用来区分请求的线程:
- Contention List:所有请求锁的线程将被首先放置到该竞争队列
- Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
- Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set
- OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
- Owner:获得锁的线程称为 Owner
- !Owner:释放锁的线程
上锁
线程获取资源对象的锁,判断_owner 是否为空。这里操作是通过 CAS 操作:比较和交换(Conmpare And Swap),比较新值和旧值的不同,替换,这里会发生 ABA 问题,接下来文章会详细说明。
如果 _owner 为 null ,直接把其赋值,指向自己, _owner = self ,同时把重入次数 _recursions = 1, 获取锁成功。
如果 _self == cur 和当前线程一致,说明是重入了, _recursions++ 即可
线程进入对象资源,处理。 同时等待当前线程的释放信号,期间一致持有对象资源的锁。
释放锁
通过 ObjectMonitor::exit 退出
把线程插入到_EntryList 中 _recursions–
再次从 _EntryList 中取出线程
调用 unpark 退出
锁升级
jdk6后,JVM对锁进行了优化,会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。
偏向锁
如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程 ID 等于 Mark Word 的 ThreadID 即可,这样就省去了大量有关锁申请的操作。
轻量级锁
轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。
重量级锁
重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。
锁消除
消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。
锁粗化
锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。
总结
synchronized 本身并不是锁,只是一个 JVM 定义的关键字。
被 synchronized 修饰的对象都有一个对应的监视器对象,多个线程同时访问对象的一系列加锁、释放锁的操作,都是通过对监视器对象的变量进行操作实现的。
synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。