Java程序员进阶必备:深入分析 Synchronized 原理

开发 后端
同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

深入分析 Synchronized 原理

我们在开发中肯定会遇到在同一个 JVM 中,存在多个线程同时操作同一个资源时,此时需要想要确保操作的结果满足预期,就需要使用同步方法。

官方解释:同步方法支持一种简单的策略来防止线程干扰和内存一致性错误:如果一个对象对多个线程可见,则对该对象变量的所有读取或写入都是通过同步方法完成的。

官方推荐使用的同步方法 (JDK 1.6后):Synchronized 基于 JVM 实现(此次主角);当然还有 ReentrantLock 基于 JDK 实现的。

我们先简单地热个身,举一个常用 synchronized 的方式(锁的是该类的实例对象)。

public class SynchronizedCodeTest {
public void testSynchronized() throws InterruptedException {
synchronized (this) {
System.out.println("进入同步代码块");
Thread.sleep(100);
System.out.println("离开同步代码块");
}
}
public static void main(String[] args) throws InterruptedException {
new SynchronizedCodeTest().testSynchronized();
}
}

Synchronized 常用场景

任何对象(都有Mark Word结构,后面会详细描述) 都可以能作为 synchronized 锁的对象,根据使用的方式不同,锁的对象和对应的粒度也是有所不同。

并发编程三大特性

简单回顾了下 synchronized ,一聊到锁就会提到 原子性、有序性、可见性,简单的介绍下这些(就不具体展开说明了,有需要的读者可以查阅相关资料,或者感兴趣的话我后续补充)。

原子性

原子性:一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。

简单理解为:如果将下单和支付2个操作看作一个整体,只要其中一个操作失败了,都算失败,反之成功。

有序性

有序性:即程序执行的顺序按照代码的先后顺序执行。

大家可能或多或少,有听说过 Java 为了提高性能允许重排序(编译器重排序 和 处理器重排序),因此程序执行可能出现乱序也是由此而来。

简单理解为:有序性保证了 同样的代码 在多线程和单线程执行的最后结果相同,按照代码的先后顺序执行。

可见性

可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。

某个类的一个成员变量 Integer A = 0;

# 线程1执行操作
A = 10
# 与此同时 线程2执行操作(B的值是0,而不是10,这就是可见性的问题)
Integer B = A;

# 常用的解决方案使用:volatile修饰 A 或者 使用synchronized修饰代码块 都可以解决这个问题

既然提到 synchronized 再多延伸出2个特性。

可重入性

synchronized monitor(锁对象) 有个计数器,获取锁时 会记录当前线程获取锁的次数,在执行完对应的代码块之后,计数器就会-1,直到计数器清零,锁就会被释放了。

不可中断性

不可中断:一个线程获取锁之后,另外一个线程处于阻塞或者等待状态,前一个不释放,后一个也一直会阻塞或者等待,不可以被中断。

Synchronized是不可中断,而 ReentrantLock是可中断(二者比较重要的区别之一)。

Synchronized 字节码

介绍完一些基本的特性后,我们正式开始进入 synchronized 实现原理分析。

# 将上面 热身例子反编译成字节码
javac -verbose SynchronizedCodeTest.java
javap -c SynchronizedCodeTest

我们主要关注下,monitorenter 和 monitorexit 这2个指令,对应的是 当前线程获取锁&计数器加一 和 释放锁&计数器减一。多个线程获取对象的监视器monitor获取是互斥。

对象,对象监视器,同步队列以及执行线程状态之间的关系

任意线程对 Object 的访问,首先要获得 Object 的监视器,如果获取失败,该线程就进入同步状态,线程状态变为 BLOCKED,当 Object 的监视器占有者释放后,在同步队列中得线程就会有机会重新获取该监视器。

Java 对象头(Mark Word)

前面提到所有对象都可以作为synchronized锁的对象,在同步的时候是获取对象的monitor,即操作Java对象头里的Mark Word 。

下面是32位为JVM Mark Word默认存储结构(无锁状态)

  • 对象的 hashCode:25位的对象标识hashCode,采用延迟加载技术。调用方法System.identityHashCode()计算,并会将结果写到该对象头中。当对象加锁后(偏向、轻量级、重量级),MarkWord的字节没有足够的空间保存hashCode,因此该值会移动到管程Monitor中。
  • **对象分代年龄 **:4位的Java对象年龄。每次 GC 未被回收累加的年龄就记录在此处,默认达到15次进入老年代(-XX:MaxTenuringThreshold 可通过该配置进行修改进入老年代的阈值,最大值为15[age 只有 4bit])。
  • 是否是偏向锁:1位的偏向锁标志位。
  • 锁标志位:2位锁标志位,4种标志位后面展示说明。

