The speed and efficiency of a long-running, data-intensive operation often improves when you split it into smaller operations running on multiple threads.

把一个相对耗时且数据操作复杂的任务分割成多个小的操作,然后分别运行在多个线程上,这能够提高完成任务的速度和效率。在多核CPU的设备上,系统可以并行运行多个线程,而不需要让每个子操作等待CPU的时间片切换。例如,如果要解码大量的图片文件并以缩略图的形式把图片显示在屏幕上,当你把每个解码操作单独用一个线程去执行时,会发现速度快了很多。

那问题来了:如何在一个Android应用中创建和使用多线程,以及如何使用线程池对象(thread pool object);如何使得代码运行在指定的线程中,以及如何让线程和UI线程通信。

主题一:如何在一个线程中执行特定代码

如何通过实现 Runnable接口得到一个能在重写的Runnable.run()方法中执行一段代码的单独的线程?

是否可以传递一个Runnable对象到另一个对象,然后这个对象可以把它附加到一个线程,并执行它?

一个或多个执行特定操作的Runnable对象有时也被称为一个任务Task。

Thread和Runnable只是两个基本的线程类,通过他们能发挥的作用有限,但是他们是强大的Android线程类的基础类。例如Android中的HandlerThread, AsyncTask和IntentService都是以它们为基础。Thread和Runnable同时也是ThreadPoolExecutor类的基础。

ThreadPoolExecutor类能自动管理线程和任务队列,甚至可以并行执行多个线程。

创建一个实现Runnable的类

public class PhotoDecodeRunnable implements Runnable {
...
@Override
public void run() {
/*
* Code you want to run on the thread goes here
*/
...
}
...
}

覆写其中的run():

class PhotoDecodeRunnable implements Runnable {
...
/*
* Defines the code to run for this task.
*/
@Override
public void run() {
// Moves the current Thread into the background
android.os.Process.setThreadPriority(android.os.Process.THREAD_PRIORITY_BACKGROUND);
...
/*
* Stores the current Thread in the PhotoTask instance,
* so that the instance
* can interrupt the Thread.
*/
mPhotoTask.setImageDecodeThread(Thread.currentThread());
...
}
...
}

需要注意的是:Remember, though, that the Runnable won't be running on the UI thread, so it can't directly modify UI objects such as View objects.

At the beginning of the run() method, set the thread to use background priority by calling Process.setThreadPriority() with THREAD_PRIORITY_BACKGROUND. This approach reduces resource competition between the Runnable object's thread and the UI thread.

你还应该通过在Runnable 自身中调用Thread.currentThread()来存储一个引用到Runnable对象的线程。

主题二:如何为多线程创建线程池

什么是线程池?线程池的含义是什么?ThreadPoolExecutor:an object that manages a pool of Thread objects and a queue of Runnable objects.

如果你想在一个数据集中重复执行一个任务,而且你只需要一个执行运行一次。这时,使用一个IntentService将能满足你的需求。为了在资源可用的的时候自动执行任务,或者允许不同的任务同时执行(或前后两者),你需要提供一个管理线程的集合。为了做这个管理线程的集合,使用一个ThreadPoolExecutor实例,当一个线程在它的线程池中变得不受约束时,它会运行队列中的一个任务。为了能执行这个任务,你所需要做的就是把它加入到这个队列。

一个线程池能运行多个并行的任务实例,因此你要能保证你的代码是线程安全的,从而你需要给会被多个线程访问的变量附上同步代码块(synchronized block)。 当一个线程在对一个变量进行写操作时,通过这个方法将能阻止另一个线程对该变量进行读取操作。典型的,这种情况会发生在静态变量上,但同样它也能突然发生在任意一个实例化操作中。

如何定义线程池?

在自己的类中实例化ThreadPoolExecutor类,必须让自定义的类做以下几件事情:

  Use static variables for thread pools 为线程池使用静态变量

