在 Java 中,线程是一个很关键的名词,也是很高频使用的一种资源。那么它的概念是什么呢,是如何定义的,用法又有哪些呢?为何说 Android 里只有一个主线程呢,什么是工作线程呢。线程又存在并发,并发机制的原理是什么。这些内容有些了解,有些又不是很清楚,所以有必要通过一篇文章的梳理,弄清其中的来龙去脉,为了之后的开发过程中提供更好的支持。
说到线程,就离不开谈到进程了,比如在 Android 中,一个应用程序基本有一个进程,但是一个进程可以有多个线程组成。在应用程序中,线程和进程是两个基本执行单元,都是可以处理比较复杂的操作,比如网络请求、
读写等等,在 Java 中我们大部分操作的是线程 (
- I/O
),当然进程也是很重要的。
- Thread
进程通常有独立执行环境,有完整的可设置为私有基本运行资源,比如,每个进程会有自己的内存空间。而线程呢,去官网的查了下,原话如下:
- Threads are sometimes called "lightweight processes". Both processes and threads provide an execution environment, but creating a new thread requires fewer resources than creating a new process.
意思就是:线程相比进程所创建的资源要少很多,都是在执行环境下的执行单元。同时,每个线程有个优先级,高的优先级比低的优先级优先执行。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。
那该如何创建线程呢,有两种方式。
使用 Runnable:
接口有个 run 方法,我们可以定义一个类实现
- Runnable
接口,
- Runnable
类有个构造函数,参数是
- Thread
,我们定义好的类可以当参数传递进去。
- Runnable
- public class HelloRunnable implements Runnable {
- public void run() {
- System.out.println("Hello from a thread!");
- }
- public static void main(String args[]) {
- (new Thread(new HelloRunnable())).start();
- }
- }
继承 Thread 类:
类它自身就包含了
- Thread
接口,我们可以定义一个子类来继承
- Runnable
类,进而在
- Thread
方法中执行相关代码。
- Run
- public class HelloThread extends Thread {
- public void run() {
- System.out.println("Hello from a thread!");
- }
- public static void main(String args[]) {
- (new HelloThread()).start();
- }
- }
从两个使用方式上看,定义好
后,都需要执行
- Thread
方法,线程才算开始执行。
- start()
当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为 "主" 线程)中运行。
应用启动时,系统会为应用创建一个名为 "主线程" 的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小工具,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自
和
- android.widget
软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。
- android.view
系统绝对不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法,例如,报告用户操作的
或生命周期回调方法)始终在进程的 UI 线程中运行。例如,当用户触摸屏幕上的按钮时,应用的 UI 线程会将触摸事件分派给小工具,而小工具反过来又设置其按下状态,并将无效请求发布到事件队列中。UI 线程从队列中取消该请求并通知小工具应该重绘自身。
- onKeyDown()
在应用执行繁重的任务以响应用户交互时,除非正确实施应用,否则这种单线程模式可能会导致性能低下。 特别地,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。一旦线程被阻塞,将无法分派任何事件,包括绘图事件。从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的 ""(ANR) 对话框。
此外,Android UI 工具包并非线程安全工具包。因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。因此,Android 的单线程模式必须遵守两条规则:
那为何 Andorid 是主线程模式呢,就不能多线程吗?在 Java 中默认情况下一个进程只有一个线程,这个线程就是主线。主线程主要处理界面交互相关的逻辑,因为用户随时会和界面发生交互,因此主线程在任何时候都必须有比较高的响应速度,否则就会产生一种界面卡顿的感觉。同样 Android 也是沿用了 Java 的线程模型,Android 是基于事件驱动机制运行,如果没有一个主线程进行调度分配,那么线程间的事件传递就会显得杂乱无章,使用起来也冗余,还有线程的安全性因素也是一个值得考虑的一个点。
既然了解主线程模式,除了 UI 线程,其他都是叫工作线程。根据单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。如果执行的操作不能很快完成,则应确保它们在单独的线程("后台" 或 "工作" 线程)中运行。例如以下代码表示一个点击监听从单独的线程下载图像并将其显示在
中:
- ImageView
- public void onClick(View v) {
- new Thread(new Runnable() {
- public void run() {
- Bitmap b = loadImageFromNetwork("http://example.com/image.png");
- mImageView.setImageBitmap(b);
- }
- }).start();
- }
咋看起来貌似没什么问题,它创建了一个线程来处理网络操作, 但是呢,它却是在 UI 线程中执行,但是,它违反了单线程模式的第二条规则:不要在 UI 线程之外访问 Android UI 工具包。
那么你会问个问题了,为什么子线程中不能更新 UI。因为 UI 访问是没有加锁的,在多个线程中访问 UI 是不安全的,如果有多个子线程都去更新 UI,会导致界面不断改变而混乱不堪。所以最好的解决办法就是只有一个线程有更新 UI 的权限。
当然,Android 提供了几种途径来从其他线程访问 UI 线程。以下列出了几种有用的方法:
- Activity.runOnUiThread(Runnable)
- View.post(Runnable)
- View.postDelayed(Runnable, long)
例如,您可以通过使用
方法修复上述代码:
- View.post(Runnable)
- public void onClick(View v) {
- new Thread(new Runnable() {
- public void run() {
- final Bitmap bitmap = loadImageFromNetwork("http://example.com/image.png");
- mImageView.post(new Runnable() {
- public void run() {
- mImageView.setImageBitmap(bitmap);
- }
- });
- }
- }).start();
- }
现在,上述实现属于线程安全型:在单独的线程中完成网络操作,而在 UI 线程中操纵
。
- ImageView
但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。 要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用
处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展
- Handler
类,此类简化了与 UI 进行交互所需执行的工作线程任务。
- AsyncTask
允许对用户界面执行异步操作。它会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而无需你亲自处理线程和 / 或处理程序。
- AsyncTask
要使用它,必须创建
子类并实现
- AsyncTask
回调方法,该方法将在后台线程池中运行。要更新 UI,必须实现
- doInBackground()
以传递
- onPostExecute()
返回的结果并在 UI 线程中运行,这样,即可安全更新 UI。稍后,您可以通过从 UI 线程调用
- doInBackground()
来运行任务。
- execute()
例如,可以通过以下方式使用
来实现上述示例:
- AsyncTask
- public void onClick(View v) {
- new DownloadImageTask().execute("http://example.com/image.png");
- }
- private class DownloadImageTask extends AsyncTask<String, Void, Bitmap> {
- /** The system calls this to perform work in a worker thread and
- * delivers it the parameters given to AsyncTask.execute() */
- protected Bitmap doInBackground(String... urls) {
- return loadImageFromNetwork(urls[0]);
- }
- /** The system calls this to perform work in the UI thread and delivers
- * the result from doInBackground() */
- protected void onPostExecute(Bitmap result) {
- mImageView.setImageBitmap(result);
- }
- }
现在 UI 是安全的,代码也得到简化,因为任务分解成了两部分:一部分应在工作线程内完成,另一部分应在 UI 线程内完成。
下面简要概述了 AsyncTask 的工作方法,但要全面了解如何使用此类,您应阅读
参考文档:
- AsyncTask
会在工作线程上自动执行
- doInBackground()
、
- onPreExecute()
和
- onPostExecute()
均在 UI 线程中调用
- onProgressUpdate()
返回的值将发送到
- doInBackground()
- onPostExecute()
中调用
- doInBackground()
,以在 UI 线程中执行
- publishProgress()
- onProgressUpdate()
说到并发,首先需要区别并发和并行这两个名词的区别。
并发性和并行性
并发是指在同一时间点只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。
并行指在同一时间点,有多条指令在多个处理器上同时执行。
那么我们为什么需要并发呢?通常是为了提高程序的运行速度或者改善程序的设计。
Java 对并发编程提供了语言级别的支持。Java 通过线程来实现并发编程。一个线程通常完成某个特定的任务,一个进程可以拥有多个线程,当这些线程一起执行的时候,就实现了并发。与操作系统中的进程相似,每个线程看起来好像拥有自己的 CPU,但是其底层是通过切分 CPU 时间来实现的。与进程不同的是,线程并不是相互独立的,它们通常要相互合作来完成一些任务。
休眠
我们可以让一个线程暂时休息一会儿。Thread 类有一个 sleep 静态方法,你可以将一个 long 类型的数据当做参数传进去,单位是毫秒,表示线程将会休眠的时间。
让步
Thread 类还有一个名为 yield() 的静态方法。这个方法的作用是为了建议当前正在运行的线程做个让步,让出 CPU 时间给别的线程来运行。程序中可能会有一个线程在某个时刻已经完成了一大部分的任务,并且这个时候让别的线程来运行比较合理。这样的情况下,就可以调用 yield() 方法进行让步。不过,调用这个方法并不能保证一定会起作用,毕竟它只是建议性的。所以,不应该用这个方法来控制程序的执行流程。
串入(join)
当一个线程 t1 在另一个线程 t2 上调用 t1.join() 方法的时候,线程 t2 将等待线程 t1 运行结束之后再开始运行。正如下面这个例子:
- public class ThreadTest {
- public static void main(String[] args) {
- SimpleThread simpleThread = new SimpleThread();
- Thread t = new Thread(simpleThread);
- t.start();
- }
- }
- public class SimpleThread implements Runnable{
- @Override
- public void run() {
- Thread tempThread = new Thread() {
- @Override
- public void run() {
- for(int i = 10; i < 15 ;i++) {
- System.out.println(i);
- }
- }
- };
- tempThread.start();
- try {
- tempThread.join(); //tempThread串入
- } catch (InterruptedException e) {
- e.printStackTrace();
- }
- for(int i = 0; i < 5; i++) {
- System.out.println(i);
- }
- }
- }
输出结果为:
- 10
- 11
- 12
- 13
- 14
- 0
- 1
- 2
- 3
- 4
我们可以给一个线程设定一个优先级。线程调度器在做调度工作的时候,优先级越高的线程越可能得到先运行的机会。Thread 类的 setPriority 方法和 getPriority 方法分别用来设置线程的优先级和获取线程的优先级。由于线程调度器根据优先级的大小来调度线程的效果在各种不同的 JVM 上差别很大,所以在绝大多数情况下,我们不应该依靠设定优先级来完成我们的工作,保持默认的优先级是一条很好的建议。
来源: http://www.cnblogs.com/cr330326/p/6402154.html