0%

Concepts of Multiprocess and Multithread

作者: luoweifu

什么是线程

什么是线程?线程与进程与有什么关系?这是一个非常抽象的问题,也是一个特别广的话题,涉及到非常多的知识。我不能确保能把它讲的话,也不能确保讲的内容全部都正确。即使这样,我也希望尽可能地把他讲通俗一点,讲的明白一点,因为这是个一直困扰我很久的,扑朔迷离的知识领域,希望通过我的理解揭开它一层一层神秘的面纱。

任务调度

线程是什么?要理解这个概念,须要先了解一下操作系统的一些相关概念。大部分操作系统(如Windows、Linux)的任务调度是采用时间片轮转的抢占式调度方式,也就是说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。任务执行的一小段时间叫做时间片,任务正在执行时的状态叫运行状态,任务执行一段时间后强制暂停去执行下一个任务,被暂停的任务就处于就绪状态等待下一个属于它的时间片的到来。这样每个任务都能得到执行,由于CPU的执行效率非常高,时间片非常短,在各个任务之间快速地切换,给人的感觉就是多个任务在“同时进行”,这也就是我们所说的并发(别觉得并发有多高深,它的实现很复杂,但它的概念很简单,就是一句话:多个任务同时执行)。多任务运行过程的示意图如下:

进程

我们都知道计算机的核心是CPU,它承担了所有的计算任务;而操作系统是计算机的管理者,它负责任务的调度、资源的分配和管理,统领整个计算机硬件;应用程序侧是具有某种功能的程序,程序是运行于操作系统之上的。

进程是一个具有一定独立功能的程序在一个数据集上的一次动态执行的过程,是操作系统进行资源分配和调度的一个独立单位,是应用程序运行的载体 进程是一种抽象的概念,从来没有统一的标准定义。进程一般由程序、数据集合和进程控制块三部分组成。程序用于描述进程要完成的功能,是控制进程执行的指令集;数据集合是程序在执行时所需要的数据和工作区;程序控制块(Program Control Block,简称PCB),包含进程的描述信息和控制信息,是进程存在的唯一标志。

进程具有的特征:
动态性:进程是程序的一次执行过程,是临时的,有生命期的,是动态产生,动态消亡的;
并发性:任何进程都可以同其他进程一起并发执行;
独立性:进程是系统进行资源分配和调度的一个独立单位;
结构性:进程由程序、数据和进程控制块三部分组成。

线程

在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。任务调度采用的是时间片轮转的抢占式调度方式,而进程是任务调度的最小单位,每个进程有各自独立的一块内存,使得各个进程之间内存地址相互隔离。

后来,随着计算机的发展,对CPU的要求越来越高,进程之间的切换开销较大,已经无法满足越来越复杂的程序的要求了。于是就发明了线程,线程是程序执行中一个单一的顺序控制流程,是程序执行流的最小单元,是处理器调度和分派的基本单位。 一个进程可以有一个或多个线程,各个线程之间共享程序的内存空间(也就是所在进程的内存空间)。 一个标准的线程由线程ID、当前指令指针(PC)、寄存器和堆栈组成。而进程由内存空间(代码、数据、进程空间、打开的文件)和一个或多个线程组成。

进程与线程的区别

前面讲了进程与线程,但可能你还觉得迷糊,感觉他们很类似。的确,进程与线程有着千丝万缕的关系,下面就让我们一起来理一理:
1.线程是程序执行的最小单位,而进程是操作系统分配资源的最小单位;
2.一个进程由一个或多个线程组成,线程是一个进程中代码的不同执行路线;
3.进程之间相互独立,但同一进程下的各个线程之间共享程序的内存空间(包括代码段、数据集、堆等)及一些进程级的资源(如打开文件和信号),某进程内的线程在其它进程不可见;
4.调度和切换:线程上下文切换比进程上下文切换要快得多。

线程与进程关系的示意图:

图 2:进程与线程的资源共享关系


图 3:单线程与多线程的关系

总之,线程和进程都是一种抽象的概念,线程是一种比进程更小的抽象,线程和进程都可用于实现并发。

在早期的操作系统中并没有线程的概念,进程是能拥有资源和独立运行的最小单位,也是程序执行的最小单位。它相当于一个进程里只有一个线程,进程本身就是线程。所以线程有时被称为轻量级进程(Lightweight Process,LWP)。

图 4:早期的操作系统只有进程,没有线程

