七月份已经过完了,2023年已经过去一半了,迎来了毕业季,也迎来了秋招,也即将迎来"金九银十”,给大家发一次干货吧,相信有很多的小伙伴都不想浪费暑假这个提升自己的机会,那今天小编给大家一个福利了,分享一些面试题,希望帮助大家早日找到理想的工作。

Java虚拟机

1、谈一谈JAVA垃圾回收机制?

垃圾回收即garbagecollection,简称GC,作用是在某块内存
不再使用时及时对其进行释放的管理机制。GC的几个重点就是怎么找到无用对象,怎么对其进行释放,何时进行GC等等
另外说一句,Hotspot VM里堆是分代回收的(分出新生代和老年代,分别进行回收),不知道ART里有没有类似的机制

2、怎么找到无用对象?

目前来说有两种主流机制,

  • 引用计数:最简单的寻找无用对象的机制,当一个对象被引用一次,引用计数+1,当失去引用时引用计数-1,当此对象引用计数为0时可以直接回收。这种方法有一个显而易见的问题:无法回收被循环引用的对象。
  • 可达性分析:从一个根对象(GC Root)开始向下搜寻,可以认为搜寻到的所有有强引用的对象都是活跃对象,所有找不到的对象都是无用对象,无用对象可能会被即刻回收,也可能进行其他操作(比如执行对象的finalize() 方法)这里还会引出一点问题:关于强引用,软引用,弱引用和虚引用的分别处理,具体可以看#27

3、如何释放无用对象?

这个具体是看回收器的实现

4、何时开始GC?

任何时候都可能,当系统觉得你内存不足了就会开始回收常见的比如分配对象内存不足时(这里的内存不足有可能不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象),当前堆内存占用超过阈值时,手动调用 System.gc() 建议开始GC时,系统整体内存不足时等

这里举个例子:
你要分配一个大对象(比如一张图片),如果内存不足, 可能会发生下面这个情况:

  1. 首先开始一次GC,这次GC不会回收被软引用引用的对 象
  2. 如果内存仍然不足,做点其他事情尝试让内存足够,常见的比如:
  • 堆整理:前面说过,内存不足有可能不是占用真的很高,可能是内存足够,但是没有连续内存空间去放这个对象,那就试试整理一下堆看看出来的空间够不够
  • 增大堆内存:尝试申请更大的堆来放对象
  1. 如果内存依然不足,再发起一次GC,这次GC会回收仅 被软引用或更弱引用引用的对象(这句话好像有点
    乱),然后再次尝试分配对象
  2. 如果内存还是不够,那没办法了,抛出OutOfMemoryError

5、回答一下什么是强、软、弱、虚引用以及它们之间的区别?

  • 强引用(StrongReference)

强引用是使用最普遍的引用。如果一个对象具有强引用,那垃圾回收器绝不会回收它。当内存空间不足,Java虚拟机宁愿抛出 OutOfMemoryError 错误,使程序异常终止,也不会靠随意回收具有强引用的对象来解决内存不足的问题。

  • 软引用(SoftReference)

如果一个对象只具有软引用,则内存空间足够,垃圾回收器就不会回收它;如果内存空间不足了,就会回收这些对象的内存。只要垃圾回收器没有回收它,该对象就可以被程序使用。软引用可用来实现内存敏感的高速缓存(下文给出示例)。

软引用可以和一个引用队列 ReferenceQueue 联合使用,如果软引用所引用的对象被垃圾回收器回收,Java虚拟机就会把这个软引用加入到与之关联的引用队列
中。

  • 弱引用(WeakReference)

弱引用与软引用的区别在于:只具有弱引用的对象拥有更短暂的生命周期。在垃圾回收器线程扫描它所管辖的内存区域的过程中,一旦发现了只具有弱引用的对象, 不管当前内存空间足够与否,都会回收它的内存。不 过,由于垃圾回收器是一个优先级很低的线程,因此不一定会很快发现那些只具有弱引用的对象。

弱引用可以和一个引用队列 联合使用,如果弱引用所引用的对象被垃圾回收,Java虚拟机就会把这个弱引用加入到与之关联的引用队列中。

  • 虚引用(PhantomReference)

“虚引用”顾名思义,就是形同虚设,与其他几种引用都不同,虚引用并不会决定对象的生命周期。如果一个对象仅持有虚引用,那么它就和没有任何引用一样,在任何时候都可能被垃圾回收器回收。虚引用主要用来跟踪对象被垃圾回收器回收的活动。虚引用与软引用和弱引用的一个区别在于:虚引用必须和引用队列 联合使用。当垃圾回收器准备回收一个对象时,如果发现它还有虚引用,就会在回收对象的内存之前,把这个虚引用加入到与之 关联的引用队列中。

6、简述JVM中类的加载机制与加载过程?

1.概述
虚拟机把描述类的数据从Class文件加载到内存,并对数据 进行校验、转换解析和初始化,最终形成可以被虚拟直接使用的java类型,这就是虚拟机的类加载机制。

