Android 面试

机器识别简历和岗位JD的匹配程度。

实习 项目 学历

信息的结构化非常重要,显眼的位置,加粗等方式。

用具体的数字来证明量化业绩和产出

写对求职有帮助的东西,注意精简,没必要的内容不要写,东西太多重点会被模糊掉

不要花哨,颜色尽量少

依据JD修改简历,更加匹配岗位要求

0,JD

岗位要求

  • [ ] 熟悉Java语言
  • [ ] 熟悉Android开发基础知识:四大组件,UI
  • [ ] 熟悉软件设计模式
  • [ ] 常见的数据结构和算法 基础扎实
  • [ ] 计算机网络(深入理解 TCP/IP 和 HTTP 网络协议)
  • [ ] Android网络编程
  • [ ] Android多线程编程(Handler)
  • [ ] 操作系统

加分:

  • [ ] 熟悉Kotlin
  • [ ] 了解Android Framework层,并有一定Android源码阅读经验;
  • [ ] 熟悉移动端 UI/网络/数据库框架优先;
  • [ ] 熟悉Android平台下的高性能编程及性能调优
  • [ ] 了解jni、多线程、反射、jvm、类加载机制等基础原理
  • [ ] 熟悉Kotlin
  • [ ] Flutter:熟悉Flutter与原生混编技术,有Flutter实际项目经验,有独立完成的APP最佳
  • [ ] 具有ACM/OI等算法经历
  • [ ] 对一些底层或框架(JVM/Android ROM/Linux/Flutter 等)深入了解
  • [ ] 以及行业技术(图片编解码/机器学习等)有深入了解
  • [ ] 独立开发过 APP,有经历过大型APP的研发周期;
  • [ ] 除了Android之外,具备其它方面开发经验:比如具有前端技能(js,vue)或脚本技能(Linux shell)优先;
  • [ ] 在校园内或互联网公司有相关实习或项目经历
  • [ ] 有个人技术博客
  • [ ] 有个人Github开源项目
  • [ ] 关注业界的技术分享

软实力:

  • [ ] 学习能力强,对新事物保有好奇心,并能快速适应新环境;
  • [ ] 有良好的沟通能力和团队协同能力;能与他人合作,共同完成目标;
  • [ ] 分析及解决问题能力:思维敏捷,可以应付各类编程复杂问题,并迅速给出解决方案
  • [ ] 对所在领域有热情,相信方法总比困难多,善于独立思考并反思总结。
  • [ ] 细心谨慎,谦逊踏实,主动好学,踏实诚恳
  • [ ] 做事儿有激情,有较强的自我驱动力和学习能力,喜欢挑战自我,追求极致;
  • [ ] 具备一定的英语阅读能力,可无障碍阅读英文技术文档;
  • [ ] 常活跃:GitHub、stackoverflow、LeetCode技术类网站;
  • [ ] GitHub上发布过项目,解决过痛点问题;

存疑:

  • [ ] 大学生数学建模比赛、“挑战赛”、机器人比赛
  • [ ] 在学校积极参与计算机相关的协会,参与其中的建设和开发
  • [ ] 专业:计算机相关(优先),数学
  • [ ] 学历:92本科以上 有限
  • [ ] 硕士优先

岗位要求

  1. 负责无线客户端和SDK产品的架构设计和开发工作;
  2. 结合数据指标驱动和分析,不断深化无线客户端技术产品,优化客户端应用的性能以及提升客户端体验
  3. 独立负责平台相关的 Android各机型的适配和优化;
  4. 独立负责Android技术难题攻关,解决各种系统限制而导致一些技术和用户体验问题;
  5. 移动平台技术研究与新技术新趋势探索。

一,常识

面试时间:

- 一面 30min
- 二面 45min

流程:

自我介绍 -》项目 -> Android -> Java

塑造你的个人形象:强(投了多家大厂,不要透露过多被拒情况)但专一(最欣赏对方公司),强调自己的学习能力和对岗位工作的兴趣。

一般一面刷掉背景与业务不符或专业知识很欠缺的,二面细问项目经历和专业问题(最筛人),三面和HR面刷人应该不多(仅了解阿里)。

问及其它厂的投递情况和进展,如实回答,切不能让对方感觉你只有这一家有希望,否则必养鱼。

每轮都有面评,最后按排序给oc(HR面挂的可能原因),HR面提前了解公司和部门方便舔。

建议自己仔细思考一下自己的定位、想得到什么、各类话术等等,多做些预案。

测开很多要求会低一些,没有项目背景的可以考虑。运维也可以考虑。

塑造你的个人形象:强(投了多家大厂,不要透露过多被拒情况)但专一(最欣赏对方公司),强调自己的学习能力和对岗位工作的兴趣。

不要怀疑面试官的专业性(特别是二面及之后),不会的就说不会,然后明确表示接下来的答案是猜的。

有的厂会让选意向,尽量不要选“全部意向均可”,有条件可以了解下各个部门和城市,没条件也尽量选一个,否则可能泡池子

日常实习要写立即到岗并尽量写长实习时间

二,面试题