后来,随着计算机的发展,对多个任务之间上下文切换的效率要求越来越高,就抽象出一个更小的概念——线程,一般一个进程会有多个(也可是一个)线程。

图 5:线程的出现,使得一个进程可以有多个线程

多线程与多核

上面提到的时间片轮转的调度方式说一个任务执行一小段时间后强制暂停去执行下一个任务,每个任务轮流执行。很多操作系统的书都说“同一时间点只有一个任务在执行”。那有人可能就要问双核处理器呢?难道两个核不是同时运行吗?

其实“同一时间点只有一个任务在执行”这句话是不准确的,至少它是不全面的。那多核处理器的情况下,线程是怎样执行呢?这就需要了解内核线程。
多核(心)处理器是指在一个处理器上集成多个运算核心从而提高计算能力,也就是有多个真正并行计算的处理核心,每一个处理核心对应一个内核线程。内核线程(Kernel Thread, KLT)就是直接由操作系统内核支持的线程,这种线程由内核来完成线程切换,内核通过操作调度器对线程进行调度,并负责将线程的任务映射到各个处理器上。一般一个处理核心对应一个内核线程,比如单核处理器对应一个内核线程,双核处理器对应两个内核线程,四核处理器对应四个内核线程。

现在的电脑一般是双核四线程、四核八线程,是采用超线程技术将一个物理处理核心模拟成两个逻辑处理核心,对应两个内核线程,所以在操作系统中看到的CPU数量是实际物理CPU数量的两倍,如你的电脑是双核四线程,打开“任务管理器\性能”可以看到4个CPU的监视器,四核八线程可以看到8个CPU的监视器。

图 6:双核四线程在Windows8下查看的结果

超线程技术就是利用特殊的硬件指令,把一个物理芯片模拟成两个逻辑处理核心,让单个处理器都能使用线程级并行计算,进而兼容多线程操作系统和软件,减少了CPU的闲置时间,提高的CPU的运行效率。这种超线程技术(如双核四线程)由处理器硬件的决定,同时也需要操作系统的支持才能在计算机中表现出来。

程序一般不会直接去使用内核线程,而是去使用内核线程的一种高级接口——轻量级进程(Light Weight Process,LWP),轻量级进程就是我们通常意义上所讲的线程(我们在这称它为用户线程),由于每个轻量级进程都由一个内核线程支持,因此只有先支持内核线程,才能有轻量级进程。用户线程与内核线程的对应关系有三种模型:一对一模型、多对一模型、多对多模型,在这以4个内核线程、3个用户线程为例对三种模型进行说明。

一对一模型

对于一对一模型来说,一个用户线程就唯一地对应一个内核线程(反过来不一定成立,一个内核线程不一定有对应的用户线程)。这样,如果CPU没有采用超线程技术(如四核四线程的计算机),一个用户线程就唯一地映射到一个物理CPU的线程,线程之间的并发是真正的并发。一对一模型使用户线程具有与内核线程一样的优点,一个线程因某种原因阻塞时其他线程的执行不受影响;此处,一对一模型也可以让多线程程序在多处理器的系统上有更好的表现。
但一对一模型也有两个缺点:1.许多操作系统限制了内核线程的数量,因此一对一模型会使用户线程的数量受到限制;2.许多操作系统内核线程调度时,上下文切换的开销较大,导致用户线程的执行效率下降。

图 7:一对一模型

多对一模型

多对一模型将多个用户线程映射到一个内核线程上,线程之间的切换由用户态的代码来进行,因此相对一对一模型,多对一模型的线程切换速度要快许多;此外,多对一模型对用户线程的数量几乎无限制。但多对一模型也有两个缺点:1.如果其中一个用户线程阻塞,那么其它所有线程都将无法执行,因为此时内核线程也随之阻塞了;2.在多处理器系统上,处理器数量的增加对多对一模型的线程性能不会有明显的增加,因为所有的用户线程都映射到一个处理器上了。

图 8:多对一模型

多对多模型

多对多模型结合了一对一模型和多对一模型的优点,将多个用户线程映射到多个内核线程上。多对多模型的优点有:1.一个用户线程的阻塞不会导致所有线程的阻塞,因为此时还有别的内核线程被调度来执行;2.多对多模型对用户线程的数量没有限制;3.在多处理器的操作系统中,多对多模型的线程也能得到一定的性能提升,但提升的幅度不如一对一模型的高。
在现在流行的操作系统中,大都采用多对多的模型。

