全面总结Android面试知识要点:Java核心基础相关(三)

202 阅读12分钟

请点赞,你的点赞对我意义重大,满足下我的虚荣心。
🔥常在河边走,哪有不湿鞋。或许面试过程中你遇到的问题就在这呢?
🔥关注我,面试不迷路~

一、假如只有一个cpu,单核,多线程还有用吗 ?

这道题想考察什么?

是否了解并发相关的理论知识

考察的知识点

  1. cpu多线程的基本概念
  2. 操作系统的调度任务机制
  3. CPU密集型和IO密集型理论

考生应该如何回答

CPU的执行速度要远大于IO的过程,因此在大多数情况下增加一些复杂的CPU计算都比增加一次IO要快。单核CPU可以通过给每个线程分配CPU时间片(时间单元)来实现多线程机制。由于CPU频率很高,故时间单元非常短。所以单核也可以实现多线程机制。

从用户体现上说,单核多线程也能够减少用户响应时间,例如web页面,也是防止IO阻塞。处理器的数量和并不影响程序结构, 所以不管处理器的个数多少, 程序都可以通过使用多线程得到简化。


二、sychronied修饰普通方法和静态方法的区别?什么是可见性?(小米)

这道题想考察什么?

是否了解Java并发编程的相关知识

考察的知识点

  1. sychronied的原理
  2. 并发的特性

考生应该如何回答

sychronied是Java中并发编程的重要关键字之一。在并发编程中synchronized一直是解决线程安全问题,它可以保证原子性,可见性,以及有序性。

  • 原子性:原子是构成物质的基本单位,所以原子的意思代表着—“不可分”。由不可分可知,具有原子性的操作也就是拒绝线程调度器中断。
  • 可见性:一个线程对共享变量的修改,另一个线程能够立刻看到,称为可见性。
  • 有序性:程序按照代码的先后顺序执行。编译器为了优化性能,有时会改变程序中语句的顺序,但是不会影响最终的结果。有序性经典的例子就是利用DCL双重检查创建单例对象。

synchronized可以修饰方法,也能够使用synchronized(obj){}定义同步代码块。

  • 修饰方法:

    1. 实例方法,作用于当前实例加锁,进入方法前需要获取当前实例的锁;
    2. 静态方法,作用于当前类对象加锁,进入方法前需要获取当前类对象的锁;
  • 修饰代码块,指定加锁对象,对给定对象加锁,进入代码块前要获得给定对象的锁。

使用sychronied修饰普通方法和静态方法,其实也等价于synchronized(this){}synchronized(class){}


三、Synchronized在JDK1.6之后做了哪些优化 (京东)

这道题想考察什么?

对并发编程的掌握

考察的知识点

并发与synchronized原理

考生如何回答

synchronized是Java中非常重要的一个关键字,对于Android开发同学来说,考虑到多线程的情况,一般都直接使用到synchronized关键字对方法或者对象上锁。但是问题是为什么加上synchronized关键字就能实现锁,它的原理是怎么回事呢?

字节码

如果我们使用javap -v xxx.class 反编译这样一个class文件

public static void main(String[] args) {
        synchronized (InjectTest.class) {
            System.out.println("hello!");
        }
}

此时我们获得到结果为:

monitor转存失败,建议直接上传图片文件

可以看到javap的输出信息中存在:monitorenter与monitorexit指令。这就代表了同步代码块的入口与出口。

这里的monitor是:对象监视器。在JVM中,每个对象都会和一个对象监视器(monitor)相关联。monitorenter指令插入到同步代码块开始的位置、monitorexit指令插入到同步代码块结束位置,jvm需要保证每个monitorenter都有一个monitorexit对应。这两个指令,本质上都是对 对象监视器(monitor)进行获取,这个过程是排他的,也就是说同一时刻只能有一个线程获取到由synchronized所保护对象的监视器。线程执行到monitorenter指令时,会尝试获取对象所对应的monitor所有权,也就是尝试获取对象的锁;而执行monitorexit,就是释放monitor的所有权。 如果其他线程已经占用了monitor,则当前线程进入阻塞状态。

