线程安全——synchronized(八)

前言

synchronized 是 Java 中的一个关键字,它是一个重量级锁,用于保证在同一个时刻,只有一个线程可以执行某个方法或者某个代码块,同时也可以保证可见性,即一个线程的变化可以被其他线程所见。

应用

synchronized 关键字最主要有以下 3 种应用方式,下面分别介绍

  1. 修饰实例方法

    修饰实例方法即为为当前实例加锁。当一个线程正在访问一个对象的 synchronized 实例方法时,其他线程不能访问该对象的其他 synchronized 方法,因为一个对象只有一把锁,当一个线程获取了该对象的锁之后,其他线程无法获取该对象的锁,所以无法访问该对象的其他 synchronized 实例方法。

    但是其他线程还是可以访问该实例对象的其他非 synchronized 方法。

  2. 修饰静态方法

    需要注意的是如果一个线程 A 调用一个实例对象的非 static synchronized 方法,而线程 B 需要调用这个实例对象所属类的静态 synchronized 方法,是允许的,不会发生互斥现象,因为访问静态 synchronized 方法占用的锁是当前类的 class 对象,而访问非静态 synchronized 方法占用的锁是当前实例对象锁。

  3. 修饰代码块

    如 Synchronized(obj) 这里的 obj 可以为类中的一个属性、也可以是当前的对象,它的同步效果和修饰普通方法一样;Synchronized 方法 (obj.class)静态代码块它的同步效果和修饰静态方法类似。

特性

  1. 原子性

    被 synchronized 修饰的类或对象的所有操作都是原子的,因为在执行操作之前必须先获得类或对象的锁,直到执行完才能释放,这中间的过程无法被中断(除了已经废弃的 stop()方法),确保了同一时刻只有一个线程操作类和对象。

  2. 可见性

    synchronized 对一个类或对象加锁时,一个线程如果要访问该类或对象必须先获得它的锁,而这个锁的状态对于其他任何线程都是可见的,并且在释放锁之前会将对变量的修改刷新到主存当中,保证资源变量的可见性。

  3. 有序性

    synchronized 保证了每个时刻都只有一个线程访问同步代码块,也就确定了线程执行同步代码块是分先后顺序的,保证了有序性。

  4. 可重入性

    synchronized 和 ReentrantLock 都是可重入锁。当一个线程试图操作一个由其他线程持有的对象锁的临界资源时,将会处于阻塞状态,但当一个线程再次请求自己持有对象锁的临界资源时,这种情况属于重入锁。通俗一点讲就是说一个线程拥有了锁仍然还可以重复申请锁。

对象头

在 JVM 中,对象是分成三部分存在的:对象头、实例数据、对其填充。

JAVA并发——synchronized_2020-03-16-21-22-59.png

实例数据和对其填充与 synchronized 无关。

对象头是我们需要关注的重点,它是 synchronized 实现锁的基础,因为 synchronized 申请锁、上锁、释放锁都与对象头有关。

对象头主要结构是由 Mark WordClass Metadata Address组成,其中 Mark Word 存储对象的 hashCode、锁信息或分代年龄或 GC 标志等信息, Class Metadata Address 是类型指针指向对象的类元数据,JVM 通过该指针确定该对象是哪个类的实例。

  1. 锁状态

JDK6 之前只有两个状态:无锁、有锁(重量级锁),而在 JDK6 之后对 synchronized 进行了优化,新增了两种状态,总共就是四个状态:无锁状态、偏向锁、轻量级锁、重量级锁。

锁的类型和状态在对象头 Mark Word 中都有记录,在申请锁、锁升级等过程中 JVM 都需要读取对象的 Mark Word 数据。

  1. Monitor

每一个锁都对应一个 monitor 对象,在 HotSpot 虚拟机中它是由 ObjectMonitor 实现的(C++实现)。

每个对象都存在着一个 monitor 与之关联,对象与其 monitor 之间的关系有存在多种实现方式,如 monitor 可以与对象一起创建销毁或当线程试图获取对象锁时自动生成,但当一个 monitor 被某个线程持有后,它便处于锁定状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
ObjectMonitor() {
_header = NULL;
_count = 0; //锁计数器
_waiters = 0,
_recursions = 0;
_object = NULL;
_owner = NULL;
_WaitSet = NULL; //处于wait状态的线程,会被加入到_WaitSet
_WaitSetLock = 0 ;
_Responsible = NULL ;
_succ = NULL ;
_cxq = NULL ;
FreeNext = NULL ;
_EntryList = NULL ; //处于等待锁block状态的线程,会被加入到该列表
_SpinFreq = 0 ;
_SpinClock = 0 ;
OwnerIsThread = 0 ;
}