2.类加载的过程

  • 加 载
  • 验 证
  • 准 备
  • 解 析
  • 初始化
  • 使 用
  • 卸载

其中验证,准备,解析三个部分称为连接。其中解析和初始化的顺序可能会不同(可能先解析后初始化,也可能先初始化后解析)。

2.1 关于初始化

5种情况会触发类的初始化

  • 遇到new,getstatic,putstatic,invokesstatic这四个字节码指令时,如果类没有被初始化
  • 使用java.lang.reflect包的方法对类进行反射时,如果类没有被初始化,则先触发其初始化
  • 当初始化一个类时,其父类没有被初始化,则需要父类先初始化
  • 虚拟机启动时,用户需要制定一个执行的主类,虚拟机会先初始化这个类
  • JDK 1.7动态语言支持时,如果一个java.lang.invoke.MethodHandle实例后的解析结果REF_getStatic,REF_putStatic,REF_invokeStatic 的 方 法 句柄,并且这个方法句柄所对应的类没有被初始化

3.类加载的过程

  • 通过一个类的全限名来获取定义此类的二进制字节流
  • 将字节流所代表的静态存储结构转化为方法区的运行时数据结构
  • 在内存中生成一个代表这个类的java.lang.Class对象, 作为方法区这个类的各种数据的访问入口

这里顺带再说下对象的加载过程

  1. 对象的创建
  • java虚拟机遇到一个new指令

  • 检查new引用代表的类是否被加载,解析和初始化

    • 加载过

    • 没有加载过,先执行相应类的加载过程

  • 虚拟机为对象分配内存

    • 对象所需要的内存大小在类加载过后便可以确定

    • 为对象分配空间的过程等同于把一块确定大小的内存从java堆中划分出来。

      • java堆绝对规整绝对规整解释:所有用过的内存放在一边,空闲的内存放在另一边中间放着一个指针作为分界点的指示器。

      • 分配过程:指针向空闲空间那边挪动一段与对象大小相等的距离。这种分配方式称为“指针碰撞”。

    • java堆不规整

      • 不规整解释:已使用的内存和未使用的内存相互交错,虚拟机维护一个列表,记录那些内存是可用的。

      • 分配过程:分配时从列表中找一块足够大的空间划分给对象实例。并更新列表上的记录。这种分配方式称为“空闲列表”。

      • java堆是否规整是由所采用的垃圾收集器是否带有 压缩整理功能决定的。

Java多线程

1、Java 中使用多线程的方式有哪些?

大概这么4种
extends Thread;
implRunnable;
implCallable通过 FutureTask包装器来创建线程;
使用ExecutorService、Callable、Future实现有返回结果的多线
程。;
extends Thread 和 implRunnable 的线程没有返回值, 而
FutureTask 和
ExecutorService(ExecutorService.submit(xxx) return Future<?> )
有返回值.

2、说一下线程的几种状态?

第一是创建状态。在生成线程对象,并没有调用该对象的
start方法,这是线程处于创建状态。
第二是就绪状态。当调用了线程对象的start方法之后,
该线程就进入了就绪状态,但是此时线程调度程序还没有把该线程设置为当前线程,此时处于就绪状态。在线程运行之后,从等待或者睡眠中回来之后,也会处于就绪
状态。
第三是运行状态。线程调度程序将处于就绪状态的线
程设置为当前线程,此时线程就进入了运行状态,开始运
行run函数当中的代码。
第四是阻塞状态。线程正在运行的时候,被暂停,通常
是为了等待某个时间的发生(比如说某项资源就绪)之后再继
续运行。sleep,suspend,wait等方法都可以导致线程阻塞。
第五是死亡状态。如果一个线程的run方法执行结束或
者调用stop方法后,该线程就会死亡。对于已经死亡的线程,
无法再使用start方法令其进入就绪。

3、如何实现多线程中的同步?

多线程同步和异步不是一回事。
几种情况,
1.就是大家说的synchronized 他可以保证原子性,保证多个
线程在操作同一方法时只有一个线程可以持有锁,并且操作
该方法,
2.就是手动调用读写锁,
3.手动操作线程的wait和notify
4.volatile我记得是没有原子性的,他可以保证内存可见性,
在多线程的情况下保证每个线程的数据都是最新的

4、谈谈线程死锁,如何有效的避免线程死锁?

