
- Handler 消息机制
- Handler 引起的内存泄露原因以及最佳解决方案
- 为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?
- Handler、Thread 和 HandlerThread 的差别
- 子线程中怎么使用 Handler?
- 为什么在子线程中创建 Handler 会抛异常?
- Handler 里藏着的 Callback 能干什么?
- Handler 的 send 和 post 的区别?
- 创建 Message 实例的最佳方式
- Message 的插入以及回收是如何进行的,如何实例化一个 Message 呢?
- 妙用 Looper 机制,或者你知道 Handler 机制的其他用途吗?
- Looper.loop()死循环一直运行是不是特别消耗 CPU 资源呢?不会造成应用卡死吗?
- MessageQueue 中如何等待消息?为何不使用 Java 中的 wait/notify 来实现阻塞等待呢?
- 延时消息的原理
- handler postDelay 这个延迟是怎么实现的?
- 如何保证在 msg.postDelay 情况下保证消息次序?
- 更新 UI 的方式有哪些
- 线程、Handler、Looper、MessageQueue 的关系?
- 多个线程给 MessageQueue 发消息,如何保证线程安全?
- View.post 和 Handler.post 的区别?
- 现在 push 一个延迟消息到消息队列里,这时候忽然退出应用程序了,会有什么问题
什么是 Handler 消息传递机制
在多线程的应用场景中,将工作线程中需更新UI的操作信息 传递到 UI主线程,从而实现 工作线程对UI的更新处理,最终实现异步消息的处理
使用Handler的原因: 多个线程并发更新UI的同时保证线程安全,将工作线程需操作UI的消息 传递到主线程,使得主线程可根据工作线程的需求 更新UI,从而避免线程操作不安全的问题
概述
Handler获取当前线程中的looper对象,looper用来从存放Message的MessageQueue中取出Message,再由Handler进行Message的分发和处理.
Message Queue(消息队列)
用来存放通过Handler发布的消息,按照先进先出执行。通常附属于某一个创建它的线程,可以通过Looper.myQueue()得到当前线程的消息队列
每个message queue都会有一个对应的Handler。Handler会向message queue通过两种方法发送消息:sendMessage或post。这两种消息都会插在message queue队尾并按先进先出执行。但通过这两种方法发送的消息执行的方式略有不同:通过sendMessage发送的是一个message对象,会被 Handler的handleMessage()函数处理;而通过post方法发送的是一个runnable对象,则会自己执行。
Handler
可以发布或者处理一个消息或者操作一个Runnable,通过Handler发布消息,消息将只会发送到与它关联的消息队列,然也只能处理该消息队列中的消息。Handler接受到消息后调用handleMessage进行处理
是Message的主要处理者,负责Message的发送,Message内容的执行处理。后台线程就是通过传进来的 Handler对象引用来sendMessage(Message) 。而使用Handler,需要implement类的 handleMessage(Message)方法,它是处理这些Message的操作内容,例如Update UI。通常需要子类化Handler来实现handleMessage方法。
Looper
是Handler和消息队列之间通讯桥梁,程序组件首先通过Handler把消息传递给Looper,Looper把消息放入队列。Looper也把消息队列里的消息广播给所有的
Looper是每条线程里的Message Queue的管家。Android没有Global的Message Queue,而Android会自动替主线程(UI线程)建立Message Queue,但在子线程里并没有建立Message Queue。所以调用Looper.getMainLooper()得到的主线程的Looper不为NULL,但调用Looper.myLooper() 得到当前线程的Looper就有可能为NULL
在Looper.loop()方法运行开始后,循环地按照接收顺序取出Message Queue里面的非NULL的Message。
一开始Message Queue里面的Message都是NULL的。当Handler.sendMessage(Message)到Message Queue,该函数里面设置了那个Message对象的target属性是当前的Handler对象。随后Looper取出了那个Message,则调用 该Message的target指向的Hander的dispatchMessage函数对Message进行处理。在dispatchMessage方法里,如何处理Message则由用户指定,三个判断,优先级从高到低:
Message里面的Callback,一个实现了Runnable接口的对象,其中run函数做处理工作;
Handler里面的mCallback指向的一个实现了Callback接口的对象,由其handleMessage进行处理;
处理消息Handler对象对应的类继承并实现了其中handleMessage函数,通过这个实现的handleMessage函数处理消息。
Handler处理完该Message (update UI) 后,Looper则设置该Message为NULL,以便回收!
主线程和其他子线程如何交互,传送信息,最终谁来执行处理信息——判断Handler对象里面的Looper对象是属于哪条线程的,则由该线程来执行
- 当Handler对象的构造函数的参数为空,则为当前所在线程的Looper;
- Looper.getMainLooper()得到的是主线程的Looper对象,Looper.myLooper()得到的是当前线程的Looper对象。
Message
消息的类型,在Handler类中的handleMessage方法中得到单个的消息进行处理。
理解为线程间交流的信息,处理数据后台线程需要更新UI,则发送Message内含一些数据给UI线程。
在单线程模型下,为了线程通信问题,Android设计了一个Message Queue(消息队列), 线程间可以通过该Message Queue并结合Handler和Looper组件进行信息交换。
Handler 的组成
Handler 消息传递机制,从名字看就可以联想到是 Handler 会发送出一个一个消息,同时系统会根据每一个不同的消息进行不同的处理流程。
Handler 由 3 个模块组成,Handler、MessageQueue、Looper:
Handler:主要作用是发送信息以及处理信息,其中发送的信息叫作 Message,可以传递数据;
MessageQueue:消息队列,由一个一个 Message 汇成,遵循先进先出规则,由 Looper 进行管理;
Looper:从 MessageQueue 里读取消息,并按消息分发机制分配到目标 Handler 进行消息处理。
Handler 的使用和代码实例
- Handler 通过 sendMessage()发送消息 Message,每个 Message 都在 MessageQueue 里排队。
- Looper 通过 loop()从 MessageQueue 里读取 Message,并按消息分发机制分配到目标 Handler 进行消息处理。
- 目标 Handler 收到需要处理的 Message 时,调用自己的 handleMessage()方法来处理 Message,因此自定义的 Handler 都需要重写 handlerMessage 方法。
1 | private void updateBluetoothStatus(int state) { |
子线程执行完耗时操作后,需要更新 UI 的时候,需要返回到主线程(子线程不能或不建议更新 UI),因此在子线程通过 Handler 机制,很容易切换到 Handler 所在的主线程去执行 updateBluetoothList()函数来更新 UI。
源码分析
Handler 机制源码分析
创建 Handler
1 | public Handler() { |
注释 2 很重要,Looper 和 MessageQueue 怎么关联在一起?那么现在就知道是在 Handler 的构造函数中关联的。重点继续看看注释 1,Looper.myLooper()做了什么?
1 | public static Looper myLooper() { |
从注释 1 看出来,仅仅是从 sThreadLocal 获取一个 Looper 变量。思路继续,我们从 sThreadLocal 变量入手,如何初始化的?在哪里使用?有 get()那就一定有 set()。继续研究源码:
1 | static final ThreadLocal<Looper> sThreadLocal = new ThreadLocal<Looper>(); |
如果某些数据在不同的线程作用域下有各自的数据副本,那么可以用 ThreadLocal 对数据进行线程隔离。从两个两个函数很容易知道,通过调用 Looper.prepare()函数即可执行到注释 2,将新 new 出来的 Looper 放到 sThreadLocal 里,供 Looper.myLooper()去获取。
MainActivity创建主线程时,会自动调用ActivityThread的1个静态的main();而main()内则会调用Looper.prepareMainLooper()为主线程生成1个Looper对象,同时也会生成其对应的MessageQueue对象。
- 主线程的Looper对象自动生成,不需手动生成;而子线程的Looper对象则需手动通过Looper.prepare()创建
- 在子线程若不手动创建Looper对象 则无法生成Handler对象
ActivityThread 的 main 方法源码如下:
1 | public static void main(String[] args) { |
ActivityThread 的 main 方法通过调用 prepareMainLooper()进而调用 prepare(false)方法,进而 new 一个 Looper 放到 sThreadLocal 里。
主线程的消息循环不允许退出,即无限循环
子线程的消息循环允许退出:调用消息队列MessageQueue的quit()
Handler 发送信息的方法有几种
最重要的源码已经分析完了,我们先来看看 Hander 如何发送信息。发送消息有两种方式,post 和 send。
post 方法
post 方法有以下两个函数 post()和 postDelayed(),用法是 Handler.post(Runnable r),举个例子:
1 | public void readLog() { |
用法非常简单,进一步看看 post 源码:
1 | public final boolean post(Runnable r){ |
可以看到只是调用了 sendMessageDelayed(),这就是 send 方法了。
注:getPostMessage()方法是将 Runnable 转换为 Message,因为 sendMessageDelayed 接收的是 Message 参数。
1 | private static Message getPostMessage(Runnable r) { |
send 方法
send 方法有 3 个,函数作用从名称上就可以一眼看出:
- sendMessage():发送带有数据的信息
- sendMessageDelayed():发送带有数据的信息并延迟执行
- sendEmptyMessage():发送空信息 就以 sendMessageDelayed 为例看看使用方法:
1 | Message logMessage = Message.obtain(); |
用法也比较简单,根据自己需要决定是否携带数据,是否延迟执行。看一下源码:
1 | public final boolean sendMessage(Message msg){ |
可以看出来,无论是 post 还是 send,最后都是用 SystemClock.uptimeMillis()获取系统开机到当前的时间,加上我们设置的 delayMillis 时间,并调用 sendMessageAtTime()方法做进一步逻辑。
注意:不能用 System.currentTimeMilis(系统时间),因为用户很可能自己修改系统时间。
1 | public boolean sendMessageAtTime(Message msg, long uptimeMillis) { |
通过注释 1,Handler 发送的消息最终发送到消息队列。
Message 源码分析
Handler 机制传递的 Message 是怎么生成的,每个 Message 里面都有什么数据。看下源码就可以很清楚的了解了:
1 | public final class Message implements Parcelable { |
通过源码,首先要知道 Message 可以传递数据,主要的方式有:
- what:用户自定义的消息识别标识
- arg1 和 arg2:只能传递 int 型参数
- obj:可以传递 object 类型参数,用得最多的就是传递 String
- data:可以使用 bundle 类型传递 可以看看 2.2.2 小节的代码案例加深理解,所以在上面注释 2 的 recycleUnchecked()方法里需要把对应的变量重置。
同时,用 Message.obtain()来获得一个 Message 实例的,因为这种方案是直接从 Message 的消息池里直接获取,避免了 new Message()的重复创建开销
Looper 源码分析
- Looper.myLooper():从 sThreadLocal 获取已创建的 Looper 实例;
- Looper.prepare():创建 Looper 实例;
- Looper.prepareMainLooper() :主线程 main 函数中调用的方法,在该方法里会调用到 Looper.prepare()来创建 Looper 实例。 剩下的源码还有:
1 | public final class Looper { |
从注释 1 可以知道,MessageQueue 是在 Looper 构建函数里生成的。这个知识点要记一下。
关键是看看注释 2,这是整个 Handler 最重要的源码了,前面所有的源码都最终服务与 loop()方法!从源码就能知道该方法中一直在死循环做三件事:
- 调用 next 方法从 MessageQueue 里获取 Message;
- 通过 dispatchMessage 方法将 Message 分配到目标 Handler;
- 通过 recycleUnchecked 方法回收 Message;
其中 next()函数待会分析 MessageQueue 一并分析,现在看看 Handler.dispatchMessage 做了什么事:
1 | public void dispatchMessage(Message msg) { |
可以看出 dispatchMessage()就是按照既定的优先级策略,决定 Message 由谁去处理:
- handleCallback(msg):对应注释 1,Message 里自带的 callback 优先级最高,实际是调用注释 4,最终调用 Runnable 重写的 run()。
- mCallback.handleMessage(msg):也就是 Handler.Callback 写法,可以看出这个方法是有返回值的,如果返回 true 则注释 3 不会运行到;代码案例见下:
1 | //创建mCallback |
- handleMessage(msg):重写 handlerMessage()方法,优先级最低:
1 | public class BaseHandler extends Handler { |
MessageQueue 源码分析
存放信息的方法:enqueueMessage()
[send()方法]Handler 发送消息的函数最后调用了 queue.enqueueMessage()将要发送的 Message 发送到 MessageQueue 里做处理。
1 | boolean enqueueMessage(Message msg, long when) { |
如果是第一个触发的信息,则作为对头的消息,否则在注释 2,根据 when(相对时间)的大小排序找到合适的插入位置。到此就完成了 Handler 通过调用 post 或者 send 方法将一个 Message 发送到 MessageQueue 并放在合适的链表位置的逻辑。那么该如何取出来呢?
获取消息的方法:next()
1 | Message next() { |
获取一个 Message 的步骤如下:
- 注释 1 是阻塞操作,nextPollTimeoutMillis 代表下一个消息到来前,还需要等待的时长,当为-1 时代表一直等待。当等待时间到了或者消息队列被唤醒,则开始从队列里获取信息;
- 注释 2,如果获取的信息是空的,则找下一个节点;
- 注释 3,获取到非空的信息后,判断是否立刻取出来还是等待一段时间后取出,但最后都会成功返回一个 Message;
- 如果队列里没有信息了,则在 for 循环里又回到了注释 1 进行阻塞等待。
也就是说 next()方法根据消息出发的相对时间,返回下一条需要执行的消息,队列中消息为空时,则会进行阻塞操作。
同步屏障机制(sync barrier)
Handler Message种类
Handler的Messgae种类分为三种:
普通消息(同步消息)
异步消息
- Handler的构造方法有个async参数,默认的构造方法此参数是false,只要在构造handler对象的时候把该参数设置为true
1
2
3
4
5
6public Handler(Callback callback, boolean async) {
......省略代码
mQueue = mLooper.mQueue;
mCallback = callback;
mAsynchronous = async;
} - 在创建Message对象时,直接调用Message的setAsynchronous()方法。
- Handler的构造方法有个async参数,默认的构造方法此参数是false,只要在构造handler对象的时候把该参数设置为true
屏障消息
同步屏障
一般来说,MessageQueue里面的所有Message是按照时间从前往后有序排列的。
同步屏障消息就是在消息队列中插入一个屏障,在屏障之后的所有普通消息都会被挡着,不能被处理。不过异步消息却例外,屏障不会挡住异步消息,因此可以认为,屏障消息就是为了确保异步消息的优先级,设置了屏障后,只能处理其后的异步消息,同步消息会被挡住,除非撤销屏障。
同步屏障是通过MessageQueue的postSyncBarrier方法开启的。
1 | private int postSyncBarrier(long when) { |
- 获取屏障的的唯一标示,标示从0开始,自加1。
- 从Message消息对象池中获取一个msg,设置msg为正在使用状态,并且重置msg的when和arg1,arg1的值设置为token值。但是这里并没有给tareget赋值。所以msag的target是否为空是判断这个msg是否是屏障消息的标志
- 创建变量pre和p,为下一步做准备。其中p被赋值为mMessages,mMessages指向消息队列中的第一个元素,所以此时p指向消息队列中的第一个元素。
- 通过对队列中的第一个Message的when和屏障的when进行比较,决定屏障消息在整个消息队列中的位置,因为消息队列中的消息都是按时间排序的。
- prev != null,代表不是消息的头部,把msg插入到消息队列中。
- prev == null,代表是消息队列的头部,把msg插入消息的头部。
通过Handler发送消息handler.sendMessage(),最终都会调用Handler.java中的enqueueMessage()方法。
1 | private boolean enqueueMessage(MessageQueue queue, Message msg, long uptimeMillis) { |
enqueueMessage()方法里为msg设置了target字段。
而上面的postSyncBarrier(),也是从Message消息对象池中获取一个msg,插入到消息队列中,唯一的不同是没有设置target字段。所以从代码层面上讲,屏障消息就是一个target为空的Message
屏障消息的工作原理
通过postSyncBarrier方法屏障就被插入到消息队列中了,那么屏障是如何挡住普通消息只允许异步消息通过的呢?
Handler的消息处理是在Looper.loop()从消息队列中获取消息,并交给Handler处理的,其中是通过MessageQueue是通过next方法来获取消息的。
1 | Message next() { |
msg.target == null时说明此时的msg是屏障消息,此时会进入到循环,遍历移动msg的位置,知道移动到的msg是异步message则退出循环,也就是说,循环的代码会过滤掉所有的同步消息,直到取出异步消息为止。
当设置了同步屏障之后,next函数将会忽略所有的同步消息,返回异步消息。换句话说就是,设置了同步屏障之后,Handler只会处理异步消息。再换句话说,同步屏障为Handler消息机制增加了一种简单的优先级机制,异步消息的优先级要高于同步消息。
移除同步屏障
同步屏障的移除是在MessageQueue.java的removeSyncBarrier()方法。
1 | public void removeSyncBarrier(int token) { |
删除屏障消息的方法很简单,就是不断遍历消息队列,知道找到屏障消息,退出循环的条件有两个,一是p.target == null,说明是屏障消息,二是p.arg1 == token,也说明p是屏障消息,因为在屏障消息入队的时候,设置过 msg.arg1 = token。找到屏障消息后,把它从消息队列中删除并回收。
总结
常见问题
Handler 引起的内存泄露原因以及最佳解决方案
内存泄露的定义:本该被回收的对象不能被回收而停留在堆内存中
内存泄露出现的原因:当一个对象已经不再被使用时,本该被回收但却因为有另外一个正在使用的对象持有它的引用从而导致它不能被回收。这就导致了内存泄漏。
因为 Handler 一般是作为 Activity 的内部类,可以发送延迟执行的消息,如果在延迟阶段,我们把 Activity 关掉,此时因为该 Activity 还被 Handler 这个内部类所持有,导致Activity 无法被回收,没有真正退出并释放相关资源,因此就造成内存泄漏。
工程上常用的方法是将 Handler 定义成静态的内部类 ,在内部持有 Activity 的弱引用,并在 Acitivity 的 onDestroy()中调用handler.removeCallbacksAndMessages(null) 及时移除所有消息。更进一步是建议将 Handler 抽离出来作为 BaseHandler,然后每个 Activity 需要用到 Handler 的时候,就去继承 BaseHandler。最佳解决方案具体代码:
1 | // 这个是BaseHandler |
为什么我们能在主线程直接使用 Handler,而不需要创建 Looper ?
ActivityThread 是主线程操作的管理者,在 ActivityThread.main() 方法中调用了 Looper.prepareMainLooper() ,该方法调用 prepare()创建 Looper。因此主线程不是不需要创建 Looper,而是系统帮我们做了。
Handler、Thread 和 HandlerThread 的差别
- Handler:是 Android 的一种异步消息机制,负责发送和处理消息,可实现子线程和主线程的消息通讯;
- Thread:Java 的一个多线程类,是 Java 进程中最小执行运算单位,用于给子类继承,创建线程;
- HandlerThread:从名字看就知道是由前面两者结合起来的。可以理解为“一个继承自 Thread 的 Handler 类”,因此本质上和父类一样是 Thread,但其内部直接实现了 Looper,我们可以直接在 HandlerThread 里面直接使用 Handler 消息机制。减少了手动调用 Looper.prepare()和 Looper.loop()这些方法。
子线程中怎么使用 Handler?
子线程中使用 Handler 需要先执行两个操作:Looper.prepare() 和 Looper.loop(),这两个函数执行顺序是不能变的。同时可以直接使用 HandlerThread 类即可。
为什么在子线程中创建 Handler 会抛异常?
不能在还没有调用 Looper.prepare() 方法的线程中创建 Handler。 因为抛出异常的地方,在 Handler 的构建函数,判断 mLooper 对象为 null 的时候, 会抛出异常
Handler 里藏着的 Callback 能干什么?
当从消息队列获取到信息后,需要分配给对应的 Handler 去处理,总共有 3 种优先级。
- handleCallback(msg):Message 里自带的 callback 优先级最高;对应 Handler 的 post 方法;
- mCallback.handleMessage(msg):也就是 Handler.Callback 写法;
- handleMessage(msg):重写 handlerMessage()方法,优先级最低;
而 Handler.Callback 处于第二优先级,当一条消息被 Callback 处理并返回 true,那么 Handler 的 handleMessage(msg) 方法就不会被调用了;但如果 Callback 处理后返回 false,那么这个消息就先后被 Handler.Callback 和 handleMessage(msg)都处理过。
Handler 的 send 和 post 的区别?
post 方法,它会把传入的 Runnable 参数赋值给 Message 的 callback 成员变量。当 Handler 进行分发消息时,msg.callback 会最优先执行。
- post 是属于 sendMessage 的一种赋值 callback 的特例
- post 和 sendMessage 本质上没有区别,两种都会涉及到内存泄露的问题
- post 方式配合 lambda 表达式写法更精简
创建 Message 实例的最佳方式
为了节省开销,Android 给 Message 设计了回收机制,所以我们在使用的时候尽量复用 Message ,减少内存消耗:
- 通过 Message 的静态方法 Message.obtain();
- 通过 Handler 的公有方法 handler.obtainMessage()。
Message 的插入以及回收是如何进行的,如何实例化一个 Message 呢?
Message 往 MessageQueue 插入消息时,会根据 when 字段(相对时间)来判断插入的顺序.
在消息执行完成之后,会进行回收消息recycleUnchecked(),Message 的成员变量设置为 0 或者 null;
实例化 Message 的时候,使用 Message.obtain 方法,这是从缓存消息池链表里直接获取的实例,可以避免 Message 的重复创建。
妙用 Looper 机制,或者你知道 Handler 机制的其他用途吗?
- 将 Runnable post 到主线程执行;
- 利用 Looper 判断当前线程是否是主线程;
1 | public boolean isMainThread() { |
Looper.loop()死循环一直运行是不是特别消耗 CPU 资源呢?不会造成应用卡死吗?
涉及 linux 多进程通讯方式:Pipe 管道通讯。Android 应用程序的主线程在进入消息循环过程前,会在内部创建一个 Linux 管道。首先在 loop()方法中,调用 queue 的 next()方法获取下一个消息,MessageQueue 没有消息时,便阻塞在 nativePollOnce()方法里,此时主线程会释放 CPU 资源进入休眠状态,因此并不特别消耗 CPU 资源。
直到等待时长到了或者有新的消息时,通过往 pipe 管道写端写入数据来唤醒主线程工作。这里采用的 epoll 机制是一种 IO 多路复用机制,可以同时监视多个描述符。当一个描述符号准备好(读或写)时,立即通知相应的程序进行读或写操作,其实质是同步 I/O,即读写是阻塞的。其实主线程大多数时候都是处于这种休眠状态,并不会消耗大量 CPU 资源,更不会造成应用卡死。
MessageQueue 中如何等待消息?为何不使用 Java 中的 wait/notify 来实现阻塞等待呢?
直接回答在 MessageQueue 的 nativePollOnce 函数阻塞,直到等待时长到了或者有新的消息时才重新唤醒 MessageQueue。其实在 Android 2.2 及其以前,确实是使用 wait/notify 来实现阻塞和唤醒,但是现在 MessageQueue 源码涉及很多 native 的方法,因此 Java 层的 wait/notify 不够用了,而 Pipe 管道通讯是很底层的 linux 跨进程通讯机制,满足 native 层开发需求。
你知道延时消息的原理吗?
首先是信息插入:会根据 when 属性(需要处理消息的相对时间)进行排序,越早的时间的 Message 插在链表的越前面;
在取消息处理时,如果时间还没到,就休眠到指定时间;如果当前时间已经到了,就返回这个消息交给 Handler 去分发,这样就实现处理延时消息了。
如何保证在 msg.postDelay 情况下保证消息次序?
handler.postDelay 不是延迟一段时间再把 Message 放到 MessageQueue 中,而是直接进入 MessageQueue,根据 when 变量(相对时间)的大小排序在消息池的链表里找到合适的插入位置,如此也保证了消息的次序的准确性。也就是本质上以 MessageQueue 的时间顺序排列和唤醒的方式结合实现的。
更新 UI 的方式有哪些
- Activity.runOnUiThread(Runnable)
- View.post(Runnable),View.postDelay(Runnable, long)
- Handler
- AsyncTask
- Rxjava
- LiveData
线程、Handler、Looper、MessageQueue 的关系?
一个线程对应一个 Looper ,同时对应一个 MessageQueue,对应多个 Handler。
多个线程给 MessageQueue 发消息,如何保证线程安全?
enqueueMessage()在插入 Message 的时候使用 synchronized 机制加锁。
你知道 IdleHandler 吗?
看看 next()源码:
1 | Message next() { |
IdleHandler 是通过 MessageQueue.addIdleHandler 来添加到 MessageQueue 的,前面提到当 MessageQueue.next 当前没有需要处理的消息时就会进入休眠,而在进入休眠之前呢,会执行注释 1,此时如果返回 true,则调用该方法后继续保留,下次队列又空闲的时候继续调用。如果返回 false,就会在注释 2 将当前的 idler 删除。
现在 push 一个延迟消息到消息队列里,这时候忽然退出应用程序了,会有什么问题
可能的问题
消息丢失: 如果消息尚未成功写入到消息队列中,应用程序退出可能导致该消息丢失。某些消息队列在处理消息写入时可能需要一些时间来确认消息已经持久化,如果在此期间应用退出,消息可能不会被保存。
消息重复: 如果消息在写入过程中应用程序退出,消息队列可能会重复接收到同一条消息。这在某些幂等性要求较高的应用中可能引发问题。
未处理的消息: 如果应用程序负责从消息队列中读取和处理消息,并在处理过程中突然退出,可能会导致一些消息未被处理。
状态不一致: 如果应用程序在处理消息的过程中维护一些状态信息,而在处理未完成时退出,可能会导致系统状态不一致。
解决方案
消息队列的可靠性:
- 使用支持持久化的消息队列(如 RabbitMQ、Kafka 等),确保消息即使在应用程序崩溃时也不会丢失。
- 确保消息在发送到队列时已经被持久化,通常可以通过消息队列的确认机制来实现。
消息幂等性:
- 设计消息处理逻辑时保证幂等性,即处理同一条消息多次不会导致副作用。例如,通过消息的唯一标识符来检测和防止重复处理。
消息确认机制:
- 使用消息确认机制(如 RabbitMQ 的 ACK 机制),确保消息在处理完成后才从队列中删除。如果处理未完成,消息队列会将消息重新投递。
事务支持:
- 在某些情况下,可以使用分布式事务,确保消息的发送和业务操作在一个原子操作中完成。例如,使用数据库和消息队列的事务性支持
重试机制:
- 为未处理的消息实现重试机制,如果处理失败,可以重新将消息推回队列并稍后重试。