当前位置: 首页 > news >正文

Android第十一次面试多线程篇

面试官​:
“你在项目里用过Handler吗?能说说它是怎么工作的吗?”

候选人​:
“当然用过!比如之前做下载功能时,需要在后台线程下载文件,然后在主线程更新进度条。这时候就得用Handler来切回主线程。简单来说,Handler就像个快递员,负责把子线程的任务包裹(比如更新UI的指令)送到主线程去执行。”

面试官追问​:
“比喻挺有意思。那这个‘快递员’是怎么把消息从子线程送到主线程的?”

候选人​:
“其实核心是靠三个东西:HandlerLooperMessageQueue。比如主线程启动时,系统会默认创建一个Looper,它内部维护了一个消息队列(MessageQueue)。当我在子线程通过Handler发送消息,这个消息会被放到主线程的队列里。然后主线程的Looper会循环检查队列,一有消息就取出来,交给对应的Handler处理。”


面试官​:
“那如果我在子线程里直接new一个Handler,会有什么问题吗?”

候选人​:
“这里有个坑!如果子线程没有提前准备Looper,直接new Handler会崩溃。比如这样——”

new Thread(() -> {// ❌ 错误写法:子线程默认没有LooperHandler handler = new Handler();
}).start();

“正确的做法是先调用Looper.prepare()创建Looper,再启动循环:”

new Thread(() -> {Looper.prepare();  // ✅ 初始化LooperHandler handler = new Handler();Looper.loop();     // 启动消息循环
}).start();

面试官​:
“提到主线程的Looper,你知道它是怎么初始化的吗?”

候选人​:
“这个我之前研究过源码!主线程的Looper是在ActivityThreadmain()方法里初始化的。大概流程是:”

  1. 启动App​:系统调用ActivityThread.main()
  2. 准备Looper​:调用Looper.prepareMainLooper(),创建主线程的Looper和消息队列。
  3. 开启循环​:调用Looper.loop(),让主线程进入无限循环,不断处理消息(比如点击事件、UI更新)。
    “所以主线程的Handler能一直运行,全靠这个死循环撑着。”

面试官​:
“实际开发中有没有遇到过Handler导致的问题?比如内存泄漏。”

候选人​:
“遇到过!比如在Activity里声明一个非静态内部类的Handler,如果Activity关闭时还有未处理的消息,Handler会持有Activity的引用,导致内存泄漏。我们项目里用了一个经典解法——”