图 9:多对多模型

查看进程与线程

一个应用程序可能是多线程的,也可能是多进程的,如何查看呢?在Windows下我们只须打开任务管理器就能查看一个应用程序的进程和线程数。按“Ctrl+Alt+Del”或右键快捷工具栏打开任务管理器。
查看进程数和线程数:

图 10:查看线程数和进程数

在“进程”选项卡下,我们可以看到一个应用程序包含的线程数。如果一个应用程序有多个进程,我们能看到每一个进程,如在上图中,Google的chrome浏览器就有多个进程。同时,如果打开了一个应用程序的多个实例也会有多个进程,如上图中我打开了两个cmd窗口,就有两个cmd进程。如果看不到线程数这一列,可以在点击“查看\选择列”菜单,增加监听的列。

查看CPU和内存的使用率:

在性能选项卡中,我们可以查看CPU和内存的使用率,根据CPU使用记录的监视器的个数还能看出逻辑处理核心的个数,如我的双核四线程的计算机就有四个监视器。

图 11:查看CPU和内存的使用率

线程的生命周期

当线程的数量小于处理器的数量时,线程的并发是真正的并发,不同的线程运行在不同的处理器上。但当线程的数量大于处理器的数量时,线程的并发会受到一些阻碍,此时并不是真正的并发,因为此时至少有一个处理器会运行多个线程。
在单个处理器运行多个线程时,并发是一种模拟出来的状态。操作系统采用时间片轮转的方式轮流执行每一个线程。现在,几乎所有的现代操作系统采用的都是时间片轮转的抢占式调度方式,如我们熟悉的Unix、Linux、Windows及Mac OS X等流行的操作系统。
我们知道线程是程序执行的最小单位,也是任务执行的最小单位。在早期只有进程的操作系统中,进程有五种状态,创建、就绪、运行、阻塞(等待)、退出。早期的进程相当于现在的只有单个线程的进程,那么现在的多线程也有五种状态,现在的多线程的生命周期与早期进程的生命周期类似。

图 12:早期进程的生命周期

进程在运行过程有三种状态:就绪、运行、阻塞,创建和退出状态描述的是进程的创建过程和退出过程。
创建:进程正在创建,还不能运行。操作系统在创建进程时要进行的工作包括分配和建立进程控制块表项、建立资源表格并分配资源、加载程序并建立地址空间;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于他的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
退出:进程已结束,所以也称结束状态,释放操作系统分配的资源。

图 13:线程的生命周期

创建:一个新的线程被创建,等待该线程被调用执行;
就绪:时间片已用完,此线程被强制暂停,等待下一个属于他的时间片到来;
运行:此线程正在执行,正在占用时间片;
阻塞:也叫等待状态,等待某一事件(如IO或另一个线程)执行完;
退出:一个线程完成任务或者其他终止条件发生,该线程终止进入退出状态,退出状态释放该线程所分配的资源。

编程思想之多线程与多进程(2)——线程优先级与线程安全

线程优先级

现在主流操作系统(如Windows、Linux、Mac OS X)的任务调度除了具有前面提到的时间片轮转的特点外,还有优先级调度(Priority Schedule)的特点。优先级调度决定了线程按照什么顺序轮流执行,在具有优先级调度的系统中,线程拥有各自的线程优先级(Thread Priority)。具有高优先级的线程会更早地执行,而低优先级的线程通常要等没有更高优先级的可执行线程时才会被执行。

线程的优先级可以由用户手动设置,此外系统也会根据不同情形调整优先级。通常情况下,频繁地进入等待状态(进入等待状态会放弃之前仍可占用的时间份额)的线程(如IO线程),比频繁进行大量计算以至于每次都把所有时间片全部用尽的线程更受操作系统的欢迎。因为频繁进入等待的线程只会占用很少的时间,这样操作系统可以处理更多的任务。我们把频繁等待的线程称之为IO密集型线程(IO Bound Thread),而把很少等待的线程称之为CPU密集型线程(CPU Bound Thread)。IO密集型线程总是比CPU密集型线程更容易得到优先级的提升。

线程饿死:

在优先级调度下,容易出现一种线程饿死的现象。一个线程饿死是说它的优先级较低,在它执行之前总是有比它优先级更高的线程等待执行,因此这个低优先级的线程始终得不到执行。当CPU密集型的线程优先级较高时,其它低优先级的线程就很可能出现饿死的情况;当IO密集型线程优先级较高时,其它线程相对不容易造成饿死的善,因为IO线程有大量的等待时间。为了避免线程饿死,调度系统通常会逐步提升那些等待了很久而得不到执行的线程的优先级。这样,一个线程只要它等待了足够长的时间,其优先级总会被提升到可以让它执行的程度,也就是说这种情况下线程始终会得到执行,只是时间的问题。

在优先级调度环境下,线程优先级的改变有三种方式:

  1. 用户指定优先级;
  2. 根据进入等待状态的频繁程度提升或降低优先级(由操作系统完成);
  3. 长时间得不到执行而被提升优先级。

线程安全与锁

在多个线程并发执行访问同一个数据时,如果不采取相应的措施,将会是非常危险的。假设你在工行有一个银行账户,两张银联卡(自己手里一张,女朋友手里一张),里面有100万。假设取钱就两个过程:1.检查账户余额,2.取出现金(如果要取出的金额 > 账户余额,则取现成功,否则取现失败)。有一天你要买房想把钱取出来,而此时你女朋友也想买一辆车(假设你们事先没有商量)。两个人都在取钱,你在A号ATM机取100万,女朋友在B号ATM机取80万。这时A号ATM检查账户余额发现有100万,可以取出;而与此同时,同一时刻B号ATM也在检查账户余额发现有100万,可以取出;这样,A、B都把钱取出来了。

100万的存款取出180万,银行就亏大发了(当然你就笑呵呵了……)!这就是线程并发的不安全性。为避免这种情况发生,我们要将多个线程对同一数据的访问同步,确保线程安全。

所谓 同步(synchronization) 就是指一个线程访问数据时,其它线程不得对同一个数据进行访问,即同一时刻只能有一个线程访问该数据,当这一线程访问结束时其它线程才能对这它进行访问。同步最常见的方式就是使用锁(Lock),也称为线程锁。锁是一种非强制机制,每一个线程在访问数据或资源之前,首先试图获取(Acquire)锁,并在访问结束之后释放(Release)锁。在锁被占用时试图获取锁,线程会进入等待状态,直到锁被释放再次变为可用。

二元信号量

二元信号量(Binary Semaphore)是一种最简单的锁,它有两种状态:占用和非占用。它 适合只能被唯一一个线程独占访问的资源。当二元信号量处于非占用状态时,第一个试图获取该二元信号量锁的线程会获得该锁,并将二元信号量锁置为占用状态,之后其它试图获取该二元信号量的线程会进入等待状态,直到该锁被释放。

信号量

多元信号量允许多个线程访问同一个资源,多元信号量简称信号量(Semaphore),对于允许多个线程并发访问的资源,这是一个很好的选择。一个初始值为N的信号量允许N个线程并发访问。线程访问资源时首先获取信号量锁,进行如下操作:

  1. 将信号量的值减1;
  2. 如果信号量的值小于0,则进入等待状态,否则继续执行;
    访问资源结束之后,线程释放信号量锁,进行如下操作:
  3. 将信号量的值加1;
  4. 如果信号量的值小于1(等于0),唤醒一个等待中的线程;

互斥量

互斥量(Mutex)和二元信号量类似,资源仅允许一个线程访问。与二元信号量不同的是,信号量在整个系统中可以被任意线程获取和释放,也就是说,同一个信号量可以由一个线程获取而由另一线程释放。互斥量则要求哪个线程获取了该互斥量锁就由哪个线程释放,其它线程越俎代庖释放互斥量是无效的。

临界区

临界区(Critical Section)是一种比互斥量更加严格的同步手段。互斥量和信号量在系统的任何进程都是可见的,也就是说一个进程创建了一个互斥量或信号量,另一进程试图获取该锁是合法的。而 临界区的作用范围仅限于本进程,其它的进程无法获取该锁。 除此之处,临界区与互斥量的性质相同。

读写锁

读写锁(Read-Write Lock)允许多个线程同时对同一个数据进行读操作,而只允许一个线程进行写操作。这是因为读操作不会改变数据的内容,是安全的;而写操作会改变数据的内容,是不安全的。对同一个读写锁,有两种获取方式:共享的(Shared)和独占的(Exclusive)。当锁处于自由状态时,试图以任何一种方式获取锁都能成功,并将锁置为对应的状态;如果锁处于共享状态,其它线程以共享方式获取该锁,仍然能成功,此时该锁分配给了多个线程;如果其它线程试图如独占的方式获取处于共享状态的锁,它必须等待所有线程释放该锁;处于独占状态的锁阻止任何线程获取该锁,不论它们以何种方式。获取读写锁的方式总结如下:
读写锁的状态 以共享方式获取 以独占方式获取
自由 成功 成功
共享 成功 等待
独占 等待 等待
表 1 :获取读写锁的方式