项目

  • [ ] 详细介绍项目

  • [ ] 最近做的项目是哪个,用到的技术栈和语言

  • [ ] 项目中怎么用的设计模式

  • [ ] 介绍项目中的难点

  • [ ] 怎么遇到的

  • [ ] 怎么解决的

  • [ ] 哪些方式解决问题

  • [ ] 项目过程中最有成就感的项目?

Android

  • [ ] 安卓有哪几种页面通讯的方式

Handler

  • [ ] Handler机制

  • [ ] Handler死循环为什么没有ANR

  • [ ] Handler延迟触发是怎么实现的(我说我不是很确定,然后拿Alarm的消息队列的机制来类比,就猜对了。)

  • [ ] 讲讲Message和消息队列

  • [ ] Binder是怎么实现的

  • [ ] handlerMessage什么时候会发生内存泄漏(要怎么预防)

  • [ ] handler和Activity两者是怎么相互引用的?

  • [ ] handler和Activity在链表中的顺序是怎么样的?

Activity

  • [ ] Activity生命周期

  • [ ] ActivityA启动后启动ActivityB的生命周期(问的很细,问了每个状态下的A和B可不可见等等这些)

  • [ ] Activity启动模式

  • [ ] Restart和正常的启动的区别

  • [ ] Activity有几种启动方式?

  • [ ] fragment的构造函数初始化,navigate的跳转。两种方式理解

  • [ ] 可以异步加载fragment吗,答案:可以。

  • [ ] serialVersionUID是否了解

UI/布局/VIew

  • [ ] 常见的布局

  • [ ] 约束布局

  • [ ] view的绘制

  • [ ] 自定义view

  • [ ] compose和view写法的优缺点

  • [ ] 使用recycleview碰到的问题

开源框架

  • [ ] retrofit使用什么设计模型搭建的?

  • [ ] okhttp对比原生的网络请求的区别?

  • [ ] glide的缓存加载机制

  • [ ] 安卓glide中与生命周期的关系

  • [ ] glide和OkHttp的任务调度是怎么实现的(比如同时发起很多请求)

Android优化

  • [ ] 事件分发机制

  • [ ] 内存泄漏

JetPack

  • [ ] jetpack全家桶用过哪些

  • [ ] livedata有什么能力

Java

面向对象

  • [ ] OOP三大特征,分别介绍

Java的面向对象编程(OOP)具有三大特征:

封装(Encapsulation)、继承(Inheritance)和多态(Polymorphism)。

  1. 封装(Encapsulation): 封装是指将数据和对数据的操作封装在一个单元内部,通过限制对数据的直接访问,确保数据的安全性和完整性。在Java中,我们可以使用类来实现封装。通过将数据成员声明为私有(private),并提供公共(public)的方法(getter和setter)来访问和修改数据,封装使得数据隐藏在类的内部,只能通过公共方法来进行访问。这样可以防止外部直接访问和修改数据,从而提供了更好的数据保护和灵活性。
  2. 继承(Inheritance): 继承是指一个类(子类)可以继承另一个类(父类)的属性和方法。子类可以继承父类的非私有成员变量和方法,并且可以在子类中添加新的成员变量和方法,以及重写(override)父类的方法。继承实现了代码的重用和扩展,可以建立类之间的层次关系,提高代码的可维护性和可扩展性。
  3. 多态(Polymorphism): 多态是指一个对象可以具有多种形态。在Java中,多态性可以通过继承和接口实现。多态性允许我们使用父类的引用来引用子类的对象,通过动态绑定,在运行时确定实际调用的方法。这样可以提高代码的灵活性和可扩展性。多态性还可以通过方法的重载(Overloading)和重写(Overriding)来实现,方法的多态性允许在不同的上下文中使用相同的方法名来执行不同的操作。

通过封装、继承和多态这三大特征,Java的面向对象编程提供了一种强大的抽象和模块化的编程方式,使得代码更易读、更易维护,并且具有良好的可扩展性和重用性。

  • [ ] 多态有编译期和运行期,介绍一下

Java多态性涉及编译期多态(静态绑定)和运行期多态(动态绑定)。

  1. 编译期多态(静态绑定): 编译期多态是在编译时确定方法的调用。在编译期,编译器根据引用变量的类型来决定调用哪个方法。如果引用变量的类型是父类或接口类型,那么编译器会根据父类或接口中定义的方法来进行静态绑定,也就是选择与引用变量类型对应的方法。这种静态绑定在编译时期就确定下来,不会受到实际对象的影响。

  2. 运行期多态(动态绑定): 运行期多态是在运行时根据对象的实际类型确定方法的调用。在运行时,虚拟机根据对象的实际类型来决定调用哪个方法。如果引用变量的类型是子类类型,并且存在父类和子类之间的方法重写(override),那么在运行时会根据实际对象的类型动态绑定调用相应的方法。这种动态绑定允许子类对象表现出与父类不同的行为,实现了多态性的关键。

总结起来,编译期多态是在编译时根据引用变量类型来确定方法的调用,而运行期多态是在运行时根据实际对象类型来确定方法的调用。编译期多态主要通过方法的重载来实现,而运行期多态主要通过方法的重写和继承来实现。运行期多态是Java面向对象编程中的重要特性,可以实现代码的灵活性和可扩展性。

  • [ ] Java是值传递还是引用传递

