1、多线程使用场景
1.1、线程与多线程
线程是程序中一个单一的顺序控制流程,而在单个程序中同时运行多个线程以完成不同的工作,称为多线程。
1.2、进程与线程
在linux中,进程和线程时类似的,线程是运行在进程里面的,一个App是运行在进程里面的。
1.3、生活中的多线程
如下图就能生动地展现多线程。
2、Android ANR的产生
2.1、ANR简介
- ANR(Application Not Response)即应用程序无响应。一般会弹出对话框进行提示,这个时候可以选择等待,也可以选择强制关闭应用。
出现ANR的原因:
- 系统繁忙
- app没有优化好
正常情况下,一个流畅合理的应用程序是不能出现ANR的,否则用户体验对大打折扣,因此在程序里对响应性能的设计很重要。
出现ANR的三种情况:
- 主要类型按键或触摸事件在特定事件(5秒)内无响应。
- BroadcastReceiver在特定时间(10秒)内无法处理完成。
- 小概率类型Service在特定的时间内无法处理完成。
2.2、代码演示
1.新建一个项目,在布局文件中添加一个按钮,给按钮添加一个单击事件,代码如下:
mButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
try {
Log.d(TAG, "onClick: start");
Thread.sleep(10000);
Log.d(TAG, "onClick: end");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
2.然后点击按钮,发现按钮点下去就没反应了这时候在屏幕上乱点几下,会弹出来一个窗口,如下所示,这就是应用出现了ANR。
拓展:在Android4.0之后的开发者选项中有一个ANR选项,如下图所示:
3、线程的两种实现方式
定义线程的方法:
1. 扩展java.lang.Thread类;
2. 实现Runnable接口
3.1、Thread类的使用方法
3.1.1、线程的状态及生命周期
- 创建(new)
- 就绪(runnable)
- 运行(running)
- 阻塞(blocked)、睡眠或等待一定的时间(time waiting)、waiting(等待被唤醒)
- 消亡(dead)
- 当需要一个线程来执行某个子任务时,就创建了一个线程。
- 但是线程只有满足需要的条件,才能进入就绪状态。
- 线程进入就绪状态后,不能马上获得CPU的执行时间
- 线程不能继续运行的原因:
- 用户主动让线程休眠
- 用户主动让线程等待
- 被同步块阻塞
- 用户主动让线程休眠
- 当由于突然中断或子任务执行完毕,线程就会消亡。
- 线程中常用方法:
- start()方法:启动线程
- run()方法:不需要用户调用,继承Thread类必须重写run()方法
- sleep()方法:相当于让线程睡眠,交出CPU,让CPU去执行其他任务。但是有一点要非常注意,sleep方法不会释放锁,也就是说如果当前线程持有对某个对象的锁,则即使调用sleep方法,其他线程也无法访问这个对象。
- yield()方法:调用yield方法会让当前线程交出CPU权限,让CPU去执行其他线程,它跟sleep方法类似,同样不会释放锁,但是yield不能控制具体的交出CPU的时间,另外,yield方法只能让拥有相同优先级的线程有获取CPU执行时间的机会。
注意,调用yield方法并不会让线程进入阻塞状态,而是让线程重回就绪状态,它只需要等待重新获取CPU执行时间,这一点是和sleep方法不一样的。 - join()方法:
join方法有三个重载版本:
假如在主线程中,调用thread.join方法,则main方法会等待thread线程执行完毕或者等待一定的时间。如果调用的是无参join方法,则等待thread执行完毕;如果调用的是指定了时间参数的join方法,则等待一定的时间。
join();
join(long millis);//参数为毫秒
join(long millis,int nanoseconds);//第一个参数是毫秒,第二个参数是纳秒 - interrupt()方法:顾名思义,即中断的意思。单独调用interrupt方法可以使得处于阻塞状态的线程抛出一个异常,也就说,它可以用来中断一个正处于阻塞状态的线程;另外,通过interrupt方法和isInterrupted()方法来停止正在运行的线程。
- start()方法:启动线程
- 以下为线程的完整
生命周期
图:
- 当需要一个线程来执行某个子任务时,就创建了一个线程。
3.1.2、上下文切换
- CPU在一个时刻只能运行一个线程,当在运行一个线程的过程中去运行另一个线程时,叫做上下文切换。
- 在切换时,需要保存线程的状态。需要保存那些数据呢?
- 程序计数器的值
- CPU寄存器状态
线程的上下文切换,实际上就是存储和恢复CPU状态的过程,它使得线程执行能够从中断点恢复执行。
3.1.3、Thread类的使用-模拟卖票
1.创建一个名为`Thread`的项目,新建一个名为`SaleTickets`类并让其继承自`Thread`类,重写其run()方法,添加一个票数的成员变量、构造方法及卖票的方法。public class SaleTickets extends Thread {
private static final String TAG = "Thread";
private int mTickets = 0;
public SaleTickets(int tickets) {
this.mTickets = tickets;
}
@Override
public void run() {
super.run();
while (mTickets > 0) {
saleTicket();
}
Log.d(TAG, Thread.currentThread().getName() + "票卖完了");
}
//卖票
private void saleTicket() {
try {
Thread.sleep(1000);//休眠1秒
} catch (InterruptedException e) {
e.printStackTrace();
}
mTickets--;
Log.d(TAG, Thread.currentThread().getName()+"卖了1张票,还剩" + mTickets + "张票");
}
}
2.在xml布局文件中添加一个按钮,在MainActivity中为按钮添加事件,开启一个线程,这里模拟开启一个窗口卖票。
public class MainActivity extends AppCompatActivity {
private Button mBtnStartWork;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnStartWork = findViewById(R.id.btnStartWork);
startSaleTickets();
}
private void startSaleTickets() {
mBtnStartWork.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SaleTickets thread1 = new SaleTickets(5);
thread1.start();
}
});
}
}
3.运行程序,点击按钮,查看日志打印结果,符合预期。 ![这里写图片描述](https://img-blog.csdn.net/20180727205352765?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70) 4.接下来,我们将线程增加至四条,修改`startSaleTickets`方法。
private void startSaleTickets() {
mBtnStartWork.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SaleTickets thread1 = new SaleTickets(3);
SaleTickets thread2 = new SaleTickets(4);
SaleTickets thread3 = new SaleTickets(5);
SaleTickets thread4 = new SaleTickets(6);
thread1.start();
thread2.start();
thread3.start();
thread4.start();
}
});
}
5.再次运行程序,日志打印结果如下。 ![这里写图片描述](https://img-blog.csdn.net/20180727205406195?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
3.1.4、Thread类的使用-join方法
1.修改`startSaleTickets`方法,代码如下:private void startSaleTickets() {
mBtnStartWork.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
SaleTickets thread1 = new SaleTickets(5);
thread1.start();
try {
Log.d(TAG, "Wait thread done!");
thread1.join();
Log.d(TAG, "Join returned!");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
}
2.运行程序,发现按钮点击后并没有回弹,直至任务执行完毕,按钮才弹回来(`具体原因参考上面join方法的介绍`),而日志打印结果也一致。 ![这里写图片描述](https://img-blog.csdn.net/2018072720542296?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
其他方法的介绍和具体的代码演示请参考这篇博客:Java Thread 的使用
3.2、实现Runnable接口
1.新建一个名为`Thread2`的项目,新建一个名为`SaleTickets`的类并实现`Runnable`接口,并实现其run方法,具体代码请参考上面项目中`SaleTickets`的类的代码,代码是一样的。 2.在MainActivity中代码中使用这个类实现模拟卖票。public class MainActivity extends AppCompatActivity {
private Button mBtnStartWork;
private SaleTickets mSaleTickets;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mBtnStartWork = findViewById(R.id.btnStartWork);
mSaleTickets = new SaleTickets(10);
mBtnStartWork.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
new Thread(mSaleTickets, "队伍1").start();
}
});
}
}
3.运行代码,查看日志打印结果。 ![这里写图片描述](https://img-blog.csdn.net/2018072720543491?watermark/2/text/aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L2NoYWl4aW5nc2k=/font/5a6L5L2T/fontsize/400/fill/I0JBQkFCMA==/dissolve/70)
4、线程间的通信
线程间通信的相关组件有:Handler、Looper、MessageQueue、Thread。4.1、职责与关系
4.1.1、职责
- Message:消息,其中包含了消息ID,消息处理对象以及处理的数据等。Message由MessageQueue统一列队,终由Handler处理。
- Handler:处理者,负责Message的发送及处理。使用Handler,我们需要实现handleMessage(Message msg)方法,以对特定的Message进行处理。
- MessageQueue:消息队列,用来存放Handler发送过来的消息,并按照先进先出(FIFO,即first in first out)的规则执行。
- Looper:消息泵,不断地从MessageQueue中抽取Message执行。一个MessageQueue需要一个Looper。
- Thead:线程,负责调度整个消息循环,即消息循环的执行场所。
4.1.2、关系
Handler、Looper和MessageQueue的关系如下图所示。
- Handler、Looper和MessageQueue是简单的三角关系,Looper和MessageQueue是一一对应的。
- 创建一个Looper的同时,会创建一个MessageQueue。
- 多个Handler可以共用同一个Looper和MessageQueue,这样的话,这些Handler就运行在同一个线程里了。
4.2、线程与更新
在主线程(UI线程)里,如果创建Handler时不传入Looper对象,那么将直接使用主线程的Looper对象(系统已经帮我们创建了);在其他线程里,如果创建Handler时不传入Looper对象,那么,这个Handler将不能接受处理消息,在这种情况下,通用的做法是:
class LooperThread extends Thread {
public Handler mHandler;
@Override
public void run() {
super.run();
Looper.prepare();
mHandler=new Handler(){
@Override
public void handleMessage(Message msg) {
super.handleMessage(msg);
}
};
Looper.loop();
}
}
即在创建Handler之前,为该线程准备好一个Looper(Looper.prepare),然后让这个Looper跑起来(Looper.loop),抽取Message,这样,Handler才能正常工作。
因此,Handler处理消息总是在创建Handler的线程里运行,而我们在消息处理中,不乏更新UI的操作,不正确的线程直接更新UI将引发异常,因此,需要时刻关心Handler在哪个线程里创建的。Handler既是消息的发起者,又是消息的处理者。
那么如何更新UI才能不出现异常呢?
在SDK中提供了4种方式可以从其他线程访问UI线程:
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable,long)
- Handler
不确定当前线程时,更新UI时尽量调用post方法。