线程
线程是程序中的一个执行流,每个线程都有自己的专有寄存器(栈指针、程序计数器等),但代码区是共享的,
即不同的线程可以执行同样的函数。
- 同步:
在一个线程执行,先执行完了前面的代码,才会执行后面的代码,是阻塞的。
- 异步:
开启一个新的线程执行,不会等前面的代码执行完,就会执行后面的代码,是非阻塞的。
- 什麽是main(UI)线程:
android启动的第一个线程。主要负责处理ui和事件的工作。
特别注意
更新ui只能在ui线程进行,不可以在其他线程更新ui,否则会崩溃。
在ui线程不可以做耗时操作,比如网络请求等,如果做耗时操作,就会阻塞ui线程,就会导致界面卡顿。会出现ANR(application not response、应用无响应)。
- 异步通讯:
那么我们要请求网络或者其他耗时操作的时候怎么办?这就涉及到异步通讯或者叫线程通讯。
先在子线程加载数据,做耗时操作,然后把取得的数据传递给ui线程,让ui线程来更新ui。
- 线程通讯的方式:
Handler(可以理解成消息获取的工具,非官方翻译)
sleep阻塞
来自Thread,没有释放锁,只能等时间到自动唤醒,或者用interrupt()强行打断,必须捕获异常
面试题:Thread.sleep()会导致ANR吗?
首先,先明白一个问题:什么是 ANR
Application Not Responding,意思是” 应用没有响应 “
以前我的理解就是 “在主线程做了耗时操作” 就会引起 ANR,现在我觉得我是错误的,ANR 的意思是应用没有响应,耗时操作实际上 并不一定会导致没有响应,我对没有响应的理解是
有人(事件或操作)发出了一个请求,但是主线程没有对这个人进行反馈(可能是没时间、可能是不想理、可能是手被绑住了没有办法理你),
这个叫没有响应
那举个例子
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
// do some blablabla...
Log.d("test", "准备sleep30秒")
Thread.sleep(30000)
Log.d("test", "sleep30秒完成")
// do some blablabla...
}
这段代码在 onCreate 中 sleep 了 30 秒,会出现 ANR 吗?
答案是
可能会,也可能不会
当主线程在 Sleep 的时候,如果 UI 线程不需要进行操作,也就是说没有消息会发送给 UI 线程并要求 UI 线程进行处理的时候,Sleep 30 秒就不会导致 ANR,因为没有出现 ANR(应用没有响应)的情况啊,没有人向线程请求什么东西,也就不需要响应了,既然没有响应了,那怎么会有 ANR 呢?
但是,线程在 Sleep 的时候,主线程有接收到需要处理的请求的时候
需要注意的是,需要处理的请求,不一定只是用户的手动触摸,也有可能是其他线程需要对线程进行 UI 更新的请求,这个时候 UI 线程正在 Sleep,根本没有办法理你(不想理你),这就符合了ANR的条件,所以会出现 ANR(比如说在这 30 秒内,点击了 返回按钮,就会出现 ANR)。
wait阻塞
来自Object,释放了锁,需要notify或者notifyAll唤醒,不需要捕获异常
面试题:怎么中断一个线程?
1、调用stop方法,不安全,已弃用
2、调用interrupt方法,通知线程应该中断了
- 如果线程处于被阻塞状态,那么线程将立即退出被阻塞状态,并抛出了一个InterruptedException异常。
- 如果线程处于正常活动状态,那么会将该线程的中断标志设置为true。被设置中断标志的线程将正常运行,不受影响。
线程间通信
- handler
- runOnUiThread
- View.post(Runnable r)
- AsyncTask.onPostExecute
handler机制
handle的post方法,会传递一个runnable进去,然后会调用getpostmessage方法
这个方法里会调用message的obtain方法获取到一个message对象,然后将这个runnable封装进这个message
然后回到前面,会调用sendMessageDelayed方法
会调用sendMessageAtTime方法
这时候会拿到messageQueue对象,里面会调用handler的enqueueMessage方法,入参就是这个messageQueue对象和message对象
在handler的enqueueMessage方法里给这个message打个标签,然后调用messageQueue的enqueueMessage方法
然后判断一下message的标签是不是为空和message是不是在使用,然后就把message放到messageQueue里
可以看handler.post方法的注解,无非就是把执行ui操作的runnablw封装成mesaage,再添加到messagequene中,添加成功就返回true,添加失败返回false
looper就是不停的检查是否有新消息,有就调用handleMessage提取并处理消息,只有主线程默认开启。
Looper.loop():让Looper开始工作,从消息队列里取消息,处理消息。注意:因为loop()方法是个循环,所以写在loop方法后面的代码不会执行,可以通过mHandler.getLooper().quit()结束循环,之后的代码才会执行。
面试题:主线程和子线程怎么用Handler进行通信?
1、如果主线程需要将消息发送到子线程,需要创建子线程的Handler,启用子线程Looper,在主线程调用子线程的Handler对象发送消息。
Thread thread = new Thread(){
@Override
public void run() {
super.run();
//初始化Looper
Looper.prepare();
//在子线程内部初始化handler即可,发送消息的代码可在主线程任意地方发送
handler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//所有的事情处理完成后要退出looper,即终止Looper循环
//这两个方法都可以,有关这两个方法的区别自行寻找答案
handler.getLooper().quit();
handler.getLooper().quitSafely();
}
};
//启动Looper循环
Looper.loop();
}
};
thread.start();
2、如果子线程需要将消息发送到主线程,需要获取主线程的Handler对象,在子线程调用主线程的Handler对象发送消息。
//在主线程中初始化Handler
private Handler handler = new Handler() {
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
//在此处处理消息
}
};
Thread thread = new Thread(){
@Override
public void run() {
super.run();
//在子线程中发送消息
handler.sendEmptyMessage(0);
}
};
thread.start();
上述代码中,在初始化handler前后均有两句关键代码,一定不能遗忘,否则你会收到以下异常
Can't create handler inside thread " + Thread.currentThread()+ " that has not called Looper.prepare()
面试题:为什么主 / 子线程通信,必须用对方线程的Handler或者对方线程的Looper创建Handler来发送消息?
因为message是存放在MessageQuene中的,Looper是用来不断轮询处理message的,而Looper是用 ThreadLocal 修饰的,ThreadLocal:即线程隔离,只被自己所属线程使用,所以必须用对方线程的Handler或者对方线程的Looper创建Handler来发送消息。
面试题:子线程中new Handler会发生什么?
会报异常(RuntimeException)。原因是:子线程中默认没有创建Looper对象,必须调用Looper.prepare()启用Looper。
面试题:handler的post()和sendMessage()的区别?
没区别。post方法只是多了个将Runnable封装成Message的过程。
面试题:Message的new方法和obtain方法的区别?
-
new方法
每次需要Message对象的时候都创建一个新的对象,每次都要去堆内存开辟对象存储空间,对象使用完后,jvm又要去对这个废弃的对象进行垃圾回收 -
obtain方法
采用单链表,减少了每次获取Message时去申请空间的时间。同时,这样也不会永无止境的去创建新对象,减小了Jvm垃圾回收的压力,提高了效率
原理图解:
咱们对着源码来看:
假设该链表初始状态如下
执行Message m = sPool;就变成下图
继续sPool = m.next;
然后m.next = null;
接下来m.flags=0;sPoolSize–;return m;便是表示m指向的对象已经从链表中取出并返回了。
再看回收recycle():
然后再看看sPoolSize是什么时候自增的。按图索骥便可找到recycle()方法和recycleUnchecked()方法。前者供开发者调用进行回收,后者执行回收操作。来看看回收操作都干了啥:
void recycleUnchecked() { // Mark the message as in use while it remains in the recycled object pool. // Clear out all other details.
flags = FLAG_IN_USE;
what = 0;
arg1 = 0;
arg2 = 0;
obj = null;
replyTo = null;
sendingUid = -1; when = 0;
target = null;
callback = null;
data = null;
synchronized (sPoolSync) { if (sPoolSize < MAX_POOL_SIZE) {
next = sPool;
sPool = this;
sPoolSize++;
}
}
}
前半段不必多说,显然是“重置”改对象的个个字段。后半段又是一个同步代码段,同样用图来解释一下(假设当前代码为message.recycle(),则需要被回收的则是message对象)。
假设当前链表如下:
执行next=sPool;
执行sPool=this;
现在可以很清楚的看到,Message类本身就组织了一个栈结构的缓冲池。并使用obtain()方法和recycler()方法来取出和放入。
面试题:asyncTask、service、intentService、handler、thread和handlerThread的区别?
-
asyncTask
适用于数据轻量,且需要及时在主线程刷新UI的时候 -
service和intentService
service不是单独的进程,它和应用程序在同一个进程,它也不是一个线程,所以它不能直接处理耗时任务,需要开启一个单独的线程来处理。如果在onStartCommand()方法里处理耗时任务,很容易引起ANR。intentService继承Service,内部自动开启一个工作线程来处理耗时任务,省去了Service手动开线程的麻烦;而且它处理完任务还会自动停止,不用像Service那样需要手动停止服务。
适用于没有UI操作,有大量数据需要同步到本地的时候。例如同步用户数据、聊天数据等。
如果对IntentService的了解仅限于此,会有种IntentService很鸡肋的观点,因为在Service中开线程进行耗时操作也不麻烦
但是IntentService还有一个特点,就是多次调用onHandleIntent函数(也就是有多个耗时任务要执行),多个耗时任务会按顺序依次执行。原理是其内置的Handler关联了任务队列,Handler通过looper取任务执行是顺序执行的。
这个特点就能解决多个耗时任务需要顺序依次执行的问题。而如果仅用service,开多个线程去执行耗时操作,就很难管理
-
handler、thread和handlerThread
handler负责处理消息,通过它可以实现子线程与主线程通讯
thread是一个线程
handlerThread继承thread,内部封装了handler。
适用于长时间的任务。例如加载数据到界面。
并发编程的关键
-
分工
关于分工,常见的 Executor,生产者-消费者模式,Fork/Join 等,这都是分工思想的体现。强调的是性能。
-
同步
一个线程执行完任务,如何通知后续线程执行。强调的是性能。
-
互斥
当多个线程同时访问一个共享变量/成员变量时,就可能发生不确定性,造成不确定性主要是有可见性、原子性、有序性这三大问题,而解决这些问题的核心就是互斥,即:同一时刻,只允许一个线程访问共享变量。强调的是正确性。
并发编程中的三个概念
原子性问题,可见性问题,有序性问题。
1.原子性
原子性:即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
一个很经典的例子就是银行账户转账问题:
比如从账户A向账户B转1000元,那么必然包括2个操作:从账户A减去1000元,往账户B加上1000元。
试想一下,如果这2个操作不具备原子性,会造成什么样的后果。假如从账户A减去1000元之后,操作突然中止。然后又从B取出了500元,取出500元之后,再执行 往账户B加上1000元 的操作。这样就会导致账户A虽然减去了1000元,但是账户B没有收到这个转过来的1000元。
所以这2个操作必须要具备原子性才能保证不出现一些意外的问题。
同样地反映到并发编程中会出现什么结果呢?
举个最简单的例子,大家想一下假如为一个32位的变量赋值过程不具备原子性的话,会发生什么后果?
i = 9;
假若一个线程执行到这个语句时,我暂且假设为一个32位的变量赋值包括两个过程:为低16位赋值,为高16位赋值。
那么就可能发生一种情况:当将低16位数值写入之后,突然被中断,而此时又有一个线程去读取i的值,那么读取到的就是错误的数据。
2.可见性
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
举个简单的例子,看下面这段代码:
//线程1执行的代码
int i = 0;
i = 10;
//线程2执行的代码
j = i;
假若执行线程1的是CPU1,执行线程2的是CPU2。由上面的分析可知,当线程1执行 i =10这句时,会先把i的初始值加载到CPU1的高速缓存中,然后赋值为10,那么在CPU1的高速缓存当中i的值变为10了,却没有立即写入到主存当中。
此时线程2执行 j = i,它会先去主存读取i的值并加载到CPU2的缓存当中,注意此时内存当中i的值还是0,那么就会使得j的值为0,而不是10.
这就是可见性问题,线程1对变量i修改了之后,线程2没有立即看到线程1修改的值。
3.有序性
有序性:即程序执行的顺序按照代码的先后顺序执行。举个简单的例子,看下面这段代码:
int i = 0;
boolean flag = false;
i = 1; //语句1
flag = true; //语句2
上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。
关于指令重排
下面解释一下什么是指令重排序,一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。
比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。
但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子:
int a = 10; //语句1
int r = 2; //语句2
a = a + 3; //语句3
r = a*a; //语句4
这段代码有4个语句,那么可能的一个执行顺序是:
那么可不可能是这个执行顺序呢: 语句2 语句1 语句4 语句3