赞
踩
关于Android Flutter 相关的面试知识点,我通过这次面试期间问道的问题进行了总结,将被问的问题第一反应就是去网上查找大家优秀的回答。希望通过这篇文章能帮助到大家。
3月26号开始了第一家公司的第一面,期间没考虑过去投其他公司的简历,主要目的是锻炼一下自己的面试,毕竟4年没面试过了,还是很虚。磕磕绊绊历时两个星期五轮面试最终拿到了第一家offer,同时公司人力那边也知道我有离职的打算就间接催促我提交OA离职,自己很被动,一冲动直接提交OA了,预留了两个星期的交接时间,也就是有两个星期的时间找工作,开始变的焦虑,就直接在boss上打开简历,并投了五家,然后一天过去没有任何反应,很是着急,直到第三天才接到4家的面试邀请,悬着的心终于松口气。
4月15号正式开始面试,4月16号请假在家安心面试,一天三家公司共经历了五轮面试,每轮面试都是一个小时左右,因为我表达了时间紧迫所以面试公司的HR跟进的很给力第三天有两个公司就都进行到了第五轮面试。4月20号基本算是拿到了3个offer也有个比较满意的公司,后续几天另外2家的复试也开始了,就没约新的面试公司,也过了。目前还有3家到了最终面,因为周期太长就直接拒绝了。
一共是面了7家公司,共经历了27轮面试,拿到了4个offer(有一家是flutter开发),3家hr面试直接拒绝没参加。7家公司有BBA大厂也有D轮中型公司。
参加的公司面试流程都很标准,会提前发邮件预约1个小时的面试时间。
也有一面很满意之后一次性解决的,3个小时的面试
说了这么多废话,进入正题吧,面试知识点,我只大概总结下,其实每个知识点都可以深入拓展,分了五个模块java、Android、网络、dart、flutter
垃圾回收需要完成两件事:找到垃圾,回收垃圾。 找到垃圾一般的话有两种方法:
新生代对象分为三个区域:Eden 区和两个 Survivor 区。新创建的对象都放在 Eden区,当 Eden 区的内存达到阈值之后会触发 Minor GC,这时会将存活的对象复制到一个 Survivor 区中,这些存活对象的生命存活计数会加一。这时 Eden 区会闲置,当再一次达到阈值触发 Minor GC 时,会将Eden区和之前一个 Survivor 区中存活的对象复制到另一个 Survivor 区中,采用的是我之前提到的复制算法,同时它们的生命存活计数也会加一。
这个过程会持续很多遍,直到对象的存活计数达到一定的阈值后会触发一个叫做晋升的现象:新生代的这个对象会被放置到老年代中。 老年代中的对象都是经过多次 GC 依然存活的生命周期很长的 Java 对象。当老年代的内存达到阈值后会触发 Major GC,采用的是标记整理算法。
JVM 的内存区域可以分为两类:线程私有和区域和线程共有的区域。 线程私有的区域:程序计数器、JVM 虚拟机栈、本地方法栈 线程共有的区域:堆、方法区、运行时常量池
其实除了程序计数器,其他的部分都会发生 OOM。
Java内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存,线程的工作内存中保存了该线程中是用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存。不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递均需要自己的工作内存和主存之间进行数据同步进行。
原子性
在Java中,为了保证原子性,提供了两个高级的字节码指令monitorenter
和monitorexit
。在synchronized的实现原理文章中,介绍过,这两个字节码,在Java中对应的关键字就是synchronized
。
因此,在Java中可以使用synchronized
来保证方法和代码块内的操作是原子性的。
可见性
Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。
Java中的volatile
关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用volatile
来保证多线程操作时变量的可见性。
除了volatile
,Java中的synchronized
和final
两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。
有序性
在Java中,可以使用synchronized
和volatile
来保证多线程之间操作的有序性。实现方式有所区别:
volatile
关键字会禁止指令重排。synchronized
关键字保证同一时刻只允许一条线程操作。
好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像synchronized
关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用synchronized
的原因。
但是synchronized
是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用
Java 中类加载分为 3 个步骤:加载、链接、初始化。
加载。 加载是将字节码数据从不同的数据源读取到JVM内存,并映射为 JVM 认可的数据结构,也就是 Class 对象的过程。数据源可以是 Jar 文件、Class 文件等等。如果数据的格式并不是 ClassFile 的结构,则会报 ClassFormatError。
**链接。**链接是类加载的核心部分,这一步分为 3 个步骤:验证、准备、解析。
验证。 验证是保证JVM安全的重要步骤。JVM需要校验字节信息是否符合规范,避免恶意信息和不规范数据危害JVM运行安全。如果验证出错,则会报VerifyError。
准备。 这一步会创建静态变量,并为静态变量开辟内存空间。
解析。 这一步会将符号引用替换为直接引用。
初始化。 初始化会为静态变量赋值,并执行静态代码块中的逻辑。
类加载器大致分为3类:启动类加载器、扩展类加载器、应用程序类加载器。
jre/lib
下的jar
文件。jre/lib/ext
下的jar
文件。classpath
下的文件。所谓的双亲委派模型就是当加载一个类时,会优先使用父类加载器加载,当父类加载器无法加载时才会使用子类加载器去加载。这么做的目的是为了避免类的重复加载。
HashMap 的内部可以看做数组+链表的复合结构。数组被分为一个个的桶(bucket)。哈希值决定了键值对在数组中的寻址。具有相同哈希值的键值对会组成链表。需要注意的是当链表长度超过阈值(默认是8)的时候会触发树化,链表会变成树形结构。
把握HashMap的原理需要关注4个方法:hash、put、get、resize。
hash方法。 将 key 的 hashCode 值的高位数据移位到低位进行异或运算。这么做的原因是有些 key 的 hashCode 值的差异集中在高位,而哈希寻址是忽略容量以上高位的,这种做法可以有效避免哈希冲突。
**put 方法。**put 方法主要有以下几个步骤:
通过 hash 方法获取 hash 值,根据 hash 值寻址。
如果未发生碰撞,直接放到桶中。
如果发生碰撞,则以链表形式放在桶后。
当链表长度大于阈值后会触发树化,将链表转换为红黑树。
如果数组长度达到阈值,会调用 resize 方法扩展容量。
**get方法。**get 方法主要有以下几个步骤:
通过 hash 方法获取 hash 值,根据 hash 值寻址。
如果与寻址到桶的 key 相等,直接返回对应的 value。
如果发生冲突,分两种情况。如果是树,则调用 getTreeNode 获取 value;如果是链表则通过循环遍历查找对应的 value。
**resize 方法。**resize 做了两件事:
将原数组扩展为原来的 2 倍
重新计算 index 索引值,将原节点重新放到新的数组中。这一步可以将原先冲突的节点分散到新的桶中。
join 方法通常是保证线程间顺序调度的一个方法,它是 Thread 类中的方法。比方说在线程 A 中执行线程 B.join()
,这时线程 A 会进入等待状态,直到线程 B 执行完毕之后才会唤醒,继续执行A线程中的后续方法。
join 方法可以传时间参数,也可以不传参数,不传参数实际上调用的是 join(0)
。它的原理其实是使用了 wait 方法,join 的原理如下:
public final synchronized void join(long millis) throws InterruptedException { long base = System.currentTimeMillis(); long now = 0; if (millis < 0) { throw new IllegalArgumentException("timeout value is negative"); } if (millis == 0) { while (isAlive()) { wait(0); } } else { while (isAlive()) { long delay = millis - now; if (delay <= 0) { break; } wait(delay); now = System.currentTimeMillis() - base; } } }
一般提到 volatile,就不得不提到内存模型相关的概念。我们都知道,在程序运行中,每条指令都是由 CPU 执行的,而指令的执行过程中,势必涉及到数据的读取和写入。程序运行中的数据都存放在主存中,这样会有一个问题,由于 CPU 的执行速度是要远高于主存的读写速度,所以直接从主存中读写数据会降低 CPU 的效率。为了解决这个问题,就有了高速缓存的概念,在每个 CPU 中都有高速缓存,它会事先从主存中读取数据,在 CPU 运算之后在合适的时候刷新到主存中。
这样的运行模式在单线程中是没有任何问题的,但在多线程中,会导致缓存一致性的问题。举个简单的例子:i=i+1
,在两个线程中执行这句代码,假设i的初始值为0。我们期望两个线程运行后得到2,那么有这样的一种情况,两个线程都从主存中读取i到各自的高速缓存中,这时候两个线程中的i都为0。在线程1执行完毕得到i=1
,将之刷新到主存后,线程2开始执行,由于线程2中的i是高速缓存中的0,所以在执行完线程2之后刷新到主存的i仍旧是1。
所以这就导致了对共享变量的缓存一致性的问题,那么为了解决这个问题,提出了缓存一致性协议:当 CPU 在写数据时,如果发现操作的是共享变量,它会通知其他 CPU 将它们内部的这个共享变量置为无效状态,当其他 CPU 读取缓存中的共享变量时,发现这个变量是无效的,它会从新从主存中读取最新的值。
在Java的多线程开发中,有三个重要概念:原子性、可见性、有序性。
volatile的原理是在生成的汇编代码中多了一个lock前缀指令,这个前缀指令相当于一个内存屏障,这个内存屏障有3个作用:
volatile
它所修饰的变量不保留拷贝,直接访问主内存中的。
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
使用场景
您只能在有限的一些情形下使用 volatile 变量替代锁。要使 volatile 变量提供理想的线程安全,必须同时满足下面两个条件:
1)对变量的写操作不依赖于当前值。
2)该变量没有包含在具有其他变量的不变式中。
volatile最适用一个线程写,多个线程读的场合。
如果有多个线程并发写操作,仍然需要使用锁或者线程安全的容器或者原子变量来代替。
synchronized
当它用来修饰一个方法或者一个代码块的时候,能够保证在同一时刻最多只有一个线程执行该段代码。
区别
线程安全包含原子性和可见性两个方面,Java的同步机制都是围绕这两个方面来确保线程安全的。
关键字volatile主要使用的场合是在多个线程中可以感知实例变量被修改,并且可以获得最新的值使用,也就是多线程读取共享变量时可以获得最新值使用。
关键字volatile提示线程每次从共享内存中读取变量,而不是私有内存中读取,这样就保证了同步数据的可见性。但是要注意的是:如果修改实例变量中的数据
ThreadLocal的作用是提供线程内的局部变量,说白了,就是在各线程内部创建一个变量的副本,相比于使用各种锁机制访问变量,ThreadLocal的思想就是用空间换时间,使各线程都能访问属于自己这一份的变量副本,变量值不互相干扰,减少同一个线程内的多个函数或者组件之间一些公共变量传递的复杂度。
get函数用来获取与当前线程关联的ThreadLocal的值,如果当前线程没有该ThreadLocal的值,则调用initialValue函数获取初始值返回,initialValue是protected类型的,所以一般我们使用时需要继承该函数,给出初始值。而set函数是用来设置当前线程的该ThreadLocal的值,remove函数用来删除ThreadLocal绑定的值,在某些情况下需要手动调用,防止内存泄露。
生产者消费者模式要保证的是当缓冲区满的时候生产者不再生产对象,当缓冲区空时,消费者不再消费对象。实现机制就是当缓冲区满时让生产者处于等待状态,当缓冲区为空时让消费者处于等待状态。当生产者生产了一个对象后会唤醒消费者,当消费者消费一个对象后会唤醒生产者。
三种种实现方式:wait 和 notify、await 和 signal、BlockingQueue。
//wait和notify import java.util.LinkedList; public class StorageWithWaitAndNotify { private final int MAX_SIZE = 10; private LinkedList<Object> list = new LinkedList<Object>(); public void produce() { synchronized (list) { while (list.size() == MAX_SIZE) { System.out.println("仓库已满:生产暂停"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.add(new Object()); System.out.println("生产了一个新产品,现库存为:" + list.size()); list.notifyAll(); } } public void consume() { synchronized (list) { while (list.size() == 0) { System.out.println("库存为0:消费暂停"); try { list.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } list.remove(); System.out.println("消费了一个产品,现库存为:" + list.size()); list.notifyAll(); } } }
import java.util.LinkedList; import java.util.concurrent.locks.Condition; import java.util.concurrent.locks.ReentrantLock; class StorageWithAwaitAndSignal { private final int MAX_SIZE = 10; private ReentrantLock mLock = new ReentrantLock(); private Condition mEmpty = mLock.newCondition(); private Condition mFull = mLock.newCondition(); private LinkedList<Object> mList = new LinkedList<Object>(); public void produce() { mLock.lock(); while (mList.size() == MAX_SIZE) { System.out.println("缓冲区满,暂停生产"); try { mFull.await(); } catch (InterruptedException e) { e.printStackTrace(); } } mList.add(new Object()); System.out.println("生产了一个新产品,现容量为:" + mList.size()); mEmpty.signalAll(); mLock.unlock(); } public void consume() { mLock.lock(); while (mList.size() == 0) { System.out.println("缓冲区为空,暂停消费"); try { mEmpty.await(); } catch (InterruptedException e) { e.printStackTrace(); } } mList.remove(); System.out.println("消费了一个产品,现容量为:" + mList.size()); mFull.signalAll(); mLock.unlock(); } }
import java.util.concurrent.LinkedBlockingQueue; public class StorageWithBlockingQueue { private final int MAX_SIZE = 10; private LinkedBlockingQueue<Object> list = new LinkedBlockingQueue<Object>(MAX_SIZE); public void produce() { if (list.size() == MAX_SIZE) { System.out.println("缓冲区已满,暂停生产"); } try { list.put(new Object()); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("生产了一个产品,现容量为:" + list.size()); } public void consume() { if (list.size() == 0) { System.out.println("缓冲区为空,暂停消费"); } try { list.take(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("消费了一个产品,现容量为:" + list.size()); } }
final 可以修饰类、变量和方法。修饰类代表这个类不可被继承。修饰变量代表此变量不可被改变。修饰方法表示此方法不可被重写 (override)。
finally 是保证重点代码一定会执行的一种机制。通常是使用 try-finally 或者 try-catch-finally 来进行文件流的关闭等操作。
finalize 是 Object 类中的一个方法,它的设计目的是保证对象在垃圾收集前完成特定资源的回收。finalize 机制现在已经不推荐使用,并且在 JDK 9已经被标记为 deprecated。
Java 中常见的单例模式实现有这么几种:饿汉式、双重判断的懒汉式、静态内部类实现的单例、枚举实现的单例。 这里着重讲一下双重判断的懒汉式和静态内部类实现的单例。
双重判断的懒汉式:
public class SingleTon { //需要注意的是volatile private static volatile SingleTon mInstance; private SingleTon() { } public static SingleTon getInstance() { if (mInstance == null) { synchronized (SingleTon.class) { if (mInstance == null) { mInstance=new SingleTon(); } } } return mInstance; } }
双重判断的懒汉式单例既满足了延迟初始化,又满足了线程安全。通过 synchronized 包裹代码来实现线程安全,通过双重判断来提高程序执行的效率。这里需要注意的是单例对象实例需要有 volatile 修饰,如果没有 volatile 修饰,在多线程情况下可能会出现问题。原因是这样的,mInstance=new SingleTon()
这一句代码并不是一个原子操作,它包含三个操作:
我们知道 JVM 会发生指令重排,正常的执行顺序是1-2-3
,但发生指令重排后可能会导致1-3-2
。我们考虑这样一种情况,当线程 A 执行到1-3-2
的3步骤暂停了,这时候线程 B 调用了 getInstance,走到了最外层的if判断上,由于最外层的 if 判断并没有 synchronized 包裹,所以可以执行到这一句,这时候由于线程 A 已经执行了步骤3,此时 mInstance 已经不为 null 了,
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。