为了有一个单一控制点用来限制CPU或涉及网络资源的Runnable类型,你可能需要有一个能管理所有线程的线程池,且每个线程都会是单个实例。比如,你可以把这个作为一部分添加到你的全局变量的声明中去:

public class PhotoManager {
...
static {
...
// Creates a single static instance of PhotoManager
sInstance = new PhotoManager();
}
...
Use a private constructor 使用私有的构造方法

让构造方法私有从而保证这是一个单例,这意味着你不需要在同步代码块(synchronized block)中额外访问这个类:

public class PhotoManager {
...
/**
* Constructs the work queues and thread pools used to download
* and decode images. Because the constructor is marked private,
* it's unavailable to other classes, even in the same package.
*/
private PhotoManager() {
...
}
Start your tasks by calling methods in the thread pool class.  通过调用线程池类中的方法,开启任务

该方法能够实例化一个Task,并添加到线程池的运行队列中。

public class PhotoManager {
...
// Called by the PhotoView to get a photo
static public PhotoTask startDownload(
PhotoView imageView,
boolean cacheFlag) {
...
// Adds a download task to the thread pool for execution
sInstance.
mDownloadThreadPool.
execute(downloadTask.getHTTPDownloadRunnable());
...
}

  Instantiate a Handler in the constructor and attach it to your app's UI thread. 在构造方法中初始化Handler实例,并将其附加到UI线程中

一个Handler允许你的APP安全地调用UI对象(例如 View对象)的方法。大多数UI对象只能从UI线程安全的代码中被修改。这个方法将会在与UI线程进行通信(Communicate with the UI Thread)这一课中进行详细的描述。

    private PhotoManager() {
...
// Defines a Handler object that's attached to the UI thread
mHandler = new Handler(Looper.getMainLooper()) {
/*
* handleMessage() defines the operations to perform when
* the Handler receives a new Message to process.
*/
@Override
public void handleMessage(Message inputMessage) {
...
}
...
}
}

设置线程池的具体属性值

一旦有了整体的类结构,你可以开始定义线程池了。为了初始化一个ThreadPoolExecutor对象,你需要提供以下数值:

  Initial pool size and maximum pool size 线程池的初始化大小和最大的大小

这个是指最初分配给线程池的线程数量,以及线程池中允许的最大线程数量。在线程池中拥有的线程数量主要取决于你的设备的CPU内核数。这个数值可以从系统服务中获取。

public class PhotoManager {
...
/*
* Gets the number of available cores
* (not always the same as the maximum number of cores)
*/
private static int NUMBER_OF_CORES =
Runtime.getRuntime().availableProcessors();
}

这个数字可能并不反映设备的物理核心数量,因为一些设备根据系统负载关闭了一个或多个CPU内核,对于这样的设备,availableProcessors()方法返回的是处于活动状态的内核数量,可能少于设备的实际内核总数。

  Keep alive time and time unit 线程保活时间和单位

这个是指线程被关闭前保持空闲状态的持续时间。这个持续时间通过时间单位值进行解译,是TimeUnit()中定义的常量之一。

  A queue of tasks 任务队列

这个传入的队列由ThreadPoolExecutor获取的Runnable对象组成。为了执行一个线程中的代码,一个线程池管理者从先进先出的队列中取出一个Runnable对象且把它附加到一个线程。当你创建线程池时需要提供一个队列对象,这个队列对象类必须实现BlockingQueue接口。为了满足你的APP的需求,你可以选择一个Android SDK中已经存在的队列实现类。为了学习更多相关的知识,可以参考ThreadPoolExecutor类的概述。

public class PhotoManager {
...
private PhotoManager() {
...
// A queue of Runnables
private final BlockingQueue<Runnable> mDecodeWorkQueue;
...
// Instantiates the queue of Runnables as a LinkedBlockingQueue
mDecodeWorkQueue = new LinkedBlockingQueue<Runnable>();
...
}
...
}

所有准备工作就绪,创建一个线程池实例

为了创建一个线程池,可以通过调用ThreadPoolExecutor()构造方法初始化一个线程池管理者对象,这样就能创建和管理一组可约束的线程了。如果线程池的初始化大小和最大大小相同,ThreadPoolExecutor在实例化的时候就会创建所有的线程对象。