一、死锁的定义
多线程以及多进程改善了系统资源的利用率并提高了系统
的处理能力。然而,并发执行也带来了新的问题——死
锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局
(互相等待),若无外力作用,这些进程都将无法向前推
进。
二、死锁产生的原因

  1. 系统资源的竞争
    通常系统中拥有的不可剥夺资源,其数量不足以满足多个进
    程运行的需要,使得进程在 运行过程中,会因争夺资源而
    陷入僵局,如磁带机、打印机等。只有对不可剥夺资源的竞
    争 才可能产生死锁,对可剥夺资源的竞争是不会引起死锁
    的。
  2. 进程推进顺序非法
    进程在运行过程中,请求和释放资源的顺序不当,也同样
    会导致死锁。例如,并发进程 P1、P2分别保持了资源
    R1、R2,而进程P1申请资源R2,进程P2申请资源R1时, 两
    者都 会因为所需资源被占用而阻塞。
    3)信号量使用不当也会造成死锁。进程间彼此相互等待对方发来的消息,结果也会使得这 些
    进程间无法继续向前推进。例如,进程A等待进程B发的消
    息,进程B又在等待进程A 发的消息,可以看出进程A和B 不
    是因为竞争同一资源,而是在等待对方的资源导致死 锁。
  3. 死锁产生的必要条件
    产生死锁必须同时满足以下四个条件,只要其中任一条件不
    成立,死锁就不会发生。
    互斥条件:进程要求对所分配的资源(如打印机)进行排
    他性控制,即在一段时间内某 资源仅为一个进程所占有。
    此时若有其他进程请求该资源,则请求进程只能等待。
    不剥夺条件:进程所获得的资源在未使用完毕之前,不能
    被其他进程强行夺走,即只能 由获得该资源的进程自己
    来释放(只能是主动释放)。
    请求和保持条件:进程已经保持了至少一个资源,但又提
    出了新的资源请求,而该资源 已被其他进程占有,此时
    请求进程被阻塞,但对自己已获得的资源保持不放。循环
    等待条件:存在一种进程资源的循环等待链,链中每一个
    进程已获得的资源同时被 链中下一个进程所请求。
/**
* 一个简单的死锁类
* 当DeadLock类的对象flag==1时(td1),先锁定o1,睡
眠500毫秒
* 而td1在睡眠的时候另一个flag==0的对象(td2)线程启
动,先锁定o2,睡眠500毫秒* td1睡眠结束后需要锁定o2才能继续执行,而此时o2已被
td2锁定;
* td2睡眠结束后需要锁定o1才能继续执行,而此时o1已被
td1锁定;
* td1、td2相互等待,都需要得到对方锁定的资源才能继续执
行,从而死锁。
*/
public class DeadLock implements Runnable { public int flag
= 1;
//静态对象是类的所有对象共享的
private static Object o1 = new Object(), o2
= new Object();
@Override
public void run() { System.out.println("flag=" +
flag); if (flag == 1) {
synchronized (o1) { try {
Thread.sleep(500);
} catch (Exception e)
{ e.printStackTrace();
}synchronized (o2)
{ System.out.println("1");
}
}
}
if (flag == 0)
{ synchronized (o2) {
try {
Thread.sleep(500);
} catch (Exception e)
{ e.printStackTrace();
}
synchronized (o1)
{ System.out.println("0");
}
}
}
}
public static void main(String[] args) {
DeadLock td1 = new DeadLock();
DeadLock td2 = new DeadLock();
td1.flag = 1;
td2.flag = 0;
//td1,td2都处于可执行状态,但JVM线程调度先执行哪
个线程是不确定的。
//td2的run()可能在td1的run()之前运行newThread(td1).start();
new Thread(td2).start();
}
}

5、如何避免死锁

在有些情况下死锁是可以避免的。三种用于避免死锁的技
术:

  1. 加锁顺序(线程按照一定的顺序加锁)Thread 1:
    lock A
    lock B
    Thread 2:
    wait for A
    lock C (when A locked)
    Thread 3:
  2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超
    过时限则放弃对该锁的请求,并释放自己占有的锁)
  3. 死锁检测
    加锁顺序
    当多个线程需要相同的一些锁,但是按照不同的顺序加
    锁,死锁就很容易发生。
    如果能确保所有的线程都是按照相同的顺序获得锁,那么
    死锁就不会发生。看下面这个例子:
Thread 1:
lock A
lock B
Thread 2:
wait for A
lock C (when A locked)
Thread 3:
2. 加锁时限(线程尝试获取锁的时候加上一定的时限,超
过时限则放弃对该锁的请求,并释放自己占有的锁)
3. 死锁检测
加锁顺序
当多个线程需要相同的一些锁,但是按照不同的顺序加
锁,死锁就很容易发生。
如果能确保所有的线程都是按照相同的顺序获得锁,那么
死锁就不会发生。看下面这个例子:
wait for A
wait for B
wait for C

如果一个线程(比如线程3)需要一些锁,那么它必须按照确定的顺序获取锁。它只有获得了从顺序上排在前面的锁之
后,才能获取后面的锁。例如,线程2和线程3只有在获取了锁A之后才能尝试获取锁C(译者注:获取锁A是获取锁C的必要条件)。因为线程1已 经拥有了锁A,所以线程2和3需要一直等到锁A被释放。然 后在它们尝试对B或C加锁之前,必须成功地对A加了锁。按照顺序加锁是一种有效的死锁预防机制。但是,这种方式需要你事先知道所有可能会用到的锁(译者注:并对这些锁做适当的排序),但总有些时候是无法预知的。加锁时限另外一个可以避免死锁的方法是在尝试获取锁的时候加一个超时时间,这也就意味着在尝试获取锁的过程中若超过了这个时限该线程则放弃对该锁请求。若一个线程没有在给定的时限内成功获得所有需要的锁,则会进行回退并释放所有已经获得的锁,然后等待一段随机的时间再重试。这段随机的等待时间让其它线程有机会尝试获取相同的这些锁,并且让该应用在没有获得锁的时候可以继续运行(译者注:加锁超时后可以先继续运行干点其它事情,再回头来重复之前加锁的逻辑)。以下是一个例子,展示了两个线程以不同的顺序尝试获取
相同的两个锁,在发生超时后回退并重试的场景:

Thread 1 locks A
Thread 2 locks B
Thread 1 attempts to lock B but is blocked
Thread 2 attempts to lock A but is blocked
Thread 1's lock attempt on B times out
Thread 1 backs up and releases A as well
Thread 1 waits randomly (e.g. 257 millis)
before retrying. Thread 2's lock attempt on A times out
Thread 2 backs up and releases B as well
Thread 2 waits randomly (e.g. 43 millis)
before retrying.

在上面的例子中,线程2比线程1早200毫秒进行重试加锁,因此它可以先成功地获取到两个锁。这时,线程1尝试获取锁A并且处于等待状态。当线程2结束时,线程1也可以顺利的获得这两个锁(除非线程2或者其它线程在线程1成功获得两个锁之前又获得其中的一些锁)。需要注意的是,由于存在锁的超时,所以我们不认为这种场景就一定是出现了死锁。也可能是因为获得了锁的线程(导致其它线程超时)需要很长的时间去完成它的任 务。此外,如果有非常多的线程同一时间去竞争同一批资源,就算有超时和回退机制,还是可能会导致这些线程重复地尝试但却始终得不到锁。如果只有两个线程,并且重试的超时时间设定为0到500毫秒之间,这种现象可能不会发生,但是如果是10个或20个线程情况就不同了。因为这些线程等待相等的重试时间的概率就高的多(或者非常接近以至于会出现问题)。(译者注:超时和重试机制是为了避免在同一时间出现的竞争,但是当线程很多时,其中两个或多个线程的超时时间一样或者接近的可能性就会很大,因此就算出现竞争而导致超时后,由于超时时间一样,它们又会同时开始重试, 导致新一轮的竞争,带来了新的问题。)这种机制存在一个问题,在Java中不能对synchronized同步块设置超时时间。你需要创建一个自定义锁,或使用Java5中java.util.concurrent包下的工具。写一个自定义锁类不复杂,但超出了本文的内容。后续的Java并发系列会涵盖自定义锁的内容。
死锁检测
死锁检测是一个更好的死锁预防机制,它主要是针对那些不可能实现按序加锁并且锁超时也不可行的场景。每当一个线程获得了锁,会在线程和锁相关的数据结构中(map、graph等等)将其记下。除此之外,每当有线程 请求锁,也需要记录在这个数据结构中。当一个线程请求锁失败时,这个线程可以遍历锁的关系图 看看是否有死锁发生。例如,线程A请求锁7,但是锁7这个时候被线程B持有,这时线程A就可以检查一下线程B是否已经请求了线程A当前所持有的锁。如果线程B确实有这样的请求,那么就是发生了死锁(线程A拥有锁1,请求锁7; 线程B拥有锁7,请求锁1)。当然,死锁一般要比两个线程互相持有对方的锁这种情况 要复杂的多。线程A等待线程B,线程B等待线程C,线程C 等待线程D,线程D又在等待线程A。线程A为了检测死锁, 它需要递进地检测所有被B请求的锁。从线程B所请求的锁开始,线程A找到了线程C,然后又找到了线程D,发现线程D请求的锁被线程A自己持有着。这是它就知道发生了死锁。下面是一幅关于四个线程(A,B,C和D)之间锁占有和请求 的关系图。像这样的数据结构就可以被用来检测死锁。

在这里插入图片描述

那么当检测出死锁时,这些线程该做些什么呢?一个可行的做法是释放所有锁,回退,并且等待一段随机的时间后重试。这个和简单的加锁超时类似,不一样的是只有死锁已经发生了才回退,而不会是因为加锁的请求超时了。虽然有回退和等待,但是如果有大量的线程竞争同一批锁,它们还是会重复地死锁(编者注:原因同超时类似,不能从根本上减轻竞争)。一个更好的方案是给这些线程设置优先级,让一个(或几个)线程回退,剩下的线程就像没发生死锁一样继续保持着它们需要的锁。如果赋予这些线程的优先级是固定不变的,同一批线程总是会拥有更高的优先级。为避免这个问题,可以在死锁发生的时候设置随机的优先级。

6、请谈谈 Thread 中 run() 与 start()的区别?

run() 和普通的成员方法一样,可以被重复调用。但是如果单独调用 run 方法,则不是在子线程中执行。start()这个方法只能被调用一次。调用这个方法后 程序会启动一个 新的线程来 执行 run 方法。注意 :调用start 后,线程处于可运行状态(并没有运行),一旦得到 cup 时间片,就开始执行run 方法,run 方法结束后,线程则立即终止。

