学习
实践
活动
专区
工具
TVP
写文章
专栏首页Java面试必知必会Java面试-如何造好synchronized这艘火箭
原创

Java面试-如何造好synchronized这艘火箭

一、从线程安全开始

1.1、诱因

  • 存在共享数据(也称临界资源)
  • 存在多条线程共同操作这些共享数据 解决的根本办法其实很简单,只要保证同一时刻有且只有一个线程能操作共享数据,其他线程必须等到该线程处理完数据后再对共享数据进行处理。

1.2、锁的内存语义

线程释放锁,JMM会把该线程中对应的本地内存中的共享变量刷新到主内存中。 线程获取锁,JMM会把线程对应的本地内存置为无效,从而使被监视器保护的临界区代码必须从主内存中读取共享变量。

1.3、互斥锁

  • 互斥性:同一时刻只允许一个线程持有某个对象锁,互斥性也成为原子性
  • 可见性:确保锁释放之前,对共享数据的修改,对后续获得该锁的线程可见 Java中synchronized即为互斥锁,它锁的不是代码,锁的都是对象。

1.4、获取对象锁的方式:

1、同步代码块

// 代码示例
/**
 * 方法中有 synchronized(this|object) {} 同步代码块
 */
private void syncObjectBlock1() {
    System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " +
            new SimpleDateFormat("HH:mm:ss").format(new Date()));
    synchronized (this) {
        // 观察一下是否是同一个示例对象
        System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1: " + this);
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_Start: " +
                    new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncObjectBlock1_End: " +
                    new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2、同步非静态方法

/**
 * synchronized 修饰非静态方法
 */
private synchronized void syncObjectMethod1() {
    System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1: " +
            new SimpleDateFormat("HH:mm:ss").format(new Date()));
    try {
        // 观察一下是否是同一个示例对象
        System.out.println(Thread.currentThread().getName() + "syncObjectMethod1: " + this);
        System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_Start: " +
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "_SyncObjectMethod1_End: " +
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

注意:同步块和同步非静态方法锁的是同一个对象,即this;同一个类的不同对象锁是互不干扰的。

1.5、获取类锁的方式:

1、同步代码块

// 代码示例
private void syncClassBlock1() {
    System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1: " +
            new SimpleDateFormat("HH:mm:ss").format(new Date()));
    synchronized (SyncThread.class) {
        try {
            System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_Start: " +
                    new SimpleDateFormat("HH:mm:ss").format(new Date()));
            Thread.sleep(1000);
            System.out.println(Thread.currentThread().getName() + "_SyncClassBlock1_End: " +
                    new SimpleDateFormat("HH:mm:ss").format(new Date()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

2、同步静态方法

private synchronized static void syncClassMethod1() {
    System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1: " +
            new SimpleDateFormat("HH:mm:ss").format(new Date()));
    try {
        System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_Start: " +
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
        Thread.sleep(1000);
        System.out.println(Thread.currentThread().getName() + "_SyncClassMethod1_End: " +
                new SimpleDateFormat("HH:mm:ss").format(new Date()));
    } catch (InterruptedException e) {
        e.printStackTrace();
    }
}

注意:对象锁和类锁是不会相互干扰的。

二、如何实现synchronized

在前文简单的了解synchronized的使用,这在面试中显然是不够的。 本章我们来讲一下它的底层实现原理,主要围绕Java对象头和Monitor,以JDK8&&hotspot JVM为叙述基础。

2.1、Monitor

Java对象在内存中的布局分为对象头、实例数据、对齐填充。 锁对象是存储在对象头中,对象头的结构为,

对象头结构

说明

Mark Word

存储对象hashcode、分代年龄、锁类型、锁标志位等信息

Class Metadata Address

对象元数据指针地址,JVM通过该指针获取对象的class信息

synchronized为重量级锁,该信息就被记录在对象的Mark Word中;Moinitor是Java对象天生自带的一把锁。每一个对象都有一个Moinitor对象与之关联,在hotspot中它由ObjectMonitor实现的,来看看它里面定义了啥?

// hotspot源码截取
// initialize the monitor, exception the semaphore, all other fields
// are simple integers or pointers
ObjectMonitor() {
  _header       = NULL;
  _count        = 0;
  _waiters      = 0,
  _recursions   = 0;
  _object       = NULL;
  _owner        = NULL;
  _WaitSet      = NULL;
  _WaitSetLock  = 0 ;
  _Responsible  = NULL ;
  _succ         = NULL ;
  _cxq          = NULL ;
  FreeNext      = NULL ;
  _EntryList    = NULL ;
  _SpinFreq     = 0 ;
  _SpinClock    = 0 ;
  OwnerIsThread = 0 ;
  _previous_owner_tid = 0;
}

重点关注以下field,

  • _owner:指向持有ObjectMonitor对象的线程
  • _WaitSet:存放处于wait状态的线程队列,即调用wait()方法的线程
  • _EntryList:存放处于等待锁block状态的线程队列
  • _count:约为_WaitSet 和 _EntryList 的节点数之和
  • _cxq: 多个线程争抢锁,会先存入这个单向链表
  • _recursions: 记录重入次数 定义中的WaitSet与EntryList,与线程的等待池和锁池可以联系起来,每个对象锁的线程都会封装至ObjectWait对象,并存储在里面;owner指向持有ObjectMonitor对象的线程,当多个线程同时访问同一段代码时候,首先会进入EntryList中,排队等候;当线程调用wait方法,那么ObjectWait对象会重新存入WaitSet,等待被唤醒。 Monitor同样存在于Java对象的对象头中,synchronized就是通过该方式获取锁,这也解释了Java中为什么任意对象都可以作为锁。

2.2、字节码分析

接下来分析一下synchronized具体在字节码层面的实现,

public void syncsTask() {
    // 同步代码块
    synchronized (this) {
        System.out.println("Hello");
    }
}

// 同步方法
public synchronized void syncTask() {
    System.out.println("Hello Again");
}

javap -v打开上述class编译之后的class文件,让我们聚焦到code区域, 首先分析syncsTask方法,

// syncsTask方法字节码
public void syncsTask();
  descriptor: ()V
  flags: ACC_PUBLIC
  Code:
    stack=2, locals=3, args_size=1
       0: aload_0
       1: dup
       2: astore_1
       3: monitorenter
       4: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       7: ldc           #3                  // String Hello
       9: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
      12: aload_1
      13: monitorexit
      14: goto          22
      17: astore_2
      18: aload_1
      19: monitorexit
      20: aload_2
      21: athrow
      22: return

显而易见,同步代码块使用的是monitorenter与monitorexit指令,monitorenter指向同步代码块开始的位置,monitorexit则指明同步代码块结束的位置,两两配对执行。 当执行monitorenter指令时,当前线程将试图获取objectref(即对象锁) 所对应的monitor的持有权,当objectref的monitor的进入计数器为0,那线程可以成功取得monitor,并将计数器值设置为1,取锁成功。如果当前线程已经拥有objectref的monitor的持有权,那它可以重入这个monitor。 又一个新概念被引入了重入,下一节介绍(挖坑)。 字节码的19行多了一个monitorexit,前面不是说配对执行吗?这怎么多了一个monitorexit指令?其实这是编译器干了一些“坏事”,为了保证方法在异常时也能够正确的配对执行,编译器自动产生了一个异常处理器,可处理所有的异常并执行monitorexit指令,释放monitor。 再来看看syncTask(),

// syncTask方法字节码
public synchronized void syncTask();
  descriptor: ()V
  flags: ACC_PUBLIC, ACC_SYNCHRONIZED
  Code:
    stack=2, locals=1, args_size=1
       0: getstatic     #2                  // Field java/lang/System.out:Ljava/io/PrintStream;
       3: ldc           #5                  // String Hello Again
       5: invokevirtual #4                  // Method java/io/PrintStream.println:(Ljava/lang/String;)V
       8: return

这里我们未看到任何的monitor相关的指令,其实方法级的同步是隐式的无需通过指令来实现,出现在flags中的ACC_SYNCHRONIZED标志,即可用来区分方法是否同步。方法在运行时会判断标志位,执行线程也会取到monitor。

2.3、可重入

重入其实一句话解释就是当一个线程再次请求自己持有对象锁的共享数据时,这种情况属于重入。 synchronized是可重入锁;ReentrantLock也是。 即同一个线程可以输出Hello World不会死锁。

// 可重入
public void syncsTask() {
    synchronized (this) {
        System.out.println("Hello");
        synchronized (this){
            System.out.println("World");
        }
    }
}

三、为何嗤之以鼻

  • 早期JDK的synchronized是重量级锁,依赖于系统的Mutex Lock(互斥)
  • 线程之间切换从用户态转换至核心态,开销大。 hotspot做了很多的优化,JDK6之后synchronized的性能已经提升。 例如,自适应自旋、锁消除、锁粗化、轻量锁、偏向锁等等,使得线程之间更高效的共享数据,解决竞争问题,提高程序执行效率。

3.1、自旋锁与自适应自旋锁

许多情况下,共享数据的锁定状态持续时间较短,切换线程不值得。 通过让线程处于忙循环等待锁释放,期间不出让CPU,减少线程的切换,该锁在JDK4就被引入。JDK6之后默认开启,处于自旋便会不再挂起线程,但如果锁占用时间过长,就不再推荐使用了,这时候应该通过参数PreBlockSpin参数来更改。 自适应自旋锁,自旋的次数不再固定,由前一次在同一个锁上的自旋时间与锁的拥有者状态来决定。

3.2、锁消除

更彻底的优化,JIT编译时,对运行上下文进行扫描,去除不可能存在竞争的锁。

public void add(String str1, String str2) {
    // StringBuffer是线程安全,由于sb只会在append方法中使用,不可能被其他线程引用
    // 因此sb属于不可能共享的资源,JVM会自动消除内部的锁
    StringBuffer sb = new StringBuffer();
    sb.append(str1).append(str2);
}

3.3、锁粗化

JVM对锁的范围进行扩大,减少锁同步的代价。

public static String copyString100Times(String target){
    int i = 0;
    StringBuffer sb = new StringBuffer();
    while (i<100){
        sb.append(target);
    }
    return sb.toString();
}

3.4、synchronized的四个演变阶段

锁膨胀的方向:无锁、偏向锁、轻量级锁、重量级锁

偏向锁

减少同一线程获取锁的代价,大多数情况锁不存在竞争,总是由一个线程获取。 指一段同步代码一直被一个线程所访问,那么该线程会自动获取锁,降低获取锁的代价,不适用于锁竞争比较激烈的多线程场合。

轻量级锁(flag可以单独开一章讲讲)

偏向锁升级而来,适用于线程交替执行同步块,自旋

重量级锁

同步块或者方法执行时间较长,追求吞吐量,详见前面小节分析。

优缺点

优点

缺点

场景

偏向锁

加锁和解锁无需CAS操作,没有额外的性能消耗,和无锁方法执行时间仅存纳秒差异

如果线程间存在锁竞争,会带来额外锁撤销的消耗

只有一个线程访问同步代码

轻量级锁

竞争的线程不会阻塞,响应速度提升

若线程长时间无法获取锁,自旋会消耗CPU

线程交替执行的同步代码

重量级锁

线程竞争不自旋,不消耗CPU

线程阻塞,响应时间缓慢,多线程下,频繁获取释放,性能消耗多

追求吞吐,同步代码执行时间长

四、写在最后

对于Java线程这块的内容文章几乎没有涉及,按照面试中的路子,其实一般是会从线程的基础切入到多线程与并发。这里再立一个flag,有关线程基础后续会开一个blog。 目前缺少源码的调用流程可视化呈现,后续涉及到本文中阐述的流程会使用图形式。 本文为Java面试造火箭之多线程与并发系列一,后续还会涉及JUC与线程池相关内容。 期待大家的关注,我们一起前行,定能造成这火箭🚀。

原创声明,本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

登录 后参与评论
0 条评论

相关文章

  • 知乎最具争议的Java面试成神笔记,GitHub已下载量已过百万

    其实,由于不仅是因为今年受疫情影响,很多公司经营不下去,公司规模缩小,造成岗位缺少。更重要的是因为初级过剩,中级缺少,高级紧缺。所以说,作为一名程序员,如果不学...

    愿天堂没有BUG
  • Android高频面试专题 - 进阶篇(二)内存泄漏

    虚拟机栈:线程私有,随线程创建而创建。栈里面是一个一个“栈帧”,每个栈帧对应一次方法调用。栈帧中存放了局部变量表(基本数据类型变量和对象引用)、操作数栈、方法出...

    Android扫地僧
  • 聊聊面试中关于并发问题的应对方案

    技术zhai
  • 【从刷面试题到构建知识体系】Java底层-synchronized锁-1

    在技术论坛中,经常看到一种言论:面试造火箭,干活拧螺丝。我们平时写的大部分代码的确是CRDU,再提一个层次,也无非就是揉进去复杂一些的业务逻辑,把一堆的CRDU...

    小端
  • 测试要不要代码能力?

    很多小伙伴都会问,实际工作中会用到代码吗?是不是不用就可以不学了?我的答案是,一定要会!!!

    小雯子打豆豆
  • 《辐射4 VR》抢先看,11个最不容错过的场景

    VRPinea
  • 爱情36技之猿门授渔

    关注一猿小讲的粉丝们都知道,在上期的文章中,Python 菇凉在 Java 那小子的带领下,一条腿已经成功的迈入了猿门。

    一猿小讲
  • 聊聊阿里面试的三个层次!

    最近去阿里的菜鸟国际做了一次面试交流,发现大公司对于面试者的知识结构考核非常严谨,可以作为我们日常工作学习的指导。虽然很多人说面试问到的东西在实际工作中很少用到...

    Java后端技术
  • 2018年 iOS 面试心得

    面试了几家公司之后,最后来到了爱奇艺(坐标上海),工作的内容是笔者个人非常感兴趣的领域。能拿到这个结果对于入行两年半&非科班的笔者来说无疑是幸运的。

    用户2932962
  • 聊聊阿里面试的三个层次

    最近去阿里的菜鸟国际做了一次面试交流,发现大公司对于面试者的知识结构考核非常严谨,可以作为我们日常工作学习的指导。虽然很多人说面试问到的东西在实际工作中很少用到...

    陈树义
  • 一起来看看阿里面试官如何面试

    最近去阿里的菜鸟国际做了一次面试交流,发现大公司对于面试者的知识结构考核非常严谨,可以作为我们日常工作学习的指导。虽然很多人说面试问到的东西在实际工作中很少用到...

    牛客网
  • 最懂程序员面试的人,不一定是最优秀的开发者

    最近刷Github,发现大量的面试题收录仓库,且关注度很高,这种现象很容易理解,程序员通过跳槽提升薪资,而跳槽需要面试,刷的面试题越多,面试成功的概率也就越大,...

    zhaoolee
  • 1000艘星舰,2050年送100万人润去火星!马斯克再吹「当代诺亚方舟」

    ---- 新智元报道   编辑:袁榭 【新智元导读】2022年6月初,马斯克再弹老调,又一次宣布要送千艘星舰上火星、完成当代的诺亚方舟壮举。 世界首富、兼...

    新智元
  • 刚刚,马斯克创造人类航天新壮举!空中炸毁火箭,然后成功实现载人舱逃逸

    刚刚,在美国火箭发射司空见惯的佛罗里达州,Space X完成了一次特殊的发射任务——也是人类航空史上的第一次。

    量子位
  • 从JVM内存模型来看并发编程中的可见性和有序性

    工作做螺丝钉,面试造火箭,我想这个是每个程序员比较头疼的事情,但是又有必须经历的流程,我们再聊聊从JVM内存模型来看并发编程中的可见性和有序性。

    35岁程序员那些事
  • 5分钟面试指南(第三十四篇 存储最后一弹)

    本部分我们会为大家提供一些python初级工程师在面试过程中遇到的常见的面试题目,期望达到的效果:

    python编程从入门到实践
  • 5分钟面试指南(第一篇)

    本部分我们会为大家提供一些python初级工程师在面试过程中遇到的常见的面试题目,期望达到的效果:

    python编程从入门到实践
  • 为什么非要问JVM?后端开发太难了

    面试官干什么玩意,就这么一家小公司,能玩到这样的程度嘛?整到这样的程度,感觉很厉害的样子,现实版的“面试造火箭”就让我遇见了吗?

    程序猿DD

扫码关注腾讯云开发者

领取腾讯云代金券