锁一共有4种状态,级别从低到高依次是:无锁状态、偏向锁状态、轻量级锁状态和重量级锁状态,这几个状态会随着竞争情况逐渐升级。锁可以升级但不能降级,意味着偏向锁升级成轻量级锁后不能降级成偏向锁。这种锁升级却不能降级的策略,目的是为了提高获得锁和释放锁的效率。

接下来分别介绍这三种锁的实现原理和步骤与上图结合思考。

偏向锁

HotSpot的作者经过研究发现,大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁。

偏向锁的获取

当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需简单地测试一下对象头的Mark Word里是否存储着指向当前线程的偏向锁。如果测试成功,表示线程已经获得了锁。如果测试失败,则需要再测试一下Mark Word中偏向锁的标识是否设置成1(表示当前是偏向锁):如果没有设置,则使用CAS竞争锁;如果设置了,则尝试使用CAS将对象头的偏向锁指向当前线程。

偏向锁的撤销

偏向锁使用了一种等到竞争出现才释放锁的机制,所以当其他线程尝试竞争偏向锁时,持有偏向锁的线程才会释放锁。

偏向锁的获得和撤销流程

线程1--展示了偏向锁获取的过程。

线程2--展示了偏向锁撤销的过程。

轻量级锁

轻量级锁介于 偏向锁与重量级锁之间,竞争的线程不会阻塞。

轻量级加锁

线程在执行同步块之前,JVM会先在当前线程的栈桢中创建用于存储锁记录的空间,并将对象头中的Mark Word复制到锁记录中,官方称为 Displaced Mark Word。然后线程尝试使用CAS将对象头中的Mark Word替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其他线程竞争锁,当前线程便尝试使用自旋来获取锁。

轻量级解锁

轻量级解锁时,会使用原子的CAS操作将Displaced Mark Word替换回到对象头,如果成功,则表示没有竞争发生。如果失败,表示当前锁存在竞争,锁就会膨胀成重量级锁。下图是两个线程同时争夺锁,导致锁膨胀的流程图。

轻量级锁及膨胀流程图

因为自旋会消耗CPU,为了避免无用的自旋(比如获得锁的线程被阻塞住了),一旦锁升级成重量级锁,就不会再恢复到轻量级锁状态。

当锁处于这个状态下,其他线程试图获取锁时,都会被阻塞住,当持有锁的线程释放锁之后会唤醒这些线程,被唤醒的线程就会进行新一轮的夺锁之争。

重量级锁

Synchronized 是通过对象内部的一个叫做 监视器锁(Monitor)来实现的。但是监视器锁本质又是依赖于底层的操作系统的Mutex Lock来实现的。而操作系统实现线程之间的切换这就需要从用户态转换到核心态,这个成本非常高,状态之间的转换需要相对比较长的时间,这就是为什么Synchronized效率低的原因。因此,这种依赖于操作系统 Mutex Lock 所实现的锁我们称之为 “重量级锁”。

三种锁的比较

分析了原理后,选择哪种锁就得看对应适用场景决定。

最后提一个 Synchronzied 避坑点(美团大佬分享):如果你的系统有很明确的 高低峰期,不建议使用 Synchronized,可以考虑使用 ReentrantLock。原因是 上面提到过 Synchronized 锁的膨胀是不可逆的,导致一旦经历了高峰期后就一直是重量级锁,性能也会由此一直达到一个瓶颈上不去了。

责任编辑:姜华 来源: 今日头条
相关推荐

2017-02-27 10:43:07

Javasynchronize

2022-10-28 10:23:27

Java多线程底层

2022-04-12 08:30:45

TomcatWeb 应用Servlet

2009-06-25 09:33:43

Java API程序员

2009-11-13 13:08:19

2020-12-07 06:23:48

Java内存

2015-08-03 09:54:26

Java线程Java

2010-09-07 14:21:22

PPPoE协议

2020-05-06 15:59:07

JavaScript程序员技术

2020-05-09 11:20:02

Java结构图虚拟机

2020-01-12 19:10:30

Java程序员数据

2011-03-23 11:01:55

LAMP 架构

2011-06-11 20:59:12

程序员

2014-08-15 14:25:48

Android程序员资源

2014-08-20 10:28:29

Android

2019-09-25 11:39:07

程序员编程技术

2022-10-24 09:00:47

画图工具程序员XMind

2021-01-19 15:59:14

程序员算法

2009-07-02 15:10:17

Java程序员面试

2021-03-29 23:05:36

程序员工具静态分析
点赞
收藏

51CTO技术栈公众号