7、synchronized和volatile关键字的区别?

1.volatile本质是在告诉jvm当前变量在寄存器(工作内存)中的值是不确定的,需要从主存中读取;synchronized则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住。
2.volatile仅能使用在变量级别;synchronized则可以使用 在变量、方法、和类级别的volatile仅能实现变量的修改可见性,不能保证原子性;而synchronized则可以保证变量的修改可见性和原子性
3.volatile不会造成线程的阻塞;synchronized可能会造成 线程的阻塞。
4.volatile标记的变量不会被编译器优化;synchronized标 记的变量可以被编译器优化

8、如何保证线程安全?

当多个线程要共享一个实例对象的值得时候,那么在考虑安全的多线程并发编程时就要保证下面3个要素:

原子性(Synchronized, Lock)

有序性(Volatile,Synchronized, Lock)
可见性(Volatile,Synchronized,Lock)

当然由于synchronized和Lock保证每个时刻只有一个线程执行同步代码,所以是线程安全的,也可以实现这一功能,但是由于线程是同步执行的,所以会影响效率。
下面是对3个要素的详细解释:
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。在Java中,基本数据类型的变量的读取和赋值操作是原子 性操作,即这些操作是不可被中断的,要么执行,要么不执行。

可见性:指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。当一个共享变量被volatile修饰时,它会保证修改的值会立 即被更新到主存,当有其他线程需要读取共享变量时,它会去内存中读取新值。普通的共享变量不能保证可见性,因为普通共享变量被修改后,什么时候被写入主存是不确定的,当其他线程去读取时,此时内存中可能还是原来的旧值,因此无法保证可见性。更新主存的步骤:当前线程将其他线程的工作内存中的缓存变量的缓存行设置为无效,然后当前线程将变量的值跟新到主存,更新成功后将其他线程的缓存行更新为新的主存地址其他线程读取变量时,发现自己的缓存行无效,它会等待缓存行对应的主存地址被更新之后,然后去对应的主存读取最新的值。

有序性:即程序执行的顺序按照代码的先后顺序执行。 在Java内存模型中,允许编译器和处理器对指令进行重排 序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。可以通过volatile关键字来保证一定的“有序性”。当在处理并发编程的时候,只要程序满足了原子性,可见性和有序性,那么程序就不会发生脏数据的问题。

9、谈谈ThreadLocal用法和原理?

ThreadLocal用于保存某个线程共享变量:对于同一个staticThreadLocal,不同线程只能从中get,set,remove自己的变量,而不会影响其他线程的变量。

1、ThreadLocal.get: 获取ThreadLocal中当前线程共享变量的
值。
2、ThreadLocal.set: 设置ThreadLocal中当前线程共享变量的
值。
3、ThreadLocal.remove: 移除ThreadLocal中当前线程共享变量的值。

4、ThreadLocal.initialValue: ThreadLocal没有被当前线程赋值时或当前线程刚调用remove方法后调用get方法, 返回此方法值。

10、Java 线程中notify 和 notifyAll有什么区别?

notify 方 法
1.方法调用之前由于也需要获取该对象的锁,所以使用的位置: synchronized方法中或者synchronized代码块中
2.通知那些可能等待该对象的对象锁的其他线程。如果有多个线程等待,则线程规划器任意挑选出其中一个wait() 状 态 的 线 程 来 发 出 通 知
3.wait()方法执行完毕之后,会立刻释放掉锁,如果没有再 次使用notify,其他wait()的线程由于没有得到通知,会继续阻塞在wait()的状态,等待其他对象调用notify或者notifyAll来唤醒。
notifyAll方法
1.使用位置和notify一样
2.notifyAll唤醒所有处于wait的线程

Handler

1、HandlerThread是什么?为什么它会存在?

作为一个Android开发,Handler机制是一定要了解的。在我面试过程中,发现很多人对Handler和Looper机制非常了解,对答如流, 但 是 却 不 知 道 何 为 HandlerThread

HandlerThread是Thread的子类,严格意义上来说就是一个线程,只是它在自己的线程里面帮我们创建了Looper HandlerThread存在的意义如下:

  1. 方便使用:a. 方便初始化,b,方便获取线程looper

  2. 保证了线程安全

我 们 一般在Thread里面 线程Looper进行初始化的代码里面,必须要对Looper.prepare(),同时要调用Loop。loop () ;

在这里插入图片描述

上面这段代码有没有问题呢?

  1. 在初始化子线程的handler的时候,我们无法将子线程的looper传值给Handler,解决办法有如下办法:
    a. 可以将Handler的初始化放到 Thread里面进行
    b. 可以创建一个独立的类继承Thread,然后,通过类的对象获取。 这 两种办法都可以,但是,这个工作 HandlerThread帮我们完成了
  2. 依据多线程的工作原理,我们在上面的代码中,调用 thread.getLooper () 的时候,此时的looper可能还没有初始化,此时是 不是可能会挂掉呢?

以上问题

HandlerThread 已经 帮我们 完美的 解决了 ,这就 是 handlerThread存在 的必要 性了。