当然这是jdk1.6之前的行为,而jdk1.6以后为了减少获得锁和释放锁带来的性能消耗,对synchronized锁进行了优化,包含偏向锁、轻量级锁、重量级锁;

关于锁类型与相关信息的信息都是存放在锁对象的对象头中 ,在了解偏向锁、轻量级锁、重量级锁之前,我们必须先认识一下什么是对象头!

Java对象头

对象在虚拟机内存中的布局分为三块区域:对象头、实例数据和对齐填充;Java对象头是实现synchronized的锁对象的基础,一般而言,synchronized使用的锁对象是存储在Java对象头里。

对象头由:存储对象自身的运行时数据的Mark Word 32位系统中4 + 指向类的指针 kClass pointer ,如果是数组对象还会有数组长度 Array Length。

其中mark word中就存储了锁状态:

image.png

在无锁状态下,mark word中的数据为:

image.png

包含对象hashcode,gc年龄,是否偏向锁与锁标志信息。

偏向锁

而偏向锁下,数据则为:

image.png

拥有锁的线程ID, epoch 大家可以先理解为校验位,同时 是否偏向锁标记由相对无锁状态下的0变为1。

首先之所以会引入偏向锁是因为:大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,为了让线程获得锁的代价更低而引入了偏向锁,减少不必要的操作。

在程序进入同步代码块时,会访问Mark Word中偏向锁的标识是否设置成1,锁标志位是否为01,若为偏向锁状态,则查看偏向锁状态下线程ID是否指向当前线程。如果是则直接执行同步代码。但是mark word中记录的线程ID如果不是当前线程,则通过CAS比较与交换尝试修改对象头获得锁。CAS操作成功则可以直接执行同步代码,否则表示有其他线程竞争,此时获得偏向锁的线程被挂起,偏向锁升级为轻量级锁 ,然后被阻塞的线程继续往下执行同步代码。

轻量级锁

image.png

轻量级锁状态下,代码进入同步块时,如果同步对象锁状态为无锁状态, 虚拟机首先将在当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝 ,接着虚拟机将使用CAS操作尝试将对象的Mark Word更新为指向Lock Record的指针 。这个操作如果成功则代表获取到了锁,但是如果失败,则会检查对象Mark Word是不是指向当前线程栈帧中的锁记录,如果是,则说明本身当前线程就拥有此对象的锁,就可以直接执行同步代码。否则说明锁对象被其他线程获取,当前线程是竞争者,那么当前线程会自旋等待锁,也就是不断重试,当重试一定次数后,总不能一直重试下去吧,太耗CPU了。所以这时候就要升级为重量级锁。

重量级锁

重量级锁就是通过对象监视器(monitor)实现,其中monitor的本质是依赖于底层操作系统的Mutex Lock实现,操作系统实现线程之间的切换需要从用户态到内核态的切换,切换成本非常高。主要是,当系统检查到锁是重量级锁之后,会把等待想要获得锁的线程进行阻塞,被阻塞的线程不会消耗cup。但是阻塞或者唤醒一个线程时,都需要操作系统来帮忙,这就需要从用户态转换到内核态,而转换状态是需要消耗很多时间的,有可能比用户执行代码的时间还要长。

四、CAS无锁编程的原理(字节跳动)

这道题想考察什么?

并发相关问题,原子操作

考察的知识点

Java并发编程,乐观锁机制

考生如何回答

Jdk中java.util.concurrent.atomic包下的类都是采用CAS来实现的。

CAS原理分析

CAS(比较与交换,Compare and swap) 是一种有名的无锁算法。无锁编程,即不使用锁的情况下实现多线程之间的变量同步,也就是在没有线程被阻塞的情况下实现变量的同步,所以也叫非阻塞同步(Non-blocking Synchronization)。实现非阻塞同步的方案称为“无锁编程算法”( Non-blocking algorithm)。

CAS机制当中使用了3个基本操作数:内存地址V,旧的预期值A,要修改的新值B。更新一个变量的时候,只有当变量的预期值A和内存地址V当中的实际值相同时,才会将内存地址V对应的值修改为B。

1.在内存地址V当中,存储着值为10的变量。

image.png

