边独立,边面试,两手都要抓,两手都要硬。

1. 给定数组array与一个数字num,要求从array中找出两个数,其和为num,并返回这两个数的下标

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
fun findTwoSum(array: IntArray, num: Int): Pair<Int, Int>? {
// 创建一个哈希表来存储数组中的数字及其索引
val map = mutableMapOf<Int, Int>()

// 遍历数组
for (i in array.indices) {
val complement = num - array[i]

// 检查哈希表中是否存在这个补数
if (map.containsKey(complement)) {
// 如果存在,返回这个数及其补数的索引
return Pair(map[complement]!!, i)
}

// 将当前数字及其索引存入哈希表
map[array[i]] = i
}

// 如果没有找到符合条件的数对,返回null
return null
}

解读:

  1. 创建一个map,在遍历的过程中,报错所有数字以及其对应的坐标,数字为键,坐标为值;
  2. 遍历时,计算出num与当前元素的差值,然后试图从map中索引,如果有索引,则命中,如果没有则继续遍历;
  3. map的作用,相当于取代了双层遍历法其中的一层遍历;

2. jvm如何判断一个对象可以被回收?

  1. 引用计数法
    优点:实现简单,时间复杂度低;
    缺点:无法解决循环引用问题;
  2. 可达性分析算法
    通过GC Roots开始标记所有可达的对象;可以被标记的,认为是活的对象,未被标记的,则认为已经不需要,可以被回收;

2.1 有哪些可以作为GC Root?

  • 虚拟机栈(栈帧中的本地变量表)中引用的对象。
  • 方法区中的类静态属性引用的对象。
  • 方法区中常量引用的对象。
  • 本地方法栈中JNI(即Native方法)引用的对象。

2.2 垃圾回收的过程是怎样的?

有三种策略:

  • 标记清除:首先标记所有可达对象,然后清除所有未被标记的对象;
  • 复制算法:将存活的对象,从一块内存复制到另外一块内存,然后清空原来的内存;
  • 标记-压缩: 首先标记所有可达的对象,然后将所有存活的对象压缩到内存的一端,清除便捷以外的空间;

3. Android的Handler是如何造成内存泄露的?

非静态内部类或者匿名内部类,会隐式的持有外部类的对象,如果在Activity中声明一个Handler,而Handler执行延迟任务,在任务结束前,Activity已经被销毁了,则Activiey泄露了;

3.1 如何解决Handler的内存泄露?

  1. Handler声明为静态内部类,并且使用弱引用持有外部类的引用;
  2. 在Activity的onDestroy中,使用HandlerremoveCallbacksAndMessages方法,移除所有未执行的消息和回调;

3.2 有没有非Activity的其他内存泄露的场景?

  1. 单例持有外部对象;
  2. 静态持有外部对象;
  3. 未取消注册监听器或者回调;
  4. WebView导致内存泄露;
  5. 异步任务;
    总结来说,就是生命周期长的对象,持有了一个生命周期短的对象的引用;

4. 应用启动白屏如何排查?如何解决?

  1. 在应用入口,比如Application的onCreate、启动Activity的onCreate中,做耗时检查,如果耗时异常,具体检查代码;
  2. 如果是Activity的onCreate中耗时长,检查是否为布局过于复杂,造成的布局解析耗时长,可以使用异步布局处理;
  3. 如果为系统机制问题导致,可以配置主题或者使用Splash库来做过渡;

5. 线上问题如何排查?

  1. 运营种子用户,提前内测,按照用户反馈,复现问题场景;
  2. 集成bugly等线上日志抓取工具,按照机型、系统等信息,复现问题场景;
  3. 如果有可能,可以通过热更新修复问题;

6. 不同任务类型(IO密集型/计算密集型)任务,如何分配线程池策略?

IO密集型任务

特点
IO密集型任务主要涉及等待外部资源(如文件系统、网络请求、数据库操作等),因此大部分时间处于等待状态,而非消耗CPU。

策略
对于IO密集型任务,可以使用较大的线程池,因为线程在等待IO操作完成时不会消耗大量CPU资源。

线程池配置
通常,线程池的大小可以设置为CPU核心数的2倍或更多。一个常见的公式是:

1
2
线程池大小 = CPU核心数 * 2
或更大,根据实际情况调整。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class IOIntensiveTaskExample {
private static final int N_THREADS = Runtime.getRuntime().availableProcessors() * 2;

public static void main(String[] args) {
ExecutorService ioThreadPool = Executors.newFixedThreadPool(N_THREADS);

for (int i = 0; i < 100; i++) {
ioThreadPool.submit(() -> {
// 模拟IO操作
performIOOperation();
});
}

ioThreadPool.shutdown();
}

private static void performIOOperation() {
try {
// 模拟IO操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
}

计算密集型任务

特点
计算密集型任务主要消耗CPU资源,执行过程中几乎不涉及等待时间,因此可以充分利用CPU。

策略
对于计算密集型任务,线程池的大小应设置为接近CPU核心数。过多的线程会导致频繁的上下文切换,反而降低性能。

线程池配置
通常,线程池的大小可以设置为CPU核心数。一个常见的公式是:

1
线程池大小 = CPU核心数
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class CPUIntensiveTaskExample {
private static final int N_THREADS = Runtime.getRuntime().availableProcessors();

public static void main(String[] args) {
ExecutorService cpuThreadPool = Executors.newFixedThreadPool(N_THREADS);

for (int i = 0; i < 100; i++) {
cpuThreadPool.submit(() -> {
// 模拟计算操作
performCPUTask();
});
}

cpuThreadPool.shutdown();
}

private static void performCPUTask() {
// 模拟计算操作
for (int i = 0; i < 1000000; i++) {
Math.pow(i, 2);
}
}
}

混合任务
在实际应用中,有些任务可能既包含IO操作又包含计算操作。这种情况下,可以考虑使用不同的线程池分别处理不同类型的任务,或者根据任务的主要特征选择适当的线程池配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import java.util.concurrent.Executors;
import java.util.concurrent.ExecutorService;

public class MixedTaskExample {
private static final int IO_THREADS = Runtime.getRuntime().availableProcessors() * 2;
private static final int CPU_THREADS = Runtime.getRuntime().availableProcessors();

public static void main(String[] args) {
ExecutorService ioThreadPool = Executors.newFixedThreadPool(IO_THREADS);
ExecutorService cpuThreadPool = Executors.newFixedThreadPool(CPU_THREADS);

for (int i = 0; i < 100; i++) {
ioThreadPool.submit(() -> {
// 模拟IO操作
performIOOperation();
});

cpuThreadPool.submit(() -> {
// 模拟计算操作
performCPUTask();
});
}

ioThreadPool.shutdown();
cpuThreadPool.shutdown();
}

private static void performIOOperation() {
try {
// 模拟IO操作
Thread.sleep(1000);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}

private static void performCPUTask() {
// 模拟计算操作
for (int i = 0; i < 1000000; i++) {
Math.pow(i, 2);
}
}
}

总结

IO密集型任务:使用较大的线程池,通常为CPU核心数的2倍或更多,以应对大量的IO等待时间。
计算密集型任务:使用较小的线程池,接近CPU核心数,以充分利用CPU资源,避免过多的线程上下文切换。
混合任务:根据任务的主要特征,分别使用不同的线程池处理IO操作和计算操作。
通过合理配置线程池,可以有效提高应用程序的性能和资源利用率。

本文采用CC-BY-SA-3.0协议,转载请注明出处
Author: boybeak