我们再看看HandlerThread源码

在这里插入图片描述

它 的 优点就在于它的多线程操作,可以帮我们保证使用Thread的handler时一定是安全的。

2、简述下Handler机制的总体原理

1.Looper 准备和开启轮循:
Looper#prepare() 初 始 化 线 程 独 有 的 Looper 以 及 MessageQueue Looper#loop() 开 启 死 循 环 读 取 MessageQueue 中下 一 个 满 足 执 行 时 间的 Message 尚无 Message 的话,调用 Native 侧的 pollOnce() 进入无限等待 存在 Message,但执行时间 when 尚未满足的话,调用 pollOnce() 时传入剩余时长参数进入有限等待

  1. Message 发送、入队和出队:
    Native 侧如果处于无限等待的话:任意线程向 Handler 发送 Message 或 Runnable 后,Message 将按照 when 条件的先后,被插 入 Handler 持有的Looper 实例所对应的 MessageQueue 中适当的位置。 MessageQueue 发现有合适的 Message 插入后将调用 Native 侧的 wake() 唤醒无限等待的线程。这将促使 MessageQueue 的读取继续进入下一次循环,此刻 Queue 中已有满足条件的 Message 则 出队返回给 Looper Native 侧如果处于有限等待的话:在等待指定时长后 epoll_wait 将返回。线程继续读取 MessageQueue, 此 刻因为时长条件将满足将其出队 Looper 处理Message 的实现:
  2. Looper 得到 Message 后回调 Message 的 callback 属性即 Runnable,或依据 target 属性即 Handler,去执行 Handler 的回调。存 在 mCallback 属 性 的 话 回 调 Handler$Callback 反 之 , 回 调 handleMessage()

3、Looper存在哪?如何可以保证线程独有?

Looper 实 例 被 管 理 在 静 态 属 性 sThreadLocal 中 ThreadLocal 内 部 通 过 ThreadLocalMap 持 有 Looper, key 为ThreadLocal 实 例 本身,value 即为Looper 实例 每个 Thread 都有一个自己的 ThreadLocalMap,这样可以保证每个线程对应一个独立的 Looper 实 例,进而保证 myLooper() 可以获得线程独有的Looper 彩蛋:一个 App 拥有几个 Looper 实例?几个 ThreadLocal 实例?几 个 MessageQueue 实 例 ? 几 个 Message 实 例 ? 几 个 Handler 实 例

一个线程只有一个 Looper 实例 一个 Looper 实例只对应着一个 MessageQueue 实例 一个 MessageQueue 实例可对应多个 Message 实 例 ,其从 Message 静态池里获取,存在 50 的上限 一个线程可以拥有多个 Handler 实例,其Handler 只是发送和执行 任 务 逻 辑的入口和出口 ThreadLocal 实例是静态的,整个进程共用一个实例。每个 Looper 存放的 ThreadLocalMap 均弱引用它作 为 key

4、如何理解ThreadLocal的作用?

首 先要明确并非不是用来切换线程的,只是为了让每个线程方便程获取自己的 Looper 实例,见 Looper#myLooper()后续可供 Handler 初始化时指定其所属的 Looper 线程 也可用来线程判断自己是否是主线程

5、主线程Main Looper和一般的Looper的异同?

区别:
Main Looper 不可 quit 主线程需要不断读取系统消息和用书输入,是进程的入口,只可被系统直接终止。进而其 Looper 在创建 的 时 候 设置了不可 quit 的志,而其他线程的 Looper 则可以也必须手动 quit Main Looper 实例 还被静 态缓存 为了 便于每 个线程 获得主 线程 Looper 实例 ,见 Looper#getMainLooper(),Main Looper 实例 还 作 为 sMainLooper 属性缓存到了 Looper 类中。

相同点:
都是通过 Looper#prepare() 间接调用 Looper 构造函数创建的实例 都被静态实例 ThreadLocal 管理,方便每个线程获取自己的 Looper 实例 彩蛋:主线程为什么不用初始化 Looper?

App 的 入 口 并 非 MainActivity, 也 不 是 Application, 而 是 ActivityThread。其 为 了 Application、ContentProvider、Activity 等组 件的运 行,必 须事先 启动不 停接受 输入的 Looper 机制 ,所以 在main()执 行 的最后将调用 prepareMainLooper() 创建 Looper 并调用 loop() 轮循。不需要我们调用,也不可能有我们调用。可以说如果主线程没有创建 Looper 的话,我们的组件也不可能运行得到!

6、Handler或者说Looper如何切换线程?

Handler 创 建的 时候指 定了其 所属线 程的 Looper,进 而持有 了 Looper 独有 的 MessageQueue Looper# loop() 会持 续读取 MessageQueue 中合 适的 Message,没 有 Message 的时 候进入 等待当 向 Handler 发 送 Message 或 Runnable 后 , 会 向 持 有 的 MessageQueue 中 插 入 Message ,Message 抵 达并 满足条 件后会 唤醒 MessageQueue 所属 的线程 ,并将 Message 返回 给 Looper Looper 接 着 回 调 Message 所 指 向 的 Handler Callback 或 Runnable,达 到 线 程 切 换 的 目 的简 言之 ,向 Handler 发送 Message 其实 是向 Handler 所属 线程的 独有 MessageQueue 插入 Message。而线 程独有 的Looper 又会 持 续读取该 MessageQueue。所以向其他线程的 Handler 发送完 Message,该线程的 Looper 将自动响应。

