谈谈关于线程和并发机制

前言

在Java中,线程是一个很关键的名词,也是很高频使用的一种资源。那么它的概念是什么呢,是如何定义的,用法又有哪些呢?为何说Android里只有一个主线程呢,什么是工作线程呢。线程又存在并发,并发机制的原理是什么。这些内容有些了解,有些又不是很清楚,所以有必要通过一篇文章的梳理,弄清其中的来龙去脉,为了之后的开发过程中提供更好的支持。

目录

  • 线程定义
  • Java线程生命周期
  • 线程用法
  • Android中的线程
  • 工作线程
  • 使用AsyncTask
  • 什么是并发
  • 并发机制原理
  • 并发具体怎么用

线程定义

说到线程,就离不开谈到进程了,比如在Android中,一个应用程序基本有一个进程,但是一个进程可以有多个线程组成。在应用程序中,线程和进程是两个基本执行单元,都是可以处理比较复杂的操作,比如网络请求、I/O读写等等,在Java中我们大部分操作的是线程(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.

意思就是:线程相比进程所创建的资源要少很多,都是在执行环境下的执行单元。同时,每个线程有个优先级,高的优先级比低的优先级优先执行。线程是操作系统能够进行运算调度的最小单位,它被包含在进程之中,是进程中的实际运作单位。

Java线程生命周期

  1. 新建状态(New):当线程对象创建后,即进入了新建状态。仅仅由java虚拟机分配内存,并初始化。如:Thread t = new MyThread();
  2. 就绪状态(Runnable):当调用线程对象的start()方法(t.start();),线程即进入就绪状态。处于就绪状态的线程,java虚拟机创建方法调用栈和程序计数器,只是说明此线程已经做好了准备,随时等待CPU调度执行,此线程并 没有执行。
  3. 运行状态(Running):当CPU开始调度处于就绪状态的线程时,执行run()方法,此时线程才得以真正执行,即进入到运行状态。注:绪状态是进入到运行状态的唯一入口,也就是说,线程要想进入运行状态执行,首先必须处于就绪状态中;
  4. 阻塞状态(Blocked):处于运行状态中的线程由于某种原因,暂时放弃对CPU的使用权,停止执行,此时进入阻塞状态,直到其进入到就绪状态,才 有机会再次被CPU调用以进入到运行状态。根据阻塞产生的原因不同,阻塞状态又可以分为三种:等待阻塞 – 运行状态中的线程执行wait()方法,使本线程进入到等待阻塞状态,JVM会把该线程放入等待池中;同步阻塞 – 线程在获取synchronized同步锁失败(因为锁被其它线程所占用),它会进入同步阻塞状态;其他阻塞 – 通过调用线程的sleep()或join()或发出了I/O请求时,线程会进入到阻塞状态。当sleep()状态超时、join()等待线程终止或者超时、或者I/O处理完毕时,线程重新转入就绪状态。
  5. 死亡状态(Dead):线程run()方法执行完了或者因异常退出了run()方法,该线程结束生命周期。 当主线程结束时,其他线程不受任何影响。

线程用法

那该如何创建线程呢,有两种方式。

  • 使用Runnable
  • 继承Thread类,定义子类

使用Runnable:

Runnable接口有个run方法,我们可以定义一个类实现Runnable接口,Thread类有个构造函数,参数是Runnable,我们定义好的类可以当参数传递进去。

1
2
3
4
5
6
7
8
9
10
11
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方法中执行相关代码。

1
2
3
4
5
6
7
8
9
10
11
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中的线程

当某个应用组件启动且该应用没有运行其他任何组件时,Android 系统会使用单个执行线程为应用启动新的 Linux 进程。默认情况下,同一应用的所有组件在相同的进程和线程(称为“主”线程)中运行。

应用启动时,系统会为应用创建一个名为“主线程”的执行线程。 此线程非常重要,因为它负责将事件分派给相应的用户界面小工具,其中包括绘图事件。 此外,它也是应用与 Android UI 工具包组件(来自 android.widget 和 android.view 软件包的组件)进行交互的线程。因此,主线程有时也称为 UI 线程。

系统绝对不会为每个组件实例创建单独的线程。运行于同一进程的所有组件均在 UI 线程中实例化,并且对每个组件的系统调用均由该线程进行分派。因此,响应系统回调的方法,例如,报告用户操作的 onKeyDown() 或生命周期回调方法)始终在进程的 UI 线程中运行。例如,当用户触摸屏幕上的按钮时,应用的 UI 线程会将触摸事件分派给小工具,而小工具反过来又设置其按下状态,并将无效请求发布到事件队列中。UI 线程从队列中取消该请求并通知小工具应该重绘自身。

在应用执行繁重的任务以响应用户交互时,除非正确实施应用,否则这种单线程模式可能会导致性能低下。 特别地,如果 UI 线程需要处理所有任务,则执行耗时很长的操作(例如,网络访问或数据库查询)将会阻塞整个 UI。一旦线程被阻塞,将无法分派任何事件,包括绘图事件。从用户的角度来看,应用显示为挂起。 更糟糕的是,如果 UI 线程被阻塞超过几秒钟时间(目前大约是 5 秒钟),用户就会看到一个让人厌烦的“应用无响应”(ANR) 对话框。