编程思想之多线程与多进程(3)——Java中的多线程

单线程

任何程序至少有一个线程,即使你没有主动地创建线程,程序从一开始执行就有一个默认的线程,被称为主线程,只有一个线程的程序称为单线程程序。如下面这一简单的代码,没有显示地创建一个线程,程序从main开始执行,main本身就是一个线程(主线程),单个线程从头执行到尾。

1
2
3
4
5
6
public static void main(String args[]) {
System.out.println("输出从1到100的数:");
for (int i = 0; i < 100; i ++) {
System.out.println(i + 1);
}
}

创建线程

单线程程序简单明了,但有时无法满足特定的需求。如一个文字处理的程序,我在打印文章的同时也要能对文字进行编辑,如果是单线程的程序则要等打印机打印完成之后你才能对文字进行编辑,但打印的过程一般比较漫长,这是我们无法容忍的。如果采用多线程,打印的时候可以单独开一个线程去打印,主线程可以继续进行文字编辑。在程序需要同时执行多个任务时,可以采用多线程。

在程序需要同时执行多个任务时,可以采用多线程。Java给多线程编程提供了内置的支持,提供了两种创建线程方法:1.通过实现Runable接口;2.通过继承Thread类。

Thread是JDK实现的对线程支持的类,Thread类本身实现了Runnable接口,所以Runnable是显示创建线程必须实现的接口; Runnable只有一个run方法,所以不管通过哪种方式创建线程,都必须实现run方法。我们可以看一个例子。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
/**
* Created with IntelliJ IDEA.
* User: luoweifu
* Date: 15-5-24
* Time: 下午9:30
* To change this template use File | Settings | File Templates.
*/

/**
* 通过实现Runnable方法
*/
class ThreadA implements Runnable {
private Thread thread;
private String threadName;
public ThreadA(String threadName) {
thread = new Thread(this, threadName);
this.threadName = threadName;
}

//实现run方法
public void run() {
for (int i = 0; i < 100; i ++) {
System.out.println(threadName + ": " + i);
}
}

public void start() {
thread.start();
}
}

/**
* 继承Thread的方法
*/
class ThreadB extends Thread {
private String threadName;

public ThreadB(String threadName) {
super(threadName);
this.threadName = threadName;
}

//实现run方法
public void run() {
for (int i = 0; i < 100; i ++) {
System.out.println(threadName + ": " + i);
}
}
}

public class Multithread{

public static void main(String args[]) {
ThreadA threadA = new ThreadA("ThreadA");
ThreadB threadB = new ThreadB("ThreadB");
threadA.start();
threadB.start();
}
}

说明:上面的例子中例举了两种实现线程的方式。大部分情况下选择实现Runnable接口的方式会优于继承Thread的方式,因为:

  1. 从 Thread 类继承会强加类层次;
  2. 有些类不能继承Thread类,如要作为线程运行的类已经是某一个类的子类了,但Java只支持单继承,所以不能再继承Thread类了。

线程同步

线程与线程之间的关系,有几种:

模型一:简单的线程,多个线程同时执行,但各个线程处理的任务毫不相干,没有数据和资源的共享,不会出现争抢资源的情况。这种情况下不管有多少个线程同时执行都是安全的,其执行模型如下:


图 1:处理相互独立的任务

模型二:复杂的线程,多个线程共享相同的数据或资源,就会出现多个线程争抢一个资源的情况。这时就容易造成数据的非预期(错误)处理,是线程不安全的,其模型如下:

图 2:多个线程共享相同的数据或资源

在出现模型二的情况时就要考虑线程的同步,确保线程的安全。Java中对线程同步的支持,最常见的方式是添加synchronized同步锁。

我们通过一个例子来看一下线程同步的应用。