2.此时线程1想要把变量的值增加1。对线程1来说,旧的预期值A=10,要修改的新值B=11。

image.png

3.在线程1要提交更新之前,另一个线程2抢先一步,把内存地址V中的变量值率先更新成了11。

image.png

4.线程1开始提交更新,首先进行A和地址V的实际值比较(Compare) ,发现A不等于V的实际值,提交失败。

image.png

5.线程1重新获取内存地址V的当前值,并重新计算想要修改的新值。此时对线程1来说,A=11,B=12。这个重新尝试的过程被称为自旋

image.png

6.这一次比较幸运,没有其他线程改变地址V的值。线程1进行Compare,发现A和地址V的实际值是相等的。

image.png

7.线程1进行SWAP,把地址V的值替换为B,也就是12。

image.png

从思想上来说,Synchronized属于悲观锁,悲观地认为程序中的并发情况严重,所以严防死守。CAS属于乐观锁,乐观地认为程序中的并发情况不那么严重,所以让线程不断去尝试更新。

CAS的缺点

ABA 问题

由于 CAS 设计机制就是获取某两个时刻(初始预期值和当前内存值)变量值,并进行比较更新,所以说如果在获取初始预期值和当前内存值这段时间间隔内,变量值由 A 变为 B 再变为 A,那么对于 CAS 来说是不可感知的,但实际上变量已经发生了变化;解决办法是在每次获取时加版本号,并且每次更新对版本号 +1,这样当发生 ABA 问题时通过版本号可以得知变量被改动过。JDK 1.5 以后的 AtomicStampedReference 类就提供了此种能力,其中的 compareAndSet 方法就是首先检查当前引用是否等于预期引用,并且当前标志是否等于预期标志,如果全部相等,则以原子方式将该引用和该标志的值设置为给定的更新值。

循环时间长开销大

所谓循环时间长开销大问题就是当 CAS 判定变量被修改了以后则放弃本次修改,但往往为了保证数据正确性该计算会以循环的方式再次发起 CAS,如果多次 CAS 判定失败,则会产生大量的时间消耗和性能浪费;如果JVM能支持处理器提供的pause指令那么效率会有一定的提升,pause指令有两个作用,第一它可以延迟流水线执行指令(de-pipeline),使CPU不会消耗过多的执行资源,延迟的时间取决于具体实现的版本,在一些处理器上延迟时间是零。第二它可以避免在退出循环的时候因内存顺序冲突(memory order violation)而引起CPU流水线被清空(CPU pipeline flush),从而提高CPU的执行效率。

只能保证一个共享变量的原子操作
  1. CAS 只对单个共享变量有效,当操作涉及跨多个共享变量时 CAS 无效;从 JDK 1.5开始提供了 AtomicReference 类来保证引用对象之间的原子性,你可以把多个变量放在一个对象里来进行 CAS 操作
  2. Unsafe是CAS的核心类,Java无法直接访问底层操作系统,而是通过本地(native)方法来访问。不过尽管如此,JVM还是开了一个后门,JDK中有一个类Unsafe,它提供了硬件级别的原子操作。
  3. valueOffset表示的是变量值在内存中的偏移地址,因为Unsafe就是根据内存偏移地址获取数据的原值的。
  4. value是用volatile修饰的,保证了多线程之间看到的value值是同一份。

今天的面试分享到此结束拉~下期在见

关注公众号:Android老皮
解锁  《Android十大板块文档》 ,让学习更贴近未来实战。已形成PDF版

内容如下

1.Android车载应用开发系统学习指南(附项目实战)
2.Android Framework学习指南,助力成为系统级开发高手
3.2023最新Android中高级面试题汇总+解析,告别零offer
4.企业级Android音视频开发学习路线+项目实战(附源码)
5.Android Jetpack从入门到精通,构建高质量UI界面
6.Flutter技术解析与实战,跨平台首要之选
7.Kotlin从入门到实战,全方面提升架构基础
8.高级Android插件化与组件化(含实战教程和源码)
9.Android 性能优化实战+360°全方面性能调优
10.Android零基础入门到精通,高手进阶之路

敲代码不易,关注一下吧。ღ( ´・ᴗ・` ) 🤔