BAT美团滴滴java面试大纲(带答案版)之三:多线程synchronized

继续面试大纲系列文章。

  从这一篇开始,我们进入ava编程中的一个重要领域---多线程!多线程就像武学中对的吸星大法,理解透了用好了可以得道成仙,俯瞰芸芸众生;而滥用则会遭其反噬。

  在多线程编程中要渡的首个“劫”,则是Synchronized。了解其底层实现,无论是在面试中还是在平时工作中,都大有裨益。我们知其然,知其所以然,才能得心应手少挖坑。

  我们知道,多线程的核心思想是通过增加线程数量来并发的运行,来提高效率,也就是数量决胜论,而不是质量决胜(提高每个线程的处理能力)。多线程编程中面临的最大挑战,是如何解决多个线程同时修改一个公用的变量所带来的变量值不确定性问题。顺着这个思路分析,常用办法,无非就是,要么对变量动手,在一个线程修改时,变量值被锁定。要么是对修改的操作动手,在该段代码执行时,对其加锁,其他线程不可以在同一时刻进入该段代码执行。

  Synchronized,正是实现了后一种办法。

Synchronized

  1. 问:你平时涉及到多线程编程多不多?谈谈你对Synchronized锁的理解
  2. 分析:多从实现原理,工作机制来描述
  3. 答:
    1. 在多线程编程中,为了达到线程安全的目的,我们往往通过加锁的方式来实现。而Synchronized正是java提供给我们的非常重要的锁之一。它属于jvm级别加锁,底层实现是:在编译过程中,在指令级别加入一些标识来实现的。例如,同步代码块,会在同步代码块首尾加入monitorenter和monitorexit字节码指令,这两个指令都需要一个reference类型的参数,指明要加锁和解锁的对象,同步方法则是通过在修饰符上加acc_synchronized标识实现。在执行到这些指令(标识)时,本质都是获取、占有、释放monitor锁对象实现互斥,也就是说,同一时刻,只能有一个线程能成功的获取到这个锁对象。我们看一段加了synchronized关键字的代码编译后的字节码。编译前: 编译后: 重点关注14行,和20行。
    public class test extends java.lang.Object{
    public test();
      Code:
       0:   aload_0
       1:   invokespecial   #1; //Method java/lang/Object."":()V
       4:   nop
       5:   return
    
    public static void main(java.lang.String[]);
      Code:
       0:   new     #2; //class Object
       3:   dup
       4:   invokespecial   #1; //Method java/lang/Object."":()V
       7:   dup
       8:   astore_1
       9:   monitorenter // Enter the monitor associated with object 
       10:  iconst_0
       11:  istore_2
       12:  nop
       13:  aload_1
       14:  monitorexit // Exit the monitor associated with object 
       15:  goto    23
       18:  astore_3
       19:  aload_1
       20:  monitorexit // Be sure to exit monitor... 
       21:  aload_3
       22:  athrow
       23:  nop
       24:  return
      Exception table:
       from   to  target type
        10    15    18   any
        18    21    18   any
    
    }
    1.  在使用Synchronized时,用到的方法是wait和notify(或notifyAll),他们的原理是,调用wait方法后,线程让出cpu资源,释放锁,进入waiting状态,进入等待队列【第一个队列】。当有其他线程调用了notify或者notifyAll唤醒时,会将等待队列里的线程对象,移入阻塞队列【第二个队列】,状态是blocked,等待锁被释放后(这个释放时机,由虚拟机来决定,人为无法干预),开始竞争锁。
    2. Synchronized无法中断正在阻塞队列或者等待队列的线程。
