文章目录
- Android常用的多线程实现方法
-
- 1.Java原生的实现
-
- 继承Thread类与实现Runnable接口的区别
- 实现Runnable接口与实现Callable接口的区别
- 2.AsyncTask
- 3.Handler
-
- Handler的基本使用
- Looper、MessageQueue、Message与Handler
- 总结
Android常用的多线程实现方法
说起多线程编程大家都不陌生,所谓线程就是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。在Android开发中,当我们执行一些耗时操作,比如发起一条网络请求时,我们不知道服务器何时会响应请求,这类操作需要放在新的线程执行,以免阻塞主线程(UI线程)。在Android开发中常用的实现多线程的方式有以下几种:
1.Java原生的实现
作为Android开发常用的语言,Java已经为我们提供了多线程的实现,主要有以下三种方式:
1.继承Thread类,重写run方法;
2.实现Runnable接口,重写run方法,实现Runnable接口的实现类的实例对象作为Thread构造函数的target;
3.通过Callable和FutureTask创建线程;
继承Thread类与实现Runnable接口的区别
那么这三种方法也是各有特点,实现Runnable接口方法相比于继承Thread类就有这几个优势:
1)由于Java不支持多继承,因此继承Thread类之后就不可以再继承别的类了,而实现Runnable接口方法可以解决单继承而带来的局限性;
2)增强程序的健壮性,代码能够被多个线程共享,代码与数据是独立的
3)适合多个相同程序代码的线程去处理同一资源的情况
4)线程池只能放入实现Runable或Callable类线程,不能直接放入继承Thread的类
针对2)和3)这两点,我们以卖票系统举个例子,通过继承Thread类方式实现多线程如下:
public class MutliThreadDemo {
public static void main(String[] args) {
MutliThread m1=new MutliThread("Window 1");
MutliThread m2=new MutliThread("Window 2");
MutliThread m3=new MutliThread("Window 3");
m1.start();
m2.start();
m3.start();
}
}
public class MutliThread extends Thread {
private int ticket=100;//每个线程都拥有100张票
public MutliThread (){}
public MutliThread (String name){
super(name);
}
@Override
public void run() {
while(ticket>0){
System.out.println(ticket--+" is saled by "+Thread.currentThread().getName());
}
}
}
从结果可以看到,每个线程分别对应100张电影票,之间并无任何关系,这就说明每个线程之间是平等的,系统会分别为其分配内存资源。而如果我们要实现资源共享,也就是说这些线程卖同一份票池,使用这种方法就无法实现了,而使用Runnable接口方式就可以解决这个问题:
public class MutliThreadDemo {
public static void main(String[] args) {
MutliThread m=new MutliThread();
Thread t1=new Thread(m);
Thread t2=new Thread(m);
Thread t3=new Thread(m);
t1.start();
t2.start();
t3.start();
}
}
public class MutliThread implements Runnable{
private int ticket=100;//每个线程都拥有100张票
public void run(){
while(ticket>0){
System.out.println(ticket--+" is saled by "+Thread.currentThread());
}
}
}
如上所示,程序在内存中仅创建了一份资源,而新建的三个线程都是访问同一资源的。
在第一种方法中中,要想开多个线程,就要先new多个自定义类MutliThread对象,每一个自定义类MyThread的对象的成员变量都相同,这样需要在栈中开辟很多内存;
在第二种方法中中,要想开多个线程,只需要new一个自定义类MutliThread的对象,再new多个Thread类的对象即可,这样就大大节约了内存。
因此,我们说Runnable接口方式相比于继承Thread类方式,适合多个相同程序的代码去处理同一个资源的情况,能够把线程同程序的代码和数据有效分离(即耦合性降低),较好的体现了Java面向对象的设计思想。
实现Runnable接口与实现Callable接口的区别
实现Runnable接口和实现Callable接口的区别主要有以下几点:
1)Runnable是自从java1.1就有了,而Callable是1.5之后才加上去的
2)实现Callable接口的任务线程能返回执行结果(需要调用FutureTask.get()方法实现,但会阻塞主线程直到获取返回结果),而实现Runnable接口的任务线程不能返回结果
3)Callable接口的call()方法允许抛出异常,而Runnable接口的run()方法的异常只能在内部消化,不能继续上抛
4)加入线程池运行,Runnable使用ExecutorService的execute方法,Callable使用submit方法
我们看到相比于Runnable接口方法,Callable接口可以返回执行结果。因此如果你希望任务在完成的能返回一个值,那么可以通过Callable接口实现。Callable接口使用泛型去定义它的返回类型,它的参数类型表示的是从方法call()(不是run())中返回的值,下面简单说一下用用法。
首先定义一个类实现java.util.concurrent包下的Callable接口,然后重写里面的call方法(相当于其他方法的run()函数),创建ExecutorService线程池,将自定义类的对象放入线程池里面,即可获取线程的返回结果,最后,关闭线程池,不再接收新的线程,未执行完的线程不会被关闭。简单的示例如下:
public static void main(String[] args) {
//创建线程池对象
ExecutorService pool = Executors.newFixedThreadPool(2) ;
//提交任务
pool.submit(new MyCallable()) ;
pool.submit(new MyCallable()) ;
//关闭线程池
pool.shutdown();
}
public class MyCallable implements Callable {
@Override
public Object call() throws Exception {
for(int x = 0 ; x < 100 ; x ++) {
System.out.println(Thread.currentThread().getName()+":"+x);
}
return null;
}
}
2.AsyncTask
AsyncTask是一个Android 已封装好的轻量级的异步类,可以用来实现多线程及多线程通信,通常用来实现一些较简单的耗时操作(如:倒计时,下载等)。它是一个抽象类,需要我们在使用的时候做具体实现。
public abstract class AsyncTask<Params, Progress, Result> {
...
}
如上所示,AsyncTask拥有三个泛型参数:
- Params 启动任务执行的输入参数,比如下载URL;
- Progress 后台任务执行的百分比,比如下载进度;
- Result 后台执行任务最终返回的结果,比如下载结果。
AsyncTask的常用方法有以下几个:
onPreExecute方法:
异步任务开始执行时,系统最先调用此方法。此方法运行在主线程中,可以对控件进行初始化等操作。
dolnBackground方法:
执行完onPreExecute方法后,系统执行此方法。此方法运行在子线程中,比较耗时的操作放在此方法中执行。
onProgressUpdate方法:
1.显示当前进度,适用于下载或扫描这类需要实时显示进度的需求。
2.此方法运行在主线程中,可以修改控件状态,例如: 显示百分比。
3.触发此方法,需要在dolnBackground中使用publishProgress方法。
publishProgress方法:
在doInBackground中使用。用于触发onProgressUpdate方法。
on PostExecute方法:
当异步任务执行完成后,系统会调用此方法。此方法运行在主线程中,可以修改控件状态,例如: 下载完成。
那么AsyncTask使用起来也非常简单,主要分为三步:
- 创建 AsyncTask 子类 & 根据需求实现核心方法
- 创建 AsyncTask子类的实例对象(即 任务实例)
- 手动调用execute(()从而执行异步线程任务
看起来还是很简单的,下面是一个网上找的简单例子。
/**
* 步骤1:创建AsyncTask子类
* 注:
* a. 继承AsyncTask类
* b. 为3个泛型参数指定类型;若不使用,可用java.lang.Void类型代替
* c. 根据需求,在AsyncTask子类内实现核心方法
*/
public class MainActivity extends AppCompatActivity {
// 线程变量
MyTask mTask;
// 主布局中的UI组件
Button button,cancel; // 加载、取消按钮
TextView text; // 更新的UI组件
ProgressBar progressBar; // 进度条
/**
* 步骤1:创建AsyncTask子类
* 注:
* a. 继承AsyncTask类
* b. 为3个泛型参数指定类型;若不使用,可用java.lang.Void类型代替
* 此处指定为:输入参数 = String类型、执行进度 = Integer类型、执行结果 = String类型
* c. 根据需求,在AsyncTask子类内实现核心方法
*/
private class MyTask extends AsyncTask<String, Integer, String> {
// 方法1:onPreExecute()
// 作用:执行 线程任务前的操作
@Override
protected void onPreExecute() {
text.setText("加载中");
// 执行前显示提示
}
// 方法2:doInBackground()
// 作用:接收输入参数、执行任务中的耗时操作、返回 线程任务执行的结果
// 此处通过计算从而模拟“加载进度”的情况
@Override
protected String doInBackground(String... params) {
try {
int count = 0;
int length = 1;
while (count<99) {
count += length;
// 可调用publishProgress()显示进度, 之后将执行onProgressUpdate()
publishProgress(count);
// 模拟耗时任务
Thread.sleep(50);
}
}catch (InterruptedException e) {
e.printStackTrace();
}
return null;
}
// 方法3:onProgressUpdate()
// 作用:在主线程 显示线程任务执行的进度
@Override
protected void onProgressUpdate(Integer... progresses) {
progressBar.setProgress(progresses[0]);
text.setText("loading..." + progresses[0] + "%");
}
// 方法4:onPostExecute()
// 作用:接收线程任务执行结果、将执行结果显示到UI组件
@Override
protected void onPostExecute(String result) {
// 执行完毕后,则更新UI
text.setText("加载完毕");
}
// 方法5:onCancelled()
// 作用:将异步任务设置为:取消状态
@Override
protected void onCancelled() {
text.setText("已取消");
progressBar.setProgress(0);
}
}
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
// 绑定UI组件
setContentView(R.layout.activity_main);
button = (Button) findViewById(R.id.button);
cancel = (Button) findViewById(R.id.cancel);
text = (TextView) findViewById(R.id.text);
progressBar = (ProgressBar) findViewById(R.id.progress_bar);
/**
* 步骤2:创建AsyncTask子类的实例对象(即 任务实例)
* 注:AsyncTask子类的实例必须在UI线程中创建
*/
mTask = new MyTask();
// 加载按钮按按下时,则启动AsyncTask
// 任务完成后更新TextView的文本
button.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
/**
* 步骤3:手动调用execute(Params... params) 从而执行异步线程任务
* 注:
* a. 必须在UI线程中调用
* b. 同一个AsyncTask实例对象只能执行1次,若执行第2次将会抛出异常
* c. 执行任务中,系统会自动调用AsyncTask的一系列方法:onPreExecute() 、doInBackground()、onProgressUpdate() 、onPostExecute()
* d. 不能手动调用上述方法
*/
mTask.execute();
}
});
cancel = (Button) findViewById(R.id.cancel);
cancel.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
// 取消一个正在执行的任务,onCancelled方法将会被调用
mTask.cancel(true);
}
});
}
}
布局文件我没有贴,主要包括两个button和一个进度条,这个例子实现了一个进度条的实时更新,点击一个按钮开始,调用AsyncTask的execute()方法开始异步请求,然后再doInBackground方法中增加进度,然后通过publishProgress方法将进度发送,这样onProgressUpdate(progresses)中可以通过progresses获取当前紧毒并对UI进行更新,最后,点击停止按钮时调用cancel方法取消这个任务。
AsyncTask很容易引起内存泄漏,如果AsyncTask被声明为Activity的非静态的内部类,那么AsyncTask会保留一个对创建了AsyncTask的Activity的引用。如果Activity已经被销毁,AsyncTask的后台线程还在执行,它将继续在内存里保留这个引用,导致Activity无法被回收,引起内存泄露。
为了避免内存泄漏,我们在使用时要特别注意:第一,在Activity生命周期结束前,去cancel AsyncTask。第二,如果一定要写成内部类的形式,对context采用WeakRefrence,在使用之前判断是否为空。
3.Handler
AsyncTask使用的优点是简单,快捷,过程可控。使用的缺点是在使用多个异步操作和并需要进行UI变更时,就会变得复杂起来,而接下来要说的Handler,结构清晰,对于多个后台任务时简单清晰。所以一般进行数据量多且复杂的操作就使用Handler,Handler也是Android面试经常考查的知识点。
Handler是Android 提供的一种异步通信机制,我们经常用它将工作线程中需更新UI的操作信息 传递到 UI主线程(只有主线程能更新UI),实现工作线程对UI的更新处理,例如:在UI界面上进行某项操作后要执行一段很耗时的代码,需要根据执行结果来更新UI,由于这是一个耗时操作,我们不知道什么时候才能完成,这个时候Handler就派上用场了。
Handler的基本使用
Handler有两种实现方式,一种是调用sendMessage方式,一种是通过post方式。基本实现如下:
sendMessage方式:
android.os.Handler handler = new Handler(){
@Override
public void handleMessage(final Message msg) {
//这里接受并处理消息,如更新UI等操作
}
};
//发送消息
handler.sendMessage(message);
post方式:
// 在主线程中创建Handler实例
private Handler mhandler = new mHandler();
// 在工作线程中发送消息到消息队列中并进行更新UI的操作
mHandler.post(new Runnable() {
@Override
public void run() {
// 需执行的UI操作
}
});
Handler使用起来非常简单方便,但实现原理我们也是要知道的,这里就涉及到Looper、MessageQueue、Message这几个Handler中的重要概念,下面来详细介绍:
Looper、MessageQueue、Message与Handler
Looper、MessageQueue、Message和Handler是handler中重要的四个类,他们分工合作实现消息的发送和处理,下面是各自的作用:
Looper:消息循环器,循环取出消息(Message),将取出的消息分发给对应的处理者(Handler)。
Message:消息,线程间通讯的数据单元。
MessageQueue:消息队列,存储Handler发送过来的消息(Message),如其名字,是一个先进先出的队列。
Handler:消息处理器,将消息传递给消息队列,然后再由对应线程从消息队列中逐个取出数据并执行。
这里有一张图比较直观的说明了它们的关系:
那么这里就有一个问题了,在上面handler的基本使用中并没有看到Looper相关调用,那Looper是如何工作的呢。其实是在主线程使用Handler系统在启动App时已经把这些提前为我们做好了,因此我们平常在主线程不需要手动调用,在ActivityThread.main方法中会调用到Looper.prepareMainLooper()和Looper.loop(),必须执行这两步使Looper完成启动才可以正常的使用,如果我们在子线程里使用Handler,必须手动执行这两个方法,否则会报错。
按照这个流程,我们先来看首先调用的Looper.prepare()里面都做了什么:
public static void prepare() {
prepare(true);
}
private static void prepare(boolean quitAllowed) {
// 规定了一个线程只有一个Looper,也就是一个线程只能调用一次Looper.prepare()
if (sThreadLocal.get() != null) {
throw new RuntimeException("Only one Looper may be created per thread");
}
// 如果当前线程没有Looper,那么就创建一个,存到sThreadLocal中
sThreadLocal.set(new Looper(quitAllowed));
}
从上面的代码可以看出,一个线程只能有一个Looper对象。当没有Looper对象时,去创建一个Looper,并存放到sThreadLocal中,sThreadLocal是一个ThreadLocal对象,ThreadLocal是一个线程内部的存储类,它提供了线程内存储变量的能力,这些变量不同之处在于每一个线程读取的变量是对应的互相独立的。通过get和set方法就可以得到当前线程对应的值。在Handler里它用来存储Looper对象的副本,并且可以通过它取得当前线程在之前存储的Looper的副本。
在Looper.prepare()中新建了一个Looper对象,下面我们来看Looper的构造方法:
private Looper(boolean quitAllowed) {
mQueue = new MessageQueue(quitAllowed);
mThread = Thread.currentThread();
}
这里的实现很简单,主要就是创建了消息队列MessageQueue,并让它供Looper持有,因为一个线程最大只有一个Looper对象,所以一个线程最多也只有一个消息队列。然后再把当前线程赋值给mThread。而MessageQueue的构造方法就是新建了一个消息队列,用于存放Message。
Looper就绪后就可以新建Handler了,Handler最基本的构造方法如下:
public Handler() {
this(null, false);
}
public Handler(Callback callback, boolean async) {
// 不相关代码
......
//得到当前线程的Looper,其实就是调用的sThreadLocal.get
mLooper = Looper.myLooper();
// 如果当前线程没有Looper就报运行时异常
if (mLooper == null) {
throw new RuntimeException(
"Can't create handler inside thread that has not called Looper.prepare()");
}
// 把得到的Looper的MessagQueue让Handler持有
mQueue = mLooper.mQueue;
// 初始化Handler的Callback
mCallback = callback;
mAsynchronous = async;
}
首先,调用了Looper.myLooper,其实就是调用sThreadLocal.get方法,来得到当前线程调用保存的Looper对象,并让Handler持有它。接下来对该Looper对象进行非空判断,避免如没有调用Looper.prepare()新建Looper对象。然后,让Handler持有Looper对象的MessageQueue,最后设置处理回调的Callback对象。这些都准备好后,我们来看loop()方法:
public static void loop() {
// 得到当前线程的Looper对象
final Looper me = myLooper();
if (me == null) {
throw new RuntimeException("No Looper; Looper.prepare() wasn't called on this thread.");
}
// 得到当前线程的MessageQueue对象
final MessageQueue queue = me.mQueue;
// 无关代码
......
// 死循环
for (;;) {
// 不断从当前线程的MessageQueue中取出Message,当MessageQueue没有元素时,方法阻塞
Message msg = queue.next(); // might block
if (msg == null) {
// No message indicates that the message queue is quitting.
return;
}
// Message.target是Handler,其实就是发送消息的Handler,这里就是调用它的dispatchMessage方法
msg.target.dispatchMessage(msg);
// 回收Message
msg.recycleUnchecked();
}
}
首先还是判断了当前线程是否有Looper,然后得到当前线程的MessageQueue。接下来,就是最关键的代码了,写了一个死循环,不断调用MessageQueue的next方法取出MessageQueue中的Message,拿到Message以后,会调用它的target的dispatchMessage方法,这个target其实就是发送消息时用到的Handler。所以就是调用Handler的dispatchMessage方法,这个方法最终绘调用到handleMessage方法,也就是我们使用时重写的那个方法。
最后就是发送消息了,上面说过使用Handler发送消息主要有两种,不过两种方式最后都会调用到sendMessageDelayed方法,所以我们就以最简单的sendMessage方法来分析。
public final boolean sendMessage(Message msg)
{
return sendMessageDelayed(msg, 0);
}
public final boolean sendMessageDelayed(Message msg, long delayMillis)
{
if (delayMillis < 0) {
delayMillis = 0;
}
return sendMessageAtTime(msg, SystemClock.uptimeMillis() + delayMillis);
}
public boolean sendMessageAtTime(Message msg, long uptimeMillis) {
// 这里拿到的MessageQueue其实就是创建时的MessageQueue,默认情况是当前线程的Looper对象的MessageQueue
// 也可以指定
MessageQueue queue = mQueue;
if (queue == null) {
RuntimeException e = new RuntimeException(
this + " sendMessageAtTime() called with no mQueue");
Log.w("Looper", e.getMessage(), e);
return false;
}
// 调用enqueueMessage,把消息加入到MessageQueue中
return enqueueMessage(queue, msg, uptimeMillis);
}
可见其主要实现是调用enqueueMessage来实现的,这里面会调用MessageQueue的enqueueMessage方法,我们来看一下它的实现:
boolean enqueueMessage(Message msg, long when) {
if (msg.target == null) {
throw new IllegalArgumentException("Message must have a target.");
}
// 一个Message,只能发送一次
if (msg.isInUse()) {
throw new IllegalStateException(msg + " This message is already in use.");
}
synchronized (this) {
if (mQuitting) {
IllegalStateException e = new IllegalStateException(
msg.target + " sending message to a Handler on a dead thread");
Log.w("MessageQueue", e.getMessage(), e);
msg.recycle();
return false;
}
// 标记Message已经使用了
msg.markInUse();
msg.when = when;
// 得到当前消息队列的头部
Message p = mMessages;
boolean needWake;
// 我们这里when为0,表示立即处理的消息
if (p == null || when == 0 || when < p.when) {
// 把消息插入到消息队列的头部
msg.next = p;
mMessages = msg;
needWake = mBlocked;
} else {
// 根据需要把消息插入到消息队列的合适位置,通常是调用xxxDelay方法,延时发送消息
needWake = mBlocked && p.target == null && msg.isAsynchronous();
Message prev;
for (;;) {
prev = p;
p = p.next;
if (p == null || when < p.when) {
break;
}
if (needWake && p.isAsynchronous()) {
needWake = false;
}
}
// 把消息插入到合适位置
msg.next = p; // invariant: p == prev.next
prev.next = msg;
}
// 如果队列阻塞了,则唤醒
if (needWake) {
nativeWake(mPtr);
}
}
return true;
}
内容很多,总结一下就是把Message加入到MessageQueue并按照当前的情况放到合适的位置。
总结
以上就是对三种常用的多线程实现的一个学习总结,相信这样总结过后对它们都有了更深刻的理解,由于Handler在我平常项目使用上频率较高,而且据说是面试经常考察的点,本篇文章也是对其着墨较多,并特别的从源码的角度进行了一定理解,但是使用时也要注意,由于Handler一般定义为Activity的内部类,会持有外部类的引用,因此很容易引起内存泄漏,这里官方给出了避免引起内存泄漏的建议是:
将Handler声明为静态类; 在外部类中,实例化WeakReference到外部类并在实例化Handler时将此对象传递给Handler,使用WeakReference对象创建对外部类成员的所有引用。