这里有几个比较重要的字段:

  1. _owner 指向持有 ObjectMonitor 对象的线程地址。

  2. _WaitSet 存放调用 wait 方法,而进入等待状态的线程的队列。

  3. _EntryList 这里是等待锁 block 状态的线程的队列。

  4. _recursions 锁的重入次数。

  5. _count 线程获取锁的次数。

对象监视器会设置几种状态用来区分请求的线程:

  • Contention List:所有请求锁的线程将被首先放置到该竞争队列
  • Entry List:Contention List 中那些有资格成为候选人的线程被移到 Entry List
  • Wait Set:那些调用 wait 方法被阻塞的线程被放置到 Wait Set
  • OnDeck:任何时刻最多只能有一个线程正在竞争锁,该线程称为 OnDeck
  • Owner:获得锁的线程称为 Owner
  • !Owner:释放锁的线程

JAVA并发——synchronized_2020-03-16-22-00-59.png

上锁

  1. 线程获取资源对象的锁,判断_owner 是否为空。这里操作是通过 CAS 操作:比较和交换(Conmpare And Swap),比较新值和旧值的不同,替换,这里会发生 ABA 问题,接下来文章会详细说明。

  2. 如果 _owner 为 null ,直接把其赋值,指向自己, _owner = self ,同时把重入次数 _recursions = 1, 获取锁成功。

  3. 如果 _self == cur 和当前线程一致,说明是重入了, _recursions++ 即可

  4. 线程进入对象资源,处理。 同时等待当前线程的释放信号,期间一致持有对象资源的锁。

释放锁

  1. 通过 ObjectMonitor::exit 退出

  2. 把线程插入到_EntryList 中 _recursions–

  3. 再次从 _EntryList 中取出线程

  4. 调用 unpark 退出

锁升级

jdk6后,JVM对锁进行了优化,会因实际情况进行膨胀升级,其膨胀方向是:无锁——>偏向锁——>轻量级锁——>重量级锁,并且膨胀方向不可逆。

  1. 偏向锁

    如果一个线程获得了锁,那么锁就进入偏向模式,此时 Mark Word 的结构也就变为偏向锁结构,当该线程再次请求锁时,无需再做任何同步操作,即获取锁的过程只需要检查 Mark Word 的锁标记位为偏向锁以及当前线程 ID 等于 Mark Word 的 ThreadID 即可,这样就省去了大量有关锁申请的操作。

  2. 轻量级锁

    轻量级锁是由偏向锁升级而来,当存在第二个线程申请同一个锁对象时,偏向锁就会立即升级为轻量级锁。注意这里的第二个线程只是申请锁,不存在两个线程同时竞争锁,可以是一前一后地交替执行同步块。

  3. 重量级锁

    重量级锁是由轻量级锁升级而来,当同一时间有多个线程竞争锁时,锁就会被升级成重量级锁,此时其申请锁带来的开销也就变大。

锁消除

消除锁是虚拟机另外一种锁的优化,这种优化更彻底,在 JIT 编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

锁粗化

锁粗化是虚拟机对另一种极端情况的优化处理,通过扩大锁的范围,避免反复加锁和释放锁。

总结

synchronized 本身并不是锁,只是一个 JVM 定义的关键字。

被 synchronized 修饰的对象都有一个对应的监视器对象,多个线程同时访问对象的一系列加锁、释放锁的操作,都是通过对监视器对象的变量进行操作实现的。

synchronized的底层也是一个基于CAS操作的等待队列,但JVM实现的更精细,把等待队列分为ContentionList和EntryList,目的是为了降低线程的出列速度;当然也实现了偏向锁,从数据结构来说二者设计没有本质区别。但synchronized还实现了自旋锁,并针对不同的系统和硬件体系进行了优化,而Lock则完全依靠系统阻塞挂起等待线程。

文章目录
  1. 1. 前言
  2. 2. 应用
  3. 3. 特性
  4. 4. 对象头
  5. 5. 上锁
  6. 6. 释放锁
  7. 7. 锁升级
  8. 8. 锁消除
  9. 9. 锁粗化
  10. 10. 总结
|