Kotlin面试题

1、请简述一下什么是 Kotlin?它有哪些特性?

kotlin和java一样也是一门jvm语言最后的编译结果都是.class文件,并且可以通过kotlin的.class文件反编译回去java代码,并且封装了许多语法糖,其中我在项目中常用的特性有

1.扩展,(使用非集成的方式 扩张一个类的方法和变量):比方说 px和dp之间的转换 之前可能需要写个Util现在,通过扩展Float的变量 最后调用的时候仅仅是 123.dp这样px转成dp了

2.lamdba表达式,函数式编程. lamdba表达式并不是kotlin 的专利,java中也有,但是有限制, 像setOnClickListener一样, 接口方法只有一个的情况才能调用, 而在kotlin中对接口的lambda也是如此,有这样的限制,但是他更推荐你使用闭包的方式而不是实现匿名接口的方式去实现这样的功能,闭包对lambda没有接口这么多的限制,另外就是函数式编程 在java8中提供了streamApi对集合进行mapsortreduce等等操作,但是对androidapi有限制,为了兼容低版本,几乎不可能使用streamApi

3.判空语法 省略了许多ifxxx==null的写法 也避免了空指针异常aaa?.toString ?: “空空如也” 当aaa为空的时候 它的值被"空空如也"替代
aaa?.let{ it. bbb
}
当aaa不为空时 执行括号内的方法

4.省略了findViewById ,使用kotlin 就可以直接用xml中定义的id 作为变量获取到这个控件,有了这个 butterknife就可以淘汰了,使用databinding也能做到,但是,非常遗憾,databinding的支持非常不好,每次修改视图,都不能及时生成,经常要rebulid才能生成.

5,默认参数 减少方法重载 fun funName(a :Int ,b:Int =123)通过如上写法 实际在java中要定义两个写法 funName(a)和funName(a,b)

6.kotlin无疑是android将来语言的趋势,我已经使用kotlin 一年了,不仅App工程中使用,而且封装的组件库也是用kotlin,另外说明,kotlin会是apk大小在混淆后增加几百k.但对于更舒适的代码来说这又算的了什么呢

2、Kotlin 中注解 @JvmOverloads的作用?

@JvmOverloads注解的作用就是:在有默认参数值的方法加上
@JvmOverloads注解,则Kotlin就会暴露多个重载方法。可以减少写构造方法。例如:没有加注解,默认参数没有起到任何作用。

fun f(a: String, b: Int = 0, c: String="abc")
{
...
}
那相当于在java中:void f(String a, int b, String c){
}
如果加上注解@JvmOverloads ,默认参数起到作用
@JvmOverloads
fun f(a: String, b: Int = 0, c: String="abc")
{
...}
相当于Java中:
三个构造方法,
void f(String a)
void f(String a, int b)
void f(String a, int b, String c)

3、Kotlin中List与MutableList的区别?

List返回的是EmptyList,MutableList返回的是一个ArrayList,查看EmptyList的源码就知道了,根本就没有提 供add 方法。