1 public class test {
2   public test() {
3   }
4   public static void main(String[] args) {
5     synchronized(new Object()){
6         int i = 0;
7     }
8   }
9 }

  4.扩展:Synchronized提供了以下几种类型的锁:偏向锁、轻量级锁、重量级锁。在大部分情况下,并不存在多线程竞争,更常见的是一个线程多次获取同一个锁。那么很多的消耗,其实是在锁的获取与释放上。Synchronized一直在被优化,可以说Synchronized虽然推出的较早,但是效率并不比后来推出的Lock差。

  1.  偏向锁:在jdk1.6中引入,目的是消除在无竞争情况下的同步原语(翻译成人话就是,即使加了synchronized关键字,但是在没有竞争的时候,没必要去做获取-持有-释放锁对象的操作,提高程序运行性能)。怎么做呢?当锁对象第一次被线程A获取时,虚拟机会把对象头中的标志位设置为01,也就是代表偏向模式。同时把代表这个线程A的ID,通过CAS方式,更新到锁对象头的MarkWord中。相同的线程下次再次申请锁的时候,只需要简单的比较线程ID即可。以上操作成功,则成功进入到同步代码块。如果此时有其他线程B来竞争该锁,分两种情况做不同的处理:
    1. 如果线程A已执行完(并不会主动的修改锁对象的状态),会直接撤销锁对象的状态为未锁定,标志位为00;
    2. 如果线程A还在持有该锁,则锁升级为轻量级锁。  
  2. 轻量级锁:也是JDK1.6中引入的,轻量级,是相对于使用互斥量的重量级锁来说的。线程发生竞争锁的时候,不会直接进入阻塞状态,而是先尝试做CAS修改操作,进入自旋,这个过程避免了线程状态切换的开销,不过要消耗cpu资源。详细过程是:
    1.   线程尝试进入同步代码块,如果锁对象未被锁定,在当前线程对应的栈帧中,建立锁记录的空间,用于存储锁对象Mark Word的拷贝。
    2. 然后JVM用CAS方式尝试将锁对象的MarkWord内容替换为指向前述“锁记录”的指针。如果成功,当前线程则持有了锁,处于轻量级锁定状态;如果失败,会首先检查当前MarkWord是否已经指向当前线程栈帧的“锁记录”,如果是,就说明当前线程已经拥有了这个锁,直接重入即可。否则就表明是其他线程持有锁,那么进入自旋(其实就是重试CAS修改操作)。
    3. 释放锁时,是使用CAS来讲MarkWord和“锁记录”里的内容互换。如果成功,成功释放;如果事变,表明当前锁存在竞争(被其他线程修改了MarkWord里的数据),此时,锁会升级为重量级锁。
  3. 重量级锁:也就是我们使用的互斥量方式实现的锁,当存在多线程竞争时,只要没拿到锁,就会进入阻塞状态,主要消耗是在阻塞-唤起-阻塞-唤起的线程状态切换上。
  4. 上面介绍的三种类型的锁,是JVM来负责管理使用哪种类型锁,以及锁的升级(注意,没有降级)。
  5. 这里涉及到锁升级,对象头MarkWord等内容,如果详细说,可能又是一大篇文章了,如果大家感兴趣,可以另起一篇。
  6. synchronized就先介绍到这里,下一篇预告:ReentrantLock相关
  7. 如果你看的爽,请点击右下角的“推荐”,是对小端坚持分享原创的最大鼓励。也可以关注小端的个人公众号 :   pnxsxb  ,会分享更多的原创技术文章。

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏Java帮帮-微信公众号-技术文章全总结

Java设计模式-命令模式

在对象的结构和创建问题都解决了之后,就剩下对象的行为问题了: 如果对象的行为设计的好,那么对象的行为就会更清晰,它们之间的协作效率就会提高. 行为型模式共有1...

3526
来自专栏along的开发之旅

Java8移除永久代

最近看深入理解Java虚拟机, 在实战OutOfMemoryError的运行时常量池溢出时, 我的Intellij提示如下:

861
来自专栏FreeBuf

一个二进制POC的诞生之旅CVE-2018-0802

背景 在潜伏17年的“噩梦公式”漏洞(CVE-2017-11882)被曝光修补之后,之前的漏洞程序EQNEDT32.EXE在windows 10系统下仍然没有开...

2699
来自专栏Golang语言社区

理解Go语言Web编程(下)

ListenAndServe函数 前面所有示例程序中,都在main函数中调用了ListenAndServe函数。下面对此函数所做的工作进行分析。该函数的实现为:...

6956
来自专栏Python中文社区

OpenStack中的RESTful API是如何实现的?

OpenStack作为一个开源的IaaS平台,各个组件和服务之间的消息传递都是通过RESTfulAPI和RPC传递,这里主要讲讲它是如何实现REST的。由于大家...

2828
来自专栏Java技术交流群809340374

史上最全Java面试266题:算法+缓存+TCP+JVM+搜索+分布式+数据库

2410
来自专栏Keegan小钢

Android项目重构之路:架构篇

去年10月底换到了新公司,做移动研发组的负责人,刚开始接手android项目时,发现该项目真的是一团糟。首先是其架构,是按功能模块进行划分的,本来按模块划分也挺...

1224
来自专栏Java Web

Java 面试知识点解析(二)——高并发编程篇

24410
来自专栏Java Web

Java 面试知识点解析(二)——高并发编程篇

在遨游了一番 Java Web 的世界之后,发现了自己的一些缺失,所以就着一篇深度好文:知名互联网公司校招 Java 开发岗面试知识点解析 ,来好好的对 Jav...

5107
来自专栏码洞

依赖注入不是Java的专利,Golang也有

笔者在使用Golang的时候就发现构建系统依赖树非常繁琐,New了很多对象,又手工代码将它们拼接起来,写了一堆非常冗繁的代码。然后就开始想,要是Golang像J...

921

扫码关注云+社区