买火车票是大家春节回家最为关注的事情,我们就简单模拟一下火车票的售票系统(为使程序简单,我们就抽出最简单的模型进行模拟):有500张从北京到赣州的火车票,在8个窗口同时出售,保证系统的稳定性和数据的原子性。

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
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
/**
* 模拟服务器的类
*/
class Service {
private String ticketName; //票名
private int totalCount; //总票数
private int remaining; //剩余票数

public Service(String ticketName, int totalCount) {
this.ticketName = ticketName;
this.totalCount = totalCount;
this.remaining = totalCount;
}

public synchronized int saleTicket(int ticketNum) {
if (remaining > 0) {
remaining -= ticketNum;
try { //暂停0.1秒,模拟真实系统中复杂计算所用的时间
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}

if (remaining >= 0) {
return remaining;
} else {
remaining += ticketNum;
return -1;
}
}
return -1;
}

public synchronized int getRemaining() {
return remaining;
}

public String getTicketName() {
return this.ticketName;
}

}

/**
* 售票程序
*/
class TicketSaler implements Runnable {
private String name;
private Service service;

public TicketSaler(String windowName, Service service) {
this.name = windowName;
this.service = service;
}

@Override
public void run() {
while (service.getRemaining() > 0) {
synchronized (this)
{
System.out.print(Thread.currentThread().getName() + "出售第" + service.getRemaining() + "张票,");
int remaining = service.saleTicket(1);
if (remaining >= 0) {
System.out.println("出票成功!剩余" + remaining + "张票.");
} else {
System.out.println("出票失败!该票已售完。");
}
}
}
}
}

测试程序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* 测试类
*/
public class TicketingSystem {
public static void main(String args[]) {
Service service = new Service("北京-->赣州", 500);
TicketSaler ticketSaler = new TicketSaler("售票程序", service);
//创建8个线程,以模拟8个窗口
Thread threads[] = new Thread[8];
for (int i = 0; i < threads.length; i++) {
threads[i] = new Thread(ticketSaler, "窗口" + (i + 1));
System.out.println("窗口" + (i + 1) + "开始出售 " + service.getTicketName() + " 的票...");
threads[i].start();
}

}
}

结果如下:

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
窗口1开始出售 北京–>赣州 的票…
窗口2开始出售 北京–>赣州 的票…
窗口3开始出售 北京–>赣州 的票…
窗口4开始出售 北京–>赣州 的票…
窗口5开始出售 北京–>赣州 的票…
窗口6开始出售 北京–>赣州 的票…
窗口7开始出售 北京–>赣州 的票…
窗口8开始出售 北京–>赣州 的票…
窗口1出售第500张票,出票成功!剩余499张票.
窗口1出售第499张票,出票成功!剩余498张票.
窗口6出售第498张票,出票成功!剩余497张票.
窗口6出售第497张票,出票成功!剩余496张票.
窗口1出售第496张票,出票成功!剩余495张票.
窗口1出售第495张票,出票成功!剩余494张票.
窗口1出售第494张票,出票成功!剩余493张票.
窗口2出售第493张票,出票成功!剩余492张票.
窗口2出售第492张票,出票成功!剩余491张票.
窗口2出售第491张票,出票成功!剩余490张票.
窗口2出售第490张票,出票成功!剩余489张票.
窗口2出售第489张票,出票成功!剩余488张票.
窗口2出售第488张票,出票成功!剩余487张票.
窗口6出售第487张票,出票成功!剩余486张票.
窗口6出售第486张票,出票成功!剩余485张票.
窗口3出售第485张票,出票成功!剩余484张票.
……

在上面的例子中,涉及到数据的更改的Service类saleTicket方法和TicketSaler类run方法都用了synchronized同步锁进行同步处理,以保证数据的准确性和原子性。

关于synchronized更详细的用法请参见:《Java中Synchronized的用法》

线程控制

在多线程程序中,除了最重要的线程同步外,还有其它的线程控制,如线程的中断、合并、优先级等。

线程等待(wait、notify、notifyAll)

Wait:使当前的线程处于等待状态;
Notify:唤醒其中一个等待线程;
notifyAll:唤醒所有等待线程。

详细用法参见:《 Java多线程中wait, notify and notifyAll的使用》

线程中断(interrupt)

