Synchronized用法原理和锁优化升级过程(面试)
简介
多线程一直是面试中的重点和难点,无论你现在处于啥级别段位,对synchronized关键字的学习避免不了,这是我的心得体会。下面咱们以面试的思维来对synchronized做一个系统的描述,如果有面试官问你,说说你对synchronized的理解?你可以从synchronized使用层面,synchronized的JVM层面,synchronized的优化层面3个方面做系统回答,说不定面试官会对你刮目相看哦!文章会有大量的代码是方便理解的,如果你有时间一定要动手敲下加深理解和记忆。如果这篇文章能对您能有所帮助是我创作路上最大欣慰。
synchronized使用层面
大家都知道synchronized是一把锁,锁究竟是什么呢?举个例子,你可以把锁理解为厕所门上那把锁的唯一钥匙,每个人要进去只能拿着这把钥匙可以去开这个厕所的门,这把钥匙在一时刻只能有一个人拥有,有钥匙的人可以反复出入厕所,在程序中我们叫做这种重复出入厕所行为叫锁的可重入。它可以修饰静态方法,实例方法和代码块 ,那下面我们一起来看看synchronized用于同步代码锁表达的意思。
- 对于普通同步方法,锁的是对象实例。
- 对于静态同步方法,锁的是类的Class对象。
- 对于同步代码块,锁的是括号中的对象。
先说下同步和异步的概念。
- 同步:交替执行。
- 异步:同时执行。
举个例子比如吃饭和看电视两件事情,先吃完饭后再去看电视,在时间维度上这两件事是有先后顺序的,叫同步。可以一边吃饭,一边看刷剧,在时间维度上是不分先后同时进行的,饭吃完了电视也看了,就可以去学习了,这就是异步,异步的好处是可以提高效率,这样你就可以节省时间去学习了。
下面我们看看代码,代码中有做了很详细的注释,可以复制到本地进行测试。如果有synchronized基础的童鞋,可以跳过锁使用层面的讲解。
1 /**
2 * @author :jiaolian
3 * @date :Created in 2020-12-17 14:48
4 * @description:测试静态方法同步和普通方法同步是不同的锁,包括synchronized修饰的静态代码块用法;
5 * @modified By:
6 * 公众号:叫练
7 */
8 public class SyncTest {
9
10 public static void main(String[] args) {
11 Service service = new Service();
12 /**
13 * 启动下面4个线程,分别测试m1-m4方法。
14 */
15 Thread threadA = new Thread(() -> Service.m1());
16 Thread threadB = new Thread(() -> Service.m2());
17 Thread threadC = new Thread(() -> service.m3());
18 Thread threadD = new Thread(() -> service.m4());
19 threadA.start();
20 threadB.start();
21 threadC.start();
22 threadD.start();
23
24 }
25
26 /**
27 * 此案例说明了synchronized修饰的静态方法和普通方法获取的不是同一把锁,因为他们是异步的,相当于是同步执行;
28 */
29 private static class Service {
30 /**
31 * m1方法synchronized修饰静态方法,锁表示锁定的是Service.class
32 */
33 public synchronized static void m1() {
34 System.out.println("m1 getlock");
35 try {
36 Thread.sleep(2000);
37 } catch (InterruptedException e) {
38 e.printStackTrace();
39 }
40 System.out.println("m1 releaselock");
41 }
42
43 /**
44 * m2方法synchronized修饰静态方法,锁表示锁定的是Service.class
45 * 当线程AB同时启动,m1和m2方法是同步的。可以证明m1和m2是同一把锁。
46 */
47 public synchronized static void m2() {
48 System.out.println("m2 getlock");
49 System.out.println("m2 releaselock");
50 }
51
52 /**
53 * m3方法synchronized修饰的普通方法,锁表示锁定的是Service service = new Service();中的service对象;
54 */
55 public synchronized void m3() {
56 System.out.println("m3 getlock");
57 try {
58 Thread.sleep(1000);
59 } catch (InterruptedException e) {
60 e.printStackTrace();
61 }
62 System.out.println("m3 releaselock");
63 }
64
65 /**
66 * 1.m4方法synchronized修饰的同步代码块,锁表示锁定的是当前对象实例,也就是Service service = new Service();中的service对象;和m3一样,是同一把锁;
67 * 2.当线程CD同时启动,m3和m4方法是同步的。可以证明m3和m4是同一把锁。
68 * 3.synchronized也可以修饰其他对象,比如synchronized (Service.class),此时m4,m1,m2方法是同步的,启动线程ABD可以证明。
69 */
70 public void m4() {
71 synchronized (this) {
72 System.out.println("m4 getlock");
73 System.out.println("m4 releaselock");
74 }
75 }
76
77 }
78 }
经过上面的测试,你可以能会有疑问,锁既然是存在的,那它存储在什么地方?答案:对象里面。下面我们用代码来证明下。
锁在对象头里面,一个对象包括对象头,实例数据和对齐填充。对象头包括MarkWord和对象指针,对象指针是指向方法区的对象类型的,,实例对象就是属性数据,一个对象可能有很多属性,属性是动态的。对齐填充是为了补齐字节数的,如果对象大小不是8字节的整数倍,需要补齐剩余的字节数,这是方便计算机来计算的。在64位机器里面,一个对象的对象头一般占12个自己大小,在64位操作系统一般占4个字节,所以MarkWord就是8个字节了。
MarkWord包括对象hashcode,偏向锁标志位,线程id和锁的标识。为了方便测试对象头的内容,需要引入maven openjdk的依赖包。
<dependency>
<groupId>org.openjdk.jol</groupId>
<artifactId>jol-core</artifactId>
<version>0.10</version>
</dependency>
/**
* @author :duyang
* @date :Created in 2020-05-14 20:21
* @description:对象占用内存
* @modified By:
*
* Fruit对象头是12字节(markword+class)
* int 占4个字节
*
* 32位机器可能占8个字节;
*
* Object对象头12 对齐填充4 一共是16
*/
public class ObjectMemory {
public static void main(String[] args) {
//System.out.print(ClassLayout.parseClass(Fruit.class).toPrintable());
System.out.print(ClassLayout.parseInstance(Fruit.class).toPrintable());
}
} /**
*Fruit 测试类
*/
public class Fruit { //占一个字节大小
private boolean flag; }
测试结果:下面画红线的3行分别表示对象头,实例数据和对齐填充。对象头是12个字节,实例数据Fruit对象的一个boolean字段flag占1个字节大小,其余3个字节是对齐填充的部分,一共是16个字节大小。
咦?你说的锁呢,怎么没有看到呢?小伙,别着急,待会我们讲到synchronized升级优化层面的时候再来详细分析一波。下面我们先分析下synchronized在JVM层面的意思。
最后上图文总结:
synchronized JVM层面
1 /**
2 * @author :jiaolian
3 * @date :Created in 2020-12-20 13:43
4 * @description:锁的jvm层面使用
5 * @modified By:
6 * 公众号:叫练
7 */
8 public class SyncJvmTest {
9 public static void main(String[] args) {
10 synchronized (SyncJvmTest.class) {
11 System.out.println("jvm同步测试");
12 }
13 }
14 }
上面的案例中,我们同步代码块中我们简单输出一句话,我们主要看看jvm中它是怎么实现的。我们用Javap -v SyncJvmTest.class反编译出上面的代码,如下图所示。
上图第一行有一个monitorenter和第六行一个monitorexit,中间的jvm指令(2-5行)对应的Java代码中的main方法的代码,synchronized就是依赖于这两个指令实现。我们来看看JVM规范中monitorenter语义。
- 每个对象都有一把锁,当一个线程进入同步代码块,都会去获取这个对象所持有monitor对象锁(C++实现),如果当前线程获取锁,会把monitor对象进入数自增1次。
- 如果该线程重复进入,会把monitor对象进入数再次自增1次。
- 当有其他线程进入,会把其他线程放入等待队列排队,直到获取锁的线程将monitor对象的进入数设置为0释放锁,其他线程才有机会获取锁。
synchronized的优化层面
synchronized是一个重量级锁,主要是因为线程竞争锁会引起操作系统用户态和内核态切换,浪费资源效率不高,在jdk1.5之前,synchronized没有做任何优化,但在jdk1.6做了性能优化,它会经历偏向锁,轻量级锁,最后才到重量级锁这个过程,在性能方面有了很大的提升,在jdk1.7的ConcurrentHashMap是基于ReentrantLock的实现了锁,但在jdk1.8之后又替换成了synchronized,就从这一点可以看出JVM团队对synchronized的性能还是挺有信心的。下面我们分别来介绍下无锁,偏向锁,轻量级锁,重量级锁。下面我们我画张图来描述这几个级别锁的在对象头存储状态。如图所示。
- 无锁。如果不加synchronized关键字,表示无锁,很好理解。
- 偏向锁。
- 升级过程:当线程进入同步块时,Markword会存储偏向线程的id并且cas将Markword锁状态标识为01,是否偏向用1表示当前处于偏向锁(对着上图来看),如果是偏向线程下次进入同步代码只要比较Markword的线程id是否和当前线程id相等,如果相等不用做任何操作就可以进入同步代码执行,如果不比较后不相等说明有其他线程竞争锁,synchronized会升级成轻量级锁。这个过程中在操作系统层面不用做内核态和用户态的切换,减少切换线程带来的资源消耗。
- 膨胀过程:当有另外线程进入,偏向锁会升级成轻量级锁。比如线程A是偏向锁,这是B线程进入,就会成轻量级锁,只要有两个线程就会升级成轻量级锁。
下面我们代码来看下偏向锁的锁状态。
1 package com.duyang.base.basic.markword;
2
3 import lombok.SneakyThrows;
4 import org.openjdk.jol.info.ClassLayout;
5
6 /**
7 * @author :jiaolian
8 * @date :Created in 2020-12-19 11:25
9 * @description:markword测试
10 * @modified By:
11 * 公众号:叫练
12 */
13 public class MarkWordTest {
14
15 private static Fruit fruit = new Fruit();
16
17 public static void main(String[] args) throws InterruptedException {
18 Task task = new Task();
19 Thread threadA = new Thread(task);
20 Thread threadB = new Thread(task);
21 Thread threadC = new Thread(task);
22 threadA.start();
23 //threadA.join();
24 //threadB.start();
25 //threadC.start();
26 }
27
28 private static class Task extends Thread {
29
30 @SneakyThrows
31 @Override
32 public void run() {
33 synchronized (fruit) {
34 System.out.println("==================="+Thread.currentThread().getId()+" ");
35 try {
36 Thread.sleep(3000);
37 } catch (InterruptedException e) {
38 e.printStackTrace();
39 }
40 System.out.print(ClassLayout.parseInstance(fruit).toPrintable());
41 }
42 }
43 }
44 }
上面代码启动线程A,控制台输出如下图所示,红色标记3个bit是101分别表示,高位的1表示是偏向锁,01是偏向锁标识位。符合偏向锁标识的情况。
- 轻量级锁。
- 升级过程:在线程运行获取锁后,会在栈帧中创造锁记录并将MarkWord复制到锁记录,然后将MarkWord指向锁记录,如果当前线程持有锁,其他线程再进入,此时其他线程会cas自旋,直到获取锁,轻量级锁适合多线程交替执行,效率高(cas只消耗cpu,我在cas原理一篇文章中详细讲过。)。
- 膨胀过程:有两种情况会膨胀成重量级锁。1种情况是cas自旋10次还没获取锁。第2种情况其他线程正在cas获取锁,第三个线程竞争获取锁,锁也会膨胀变成重量级锁。
下面我们代码来测试下轻量级锁的锁状态。
打开23行-24行代码,执行线程A,B,我的目的是顺序执行线程A B ,所以我在代码中先执行threadA.join(),让A线程先执行完毕,再执行B线程,如下图所示MarkWord锁状态变化,线程A开始是偏向锁用101表示,执行线程B就变成轻量级锁了,锁状态变成了00,符合轻量级锁锁状态。证明完毕。
- 重量级锁。重量级锁升级后是不可逆的,也就是说重量锁不可以再变为轻量级锁。
打开25行代码,执行线程A,B,C,我的目的是先执行线程A,在代码中先执行threadA.join(),让A线程先执行完毕,然后再同时执行线程BC ,如下图所示看看MarkWord锁状态变化,线程A开始是偏向锁,到同时执行线程BC,因为有激烈竞争,属于轻量级锁膨胀条件第2种情况,当其他线程正在cas获取锁,第三个线程竞争获取锁,锁也会膨胀变成重量级锁。此时BC线程锁状态都变成了10,这种情况符合重量级锁锁状态。膨胀重量级锁证明完毕。
到此为止,我们已经把synchronized锁升级过程中的锁状态通过代码的形式都证明了一遍,希望对你有帮助。下图是自己总结。
总结
多线程synchronized一直是个很重要的话题,也是面试中常见的考点。希望大家都能尽快理解掌握,分享给你们希望你们喜欢!
我是叫练,多叫多练,欢迎大家和我一起讨论交流,我会尽快回复大家,喜欢点赞和关注哦!公众号【叫练】。
- 清除所有标记
- 清除选中的标记
- 错误类型
- 无错字 - 写作(在线版)
Synchronized用法原理和锁优化升级过程(面试)的更多相关文章
- java并发笔记之四synchronized 锁的膨胀过程(锁的升级过程)深入剖析
警告⚠️:本文耗时很长,先做好心理准备,建议PC端浏览器浏览效果更佳. 本篇我们讲通过大量实例代码及hotspot源码分析偏向锁(批量重偏向.批量撤销).轻量级锁.重量级锁及锁的膨胀过程(也就是锁的升 ...
- synchronized(三) 锁的膨胀过程(锁的升级过程)深入剖析
警告⚠️:本文耗时很长,先做好心理准备................哈哈哈 本篇我们讲通过大量实例代码及hotspot源码分析偏向锁(批量重偏向.批量撤销).轻量级锁.重量级锁及锁的膨胀过程(也就是 ...
- synchronized的实现原理及锁优化
记得刚刚开始学习Java的时候,一遇到多线程情况就是synchronized.对于当时的我们来说,synchronized是如此的神奇且强大.我们赋予它一个名字“同步”,也成为我们解决多线程情况的良药 ...
- synchronized底层实现原理及锁优化
一.概述 1.synchronized作用 原子性:synchronized保证语句块内操作是原子的 可见性:synchronized保证可见性(通过"在执行unlock之前,必须先把此变量 ...
- 深入介绍Java中的锁[原理、锁优化、CAS、AQS]
1.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 2.锁实现的基本原理 2.1.volatile Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新, ...
- Java中的锁[原理、锁优化、CAS、AQS]
1.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 2.锁实现的基本原理 2.1.volatile Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新, ...
- Java中的锁原理、锁优化、CAS、AQS详解!
阅读本文大概需要 2.8 分钟. 来源:jianshu.com/p/e674ee68fd3f 一.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 二.锁实现的基本原理 2.1.v ...
- Java 中的锁原理、锁优化、CAS、AQS 详解!(转)
1.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 2.锁实现的基本原理 2.1.volatile Java编程语言允许线程访问共享变量, 为了确保共享变量能被准确和一致地更新, ...
- Java 中的锁原理、锁优化、CAS、AQS 详解!
来源:jianshu.com/p/e674ee68fd3f 1.为什么要用锁? 锁-是为了解决并发操作引起的脏读.数据不一致的问题. 2.锁实现的基本原理 2.1.volatile Java编程语言允 ...
随机推荐
- ubuntu安装imagick扩展
注意:安装该扩展不要求安装ImageMagick从http://pecl.php.net/package/imagick找到imagick的最新的版本 Linux代码 wget http://pecl ...
- iPhone/iOS开启个人热点的相关位置调整小结
冬至已到,圣诞将近,最近公司项目实在太多,三四个项目反复的切换真的让人焦头烂额,趁今天有点空,把维护的三个项目顺利送出,刚好可以缕缕思路,记录一下最近遇到的问题.说不着急那是假的,客户一天天的催的确实 ...
- 精尽MyBatis源码分析 - 文章导读
该系列文档是本人在学习 Mybatis 的源码过程中总结下来的,可能对读者不太友好,请结合我的源码注释(Mybatis源码分析 GitHub 地址.Mybatis-Spring 源码分析 GitHub ...
- H3CNE认证(题库)
H3CNE考试的题库,均为发烧友收集的,拥有将近认证考试的百分之八十五的题,但答案不具备官方性,但是题库具有解析. https://huxiaoyao.lanzous.com/b01tr2skd 密码 ...
- vue微博回调接口
1.vue微博回调空页面 注:微博回调空页面为: http://127.0.0.1:8888/oauth/callback/ 1.1 页面路径 components\oauth.vue <tem ...
- 怎么用fio测试存储性能
1 /// -rw=read(100%顺序读) -rw=write(100%顺序写) -rw=randread(100%随机读) -rw=randwrite(100%随机写), 2 ///-rw=rw ...
- 软件工程与UML第一次作业
这个作业属于哪个课程 https://edu.cnblogs.com/campus/fzzcxy/2018SE2/ 这个作业要求在哪里 https://edu.cnblogs.com/campus/f ...
- 发现了一个关于 gin 1.3.0 框架的 bug
gin 1.3.0 框架 http 响应数据错乱问题排查 问题概述 客户端同时发起多个http请求,gin接受到请求后,其中一个接口响应内容为空,另外一个接口响应内容包含接口1,接口2的响应内容,导致 ...
- JZOJ2020年8月13日提高组反思
JZOJ2020年8月13日提高组反思 T1 打了3h+,然后自己的小数据都没过 果断选择交对拍的暴力 下次还是注意时间吧 T2 一下三题都没时间打了 看了题目觉得特别烦人(有式子) 再看发现式子类似 ...
- 第11.24节 Python 中re模块的其他函数
一. re.compile函数 正则表达式编译函数,在后面章节专门介绍. 二. re.escape(pattern) re.escape是一个工具函数,用于对字符串pattern中所有可能被视为正则表 ...