在Java中,参数传递是通过值传递(pass by value)进行的。这意味着当我们将一个变量作为参数传递给方法时,实际上是将变量的值复制一份传递给方法,而不是将变量本身传递给方法。

无论是基本数据类型还是对象引用,它们在方法调用时都按值传递。对于基本数据类型,传递的是值的拷贝;对于对象引用,传递的是引用值的拷贝,也就是说,方法内部的引用和方法外部的引用指向的是同一个对象。

然而,混淆在于对象的内容和对象的引用。当我们将一个对象作为参数传递给方法时,虽然对象引用按值传递,但方法内部仍然可以通过这个引用修改对象的状态。这是因为对象的引用指向的是堆内存中的对象,方法内部可以通过引用修改对象的属性值。这种行为可能导致误认为Java是按引用传递,但实际上对象引用本身还是按值传递的。

总结起来,Java是通过值传递进行参数传递的,无论是基本数据类型还是对象引用。对于对象引用,通过传递引用值的拷贝,我们可以在方法内部修改对象的状态。

  • [ ] Java访问权限以及这些访问权限的区别,排序

在Java中,访问权限用于控制类、方法、变量以及构造函数的可访问性。Java提供了四种访问权限修饰符,按照从最高访问权限到最低访问权限的顺序排列如下:

  1. public(公共): public是最高级别的访问权限修饰符。被public修饰的类、方法、变量或构造函数可以被任何其他类访问,无论是在同一包内还是在不同的包中。

  2. protected(受保护): protected修饰符的成员在类内部、同一包内以及其子类中可访问。被protected修饰的方法、变量或构造函数对外部包中的类不可见。

  3. 默认(无修饰符): 默认访问权限,即没有明确指定任何访问修饰符。默认修饰符的成员可以在同一包内访问,但在其他包中是不可见的。如果没有使用任何访问修饰符,就默认为默认访问权限。

  4. private(私有): private是最低级别的访问权限修饰符。被private修饰的成员只能在同一类内部访问,其他类无法访问。私有成员对于类外部是完全隐藏的。 通过这些访问权限修饰符,我们可以控制类的封装性和成员的可见性,从而实现良好的封装和信息隐藏。

总结访问权限的排序是:public > protected > 默认 > private。

  • [ ] 抽象类和类的区别

抽象类和普通类在Java中有一些关键区别,如下所示:

  1. 实例化能力:普通类可以直接实例化为对象,而抽象类不能直接实例化。抽象类通常被设计为作为其他类的基类,它提供了一些通用的属性和方法,但需要子类来扩展和实现。

  2. 完整性:普通类可以是完整的类,即它可以包含成员变量、成员方法和构造函数的实现。而抽象类可以包含抽象方法(没有实现),也可以包含实例方法的实现,但不能被实例化。

  3. 继承限制:普通类可以直接继承自其他类或实现接口。而抽象类既可以继承自其他类,也可以实现接口。一个类只能继承一个抽象类,但可以实现多个接口。

  4. 设计目的:普通类用于具体的对象实例化,它们提供了完整的实现。而抽象类用于被继承,它提供了一种通用的结构和行为,由其子类来扩展和实现具体细节。

  5. 强制实现:如果一个类继承了一个抽象类,它必须实现抽象类中所有的抽象方法,除非该子类也是一个抽象类。普通类没有这个要求,它可以选择性地实现父类中的方法。

    总结来说,抽象类是一种特殊的类,它不能被实例化,只能被继承。它可以包含抽象方法和具体方法,作为其他类的基类,提供了一种抽象和扩展的机制。普通类是可以被实例化的类,它可以包含属性和方法的完整实现。

  • [ ] 抽象类和接口的区别

抽象类和接口在Java中有以下区别:

  1. 实现方式:抽象类通过继承的方式被其他类所实现,而接口通过实现的方式被类实现。一个类只能继承一个抽象类,但可以实现多个接口。

  2. 内容:抽象类可以包含抽象方法和具体方法的实现,也可以包含成员变量。接口只能包含抽象方法和常量(即静态final变量),不包含具体方法或实例变量。

  3. 构造函数:抽象类可以有构造函数,而接口不能有构造函数。抽象类的构造函数用于子类实例化时的初始化工作。

  4. 多重继承:Java中不支持类的多重继承,但支持接口的多重实现。一个类可以实现多个接口,从而具备多个接口的特性。

  5. 设计目的:抽象类用于表示一种类的层次结构,其中它本身可能包含具体方法的实现,提供了一种共享代码和扩展性的机制。接口则用于定义一组方法的契约,以实现类能够遵循的行为规范,提供了一种规范和多态性的机制。

  6. 使用场景:抽象类适合于具有共享代码和共同行为的类,它们之间存在继承关系。接口适用于描述多个类之间共享的行为,不考虑它们之间的关系。

    总结来说,抽象类用于表示一种类的层次结构,它可以包含抽象方法和具体方法的实现,提供了代码共享和扩展的机制。接口用于定义一组方法的契约,类实现接口来遵循契约,提供了规范和多态性的机制。

  • [ ] StringBuilder和StringBuffer区别