在Java提供的线程支持类Thread中,有三个用于线程中断的方法:
public void interrupt(); 中断线程。
public static boolean interrupted(); 是一个静态方法,用于测试当前线程是否已经中断,并将线程的中断状态 清除。所以如果线程已经中断,调用两次interrupted,第二次时会返回false,因为第一次返回true后会清除中断状态。
public boolean isInterrupted(); 测试线程是否已经中断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
**
* 打印线程
*/
class Printer implements Runnable {
public void run() {
while (!Thread.currentThread().isInterrupted()) { //如果当前线程未被中断,则执行打印工作
System.out.println(Thread.currentThread().getName() + "打印中… …");
}
if (Thread.currentThread().isInterrupted()) {
System.out.println("interrupted:" + Thread.interrupted()); //返回当前线程的状态,并清除状态
System.out.println("isInterrupted:" + Thread.currentThread().isInterrupted());
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
Printer printer = new Printer();
Thread printerThread = new Thread(printer, "打印线程");
printerThread.start();
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("有紧急任务出现,需中断打印线程.");
System.out.println("中断前的状态:" + printerThread.isInterrupted());
printerThread.interrupt(); // 中断打印线程
System.out.println("中断前的状态:" + printerThread.isInterrupted());
1
2
3
4
5
6
7
8
9
10
打印线程打印中… …
… …
打印线程打印中… …
有紧急任务出现,需中断打印线程.
打印线程打印中… …
中断前的状态:false
打印线程打印中… …
中断前的状态:true
interrupted:true
isInterrupted:false

线程合并(join)

所谓合并,就是等待其它线程执行完,再执行当前线程,执行起来的效果就好像把其它线程合并到当前线程执行一样。其执行关系如下:


图 4:线程合并的过程

public final void join()
等待该线程终止

public final void join(long millis);
等待该线程终止的时间最长为 millis 毫秒。超时为 0 意味着要一直等下去。

public final void join(long millis, int nanos)
等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒

这个常见的一个应用就是安装程序,很多大的软件都会包含多个插件,如果选择完整安装,则要等所有的插件都安装完成才能结束,且插件与插件之间还可能会有依赖关系。

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
33
34
35
/**
* 插件1
*/
class Plugin1 implements Runnable {

@Override
public void run() {
System.out.println("插件1开始安装.");
System.out.println("安装中...");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("插件1完成安装.");
}
}

/**
* 插件2
*/
class Plugin2 implements Runnable {

@Override
public void run() {
System.out.println("插件2开始安装.");
System.out.println("安装中...");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("插件2完成安装.");
}
}
1
2
3
4
5
6
7
8
9
10
11
12
System.out.println("主线程开启...");
Thread thread1 = new Thread(new Plugin1());
Thread thread2 = new Thread(new Plugin2());
try {
thread1.start(); //开始插件1的安装
thread1.join(); //等插件1的安装线程结束
thread2.start(); //再开始插件2的安装
thread2.join(); //等插件2的安装线程结束,才能回到主线程
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("主线程结束,程序安装完成!");

优先级(Priority)

线程优先级是指获得CPU资源的优先程序。优先级高的容易获得CPU资源,优先级底的较难获得CPU资源,表现出来的情况就是优先级越高执行的时间越多。

Java中通过getPriority和setPriority方法获取和设置线程的优先级。Thread类提供了三个表示优先级的常量:MIN_PRIORITY优先级最低,为1;NORM_PRIORITY是正常的优先级;为5,MAX_PRIORITY优先级最高,为10。我们创建线程对象后,如果不显示的设置优先级的话,默认为5。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* 优先级
*/
class PriorityThread implements Runnable{
@Override
public void run() {
for (int i = 0; i < 1000; i ++) {
System.out.println(Thread.currentThread().getName() + ": " + i);
}
}
}

//创建三个线程
Thread thread1 = new Thread(new PriorityThread(), "Thread1");
Thread thread2 = new Thread(new PriorityThread(), "Thread2");
Thread thread3 = new Thread(new PriorityThread(), "Thread3");
//设置优先级
thread1.setPriority(Thread.MAX_PRIORITY);
thread2.setPriority(8);
//开始执行线程
thread3.start();
thread2.start();
thread1.start();

从结果中我们可以看到线程thread1明显比线程thread3执行的快。

Java多线程中wait, notify and notifyAll的使用

在Java的Object类中有三个final的方法允许线程之间进行资源对象锁的通信,他们分别是: wait(), notify() and notifyAll()。

调用这些方法的当前线程必须拥有此对象监视器,否则将会报java.lang.IllegalMonitorStateException exception异常。

wait

Object的wait方法有三个重载方法,其中一个方法wait() 是无限期(一直)等待,直到其它线程调用notify或notifyAll方法唤醒当前的线程;另外两个方法wait(long timeout) 和wait(long timeout, int nanos)允许传入 当前线程在被唤醒之前需要等待的时间,timeout为毫秒数,nanos为纳秒数。

notify

notify方法只唤醒一个等待(对象的)线程并使该线程开始执行。所以如果有多个线程等待一个对象,这个方法只会唤醒其中一个线程,选择哪个线程取决于操作系统对多线程管理的实现。

notifyAll

notifyAll 会唤醒所有等待(对象的)线程,尽管哪一个线程将会第一个处理取决于操作系统的实现。

这些方法可以使用于“生产者-消费者”问题,消费者是在队列中等待对象的线程,生产者是在队列中释放对象并通知其他线程的线程。

让我们来看一个多线程作用于同一个对象的例子,我们使用wait, notify and notifyAll方法。

通过实例来理解

Message

一个Java bean类,线程将会使用它并调用wait和notify方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.journaldev.concurrency;

public class Message {
private String msg;

public Message(String str){
this.msg=str;
}

public String getMsg() {
return msg;
}

public void setMsg(String str) {
this.msg=str;
}

}

Waiter

一个Waiter类,等待其它的线程调用notify方法以唤醒线程完成处理。注意等待线程必须通过加synchronized同步锁拥有Message对象的监视器。

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
package com.journaldev.concurrency;

public class Waiter implements Runnable{

private Message msg;

public Waiter(Message m){
this.msg=m;
}

@Override
public void run() {
String name = Thread.currentThread().getName();
synchronized (msg) {
try{
System.out.println(name+" waiting to get notified at time:"+System.currentTimeMillis());
msg.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println(name+" waiter thread got notified at time:"+System.currentTimeMillis());
//process the message now
System.out.println(name+" processed: "+msg.getMsg());
}
}

}

Notifier

一个Notifier类,处理Message对象并调用notify方法唤醒等待Message对象的线程。注意synchronized代码块被用于持有Message对象的监视器。

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
package com.journaldev.concurrency;

public class Notifier implements Runnable {

private Message msg;

public Notifier(Message msg) {
this.msg = msg;
}

@Override
public void run() {
String name = Thread.currentThread().getName();
System.out.println(name+" started");
try {
Thread.sleep(1000);
synchronized (msg) {
msg.setMsg(name+" Notifier work done");
msg.notify();
// msg.notifyAll();
}
} catch (InterruptedException e) {
e.printStackTrace();
}

}

}

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package com.journaldev.concurrency;

public class WaitNotifyTest {

public static void main(String[] args) {
Message msg = new Message("process it");
Waiter waiter = new Waiter(msg);
new Thread(waiter,"waiter").start();

Waiter waiter1 = new Waiter(msg);
new Thread(waiter1, "waiter1").start();

Notifier notifier = new Notifier(msg);
new Thread(notifier, "notifier").start();
System.out.println("All the threads are started");
}

}

当我们调用以上的代码时可以看到以下的输出,但并没有结束(完成),因为有两个线程等待同一个Message对象,但notify()方法只能唤醒一个线程,另一个线程仍然在等待被唤醒。

notify()

1
2
3
4
5
6
7
waiter waiting to get notified at time:1356318734009
waiter1 waiting to get notified at time:1356318734010
All the threads are started
notifier started
waiter waiter thread got notified at time:1356318735011
waiter processed: notifier Notifying work done
如果我们注释掉Notifier类中的notify() 方法的调用,并打开notifyAll() 方法的调用,将会有以下的输出信息。

notifyAll()

1
2
3
4
5
6
7
8
waiter waiting to get notified at time:1356318917118
waiter1 waiting to get notified at time:1356318917118
All the threads are started
notifier started
waiter1 waiter thread got notified at time:1356318918120
waiter1 processed: notifier Notifying work done
waiter waiter thread got notified at time:1356318918120
waiter processed: notifier Notifying work done

一旦notifyAll()方法唤醒所有的Waiter线程,程序将会执行完成并退出。

原文地址:http://www.journaldev.com/1037/java-thread-wait-notify-and-notifyall-example

参考阅读

编程思想之多线程与多进程(1)——以操作系统的角度述说线程与进程
编程思想之多线程与多进程(2)——线程优先级与线程安全
编程思想之多线程与多进程(3)——Java中的多线程
编程思想之多线程与多进程(4)——C++中的多线程
Java多线程中wait, notify and notifyAll的使用

欢迎关注我的其它发布渠道