    private PhotoManager() {
...
// Sets the amount of time an idle thread waits before terminating
private static final int KEEP_ALIVE_TIME = 1;
// Sets the Time Unit to seconds
private static final TimeUnit KEEP_ALIVE_TIME_UNIT = TimeUnit.SECONDS;
// Creates a thread pool manager
mDecodeThreadPool = new ThreadPoolExecutor(
NUMBER_OF_CORES, // Initial pool size
NUMBER_OF_CORES, // Max pool size
KEEP_ALIVE_TIME,
KEEP_ALIVE_TIME_UNIT,
mDecodeWorkQueue);
}

主题三:如何在线程池中的一个线程中执行特定代码

为了达到这个目的,你需要把任务添加到线程池的工作队列中去,当一个线程变成可运行状态时,ThreadPoolExecutor从工作队列中取出一个任务,然后在该线程中执行。

此外,我们还能够去停止一个正在执行的任务,这个任务可能在刚开始执行时是你想要的,但后来发现它所做的工作并不是你所需要的;你可以取消线程正在执行的任务,而不是浪费处理器的运行时间。例如你正在从网络上下载图片且对下载的图片进行了缓存,当检测到正在下载的图片在缓存中已经存在时,你可能希望停止这个下载任务。当然,这取决于你编写APP的方式,因为可能压在你启动下载任务之前无法获知是否需要启动这个任务。

如何启动线程池中的线程以便执行任务?

为了在一个特定的线程池的线程里开启一个任务,可以通过调用ThreadPoolExecutor.execute(),它需要提供一个Runnable类型的参数,这个调用会把该任务添加到这个线程池中的工作队列。当一个空闲的线程进入可执行状态时,线程管理者从工作队列中取出等待时间最长的那个任务,并且在线程中执行它。

public class PhotoManager {
public void handleState(PhotoTask photoTask, int state) {
switch (state) {
// The task finished downloading the image
case DOWNLOAD_COMPLETE:
// Decodes the image
mDecodeThreadPool.execute(
photoTask.getPhotoDecodeRunnable());
...
}
...
}
...
}

当ThreadPoolExecutor在一个线程中开启一个Runnable后,它会自动调用Runnable的run()。

如何中断正在被执行的代码?

为了停止执行一个任务,你必须中断执行这个任务的线程。在准备做这件事之前,当你创建一个任务时,你需要存储处理该任务的线程。

class PhotoDecodeRunnable implements Runnable {
// Defines the code to run for this task
public void run() {
/*
* Stores the current Thread in the
* object that contains PhotoDecodeRunnable
*/
mPhotoTask.setImageDecodeThread(Thread.currentThread());
...
}
...
}

Thread.currentThread()获取到当前执行该Runnable的线程。

想要中断一个线程,可以调用Thread.interrupt()。需要注意的是这些线程对象都被系统控制,系统可以在App进程之外修改该对象。因此,在中断线程之前,需要把这段代码放在一个同步代码块中对这个线程的访问加锁来解决。

public class PhotoManager {
public static void cancelAll() {
/*
* Creates an array of Runnables that's the same size as the
* thread pool work queue
*/
Runnable[] runnableArray = new Runnable[mDecodeWorkQueue.size()];
// Populates the array with the Runnables in the queue
mDecodeWorkQueue.toArray(runnableArray);
// Stores the array length in order to iterate over the array
int len = runnableArray.length;
/*
* Iterates over the array of Runnables and interrupts each one's Thread.
*/
synchronized (sInstance) {
// Iterates over the array of tasks
for (int runnableIndex = 0; runnableIndex < len; runnableIndex++) {
// Gets the current thread
Thread thread = runnableArray[taskArrayIndex].mThread;
// if the Thread exists, post an interrupt to it
if (null != thread) {
thread.interrupt();
}
}
}
}
...
}

在大多数情况下,通过调用Thread.interrupt()能立即中断这个线程,然而他只能停止那些处于等待状态的线程,却不能中断那些占据CPU或者耗时的连接网络的任务。为了避免拖慢系统速度或造成系统死锁,在尝试执行耗时操作之前,你应该测试当前是否存在处于挂起状态的中断请求:

/*
* Before continuing, checks to see that the Thread hasn't
* been interrupted
*/
if (Thread.interrupted()) {
return;
}
...
// Decodes a byte array into a Bitmap (CPU-intensive)
BitmapFactory.decodeByteArray(
imageBuffer, 0, imageBuffer.length, bitmapOptions);
...

主题四:如何与UI线程通信

从执行的任务中发送数据给运行在UI线程中的对象,或者显示后台运行进度...这个功能允许你的任务可以做后台工作,然后把得到的结果数据转移给UI元素使用,例如位图数据。

Every app has its own special thread that runs UI objects such as View objects; this thread is called the UI thread. Only objects running on the UI thread have access to other objects on that thread. 任何一个App都有自己特定的一个线程用来运行UI对象,这个线程被称为是:“UI线程”。只有运行在UI线程中的对象才能够访问在该线程中的其他对象。

因为你的任务执行的线程来自一个线程池而不是执行在UI线程,所以他们不能访问UI对象。为了把数据从一个后台线程转移到UI线程,需要使用一个运行在UI线程里的Handler。

在UI线程中定义一个Handler

Handler属于Android系统的线程管理框架的一部分。一个Handler对象用于接收消息和执行处理消息的代码。一般情况下,如果你为一个新线程创建了一个Handler,你还需要创建一个Handler,让它与一个已经存在的线程关联,用于这两个线程之间的通信。如果你把一个Handler关联到UI线程,处理消息的代码就会在UI线程中执行。

你可以在一个用于创建你的线程池的类的构造方法中实例化一个Handler对象,并把它定义为全局变量,然后通过使用Handler (Looper) 这一构造方法实例化它,用于关联到UI线程。Handler(Looper)这一构造方法需要传入了一个Looper对象,它是Android系统的线程管理框架中的另一部分。当你在一个特定的Looper实例的基础上去实例化一个Handler时,这个Handler与Looper运行在同一个线程里。

private PhotoManager() {
...
// Defines a Handler object that's attached to the UI thread
mHandler = new Handler(Looper.getMainLooper()) {
...

如上所述,Looper.getMainLooper()获取到的是主线程的Looper实例,因此该Hanlder则是UI线程的Handler实例。

在这个Handler里需要重写handleMessage()方法;当这个Handler接收到由该Handler管理的线程发送过来的新消息时,Android系统会自动调用这个方法,而所有线程对应的Handler都会收到相同信息。

        /*
* handleMessage() defines the operations to perform when
* the Handler receives a new Message to process.
*/
@Override
public void handleMessage(Message inputMessage) {
// Gets the image task from the incoming Message object.
PhotoTask photoTask = (PhotoTask) inputMessage.obj;
...
}
...
}
}

如何把数据从任务中转移到UI线程

为了从一个运行在后台线程的任务对象中转移数据到UI线程中的一个对象,首先需要存储任务对象中的数据和UI对象的引用;接下来传递任务对象和状态码给实例化Handler的那个对象。在这个对象里,发送一个包含任务对象和状态的Message给Handler也运行在UI线程中,所以它可以把数据转移到UI线程。

比如这里有一个Runnable,它运行在一个编码了一个Bitmap且存储这个Bitmap到父类PhotoTask对象里的后台线程。这个Runnable同样也存储了状态码DECODE_STATE_COMPLETED。

// A class that decodes photo files into Bitmaps
class PhotoDecodeRunnable implements Runnable {
...
PhotoDecodeRunnable(PhotoTask downloadTask) {
mPhotoTask = downloadTask;
}
...
// Gets the downloaded byte array
byte[] imageBuffer = mPhotoTask.getByteBuffer();
...
// Runs the code for this task
public void run() {
...
// Tries to decode the image buffer
returnBitmap = BitmapFactory.decodeByteArray(
imageBuffer,
0,
imageBuffer.length,
bitmapOptions
);
...
// Sets the ImageView Bitmap
mPhotoTask.setImage(returnBitmap);
// Reports a status of "completed"
mPhotoTask.handleDecodeState(DECODE_STATE_COMPLETED);
...
}
...
}
...

PhotoTask类还包含一个用于给ImageView显示Bitmap的handler。虽然Bitmap和ImageViewImageView的引用在同一个对象中,但你不能把这个Bitmap分配给ImageView去显示,因为它们并没有运行在UI线程中。

发送状态到更高层次的对象

PhotoTask是下一个层次更高的对象,它包含将要展示数据的编码数据和View对象的引用。它会收到一个来自PhotoDecodeRunnable的状态码,并把这个状态码单独传递到一个包含线程池和Handler实例的对象:

public class PhotoTask {
...
// Gets a handle to the object that creates the thread pools
sPhotoManager = PhotoManager.getInstance();
...
public void handleDecodeState(int state) {
int outState;
// Converts the decode state to the overall state.
switch(state) {
case PhotoDecodeRunnable.DECODE_STATE_COMPLETED:
outState = PhotoManager.TASK_COMPLETE;
break;
...
}
...
// Calls the generalized state method
handleState(outState);
}
...
// Passes the state to PhotoManager
void handleState(int state) {
/*
* Passes a handle to this task and the
* current state to the class that created
* the thread pools
*/
sPhotoManager.handleState(this, state);
}
...
}

转移数据到UI线程

从PhotoTask对象那里,PhotoManager对象收到了一个状态码和一个PhotoTask对象的handler。因为状态码是TASK_COMPLETE,所以创建一个Message应该包含状态和任务对象,然后把它发送给Handler。

public class PhotoManager {
...
// Handle status messages from tasks
public void handleState(PhotoTask photoTask, int state) {
switch (state) {
...
// The task finished downloading and decoding the image
case TASK_COMPLETE:
/*
* Creates a message for the Handler
* with the state and the task object
*/
Message completeMessage =
mHandler.obtainMessage(state, photoTask);
completeMessage.sendToTarget();
break;
...
}
...
}

最终,Handler.handleMessage()会检查每个传入进来的Message,如果状态码是TASK_COMPLETE,这时任务就完成了,而传入的Message里的PhotoTask对象里同时包含一个Bitmap和一个ImageView。因为Handler.handleMessage()运行在UI线程里,所以它能安全地转移Bitmap数据给ImageView。

public class PhotoManager {
...
// Handle status messages from tasks
public void handleState(PhotoTask photoTask, int state) {
switch (state) {
...
// The task finished downloading and decoding the image
case TASK_COMPLETE:
/*
* Creates a message for the Handler
* with the state and the task object
*/
Message completeMessage =
mHandler.obtainMessage(state, photoTask);
completeMessage.sendToTarget();
break;
...
}
...
}

Android多线程的使用的更多相关文章

