前言
毫无疑问,synchronized 是我们用过的第一个并发关键字,很多博文都在讲解这个技术。不过大多数讲解还停留在对 synchronized 的使用层面,其底层的很多原理和优化,很多人可能并不知晓。因此本文将通过对 synchronized 的大量 C 源码分析,让大家对他的了解更加透彻点。
本篇将从为什么要引入 synchronized,常见的使用方式,存在的问题以及优化部分这四个方面描述,话不多说,开始表演。
可见性问题及解决
概念描述
指一个线程对共享变量进行修改,另一个能立刻获取到修改后的最新值。
代码展示
类:
public class Example1 {
复制代码
运行结果:
分析
这边先要了解下 Java 的内存模式,不明白的可点击传送门,todo。
下图线程 t1,t2 从主内存分别获取 flag=true,t1 空循环,直到 flag 为 false 的时候退出循环。t2 拿到 flag 的值,将其改为 false,并写入到主内存。此时主内存和线程 t2 的工作内存中 flag 均为 false,但是线程 t1 工作内存中的 flag 还是 true,所以一直退不了循环,程序将一直执行。
synchronized 如何解决可见性
首先我们尝试在 t1 线程中加一行打印语句,看看效果。
代码:
public class Example1 {
复制代码
运行结果:
我们发现 if 里面的语句已经打印出来了,线程 1 已经感知到线程 2 对 flag 的修改,即这条打印语句已经影响了可见性。这是为啥?
答案就是 println 中,我们看下源码:
println 有个上锁的过程,即操作如下:
获取同步锁。
清空自己工作内存上的变量。
从主内存获取最新值,并加载到工作内存中。
打印并输出。
所以这里解释了为什么线程 t1 加了打印语句之后,t1 立刻能感知 t2 对 flag 的修改。因为每次打印的时候其都从主内存上获取了最新值,当 t2 修改的时候,t1 立刻从主内存获取了值,所以进入了 if 语句,并最终能跳出循环。
synchronized 的原理就是清空自己工作内存上的值,通过将主内存最新值刷新到工作内存中,让各个线程能互相感知修改。
原子性问题及解决
概念描述
在一次或多个操作中,要不所有操作都执行,要不所有操作都不执行。
代码展示
类:
public class Example2 {
复制代码
运行结果:
分析
每个线程执行的逻辑是循环 1 万次,每次加 1,那我们希望的结果是 2 万,但是实际上结果是不足 2 万的。我们先用 javap 命令反汇编,我们看到很多代码,但是 number++ 涉及的指令有四句,具体看第二张图。
如果有多条线程执行这段 number++ 代码,当前 number 为 0,线程 1 先执行到 iconst_1 指令,即将执行 iadd 操作,而线程 2 执行到 getstatic 指令,这个时候 number 值还没有改变,所以线程 2 获取到的静态字段是 0,线程 1 执行完 iadd 操作,number 变为 1,线程 2 执行完 iadd 操作,number 还是 1。这个时候就发现问题了,做了两次 number++ 操作,但是 number 只增加了 1。
并发编程时,会出现原子性问题,当一个线程对共享变量操作到一半的时候,另外一个线程也有可能来操作共享变量,这个时候就出现了问题。
synchronized 如何解决原子性问题
在上面的分析中,我们已经知道发生问题的原因,number++ 是由四条指令组成,没有保证原子操作。所以,我们只要将 number++ 作为一个整体就行,即保证他的原子性。具体代码如下:
public class Example2 {
复制代码
我们看到最终 number 为 20000,那为什么要加上 synchronized,结果就正确了?我们再反编译下 Example2,可以看到在四行指令前后分别有 monitorenter 和 monitorexist,线程 1 在执行中间指令时,其他线程不可以进入 monitorenter,需要等线程 1 执行完 monitorexist,其他进程才能继续 monitorenter,进行自增操作。
有序性问题及解决
概念描述
代码中程序执行的顺序,Java 在编译和运行时会对代码进行优化,这样会导致我们最终的执行顺序并不是我们编写代码的书写顺序。
代码展示
咱先来看一个概念,重排序
,也就是语句的执行顺序会被重新安排。其主要分为三种:
编译器优化的重排序:可以重新安排语句的执行顺序。
指令级并行的重排序:现代处理器采用指令级并行技术,将多条指令重叠执行。
内存系统的重排序:由于处理器使用缓存和读写缓冲区,所以看上去可能是乱序的。
上面代码中的 a = new A(); 可能被被 JVM 分解成如下代码:
复制代码
复制代码
一旦假设发生了这样的重排序,比如线程 A 在执行了步骤 1 和步骤 3,但是步骤 2 还没有执行完。这个时候线程 B 进入了第一个语句,它会判断 a 不为空,即直接返回了 a。其实这是一个未初始化完成的 a,即会出现问题。
synchronized 如何解决有序性问题
给上面的三个步骤加上一个 synchronized 关键字,即使发生重排序也不会出现问题。线程 A 在执行步骤 1 和步骤 3 时,线程 B 因为没法获取到锁,所以也不能进入第一个语句。只有线程 A 都执行完,释放锁,线程 B 才能重新获取锁,再执行相关操作。
synchronized 的常见使用方式
修饰代码块(同步代码块)
synchronized (object) {
复制代码
修饰方法
synchronized void test(){
复制代码
synchronized 不能继承?(插曲)
父类 A:
public class A {
复制代码
子类 B:(未重写 test 方法)
public class B extends A {
复制代码
子类 C:(重写 test 方法)
public class C extends A {
复制代码
线程 A:
public class ThreadA extends Thread {
复制代码
线程 B:
public class ThreadB extends Thread {
复制代码
线程 C:
public class ThreadC extends Thread{
复制代码
测试类 test:
public class test {
复制代码
运行结果:
子类 B 继承了父类 A, 但是没有重写 test 方法,ThreadB 仍然是同步的。子类 C 继承了父类 A,也重写了 test 方法,但是未明确写上 synchronized,所以这个方法并不是同步方法。只有显式的写上 synchronized 关键字,才是同步方法。
所以 synchronized 不能继承这句话有歧义,我们只要记住子类如果想要重写父类的同步方法,synchronized 关键字一定要显示写出,否则无效。
修饰静态方法
synchronized static void test(){
复制代码
修饰类
synchronized (Example2.class) {
复制代码
Java 对象 Mark Word
在 JVM 中,对象在内存中的布局分为三块区域:对象头,实例数据和对齐数据,如下图:
其中 Mark Word 值在不同锁状态
下的展示如下:(重点看线程 id,是否为偏向锁,锁标志位信息)
在 64 位系统中,Mark Word 占了 8 个字节,类型指针占了 8 个字节,一共是16
个字节。Talk is cheap. Show me the code. 咱来看代码。
- 我们想要看 Java 对象的 Mark Word,先要加载一个 jar 包,在 pom.xml 添加即可。
复制代码
- 新建一个对象 A,拥有初始值为 666 的变量 x。
public class A {
复制代码
- 新建一个测试类 test,这涉及到刚才加载的 jar,我们打印 Java 对象。
import org.openjdk.jol.info.ClassLayout;
复制代码
- 我们发现对象头(object header)占了
12
个字节,为啥和上面说的 16 个字节不一样。
- 其实上是默认开启了指针压缩,我们需要关闭指针压缩,也就是添加
-XX:-UseCompressedOops
配置。
- 再次执行,发现对象头为 16 个字节。
偏向锁
什么是偏向锁
JDK1.6 之前锁为重量级锁(待会说,只要知道他和内核交互,消耗资源),1.6 之后 Java 设计人员发现很多情况下并不存在多个线程竞争的关系,所以为了资源问题引入了无锁
,偏向锁
,轻量级锁
,重量级锁
的概念。先说偏向锁,他是偏心,偏袒的意思,这个锁会偏向于第一个获取他的线程。
偏向锁演示
- 创建并启动一个线程,run 方法里面用了 synchronized 关键字,功能是打印 this 的 Java 对象。
public class test {
复制代码
标红的地方为 000,根据之前 Mark Word 在不同状态下的标志,得此为无锁状态。理论上一个线程使用 synchronized 关键字,应为偏向锁。
- 实际上偏向锁在 JDK1.6 之后是默认开启的,但是启动时间有延迟,所以需要添加参数
-XX:BiasedLockingStartupDelay=0
,让其在程序启动时立刻启动。
- 重新运行下代码,发现标红地方 101,对比 Mark Word 在不同状态下的标志,得此状态为偏向锁。
偏向锁原理图解
- 在线程的 run 方法中,刚执行到 synchronized,会判断当前对象是否为偏向锁和锁标志,没有任何线程执行该对象,我们可以看到是否为偏向锁为 0,锁标志位 01,即无锁状态。
- 线程会将自己的 id 赋值给 markword,即将原来的 hashcode 值改为线程 id,是否是偏向锁改为 1,表示线程拥有对象锁,可以执行下面的业务逻辑。
如果 synchronized 执行完,对象还是偏向锁状态;如果线程结束之后,会撤销偏向锁,将该对象还原成无锁状态。
- 如果同一个线程中又对该对象进行加锁操作,我们只要对比
对象的线程 id
是否与线程 id
相同,如果相同即为线程锁重入问题。
优势
加锁和解锁不需要额外的消耗,和执行非同步方法相比只有纳秒级的差距。
白话翻译
线程 1 锁定对象 this,他发现对象为无锁状态,所以将线程 id 赋值给对象的 Mark Word 字段,表示对象为线程 1 专用,即使他退出了同步代码,其他线程也不能使用该对象。
同学 A 去自习教室 C,他发现教室无人,所以在门口写了个名字,表示当前教室有人在使用,这样即使他出去吃了饭,其他同学也不能使用这个房间。
轻量锁
什么是轻量级锁
在多线程交替同步代码块的情况下,线程间没有竞争,使用轻量级锁可以避免重量级锁引入的性能消耗。
轻量级图解
- 在刚才偏向锁的基础上,如果有另外一个线程也想错峰使用该资源,通过对比线程 id 是否相同,Java 内存会立刻撤销偏向锁(需要等待全局安全点),进行锁升级的操作。
- 撤销完轻量级锁,会在线程 1 的方法栈中新增一个锁记录,对象的 Mark Word 与锁记录交换。
优势
线程不竞争的时候,避免直接使用重量级锁,提高了程序的响应速度。
白话翻译
在刚才偏向锁的基础上,另外一个线程也想要获取资源,所以线程 1 需要撤销偏向锁,升级为轻量锁。
同学 A 在使用自习教室外面写了自己的名字,所以同学 B 来也想要使用自习教室,他需要提醒同学 A,不能使用偏向锁,同学 A 将自习教室门口的名字擦掉,换成了一个书包,里面是自己的书籍。这样在同学 A 不使用自习教室的时候,同学 B 也能使用自习教室,只需要将自己的书包也挂在外面即可。这样下次来使用的同学就能知道已经有人占用了该教室。
重量级锁
什么是重量级锁
当多线程之间发生竞争,Java 内存会申请一个 Monitor 对象来实现。
重量级锁原理图解
在刚才的轻量级锁的基础上,线程 2 也想要申请资源,发现锁的标志位为 00,即为轻量级锁,所以向内存申请一个 Monitor, 让对象的 MarkWord 指向 Monitor 地址,并将 ower 指针指向线程 1 的地址,线程 2 放在等待队列里面,等线程 1 指向完毕,释放锁资源。
Monitor 源码分析
环境搭建
我们去官网http://openjdk.java.net/找下 open 源码,也可以通过其他途径下载。源码是 C 实现的,可以通过 DEV C++ 工具打开,效果如下图:
构造函数
我们先看下\hotspot\src\share\vm\runtime\ObjectMonitor.hpp
,以. hpp 结尾的文件是导入的一些包和一些声明,之后可以被. cpp 文件导入。
ObjectMonitor() {
复制代码
锁竞争的过程
我们先看下\hotspot\src\share\vm\interpreter\interpreterRuntime.cpp
,IRT_ENTRY_NO_ASYNC
即为锁竞争过程。
复制代码
slow_enter 实际上调用的 ObjectMonitor.cpp 的 enter 方法
void ATTR ObjectMonitor::enter(TRAPS) {
复制代码
白话翻译
同学 A 在使用自习教室的时候,同学 B 在同一时刻也想使用自习教室,那就发生了竞争关系。所以同学 B 在 A 运行过程中,加入等待队列。如果此时同学 C 也要使用该教室,也会加入等待队列。等同学 A 使用结束,同学 B 和 C 将竞争自习教室。
自旋优化
自旋优化比较简单,如果将其他线程加入等待队列,那之后唤醒并运行线程需要消耗资源,所以设计人员让其空转一会,看看线程能不能一会结束了,这样就不要在加入等待队列。
白话来说,如果同学 A 在使用自习教室,同学 B 可以回宿舍,等 A 使用结束再来,但是 B 回宿舍再来的过程需要 1 个小时,而 A 只要 10 分钟就结束了。所以 B 可以先不回宿舍,而是在门口等个 10 分钟,以防止来回时间的浪费。
结语
唉呀妈呀,终于结束了,累死了。终于将 synchronized 写完了,如果有不正确的地方,还需要各位指正。如果觉得写得还行,麻烦帮我点赞,评论哈。
参考资料
Java 中 System.out.println() 为何会影响内存可见性
发布于: 2020 年 06 月 05 日阅读数: 175
https://xie.infoq.cn/article/ae57ae101b6ecaf6bbeedcb42