// ✅ 正确写法:静态内部类 + 弱引用
static class SafeHandler extends Handler {private WeakReference<Activity> mActivity;SafeHandler(Activity activity) {mActivity = new WeakReference<>(activity);}@Overridepublic void handleMessage(Message msg) {Activity activity = mActivity.get();if (activity == null) return;// 处理消息...}
}

“另外,在Activity的onDestroy()里,还要调用handler.removeCallbacksAndMessages(null)清空所有消息。”


面试官​:
“假设现在有个需求:每隔1秒更新一次UI上的计时器。用Handler怎么实现?”

候选人​:
“可以用postDelayed()递归调用。比如这样——”

private int count = 0;
private Handler handler = new Handler();private void startTimer() {handler.postDelayed(new Runnable() {@Overridepublic void run() {textView.setText(String.valueOf(++count));handler.postDelayed(this, 1000); // 递归调用}}, 1000);
}// 停止时调用
private void stopTimer() {handler.removeCallbacksAndMessages(null);
}

“不过要注意及时移除回调,否则退出页面时可能还在后台跑。”


面试官​:
“如果不用Handler,还有其他方式实现线程间通信吗?”

候选人​:
“当然!比如用AsyncTask(虽然过时了)、LiveData+协程,或者RxJava的线程切换。比如协程可以这样写——”

// 在ViewModel里
fun startTask() {viewModelScope.launch(Dispatchers.IO) {val data = fetchData() // 后台执行withContext(Dispatchers.Main) {updateUI(data)      // 切回主线程}}
}

“不过Handler的优势是更底层,适合需要精细控制消息队列的场景,比如实现定时任务或延迟操作。”


面试官​:
“最后一个问题:为什么主线程的Looper不会导致ANR?”

候选人​:
“这是个好问题!虽然Looper.loop()是死循环,但主线程大部分时间处于休眠状态,通过Linux的epoll机制监听消息队列。当没有消息时,线程会释放CPU进入休眠;有新消息(比如点击事件、屏幕刷新信号)时,线程被唤醒处理消息。所以只要不在主线程做耗时操作,循环本身不会阻塞,也就不会ANR。”


面试官​:
“回答得很清晰。你还有什么问题想问吗?”

候选人​:
“咱们项目中有没有特别依赖Handler的场景?比如自定义消息协议或者复杂定时任务?”

面试官​:
(假设回答)
“有的!比如IM模块的消息重发机制,用Handler管理消息的延迟重试;还有首页的轮播图动画,用Handler的postDelayed实现自动切换。之后你可以参与这部分优化。”

Handler 的工作流程与底层原理

1. ​核心组件及其关系
  • Handler​:消息的发送者和处理者。
  • Looper​:消息循环的核心,每个线程有且只有一个。
  • MessageQueue​:消息队列,按时间排序存储消息(单链表实现)。
  • Message​:携带数据和目标Handler的单元。

关系图​:

Thread├── Looper│     └── MessageQueue└── Handler(关联到Looper)
2. ​工作流程
步骤1:初始化Looper(子线程)​
  • 子线程中必须手动调用 Looper.prepare() 创建Looper。
  • Looper.loop() 启动消息循环。
new Thread(() -> {Looper.prepare(); // 初始化Looper(内部创建MessageQueue)Handler handler = new Handler(Looper.myLooper());Looper.loop();    // 开始循环处理消息
}).start();
步骤2:发送消息
  • 通过 Handler.sendMessage()Handler.post(Runnable) 发送消息。
Message msg = Message.obtain();
msg.what = 1;
msg.obj = "Data";
handler.sendMessage(msg); // 消息入队到MessageQueue
步骤3:消息处理
  • Looper从MessageQueue取出消息,调用目标Handler的 dispatchMessage()
  • 最终触发 handleMessage()Runnable.run()
public void handleMessage(Message msg) {switch (msg.what) {case 1: updateUI((String) msg.obj); break;}
}
3. ​底层原理
消息存储(MessageQueue)​
  • 数据结构​:单链表按 when(执行时间)排序。
  • 入队操作​:enqueueMessage() 添加消息到链表合适位置。
  • 出队操作​:next() 取出下一条消息(可能阻塞)。
消息循环(Looper.loop())​
  • 无限循环​:持续调用 MessageQueue.next()
  • 阻塞唤醒机制​:
    • 队列为空时,通过 epollpipe 进入休眠。
    • 新消息入队时唤醒线程(通过 nativeWake())。
线程隔离(ThreadLocal)​
  • Looper存储​:每个线程的Looper实例通过 ThreadLocal<Looper> 保存。
  • 获取方式​:Looper.myLooper() 返回当前线程的Looper。
4. ​主线程的Looper
  • 默认初始化​:ActivityThread的 main() 方法调用 Looper.prepareMainLooper()
  • 永不退出​:主线程的Looper循环保证应用持续响应事件。
// ActivityThread.java
public static void main(String[] args) {Looper.prepareMainLooper(); Looper.loop(); // 主线程进入无限循环
}
5. ​性能优化与注意事项
  • 避免阻塞主线程​:耗时操作仍要放在子线程。
  • 内存泄漏​:Handler持有外部类(如Activity)引用时,需用弱引用。
static class SafeHandler extends Handler {private WeakReference<Activity> mActivity;SafeHandler(Activity activity) {mActivity = new WeakReference<>(activity);}public void handleMessage(Message msg) {Activity activity = mActivity.get();if (activity == null) return;// 处理消息}
}
6. ​代码示例:子线程与主线程通信
// 主线程创建Handler
Handler mainHandler = new Handler(Looper.getMainLooper());// 子线程执行任务后更新UI
new Thread(() -> {String result = doBackgroundWork();mainHandler.post(() -> textView.setText(result)); // 切换到主线程
}).start();

Kotlin协程详解:原理、优势与应用场景

一、协程的核心概念

协程(Coroutine)​​ 是一种轻量级的并发设计模式,允许以同步编码风格实现异步任务。它通过挂起(Suspend)和恢复(Resume)机制管理任务,避免传统多线程开发的复杂性。


二、协程为何优于线程?​
对比维度协程线程
创建开销极低(约几十字节)高(约1MB内存 + 系统调用开销)
切换成本无需操作系统介入(用户态切换)需内核介入(上下文切换开销大)
并发数量单线程可运行数万协程受限于系统资源(通常数百个)
开发复杂度同步代码风格,逻辑清晰需处理锁、回调、线程同步等问题
资源利用率挂起时释放线程,避免阻塞线程阻塞时资源浪费

三、协程的工作原理
1. 挂起与恢复
  • 挂起函数(Suspend Function)​​:
    使用suspend关键字标记的函数,可在不阻塞线程的情况下暂停执行(如等待网络响应)。
    suspend fun fetchData(): String {delay(1000) // 模拟耗时操作,非阻塞return "Data loaded"
    }
  • 底层机制​:
    协程通过状态机Continuation实现挂起恢复。编译器将挂起函数转换为带有回调的状态机代码。
2. 协程调度器(Dispatchers)​
  • 调度线程池​:
    launch(Dispatchers.IO) { /* 执行IO操作 */ }
    launch(Dispatchers.Main) { /* 更新UI */ }
  • 调度策略​:
    • IO​:适用于网络、文件操作(线程池优化)
    • Default​:CPU密集型任务(如排序、计算)
    • Main​:UI线程操作(Android主线程)
3. 结构化并发
  • 协程作用域(CoroutineScope)​​:
    管理协程生命周期(如viewModelScopelifecycleScope),确保任务不会泄漏。
    viewModelScope.launch {val data = fetchData() // 自动绑定ViewModel生命周期updateUI(data)
    }

四、协程如何避免回调地狱?​
1. 回调地狱示例
// 传统嵌套回调
api.getUser { user ->api.getProfile(user) { profile ->api.getFriends(profile) { friends ->updateUI(friends) // 嵌套层级深,难维护}}
}
2. 协程解决方案
viewModelScope.launch {try {val user = api.getUser()        // 挂起,不阻塞线程val profile = api.getProfile(user)val friends = api.getFriends(profile)updateUI(friends)               // 顺序执行,逻辑清晰} catch (e: Exception) {showError(e)}
}
  • 优势​:
    • 线性代码​:消除嵌套,可读性高
    • 统一异常处理​:try/catch捕获所有异步错误

五、协程的典型应用场景
  1. 网络请求​:
    suspend fun loadData() = withContext(Dispatchers.IO) {retrofitService.fetchData()
    }
  2. 数据库操作​:
    fun insertUser(user: User) = viewModelScope.launch(Dispatchers.IO) {database.userDao().insert(user)
    }
  3. 并发任务组合​:
    val result1 = async { task1() }
    val result2 = async { task2() }
    val combined = result1.await() + result2.await() // 并行执行
  4. 超时与重试​:
    val data = withTimeoutOrNull(5000) { // 5秒超时retry(3) { // 重试3次fetchData()}
    }

六、协程与线程的底层协作
  • 线程池复用​:
    协程调度器基于ExecutorService,通过线程池复用减少开销。
  • 挂起优化​:
    挂起时释放线程资源,交由其他协程使用,最大化利用CPU。

Kotlin协程实战

面试官​:
“你在项目里用过Kotlin协程吗?能举个实际例子说说它解决了什么问题吗?”

候选人​:
“当然!之前做商品详情页的时候,需要同时调三个接口:商品信息、用户评论、推荐列表。如果用传统的回调,代码会嵌套三层,像俄罗斯套娃一样,维护起来特别头疼。后来换成协程,代码直接‘拉直’了——”

viewModelScope.launch {try {// 同步写法,其实是异步执行!val product = api.fetchProductDetails()  // 第一个接口val comments = api.fetchComments(product.id)  // 等第一个完成后调第二个val recommendations = api.fetchRecommendations()  // 等前两个都完成// 统一更新UIshowData(product, comments, recommendations)} catch (e: Exception) {// 一个try/catch抓住所有网络错误showErrorToast("加载失败,请重试")}
}

“这样一来,代码像写同步逻辑一样直观,新人也能快速看懂,而且异常处理集中,不会漏掉某个回调里的错误。”


面试官追问​:
“听起来确实简洁。那协程到底是怎么做到‘假装’同步的?底层不会卡住主线程吗?”

候选人​:
“这就是协程的聪明之处!比如api.fetchProductDetails()是个挂起函数,执行到的时候,协程会悄悄挂起,把线程让出来去处理其他任务。等网络数据回来了,协程再‘醒来’,从刚才挂起的位置继续执行。整个过程主线程完全没被阻塞,用户滑屏幕照样流畅。”

面试官​:
“那如果我要同时调三个接口,等所有结果一起回来再刷新界面,用协程怎么优化?”

候选人​:
“这时候可以用async并发!比如这样——”

viewModelScope.launch {// 同时发起三个请求val productDeferred = async { api.fetchProductDetails() }val commentsDeferred = async { api.fetchComments() }val recommendationsDeferred = async { api.fetchRecommendations() }// 等三个全部完成(实际耗时等于最慢的那个接口)val product = productDeferred.await()val comments = commentsDeferred.await()val recommendations = recommendationsDeferred.await()updateUI(product, comments, recommendations)
}

“比串行调用快多了!之前用回调得用计数器或者RxJava的zip操作符,协程两行代码搞定。”


面试官​:
“协程会不会导致内存泄漏?比如页面关了但请求还没回来。”

候选人​:
“这就是结构化并发的优势了!比如用viewModelScope启动协程,当ViewModel被销毁时,所有关联的协程会自动取消。如果这时候正在等网络请求,会直接中断,避免内存泄漏。我们之前有个页面没注意这个,用户快速进出会导致旧的请求继续回调,用了viewModelScope后问题彻底解决。”


面试官​:
“如果遇到老代码用回调的API,怎么接入协程?”

候选人​:
“Kotlin给了逃生舱!比如有个老版SDK用回调返回数据,可以用suspendCoroutine把它包成挂起函数——”

suspend fun legacyFetchData(): String = suspendCoroutine { continuation ->legacyApi.getData(object : Callback {override fun onSuccess(result: String) {continuation.resume(result) // 成功时恢复协程}override fun onFailure(error: Throwable) {continuation.resumeWithException(error) // 抛异常}})
}// 新代码里直接调用
viewModelScope.launch {val data = legacyFetchData() // 像普通挂起函数一样用
}

“这样旧代码也能融入协程体系,团队迁移成本低。”


面试官​:
“假设你要下载10张图片,怎么用协程控制并发,同时不拖垮手机?”

候选人​:
“这时候得用协程的调度器!比如这样——”

// 限制最多同时3个下载
val dispatcher = Dispatchers.IO.limitedParallelism(3) 
viewModelScope.launch {val jobs = (1..10).map { index ->launch(dispatcher) {  // 限制并发的协程池downloadImage(index) }}jobs.joinAll() // 等所有下载完成showToast("下载完成!")
}

“如果用普通线程池,开10个线程内存可能扛不住。协程的Dispatchers.IO自带线程复用,加上并发限制,既高效又省资源。”


面试官​:
“最后一个问题:为什么说协程适合Android开发?”

候选人​:
“三个字——快、稳、省!

  1. ​:代码写起来像同步,维护效率高,再也不用和回调地狱搏斗。
  2. ​:结构化并发自动管理生命周期,结合viewModelScope/lifecycleScope,内存泄漏少一半。
  3. ​:一个线程跑几百个协程,用过的都说像‘开挂’,尤其低端机上效果明显。”

面试官​:
“回答得很到位。你还有什么问题吗?”

候选人​:
“咱们团队用协程遇到过印象深刻的坑吗?比如和LiveData、Room结合时的注意点?”

面试官​:
(假设回答)
“早期在Room里混用协程和RxJava时有过线程冲突,后来统一用协程+Flow就解决了。你提到的Dispatchers.IO限制并发,我们在大文件上传模块也用过类似优化!”


线程池实战

面试官​:
“你在项目中是怎么管理多线程任务的?比如同时处理多个网络请求。”

候选人​:
“我们用的是线程池。比如在APP首页要同时加载多个图片,如果每个请求都开一个新线程,手机扛不住,容易卡顿或者OOM。线程池就像个‘线程调度中心’,复用已有的线程,避免频繁创建销毁的开销。”

面试官追问​:
“那具体怎么配置线程池?比如核心线程数、队列这些参数怎么定?”

候选人​:
“得看任务类型。比如我们有个需求是批量上传图片,这种IO密集型任务,核心线程数可以设成CPU核心数的两倍左右,比如4核手机就设8个。队列用无界队列(比如LinkedBlockingQueue),保证任务不丢失。但如果是CPU密集型任务,比如视频转码,核心线程数应该和CPU数差不多,避免线程切换拖慢速度。”


面试官​:
“如果任务太多,线程池处理不过来了怎么办?”

候选人​:
“这时候就得看拒绝策略了。比如我们遇到过用户疯狂点击触发大量请求,队列满了之后默认策略是抛异常,结果直接崩溃。后来改成了CallerRunsPolicy,让提交任务的线程自己执行,这样至少不会崩,但得注意别阻塞主线程。”

ThreadPoolExecutor executor = new ThreadPoolExecutor(4, // 核心线程8, // 最大线程30, TimeUnit.SECONDS,new LinkedBlockingQueue<>(100), // 队列容量100new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略
);

面试官​:
“提到线程池类型,你知道Executors.newCachedThreadPool()有什么坑吗?”

候选人​:
“这个坑踩过!CachedThreadPool的最大线程数设置的是Integer.MAX_VALUE,如果任务无限提交,线程数暴增,直接OOM。我们之前有个日志上报功能用了它,结果用户疯狂操作时内存爆炸。后来改成自定义线程池,限制最大线程数,问题才解决。”


面试官​:
“如果让你设计一个定时任务,比如每隔5秒检查一次消息,用线程池怎么实现?”

候选人​:
“可以用ScheduledThreadPool,比如这样——”

ScheduledExecutorService scheduler = Executors.newScheduledThreadPool(1);
scheduler.scheduleAtFixedRate(() -> {checkNewMessage(); // 执行任务
}, 0, 5, TimeUnit.SECONDS);

“不过要注意任务执行时间别超过间隔,否则会堆积。比如任务要跑10秒,间隔设5秒,就会变成每10秒执行一次。”


面试官​:
“实际开发中,怎么确保线程池里的任务不被重复执行?”

候选人​:
“我们项目里遇到过!比如用户快速点击按钮触发多次提交。解决办法是给任务加唯一ID,用ConcurrentHashMap记录正在执行的任务,提交前先检查是否存在——”

ConcurrentHashMap<String, Boolean> taskMap = new ConcurrentHashMap<>();void submitTask(String taskId, Runnable task) {if (taskMap.putIfAbsent(taskId, true) == null) {executor.execute(() -> {try {task.run();} finally {taskMap.remove(taskId);}});}
}

面试官​:
“最后一个问题:为什么推荐用ThreadPoolExecutor而不是Executors?”

候选人​:
Executors的方法虽然方便,但隐藏了参数细节,容易踩坑。比如newFixedThreadPool用的无界队列,任务堆积可能内存溢出。直接new ThreadPoolExecutor可以明确指定核心线程数、队列容量和拒绝策略,对资源控制更精细。”


面试官​:
“回答得很到位。你还有什么问题想问吗?”

候选人​:
“咱们项目里线程池一般用在哪些场景?有没有特别复杂的配置案例?”

面试官​:
(假设回答)
“比如首页的瀑布流图片加载用了IO密集型线程池,后台数据同步用了单线程池保证顺序。有个复杂案例是结合PriorityBlockingQueue实现任务优先级,高优先级任务插队执行。”

 Android第三次面试总结之activity和线程池篇(补充)_android线程池面试题-CSDN博客https://blog.csdn.net/2301_80329517/article/details/147700637

Android第三次面试总结(activity和线程池)_android线程池面试题-CSDN博客https://blog.csdn.net/2301_80329517/article/details/146325189?spm=1011.2415.3001.5331

相关文章:

  • UE5蓝图中播放背景音乐和使用代码播放声音
  • 【案例分享】蓝牙红外线影音遥控键盘:瑞昱RTL8752CJF
  • 阿里云服务器邮件发送失败(dail tcp xxxx:25: i/o timeout)因为阿里云默认禁用 25 端口
  • ZYNQ移植FreeRTOS和固化和openAMP双核
  • 文件夹的命名与分类
  • Java设计模式之迭代器模式详解
  • tiktoken学习
  • 【通关文件操作(下)】--文件的顺序读写(续),sprintf和sscanf函数,文件的随机读写,文件缓冲区,更新文件
  • 第4讲、Odoo 18 模块系统源码全解与架构深度剖析【modules】
  • Parsel深度解析:从入门到高阶的网页数据抓取艺术
  • Spring Boot整活指南:从Helo World到“真香”定律
  • KeePass安装与KeePass设置中文教程
  • 自扶正救生艇,乘风破浪,守护生命
  • 差分互连的串扰-信号与电源完整性
  • Linux---系统守护systemd(System Daemon)
  • IAR无法跳转定义,IARstm8跳转显示路径出错,系统库文件文件名后有[RO]
  • SOC-ESP32S3部分:17-I2C驱动实例-EEPROM温湿度传感器
  • Java开发经验——阿里巴巴编码规范实践解析6
  • Rust使用Cargo构建项目
  • 常见的分词算法
  • 最好的手机资源网站/太原关键词优化公司
  • 网站做友链/可以免费发广告的网站
  • 做网站都需要用到什么/网络营销的六大特征
  • 导航网站的广告怎么做的/seo网络优化公司
  • html模板代码免费下载/seo工具是什么意思
  • 网站没有后台怎么更新文章/百度开放平台登录