  1. android 多线程

    本章讲述在android开发中,多线程的应用.多线程能够处理耗时的操作并优化程序的性能.本章主要介绍知识点,AsyncTask,Java线程池,ThreadPoolExecutor线程池类.本章案例只 ...

  2. Android多线程分析之五:使用AsyncTask异步下载图像

    Android多线程分析之五:使用AsyncTask异步下载图像 罗朝辉 (http://www.cnblogs.com/kesalin) CC 许可,转载请注明出处 在本系列文章的第一篇<An ...

  3. Android多线程分析之四:MessageQueue的实现

    Android多线程分析之四:MessageQueue的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处 在前面两篇文章<Androi ...

  4. Android多线程分析之三:Handler,Looper的实现

    Android多线程分析之三:Handler,Looper的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处 在前文<Android多 ...

  5. Android多线程分析之二:Thread的实现

    Android多线程分析之二:Thread的实现 罗朝辉 (http://www.cnblogs.com/kesalin/) CC 许可,转载请注明出处   在前文<Android多线程分析之一 ...

  6. Android多线程分析之一:使用Thread异步下载图像

    Android多线程分析之一:使用Thread异步下载图像 罗朝辉 (http://www.cnblogs.com/kesalin) CC 许可,转载请注明出处   打算整理一下对 Android F ...

  7. 无废话Android之smartimageview使用、android多线程下载、显式意图激活另外一个activity,检查网络是否可用定位到网络的位置、隐式意图激活另外一个activity、隐式意图的配置,自定义隐式意图、在不同activity之间数据传递(5)

    1.smartimageview使用 <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android&q ...

  8. android: 多线程编程基础

    9.1   服务是什么 服务(Service)是 Android 中实现程序后台运行的解决方案,它非常适合用于去执行那 些不需要和用户交互而且还要求长期运行的任务.服务的运行不依赖于任何用户界面,即使 ...

  9. android程序---->android多线程下载(一)

    多线程下载是加快下载速度的一种方式,通过开启多个线程去执行一个任务,可以使任务的执行速度变快.多线程的任务下载时常都会使用得到断点续传下载,就是我们在一次下载未结束时退出下载,第二次下载时会接着第一次 ...

  10. Android多线程----异步消息处理机制之Handler详解

    ​[声明] 欢迎转载,但请保留文章原始出处→_→ 生命壹号:http://www.cnblogs.com/smyhvae/ 文章来源:http://www.cnblogs.com/smyhvae/p/ ...

随机推荐

  1. Django中各目录文件的作用

    一般的项目结构如下(大同小异) my_site是一个项目,blog是项目下的应用之一,可以使用创建命令创建更多的应用. 最上层的django文件夹: 自己手动创建,名字随意. 第二层my_site文件 ...

  2. 数据结构c++实现代码-链表

    /*节点Node的相关声明与定义*/ //Node.h #include<iostream> using namespace std; template<typename T> ...

  3. 滴水穿石-08IO

    1.0 File a:构造方法 package d8; import java.io.File; public class FileGouZao { public static void main(S ...

  4. Python关键字及其用法

    Python有哪些关键字 -Python常用的关键字  and, del, from, not, while, as, elif, global, or, with, assert, else, if ...

  5. BZoj 2301 Problem b(容斥定理+莫比乌斯反演)

    2301: [HAOI2011]Problem b Time Limit: 50 Sec  Memory Limit: 256 MB Submit: 7732  Solved: 3750 [Submi ...

  6. 阻止Java反编译蛋疼方法

    public class landv { private static String companyName="landv测试"; public static void main( ...

  7. 【C++ Primer | 15】继承的构造函数

    继承的构造函数 子类为完成基类初始化,在C++11之前,需要在初始化列表调用基类的构造函数,从而完成构造函数的传递.如果基类拥有多个构造函数,那么子类也需要实现多个与基类构造函数对应的构造函数. cl ...

  8. 经典平衡二叉树(AVL树)

    二叉查找树(BSTree)中进行查找.插入和删除操作的时间复杂度都是O(h),其中h为树的高度.BST的高度直接影响到操作实现的性能,最坏情况下,二叉查找树会退化成一个单链表,比如插入的节点序列本身就 ...

  9. ASP.NET Core 2.0 新功能汇总

    前言 ASP.NET Core 的变化和发展速度是飞快的,当你发现你还没有掌握 ASP.NET Core 1.0 的时候, 2.0 已经快要发布了,目前 2.0 处于 Preview 1 版本,意味着 ...

  10. markdown 语法小结

    1 标题 # 一级标题 ## 二级标题 2  字体加粗和斜体 *斜体* **加粗** 3.引用 > 4.换行 空行 或两个空格+tab 5.无序列表 + 第一个 + 第二个 - 第一个 - 第二 ...