在Java中,String、StringBuilder和StringBuffer是与字符串相关的类,它们有一些重要的区别和用途。

  1. String(字符串): String是不可变的字符序列,一旦创建,就不能被修改。每次对字符串的操作都会创建一个新的字符串对象,因此在频繁修改字符串的情况下,会导致大量的对象创建和内存开销。由于不可变性,String对象可以被安全地共享,使得字符串处理更加安全和可靠。

  2. StringBuilder(可变字符串): StringBuilder是可变的字符序列,它可以在原始对象上进行直接修改而不创建新的对象。StringBuilder适用于需要频繁进行字符串拼接、修改或插入操作的场景,因为它具有更高的性能和效率。然而,StringBuilder不是线程安全的,不能在多线程环境下使用。

  3. StringBuffer(可变字符串,线程安全): StringBuffer也是可变的字符序列,类似于StringBuilder,可以进行原地修改。不同之处在于StringBuffer是线程安全的,可以在多线程环境下使用。然而,由于线程安全的特性,StringBuffer的性能相对较低,因此在单线程环境下,推荐使用StringBuilder而不是StringBuffer。

    需要根据具体的需求来选择适当的字符串类型。如果字符串需要频繁修改,推荐使用StringBuilder;如果需要线程安全,可以使用StringBuffer;如果字符串是不可变的,可以使用String。

  • [ ] 谈谈try catch

throws:直接把异常丢给调用方处理

try-catch-finally:

  • try:放可能出错误的代码
  • catch:可以多个,根据代码出现的不同问题进入不同的catch然后执行对应的异常处理方案
  • finally:不管有没有异常都会执行,一般我们在这里做资源释放或者清理的工作

虽然写这个对程序员来说有的时候懒,但是这个是为了程序稳定性和再更细微粒度上做故障预防和排除

  • [ ] 谈谈static和final

当谈到Java中的static和final关键字时,以下是对它们的详细解释和用法:

static关键字:

  • 静态成员变量:使用static修饰的成员变量是属于类的,而不是属于类的实例。所有的类实例共享相同的静态变量,可以通过类名直接访问,无需创建对象。
  • 静态方法:使用static修饰的方法是属于类的,而不是属于类的实例。静态方法可以直接通过类名调用,无需创建对象。静态方法只能访问静态成员变量和调用其他静态方法,不能访问非静态成员变量和非静态方法。
  • 静态代码块:使用static修饰的代码块称为静态代码块,它在类加载时执行,用于对静态成员进行初始化操作。

final关键字:

  • final变量:使用final修饰的变量是不可修改的,即它们的值在初始化后不能再被改变。final变量必须在声明时或构造函数中进行初始化,且不能被再次赋值。final变量可以是基本数据类型或引用类型。

  • final方法:使用final修饰的方法不能被子类重写或覆盖。这样可以防止子类修改或扩展父类中的方法,确保方法的实现不会被改变。

  • final类:使用final修饰的类是不可继承的,即它不能有子类。这样可以防止其他类继承该类,保护类的完整性和安全性。

static和final的主要区别如下:

  • static关键字用于创建类级别的成员,可以在类的所有实例之间共享。final关键字用于创建不可变的变量、方法或类,具有常量性质。
  • static关键字可以修饰成员变量、方法和代码块,final关键字可以修饰变量、方法和类。
  • static成员可以通过类名直接访问,final成员只能被初始化一次且不能再被修改。 在使用static和final时,需要注意以下几点:
  • 尽量避免滥用static关键字,因为它会导致全局状态和耦合性增加。合理使用static可以提高代码的可读性和性能。
  • final关键字可以用于提高代码的安全性和稳定性,特别是在多线程环境下。
  • 使用final关键字对于常量的定义非常有用,可以提供清晰的意图和编译时检查。
    总结来说,static关键字用于创建类级别的成员,final关键字用于创建不可变的变量、方法或类。
  • [ ] 四种引用方式:强软弱虚

当我们在Java中使用对象时,可以使用四种不同的引用类型来控制对象的生命周期和垃圾回收行为。这四种引用类型分别是:

  1. 强引用(Strong Reference):强引用是最常见的引用类型。当我们通过一个变量来引用一个对象时,实际上是创建了一个强引用。只要强引用存在,垃圾回收器就不会回收被引用的对象。即使系统内存不足时,也不会回收强引用对象,这可能会导致内存泄漏。

    示例代码:

    1
    java Object obj = new Object(); // 创建一个强引用 
  2. 软引用(Soft Reference):软引用是一种相对强引用更弱的引用类型。当系统内存不足时,垃圾回收器会尝试回收软引用对象。通过软引用可以实现一些对内存敏感的缓存机制。在Java中,可以使用SoftReference类来创建软引用。

示例代码:

1
java SoftReference<Object> softRef = new SoftReference<>(new Object()); // 创建一个软引用 
  1. 弱引用(Weak Reference):弱引用也是一种相对强引用更弱的引用类型。与软引用不同的是,弱引用的对象在下一次垃圾回收时总是会被回收,不论内存是否足够。在Java中,可以使用WeakReference类来创建弱引用。

    示例代码:

    1
    ```java WeakReference<Object> weakRef = new WeakReference<>(new Object()); // 创建一弱引用
  2. 虚引用(Phantom Reference):虚引用是最弱的引用类型。虚引用的存在几乎没有引用的作用,它主要用于跟踪对象被垃圾回收器回收的状态。在Java中,可以使用PhantomReference类来创建虚引用。虚引用必须与引用队列(ReferenceQueue)一起使用。

    示例代码:

    1
    java ReferenceQueue<Object> queue = new ReferenceQueue<>(); PhantomReference<Object> phantomRef = new PhantomReference<>(new Object(), queue); // 创建一个虚引用 
  3. 总结一下,这四种引用类型提供了不同级别的引用强度,从强到弱分别是强引用、软引用、弱引用和虚引用。这些引用类型在不同的场景下有着不同的用途,可以根据需求选择合适的引用类型来管理对象的生命周期和内存使用。

  • [ ] volatile作用和原理 用了就能保证线程安全吗 可以举个例子吗 那怎么保证原子性的操作呢

作用:

volatile 是 Java 中的一个关键字,它具有两个主要作用:

  1. 可见性:当一个变量被声明为 volatile,对该变量的写操作将立即被刷新到主内存中,而对该变量的读操作将从主内存中读取最新的值,而不是使用线程的本地缓存。这样可以确保不同线程之间对该变量的读写操作是可见的,避免了线程之间的数据不一致性问题。
  2. 禁止指令重排序:volatile 关键字可以防止指令重排序优化,确保特定操作的执行顺序按照程序代码的顺序来进行,从而避免了一些多线程下的问题。

原理:

volatile 关键字的实现原理涉及到处理器缓存和内存屏障(Memory Barrier)的概念。 在多线程环境中,每个线程都有自己的线程栈和工作内存,线程对共享变量的读写操作通常是在工作内存中进行的。为了提高程序的执行效率,处理器会对读写操作进行优化,使用处理器缓存(CPU Cache)来存储最近被访问的数据,减少对主内存的访问。 然而,这种优化可能导致可见性问题。当一个线程修改了共享变量的值时,由于处理器缓存的存在,其他线程可能无法立即感知到这个变化,仍然使用缓存中的旧值。这就导致了线程之间的数据不一致性。 在这种情况下,使用 volatile 关键字声明的变量,会告诉编译器和处理器,该变量是共享的,需要遵循特定的语义规则: 1. 对 volatile 变量的写操作会立即刷新到主内存中,确保其他线程可以立即看到最新的值。 2. 对 volatile 变量的读操作会从主内存中读取最新的值,而不是使用线程的本地缓存。 为了实现这种可见性,处理器会在写操作后插入内存屏障(Memory Barrier),确保写操作先于后续的读操作。内存屏障会阻止处理器对指令进行重排序,保证了 volatile 变量的写-读操作顺序。 需要注意的是,volatile 关键字并不能保证复合操作的原子性。如果一个操作涉及多个变量的读写,并且需要保证原子性,仍然需要使用锁或其他同步机制。 总结起来,volatile 关键字通过使用处理器缓存和内存屏障来实现可见性,保证共享变量的写操作对其他线程立即可见,以及禁止指令重排序优化。这样可以在多线程环境中保证线程之间对共享变量的操作是可见的,从而避免了数据不一致性的问题。

用了就能线程安全?

使用 volatile 关键字可以提供可见性和禁止指令重排序的特性,但并不能保证线程安全。 volatile 关键字主要解决的是可见性问题,即保证对 volatile 变量的写操作对其他线程是可见的。这意味着当一个线程修改了 volatile 变量的值时,其他线程能够立即看到最新的值。这在一些简单的标志位或状态标识等场景下是有用的。 然而,线程安全不仅仅涉及到可见性,还包括原子性和有序性。volatile 并不能保证复合操作的原子性,即多个操作的执行不会被其他线程中断,因此在需要保证原子性的场景下,仍然需要使用锁或其他同步机制。 此外,volatile 关键字也无法保证操作的有序性。指令重排序是编译器和处理器为了提高执行效率而进行的优化,但有时候会导致程序执行顺序与代码的顺序不一致。volatile 可以防止特定操作的指令重排序,但并不能保证所有操作的有序性。 综上所述,虽然 volatile 关键字在某些场景下可以提供一定程度的线程安全性,但它并不能解决所有的线程安全问题。对于更复杂的线程安全需求,需要使用其他的同步机制,如锁、原子类或并发容器等。

举个例子

当涉及到复合操作或需要保证原子性的情况时,volatile 关键字并不足够。下面是一个例子来说明这一点:

1
2
3
4
5
6
7
8
9
10
11
public class VolatileExample {
private volatile int counter = 0;

public void increment() {
counter++;
}

public int getCounter() {
return counter;
}
}

在上面的例子中,我们有一个 counter 变量,用于计数。increment() 方法对 counter 变量进行自增操作,而 getCounter() 方法用于获取当前的计数值。

尽管 counter 变量被声明为 volatile,它可以确保可见性,即当一个线程调用 increment() 方法修改了 counter 的值后,其他线程能够立即看到最新的值。但是,由于自增操作不是原子性的,即它涉及到读取、修改和写入三个步骤,可能会发生竞态条件。

假设有两个线程同时执行如下代码:

1
2
3
4
5
6
7
VolatileExample example = new VolatileExample();

// 线程1执行
example.increment();

// 线程2执行
int counter = example.getCounter();

在这种情况下,由于自增操作不是原子性的,可能会发生以下情况:

  1. 线程1读取 counter 的值为 X。
  2. 线程2读取 counter 的值为 X。
  3. 线程1进行自增操作,得到结果为 X + 1。
  4. 线程1将新值写回 counter
  5. 线程2将旧值 X 返回,而不是预期的 X + 1。

由于 volatile 关键字无法保证复合操作的原子性,上述情况可能导致结果的不一致性。

为了保证原子性操作,可以使用锁或原子类,例如使用 synchronized 关键字:

1
2
3
4
5
6
7
8
9
10
11
public class SynchronizedExample {
private int counter = 0;

public synchronized void increment() {
counter++;
}

public synchronized int getCounter() {
return counter;
}
}

在上面的例子中,通过使用 synchronized 关键字修饰 increment()getCounter() 方法,确保了对 counter 变量的读写操作是原子性的,从而避免了竞态条件和数据不一致性的问题。

总结:volatile 关键字适用于简单的标志位或状态标识等场景,可以提供可见性和禁止指令重排序的特性。但在涉及复合操作或需要保证原子性的情况下,需要使用锁或原子类来保证线程安全性。

如何保证原子性的操作?

要保证原子性的操作,可以采用以下几种方式:

  1. 使用锁(synchronized):使用 synchronized 关键字来对共享资源进行加锁,确保在同一时刻只有一个线程可以访问该资源。通过同步机制,可以保证对共享资源的操作是原子的。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public class Counter {
private int count = 0;

public synchronized void increment() {
count++;
}

public synchronized void decrement() {
count--;
}

public synchronized int getCount() {
return count;
}
}
  1. 使用原子类:Java 提供了一系列原子类,如 AtomicIntegerAtomicLong 等,它们提供了针对基本类型的原子操作方法,保证了操作的原子性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.atomic.AtomicInteger;

public class Counter {
private AtomicInteger count = new AtomicInteger(0);

public void increment() {
count.incrementAndGet();
}

public void decrement() {
count.decrementAndGet();
}

public int getCount() {
return count.get();
}
}
  1. 使用并发容器:Java 提供了一些并发容器,如 ConcurrentHashMapConcurrentLinkedQueue 等,它们使用了一些内部机制来保证并发访问时的原子性和线程安全性。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import java.util.concurrent.ConcurrentHashMap;

public class Counter {
private ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

public void increment(String key) {
map.put(key, map.getOrDefault(key, 0) + 1);
}

public void decrement(String key) {
map.put(key, map.getOrDefault(key, 0) - 1);
}

public int getCount(String key) {
return map.getOrDefault(key, 0);
}
}

使用 volatile 并不能保证线程安全。尽管 volatile 可以确保可见性,但它无法解决线程安全的其他问题,比如原子性和操作顺序问题。

对于需要保证原子性的操作,可以使用以下方法之一:

  1. 使用原子类(Atomic Class):Java 提供了一系列原子类,如 AtomicIntegerAtomicLongAtomicReference 等。这些类提供了基于原子操作的方法,可以保证操作的原子性。
  2. 使用锁(Lock):使用锁机制如 synchronizedReentrantLock 来保证临界区的原子性。

下面是一个简单的例子来说明 volatile 的作用:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class VolatileExample {
private volatile boolean flag = false;

public void setFlag() {
flag = true;
}

public void doSomething() {
while (!flag) {
// 等待 flag 变为 true
}
// 执行其他操作
}
}

在上面的例子中,flag 变量被声明为 volatile,确保了在一个线程中调用 setFlag() 方法后,另一个线程中的 doSomething() 方法能够立即感知到 flag 的变化,避免了线程之间的数据不一致性问题。 需要注意的是,虽然 volatile 关键字提供了可见性和禁止指令重排序的特性,但它并不能保证复合操作的原子性。对于复合操作的原子性,需要使用其他的同步机制,如锁或原子类。

集合

  • [ ] 数组和链表的区别2

  • [ ] hashmap的底层实现,时间复杂度

  • [ ] 谈谈HashMap和LinkedHashMap

  • [ ] hashmap和hashtable的区别?

  • [ ] ArrayList的数据结构实现

  • [ ] wait和sleep的区别

  • [ ] HashSet是线程安全的吗,怎么能获得一个安全的,使用锁的话如何提高效率

  • [ ] arraylist为什么不声明大小

  • [ ] arraylist和vector的区别 有基于arraylist写过安全的容器吗? 如何实现arraylist的安全呢? 用线程安全的容器一定能保证线程安全吗?

  • [ ] hashmap在最佳的情况下的时间复杂度?

  • [ ] hashmap在最差情况下的时间复杂度?这时候它是一个什么数据结构?

  • [ ] 如果hashmap线程不安全如何让其变成线程安全?

  • [ ] Map和Set

  • [ ] 继承为什么不能继承 多个类

  • [ ] 为什么不能向下转型

JVM

  • [ ] 内存回收

  • [ ] GC算法

  • [ ] Full GC和其他GC的区别

  • [ ] 可达性分析是有GC Root,什么东西可以是GC Root

  • [ ] 用过JUC的什么

普通

  • [ ] 介绍一下读写锁
  • [ ] 编译流程?
  • [ ] java里的强类型和弱类型
  • [ ] 双亲委派(写一个路径名字一样的String,new String()会创建自己写的还是系统的)
  • [ ] 受检异常和非受检异常
  • [ ] ==与equals
  • [ ] 深拷贝和浅拷贝区别
  • [ ] 对多线程的理解

Kotlin

  • [ ] 作用域函数(应用场景)

  • [ ] 高阶函数(概念)

  • [ ] kotlin和java一块编译碰到啥问题

  • [ ] ::funName 双冒号的写法的理解

  • [ ] 泛型 out in 与Java泛型中的联系和区别

计网

  • [ ] 计算机七层模式和四层模型分别介绍一下,有哪些协议路由器和交换机分别在哪一层

  • [ ] ipv4地址空间这么小怎么够用,NAT。用同一个IP怎么区分。

TCP/UDP

  • [ ] TCP和UDP场景应用

  • [ ] UDP有什么办法可靠吗

  • [ ] UDP有哪些协议

  • [ ] 介绍一下tcp udp区别

  • [ ] 介绍一下tcp三次握手,有什么攻击手段(flooding syn attack)

  • [ ] session key和cookie是什么,两者有啥区别,分别储存在哪里

  • [ ] post请求和get请求有什么区别

  • [ ] tcp的滑动窗口控制协议?

  • [ ] tcp如何实现可靠传输

  • [ ] https 如何加密

  • [ ] 对称加密和非对称加密的区别

  • [ ] tcp断开为什么需要四次挥手 如何A要断开 B没回复 怎么办?我回复超时重传 面试官追问 如果B一直不回 会无限发下去吗

HTTP/HTTPS

  • [ ] HTTP的请求包含什么内容

  • [ ] HTTP的数据体包含什么属性

  • [ ] HTTP是不是长连接

  • [ ] http的连接过程以及如何进行攻击

  • [ ] http 1.0 和 2.0

  • [ ] https和http的改进

  • [ ] http状态码

  • [ ] http状态码302和307区别

  • [ ] HTTP有加密吗?

  • [ ] HTTPS的S指的是什么?

  • [ ] TLS/SSL在哪一个阶段发挥它的作用?(三次握手之后)

  • [ ] http 2.0和3.0

  • [ ] dns解析的过程

  • [ ] 应用层协议有哪些

  • [ ] 输入网址到看到页面的整个过程(尽可能的详细, 尽量把知道的协议都说一遍)

OS

内存

  • [ ] 堆内存和栈内存的区别(内存管理,碎片化)

  • [ ] 虚拟内存相比物理内存有哪些优点

  • [ ] os为什么设计虚拟内存

  • [ ] 32位机器的虚拟内存有多大

  • [ ] ipc虚拟内存和智能指针

  • [ ] 内存空间介绍

  • [ ] 共享内存的使用场景

  • [ ] 多线程操作共享内存需要注意什么问题

  • [ ] 内存分页和分段

进程线程

  • [ ] 进程的几个状态

  • [ ] 什么时候会进入阻塞状态

  • [ ] 进程调度算法有哪些

  • [ ] 线程间资源共享

  • [ ] 线程之间的通讯

  • [ ] 进程通信的方式

  • [ ] 进程线程的区别,如果只有一个打印机,怎么保证只有一个进程访问(patterson算法)

  • [ ] 介绍一下死锁

  • [ ] 锁分为哪些种类

  • [ ] 怎么避免死锁,怎么处理这玩意

  • [ ] 说一下互斥锁和信号量

  • [ ] 内核态和用户态

算法

喜欢问:优化

  • [ ] 两个栈实现一个队列

排序

  • [ ] 排序算法知道哪些?

  • [ ] 排序,哪些是稳定的,哪些是不稳定的

  • [ ] 快速排序是不是稳定的,为什么

  • [ ] 快排的流程

  • [ ] 快排

  • [ ] 归并排序的过程 复杂度多少(结合过程)

  • [ ] 给升序排列的整数数组,输出每个数字平方后的升序排列数组

  • [ ] 用堆实现小跟堆

  • [ ] 给一个int数,判断是不是回文平方数(既是回文数也可以有一个整数平方根),禁止使用Math函数

  • [ ] 了解哪些数据结构

  • [ ] 项目中用到哪些

(项目一般都是数组或者链表)树、栈等结构为什么存在

  • [ ] 反转链表

LRU

  • [ ] 第一个是对比如lru算法的情景,用什么数据结构去存储

  • [ ] 手写一个基于读写锁的线程安全的List

遍历

  • [ ] 二叉树的中序遍历

  • [ ] 层序遍历

  • [ ] 图的遍历算法

  • [ ] 树的遍历算法

  • [ ] 反转字符串

  • [ ] 上楼梯动态规划,如果第M阶梯是不能踩的

其他

软件设计架构模ok:

  • [x] MVC,MVP,MVVM 各自介绍和区别

MVC:

Model:数据类型,以及对数据的CRUD

View:界面展示,用户交互

Controller:Model和View的中间人

解决业务界面分析,解耦。但C臃肿

MVP:

改C为P,M和V不能直接通信,全部P中转。

解耦plus,但是P代码量大,部分View功能。

MVVM:

用ViewModel代替Presenter,实行数据绑定。

View变通,ViewModel也变。

这是单项,双向的。VM变V也变。

这个减少代码量。

MVI:V发出意图给Model然后Model新状态给V,实现同步。

软件设计模式

  • [ ] 设计模式有什么了解,分别能做什么场景

  • [ ] 单例模式的线程安全问题

  • [ ] 手写单例模式 在项目中用过吗 有什么优劣吗

  • [ ] 单例的生命周期有限制吗

  • [ ] 单例模式的实现方式

  • [ ] 手写单例模式的懒汉式

  • [ ] 写一个单利线程安全且高性能的实例

  • [ ] 观察者模式介绍一下

Git🆗

  • [x] 讲讲git rebase 和git merge的区别

git merge:可以保留完整Git版本合并更改记录

git rebase:可以保持线性干净的版本控制记录

在项目开发时,自己开发好模块在准备合并到主branch前自己gitrebase一下,然后再 git merge到主版本

  • [x] Git指令有哪些

git init:初始化

git status:看状态

git log:看提交日志

git add:添加暂存区

git commit:添加本地仓库

git push:同步GitHub

git clone:新项目进去克隆远程项目到本地

git pull:拉取远程仓库状态到本地并合并

git merge:合并分支并保留完整记录

git rebase:合并成线性记录

git branch:拉一个新分支

git chekout:切换分支

git cherry-pick commit-hash:将其他分支的依次提交合并到自己分支

  • [x] Git把别人分支的一次提交合并到自己分支 用什么command

git cherry-pick commit-hash

学习能力🆗

  • [x] 自我学习能力如何?通过哪些渠道方式提升自己?

自我学习能力:自驱性强(技术全自学),韧性好(遇到问题必解决),认知高(选择,信息,决策,效率)

提升自己渠道:见人(书籍/公众号/私课),做事(验证,重复)。

HR面

  • [x] 未来职业规划

分类讨论:

短期:

  • 一直日常实习到大三结束
  • 如果有转正机会就争取转正
  • 没有就大三下冲提前批,秋招,春招
  • 目标是大厂

中期:

  • 快速熟悉公司工作环境和流程,融入公司开始工作
  • 完成好自己的小工作模块转正
  • 成为骨干
  • 成为主程

长期:

  • 深度上:坚持Android原生,提升自己这方面的技术。成为高级Android开发
  • 广度:配合公司hybid技术,学习大前端 or 学framework
  • [x] base地选择

我都可以,听从安排。

  • [x] 实习时间

立即到岗,期间全勤。

8~12个月,每周6天。

  • [x] 非常确认后面确认了offer可以来?

大厂:我坑定来你家,不为别的,就是看对眼,喜欢!稀罕!

小厂:如果argue一下钱,那我就坑定来。因为想家/女朋友/喜欢这个城市/人情味

  • [x] 可以实习到什么时间

大三下~更久

  • [x] 有读博、公务员计划?

无,从大一开始就铁定工作。

  • [x] 本科专业是信管,想走程序员为啥不读计算机,为什么想要走这个方向
  1. 填计算机被调剂来信管
  2. 家庭情况
  3. 个人性格追求
  4. 真喜欢技术,享受
  5. Andorid很好玩
  • [x] 你专业有哪些课,技术都是自学的吗

计算机 + 管理 = 信管

基础:高数 线代 概率论 (离散)大物 政治

计算机:

C Java C++ Python

数据结构和算法 计网 操作系统 计组

数据库 大数据(数据分析 数据挖掘) AI人工智能&机械学习 商务智能

Java EE /Android/测试

管理:

经济学 管理学 运筹学 营销学 商务分析 财务

信息系统 ERP

  • [x] 大学取得的最大成就是什么

内在:认知 + 情绪 + 成长(大)

外在:就业最好 + 靠自己还债和养活自己甚至反哺家里

  • [x] 说一下如何学习,学习方法。

重复

  • [x] 兴趣爱好

写代码

3C数码

  • [x] 对职位有了解吗

开发 -> 客户端 ->Android -> 原生 APP开发

纯粹 性能 门槛


  • [ ] 入职了以后发现没有达到职位要求怎么办,怎么样尽快开启工作

  • [ ] 希望在团队中担任什么角色

  • [ ] 毕业规划

  • [ ] 自己最大的优点和缺点

  • [ ] 自己遇到过的挫折有哪些

  • [ ] 整个学习期间影响最深的事情

  • [ ] 学校课程学习

  • [ ] 网络协议的差别

  • [ ] linux系统和别的嵌入式系统android, ios的差别

  • [ ] c,python,java的区别极其优缺点

  • [ ] 平时关注前沿技术资讯吗,介绍一两个最新的

  • [ ] 自己大学期间有没有学过什么其他的技术

  • [ ] 技术栈会哪些?其他语言会吗?

  • [ ] Java和c++区别?

  • [ ] 解决过的最难的问题是什么以及对应解决方法

  • [ ] 如果碰到一些新问题、新技术有什么好的学习方法