多线程一直是面试中的重点和难点,不管你如今处于啥级别段位,对synchronized关键字的学习避免不了,这是个人心得体会。下面我们以面试的思惟来对synchronized作一个系统的描述,若是有面试官问你,说说你对synchronized的理解?你能够从synchronized使用层面,synchronized的JVM层面,synchronized的优化层面3个方面作系统回答,说不定面试官会对你另眼相看哦!文章会有大量的代码是方便理解的,若是你有时间必定要动手敲下加深理解和记忆。若是这篇文章能对您能有所帮助是我创做路上最大欣慰。java
你们都知道synchronized是一把锁,锁到底是什么呢?举个例子,你能够把锁理解为厕所门上那把锁的惟一钥匙,每一个人要进去只能拿着这把钥匙能够去开这个厕所的门,这把钥匙在一时刻只能有一我的拥有,有钥匙的人能够反复出入厕所,在程序中咱们叫作这种重复出入厕所行为叫锁的可重入。它能够修饰静态方法,实例方法和代码块 ,那下面咱们一块儿来看看synchronized用于同步代码锁表达的意思。面试
先说下同步和异步的概念。性能优化
举个例子好比吃饭和看电视两件事情,先吃完饭后再去看电视,在时间维度上这两件事是有前后顺序的,叫同步。能够一边吃饭,一边看刷剧,在时间维度上是不分前后同时进行的,饭吃完了电视也看了,就能够去学习了,这就是异步,异步的好处是能够提升效率,这样你就能够节省时间去学习了。多线程
下面咱们看看代码,代码中有作了很详细的注释,能够复制到本地进行测试。若是有synchronized基础的童鞋,能够跳过锁使用层面的讲解。异步
/** * @author :jiaolian * @date :Created in 2020-12-17 14:48 * @description:测试静态方法同步和普通方法同步是不一样的锁,包括synchronized修饰的静态代码块用法; * @modified By: * 公众号:叫练 */ public class SyncTest { public static void main(String[] args) { Service service = new Service(); /** * 启动下面4个线程,分别测试m1-m4方法。 */ Thread threadA = new Thread(() -> Service.m1()); Thread threadB = new Thread(() -> Service.m2()); Thread threadC = new Thread(() -> service.m3()); Thread threadD = new Thread(() -> service.m4()); threadA.start(); threadB.start(); threadC.start(); threadD.start(); } /** * 此案例说明了synchronized修饰的静态方法和普通方法获取的不是同一把锁,由于他们是异步的,至关因而同步执行; */ private static class Service { /** * m1方法synchronized修饰静态方法,锁表示锁定的是Service.class */ public synchronized static void m1() { System.out.println("m1 getlock"); try { Thread.sleep(2000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m1 releaselock"); } /** * m2方法synchronized修饰静态方法,锁表示锁定的是Service.class * 当线程AB同时启动,m1和m2方法是同步的。能够证实m1和m2是同一把锁。 */ public synchronized static void m2() { System.out.println("m2 getlock"); System.out.println("m2 releaselock"); } /** * m3方法synchronized修饰的普通方法,锁表示锁定的是Service service = new Service();中的service对象; */ public synchronized void m3() { System.out.println("m3 getlock"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("m3 releaselock"); } /** * 1.m4方法synchronized修饰的同步代码块,锁表示锁定的是当前对象实例,也就是Service service = new Service();中的service对象;和m3同样,是同一把锁; * 2.当线程CD同时启动,m3和m4方法是同步的。能够证实m3和m4是同一把锁。 * 3.synchronized也能够修饰其余对象,好比synchronized (Service.class),此时m4,m1,m2方法是同步的,启动线程ABD能够证实。 */ public void m4() { synchronized (this) { System.out.println("m4 getlock"); System.out.println("m4 releaselock"); } } } }
通过上面的测试,你能够能会有疑问,锁既然是存在的,那它存储在什么地方?答案:对象里面。下面咱们用代码来证实下。jvm
锁在对象头里面,一个对象包括对象头,实例数据和对齐填充。对象头包括MarkWord和对象指针,对象指针是指向方法区的对象类型的,,实例对象就是属性数据,一个对象可能有不少属性,属性是动态的。对齐填充是为了补齐字节数的,若是对象大小不是8字节的整数倍,须要补齐剩余的字节数,这是方便计算机来计算的。在64位机器里面,一个对象的对象头通常占12个本身大小,在64位操做系统通常占4个字节,因此MarkWord就是8个字节了。maven
MarkWord包括对象hashcode,偏向锁标志位,线程id和锁的标识。为了方便测试对象头的内容,须要引入maven openjdk的依赖包。ide
<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层面的意思。学习
最后上图文总结:
/** * @author :jiaolian * @date :Created in 2020-12-20 13:43 * @description:锁的jvm层面使用 * @modified By: * 公众号:叫练 */ public class SyncJvmTest { public static void main(String[] args) { synchronized (SyncJvmTest.class) { System.out.println("jvm同步测试"); } } }
上面的案例中,咱们同步代码块中咱们简单输出一句话,咱们主要看看jvm中它是怎么实现的。咱们用Javap -v SyncJvmTest.class反编译出上面的代码,以下图所示。
上图第一行有一个monitorenter和第六行一个monitorexit,中间的jvm指令(2-5行)对应的Java代码中的main方法的代码,synchronized就是依赖于这两个指令实现。咱们来看看JVM规范中monitorenter语义。
synchronized是一个重量级锁,主要是由于线程竞争锁会引发操做系统用户态和内核态切换,浪费资源效率不高,在jdk1.5以前,synchronized没有作任何优化,但在jdk1.6作了性能优化,它会经历偏向锁,轻量级锁,最后才到重量级锁这个过程,在性能方面有了很大的提高,在jdk1.7的ConcurrentHashMap是基于ReentrantLock的实现了锁,但在jdk1.8以后又替换成了synchronized,就从这一点能够看出JVM团队对synchronized的性能仍是挺有信心的。下面咱们分别来介绍下无锁,偏向锁,轻量级锁,重量级锁。下面咱们我画张图来描述这几个级别锁的在对象头存储状态。如图所示。
下面咱们代码来看下偏向锁的锁状态。
package com.duyang.base.basic.markword; import lombok.SneakyThrows; import org.openjdk.jol.info.ClassLayout; /** * @author :jiaolian * @date :Created in 2020-12-19 11:25 * @description:markword测试 * @modified By: * 公众号:叫练 */ public class MarkWordTest { private static Fruit fruit = new Fruit(); public static void main(String[] args) throws InterruptedException { Task task = new Task(); Thread threadA = new Thread(task); Thread threadB = new Thread(task); Thread threadC = new Thread(task); threadA.start(); //threadA.join(); //threadB.start(); //threadC.start(); } private static class Task extends Thread { @SneakyThrows @Override public void run() { synchronized (fruit) { System.out.println("==================="+Thread.currentThread().getId()+" "); try { Thread.sleep(3000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.print(ClassLayout.parseInstance(fruit).toPrintable()); } } } }
上面代码启动线程A,控制台输出以下图所示,红色标记3个bit是101分别表示,高位的1表示是偏向锁,01是偏向锁标识位。符合偏向锁标识的状况。
下面咱们代码来测试下轻量级锁的锁状态。
打开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一直是个很重要的话题,也是面试中常见的考点。但愿你们都能尽快理解掌握,分享给大家但愿大家喜欢!
我是叫练,多叫多练,欢迎你们和我一块儿讨论交流,我会尽快回复你们,喜欢点赞和关注哦!公众号【叫练】。