internal object EmptyList : List, Serializable, RandomAccess {
private const val serialVersionUID: Long =
-7390468764508069838L
override fun equals(other: Any?): Boolean = other is
List<*> && other.isEmpty() override fun hashCode():
Int = 1
override fun toString(): String = "[]" override val size: Int get() = 0 override fun
isEmpty(): Boolean = true override funcontains(element: Nothing):
Boolean = false
override fun containsAll(elements:
Collection<Nothing>): Boolean =
elements.isEmpty()
override fun get(index: Int): Nothing = throw
IndexOutOfBoundsException("Empty list doesn't contain
element at index $index.")
override fun indexOf(element: Nothing): Int =
-1
override fun lastIndexOf(element: Nothing):
Int = -1
override fun iterator(): Iterator<Nothing> =
EmptyIterator
override fun listIterator(): ListIterator<Nothing>
= EmptyIterator override fun listIterator(index:
Int): ListIterator<Nothing> {
if (index != 0) throw
IndexOutOfBoundsException("Index: $index")
return EmptyIterator
}override fun subList(fromIndex: Int, toIndex: Int):
List<Nothing> {
if (fromIndex == 0 && toIndex == 0) return
this
throw
IndexOutOfBoundsException("fromIndex:
$fromIndex, toIndex: $toIndex")
}
private fun readResolve(): Any = EmptyList

4、什么是委托属性?请简要说说其使用场景和原理?

属性委托
有些常见的属性操作,我们可以通过委托方式,让它实现,例如:

  • azy 延迟属性: 值只在第一次访问的时候计算
  • observable 可观察属性:属性发生改变时通知
  • map 集合: 将属性存在一个map集合里面

类委托
可以通过类委托来减少 extend类委托的时,编译器回优使用自身重新函数,而不是委托对象的函数

interface
Base{ fun
print()
}
case BaseImpl(var x: Int):Base{
override fun
print(){ print(x)
}
}
// Derived 的 print 实现会通过构造函数的b对象来完成
class Derived(b: base): Base by b

5、请举例说明Kotlin中with与apply函数的应用场景和区别?

with不怎么使用,因为它确实不防空; 经常使用的是runapply

  1. run 闭包返回结果是闭包的执行结果; apply 返回的是调用者本身。
  2. 使用上的差别: run 更倾向于做一些其他复杂逻辑操作,而 apply 更多的是对调用者自身配置。
  3. 大部分情况下,如果不是对调用者本身进行设置,我会使用 run。

6、Kotlin中 Unit 类型的作用以及与Java中Void 的区别?

Unit : Kotlin 中Any的子类, 方法的返回类型为Unit时,可以省略;
Void:Java中的方法无法回类型时使用,但是不能省略;
Nothing:任何类型的子类,编译器对其有优化,有一定的推导能力,另外其常常和抛出异常一起使用;

由于篇幅原因,这里还有我整理的2021秋招到2023年春招各一、二线互联网公司的Android面试题,内容包括Java基础、Activity、Fragment、Service、IPC、View、性能优化、 设计模式、第三方开源框架、Framework源码等相关面试题就没展示了,需要的威信扫码下方直接获取。

面试题

第一章 算法和数据结构面试题

  • 请说一说HashMap,SparseArrary原理,SparseArrary相比HashMap的优点、ConcurrentHashMap如何实现线程安全?
  • 请说一说HashMap原理,存取过程,为什么用红黑树,红黑树与完全二叉树对比,HashTab、concurrentHashMap,concurrent包里有啥?
  • 请说一说hashmap put()底层原理,发生冲突时,如何去添加(顺着链表去遍历,挨个比较key值是否一致,如果一致,就覆盖替换,不一致遍历结束后,插入该位置) ?
  • 请说一说ArrayList 如何保证线程安全,除了加关键字的方式 ?
  • 请说一说ArrayList、HashMap、LinkedHashMap ?
  • 请说一说HashMap实现原理,扩容的条件,链表转红黑树的条件是什么 ?

  • 在这里插入图片描述

第二章 Java核心基础面试题

  • Java中提供了抽象类还有接口,开发中如何去选择呢?
  • 重载和重写是什么意思,区别是什么?
  • 静态内部类是什么?和非静态内部类的区别是什么?
  • Java中在传参数时是将值进行传递,还是传递引用?
  • 使用equals和==进行比较的区别
  • String s = new String(“xxx”);创建了几个String对象?

在这里插入图片描述

第三章 Java深入泛型与注解面试题

  • 泛型是什么,泛型擦除呢?
  • List<String>能否转为List<Object>
  • Java的泛型中super 和 extends 有什么区别?
  • 注解是什么?有哪些使用场景?

  • 在这里插入图片描述

第四章 Java并发编程面试题

  • 假如只有一个cpu,单核,多线程还有用吗 ?
  • sychronied修饰普通方法和静态方法的区别?什么是可见性?
  • Synchronized在JDK1.6之后做了哪些优化
  • CAS无锁编程的原理
  • AQS原理
  • ReentrantLock的实现原理

在这里插入图片描述



第十章 Framework内核解析面试题

  • Android中多进程通信的方式有哪些?
  • 描述下Binder机制原理?
  • 为什么 Android 要采用 Binder 作为 IPC 机制?
  • Binder线程池的工作过程是什么样?
  • AIDL 的全称是什么?如何工作?能处理哪些类型的数据?
  • Android中Pid&Uid的区别和联系

在这里插入图片描述

第十一章 Android组件内核面试题

  • Acitvity的生命周期,如何摧毁一个Activity?
  • Activity的4大启动模式,与开发中需要注意的问题,如onNewIntent() 的调用
  • Intent显示跳转与隐式跳转,如何使用?
  • Activity A跳转B,B跳转C,A不能直接跳转到C,A如何传递消息给C?
  • Activity如何保存状态的?
  • 请描诉Activity的启动流程,从点击图标开始。

在这里插入图片描述

第十二章 程序性能优化与数据持久化面试题

  • 一张图片100x100在内存中的大小?
  • 内存优化,内存抖动和内存泄漏。
  • 什么时候会发生内存泄漏?举几个例子
  • Bitmap压缩,质量100%与90%的区别?
  • TraceView的使用,查找CPU占用
  • 内存泄漏查找

在这里插入图片描述

第十三章 开源框架面试题

  • 组件化在项目中的意义
  • 组件化中的ARouter原理
  • 谈一下你对APT技术的理解
  • 谈谈Glide框架的缓存机制设计
  • 谈谈你对Glide生命周期的理解
  • 项目中使用Glide框架出现内存溢出,应该是什么原因?

在这里插入图片描述

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