此外,Android UI 工具包并非线程安全工具包。因此,您不得通过工作线程操纵 UI,而只能通过 UI 线程操纵用户界面。因此,Android 的单线程模式必须遵守两条规则:

  1. 不要阻塞 UI 线程
  2. 不要在 UI 线程之外访问 Android UI 工具包

那为何Andorid是主线程模式呢,就不能多线程吗?在Java中默认情况下一个进程只有一个线程,这个线程就是主线。主线程主要处理界面交互相关的逻辑,因为用户随时会和界面发生交互,因此主线程在任何时候都必须有比较高的响应速度,否则就会产生一种界面卡顿的感觉。同样Android也是沿用了Java的线程模型,Android是基于事件驱动机制运行,如果没有一个主线程进行调度分配,那么线程间的事件传递就会显得杂乱无章,使用起来也冗余,还有线程的安全性因素也是一个值得考虑的一个点。

工作线程

既然了解主线程模式,除了UI线程,其他都是叫工作线程。根据单线程模式,要保证应用 UI 的响应能力,关键是不能阻塞 UI 线程。如果执行的操作不能很快完成,则应确保它们在单独的线程(“后台”或“工作”线程)中运行。例如以下代码表示一个点击监听从单独的线程下载图像并将其显示在 ImageView 中:

1
2
3
4
5
6
7
8
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) 方法修复上述代码:

1
2
3
4
5
6
7
8
9
10
11
12
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

但是,随着操作日趋复杂,这类代码也会变得复杂且难以维护。 要通过工作线程处理更复杂的交互,可以考虑在工作线程中使用 Handler 处理来自 UI 线程的消息。当然,最好的解决方案或许是扩展 AsyncTask 类,此类简化了与 UI 进行交互所需执行的工作线程任务。

使用 AsyncTask

AsyncTask 允许对用户界面执行异步操作。它会先阻塞工作线程中的操作,然后在 UI 线程中发布结果,而无需你亲自处理线程和/或处理程序。

要使用它,必须创建 AsyncTask 子类并实现 doInBackground() 回调方法,该方法将在后台线程池中运行。要更新 UI,必须实现 onPostExecute() 以传递doInBackground() 返回的结果并在 UI 线程中运行,这样,即可安全更新 UI。稍后,您可以通过从 UI 线程调用 execute() 来运行任务。

例如,可以通过以下方式使用 AsyncTask 来实现上述示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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() 和 onProgressUpdate() 均在 UI 线程中调用
  • doInBackground() 返回的值将发送到 onPostExecute()
  • 您可以随时在 doInBackground() 中调用publishProgress(),以在 UI 线程中执行 onProgressUpdate()
  • 您可以随时取消任何线程中的任务

什么是并发

说到并发,首先需要区别并发和并行这两个名词的区别。

并发性和并行性

  • 并发是指在同一时间点只能有一条指令执行,但多个进程指令被快速轮换执行,使得在宏观上具有多个进程同时执行的效果。

  • 并行指在同一时间点,有多条指令在多个处理器上同时执行。

那么我们为什么需要并发呢?通常是为了提高程序的运行速度或者改善程序的设计。

并发机制原理

Java对并发编程提供了语言级别的支持。Java通过线程来实现并发编程。一个线程通常完成某个特定的任务,一个进程可以拥有多个线程,当这些线程一起执行的时候,就实现了并发。与操作系统中的进程相似,每个线程看起来好像拥有自己的CPU,但是其底层是通过切分CPU时间来实现的。与进程不同的是,线程并不是相互独立的,它们通常要相互合作来完成一些任务。

并发具体怎么用

休眠

我们可以让一个线程暂时休息一会儿。Thread类有一个sleep静态方法,你可以将一个long类型的数据当做参数传进去,单位是毫秒,表示线程将会休眠的时间。

让步

Thread类还有一个名为yield()的静态方法。这个方法的作用是为了建议当前正在运行的线程做个让步,让出CPU时间给别的线程来运行。程序中可能会有一个线程在某个时刻已经完成了一大部分的任务,并且这个时候让别的线程来运行比较合理。这样的情况下,就可以调用yield()方法进行让步。不过,调用这个方法并不能保证一定会起作用,毕竟它只是建议性的。所以,不应该用这个方法来控制程序的执行流程。

串入(join)

当一个线程t1在另一个线程t2上调用**t1.join()**方法的时候,线程t2将等待线程t1运行结束之后再开始运行。正如下面这个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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);
}
}
}

输出结果为:

1
2
3
4
5
6
7
8
9
10
10
11
12
13
14
0
1
2
3
4

优先级

我们可以给一个线程设定一个优先级。线程调度器在做调度工作的时候,优先级越高的线程越可能得到先运行的机会。Thread类的setPriority方法和getPriority方法分别用来设置线程的优先级和获取线程的优先级。由于线程调度器根据优先级的大小来调度线程的效果在各种不同的JVM上差别很大,所以在绝大多数情况下,我们不应该依靠设定优先级来完成我们的工作,保持默认的优先级是一条很好的建议。

,