Windows等操作系统均支持多线程进程的并发处理机制。操作系统支持多线程,使多个程序能够并发执行,以改善资源使用率和提高系统效率;操作系统支持多线程,能够减少程序并发时所付出的时间和空间开销,使得开发粒度更细,并发性更好。
进程
进程是一个程序关于某个数据集合的一次执行过程,是操作系统进行资源分配和保护的基本单位。进程具有以下特性:
①结构性。进程包含了数据集合和运行于其上的程序。每个进程至少由三要素组成:程序块、数据块和进程控制块。进程控制块(Process Control Block, PCB)描述和记录进程的动态变化过程,使进程能正确运行。
②独立性。进程既是系统中资源分配和保护的基本单位,也是系统调度的独立单位(单线程进程),每个进程都以各自独立的速度在CPU上运行。
③动态性。进程是程序在数据集合上的一次执行过程,是动态概念。它的生命周期在多个状态间变化,由创建而产生,由调度而执行,因等待条件而阻塞,由撤销而消亡。程序是一组有序指令序列,是静态概念,程序作为一种系统资源是永久存在的。
④并发性。进程的并发性是指一组进程的执行在时间上是重叠的。
⑤交互性。多个进程可以共享变量,通过共享变量实现互相通信,多个进程之间能够协作完成一个任务。
线程
线程是进程中能够独立执行的实体(控制流),是处理器调度和分配的基本单位。线程是进程的组成部分,每个进程内允许包含多个并发执行的线程。同一个进程中的所有线程共享进程获得的内存空间和资源,但不拥有资源。
支持多线程的进程成为多线程进程。
线程的主要特性如下:
①结构性。线程是操作系统调度的基本单位,具有唯一的标识符合线程控制块,其中包含调度所需的一切信息。
②动态性。线程是动态的,而且有状态变化。当创建一个进程时,同时至少为其创建一个线程,需要时再创建其他线程。终止一个进程将导致进程中的所有线程终止。
③并发性。同一进程的多个线程可在一个或多个处理器上并发或并行的执行,进程之间的并发执行演变为线程之间的并发执行。在单处理器上,从宏观上看,在一个时间段中有几个线程都处于运行状态;在微观上看,任意时刻仅有一个线程在处理器上运行。并发的实质是一个处理器在多个线程之间的多路复用,是对有限的物理资源强制行使多用户共享,消除计算机不见之间的互等现象,提高系统资源利用率。
④共享性。同一进程的所有线程共享但部拥有进程的状态和资源,且驻留在进程的内存空间中,可以访问相同的数据。所有线程之间需要有通信和同步机制。
线程的状态
线程在其生命周期中经历着状态的变化,线程状态包括5种:新建、就绪、运行、阻塞、终止。
就绪(ready)态——进程具备运行条件,等待系统分配处理器以便运行。
运行(running)态——进场占用处理器正在运行。
阻塞(blocked)态——进程不具备运行条件,正在等待某个事件的完成。
线程在执行过程中的任一时刻,处于一种状态,根据运行条件在多个状态之间转变。一个进程创建后处于就绪态,运行中因等待条件处于阻塞态。
线程调度
任一时刻只有一个线程能够占用一个处理器运行,按照什么原则决定就绪队列中的哪个线程能够获得处理器就是线程调度的任务。
线程调度的功能就是按照某种原则选择一个线程使它获得处理器运行。线程调度是操作系统的核心部分,线程调度策略的优劣直接影响到操作系统的性能。
线程调度采用剥夺方式,当一个线程正在处理器上执行时,操作系统可以根据规定的原则剥夺它的处理器使用权,而把处理器分配给其他线程使用。常用的剥夺原则有两种:一是高优先级线程可以剥夺低优先级线程运行;二是当运行线程时间使用完后被剥夺处理器。
并发程序设计
顺序程序设计方法是指,程序模块按照语句次序顺序执行,其特性为:①执行的顺序性;②运行环境的封闭性;③执行结果的确定性;④计算结果的可再现性。
并发程序设计方法是指,将一个程序分为若干可同时执行的程序模块,每个程序模块和它执行时所处理的数据结合组成一个进程。操作系统以进程作为系统资源分配的基本单位,以线程系统调度的基本单位。其特性如下:
①并发执行的线程之间不具有顺序性。线程由操作系统调度执行,不会按照语句的书写顺序执行。
②运行环境不再是封闭的,一个线程的执行可能影响其他线程的执行结果。(计算过程不可再现)
③共享变量的多个线程(成为交互线程)之间实现线程通信,能够协作完成一个任务,也会出现与时间有关的错误。
④并发多线程程序设计的优点是,提高了系统性能,具体表现为快速切换线程、减少系统管理开销、线程通信易于实现、并发按程序提高、节省内存空间。
Java的线程对象
Java支持内置的多线程机制。Java语言包中的Runnable接口约定线程的执行方法,Thread类提供创建、管理和控制线程对象的方法。
在java中可有两种方式实现多线程,一种是继承Thread类,一种是实现Runnable接口;Thread类是在java.lang包中定义的。一个类只要继承了Thread类同时覆写了本类中的run()方法就可以实现多线程操作了,但是一个类只能继承一个父类,这是此方法的局限。
public class NumberThread extends Thread{
private int first;
public NumberThread(String name, int first){
super(name);
this.first = first;
}
public void run(){
System.out.print(this.getName() + ":");
for (int i = first; i < 100; i += 2){
System.out.print(i + " ");
}
System.out.println(this.getName() + "End!\n");
}
public static void main(String[] args){
System.out.println("Current Thread:" +Thread.currentThread().getName());
NumberThread thread1 = new NumberThread("JISHU", 1);
NumberThread thread2 = new NumberThread("OUSHU", 2);
thread1.start();
thread2.start();
System.out.println("Active Count:" + Thread.activeCount());
}
}
在JDK的安装路径下,src.zip是全部的java源程序,通过此代码找到Thread中的start()方法的定义,可以发现此方法中使用了private native void start0();其中native关键字表示可以调用操作系统的底层函数,那么这样的技术成为JNI技术(java Native Interface)。
在实际开发中一个多线程的操作很少使用Thread类,而是通过Runnable接口完成。但是在使用Runnable定义的子类中没有start()方法,只有Thread类中才有。此时观察Thread类,有一个构造方法:public Thread(Runnable targer)此构造方法接受Runnable的子类实例,也就是说可以通过Thread类来启动Runnable实现的多线程。(start()可以协调系统的资源)
public class NumberRunnable implements Runnable{
private int first;
public NumberRunnable(int first){
this.first = first;
}
public void run(){
for (int i = first; i < 50; i += 2){
System.out.print(i + " ");
}
System.out.println("End!\n");
}
public static void main(String[] args){
NumberRunnable target = new NumberRunnable(1);
Thread thread1 = new Thread(target, "JISHU");
thread1.start();
new Thread(new NumberRunnable(2), "OUSHU").start();
}
}
两种实现方式的区别和联系:
在程序开发中只要是多线程肯定永远以实现Runnable接口为主,因为实现Runnable接口相比继承Thread类有如下好处:
①适合多个相同的程序代码的线程去处理同一个资源(适合于资源共享)
②可以避免java中的单继承的限制
③增加程序的健壮性,代码可以被多个线程共享,代码和数据独立。
用一个买票程序来说明两种实现方式的区别:
继承Thread类:
public class TicketThread extends Thread {
private int count = 10;
public TicketThread(String name){
super(name);
}
public void run(){
while (count > 0){
System.out.println(Thread.currentThread().getName() + " is selling ticket: " + count);
count--;
}
}
public static void main(String[] args){
TicketThread thread1 = new TicketThread("窗口1");
TicketThread thread2 = new TicketThread("窗口2");
thread1.start();
thread2.start();
}
}
输出结果为:
窗口2 is selling ticket: 10
窗口2 is selling ticket: 9
窗口2 is selling ticket: 8
窗口2 is selling ticket: 7
窗口2 is selling ticket: 6
窗口2 is selling ticket: 5
窗口2 is selling ticket: 4
窗口2 is selling ticket: 3
窗口2 is selling ticket: 2
窗口2 is selling ticket: 1
窗口1 is selling ticket: 10
窗口1 is selling ticket: 9
窗口1 is selling ticket: 8
窗口1 is selling ticket: 7
窗口1 is selling ticket: 6
窗口1 is selling ticket: 5
窗口1 is selling ticket: 4
窗口1 is selling ticket: 3
窗口1 is selling ticket: 2
窗口1 is selling ticket: 1
实现Runnable接口:
public class TicketRunnable implements Runnable{
private int count = 10;
public void run(){
while (count > 0){
System.out.println(Thread.currentThread().getName() + " is selling ticket: " + count--);
}
}
public static void main(String[] args){
TicketRunnable target = new TicketRunnable();
new Thread(target, "窗口1").start();
new Thread(target, "窗口2").start();
}
}
输出结果为:
窗口1 is selling ticket: 9
窗口1 is selling ticket: 8
窗口1 is selling ticket: 7
窗口1 is selling ticket: 6
窗口1 is selling ticket: 5
窗口1 is selling ticket: 4
窗口1 is selling ticket: 3
窗口1 is selling ticket: 2
窗口1 is selling ticket: 1
窗口2 is selling ticket: 10
如果多次执行,可以发现上述程序可能出现卖出编号为0的票的情况,这里涉及线程的同步机制,将在后续文章中提到。
线程对象的优先级
Java提供10个等级的线程优先级,分别用1~10表示,优先级最低为1,最高为10,默认值是5。Thread类声明了以下三个表示优先级的公有静态常量:
public static final int MIN_PRIORITY = 1; //最低优先级
public static final int MAX_PRIORITY = 2; //最高优先级
public static final int NORM_PRIORITY = 3; //默认优先级
每个线程兑现创建时自动获得默认优先级5,调用setPriority()方法可以改变线程对象的优先级。
线程对象的生命周期
6种线程状态:
①新建态(NEW)。已创建,未启动。
②运行态(RUNNABLE)。从操作系统角度看,处于新建态的线程启动后,进入就绪态,再由操作系统调度执行而成为运行态。由于线程调度由操作系统控制和管理,程序无法控制,无法区分就绪态和运行态。所以,从程序设计角度看,线程启动后即进入运行态RUNNABLE。进入运行态的线程执行其run()方法。
③阻塞态(BLOCKED)和等待态。一个运行态的线程因某种原因不嫩继续运行时,进入阻塞态或等待态。等待态有两种:WAITING(等待时间不确定)和TIMED_WAITING(等待时间确定)。
④终止态(TERMINATED)。线程对象停止运行未被撤销时是终止态。
Thread类中改变和判断线程状态的方法:
- start() 新建态到运行态。
- isAlive() 判断线程是否为活动状态。当一个线程未被终止时,返回true,此时线程处于运行态、阻塞态、等待态之一。但一个线程未启动或已终止时,返回false。
- sleep() 方法使当前进程停止执行若干毫秒,线程由运行态进行等待态,睡眠时间到,线程可再次进行运行态。
- interrupt()方法为当前线程设置一个中断标记,以便于run()方法运行时使用IsInterrupted()能够检测到。此时,线程爱sleep()之类的方法中被阻塞时,由sleep()方法抛出java.lang.InterruptedException,线程中断异常,可捕获这个异常进行中断处理操作。
interrupt()只是为线程设置一个中断标记,并没有中断线程运行,该方法没有抛出异常。一个线程被设置了中断标记后仍可运行,isAlive()返回true。
当抛出一个InterruptedException异常时,记录该线程中断情况的标记将会被清除,这样再调用isInterrupted()将返回false。
线程的同步机制
交互线程间存在两种关系:竞争关系和协作关系。
对于竞争关系的交互线程间需要采用线程互斥方式解决共享资源冲突问题;对于协作关系的交互线程间需要采用线程同步方式解决线程间通信及因执行速度不一致而引起的不同步问题。
交互的并发线程是指他们共享某些变量,一个线程的执行可能影响到其他线程的执行结果,交互的并发线程之间具有制约关系。
无关线程间并发执行时,不会产生于时间有关的错误。
资源竞争出现了两个问题:一个是死锁(deadlock)问题,一组线程如果都获得了部分资源,还想得到其他线程所占用的资源,最终所有的线程将陷入死锁;另一个是饥饿(starvation)问题,一个线程由于其他线程总是优先于它而被无限期拖延。
线程互斥和临界区管理
线程互斥是解决线程间竞争关系的手段。线程互斥(mutual exclusion)是指若干个线程要使用同一共享资源时,任何时刻最多允许一个线程去使用,其他要使用该资源的线程必须等待,直到占有资源的线程释放该资源。
共享变量代表的资源成为临界资源(critical resource),并发线程中与共享变量有关的程序段成为临界区(critical section)。由于与同一变量有关的临界区分散在各有关线程的程序段中,而各线程的执行速度不可预知,因此,操作系统对共享一个变量的若干线程各自进入临界区有以下3个调度原则:
①一次至多一个线程能够在它的临界区内
②不能让一个线程无限期的停留在它的临界区内
③不能强迫一个线程无限地等待进入它的临界区。特别地,进入临界区的任一线程不能妨碍等待进入的其他线程的进展。
把临界区的调度原则总结成四句话:无空等待、有空让进、择一而入、算法可行。
操作系统提供“互斥锁”机制实现并发线程互斥地进入临界区,对共享资源进行操作。
Java的互斥机制实现
Java提供关键字synchronized用于声明一段程序为临界区,使线程对临界资源采用互斥使用方式。synchronized有两种用法:声明一条语句,或者声明一个方法。
①同步语句
synchronized(对象)
语句
其中,对象是多个线程共同操作的公共变量,即需要被锁定的临界资源。
②同步方法
synchronized 方法声明
同步方法的方法体成为临界区,互斥使用(锁定)的是调用该方法的对象。
线程间的协作关系与线程同步
当合作线程中的一个到达协调点后,在尚未得到其伙伴线程发来的信号之前应先阻塞自己,直到其他合作线程发来协调信号后方被唤醒并继续执行。这种协作线程之间相互等待对方消息或信号的协调关系成为线程同步。
线程同步是解决线程间协作关系的手段。线程同步(synchronization)是指两个以上线程基于某个条件来协调它们的活动。一个线程的执行依赖于另一个线程的信号,当一个线程没有得到来自于另一个线程的信号时则需要等待,直到信号到达才被唤醒。
线程互斥是一种特殊的线程同步机制,即逐次使用互斥共享资源,也是对线程使用资源次序上的一种协调。
操作系统实现线程同步有一种成为信号量和PV操作。测试信号量状态的操作成为P操作,改变信号量状态的操作称为V操作,这两种操作是互斥的,并且执行时不能被打断。多个线程之间彼此根据信号量的状态确定谁该执行。
Java的线程通信方法
java.lang.Object类提供wait()、notify()、和notifyAll()方法实现线程间通信。
注意:
- 无论synchronized关键字加在方法上还是对象上,它取得的锁都是对象,而不是把一段代码或函数当作锁—-而且同步方法很可能还会被其他线程的对象访问。
- 每个对象只有一个锁(lock)与之相关联。
- 实现同步是要很大的系统开销作为代价的,甚至可能造成死锁,所以尽量避免无谓的同步控制。
synchronized作为函数修饰符时,它锁定的是调用这个同步方法对象。也就是说,当一个对象P1在不同的线程中执行这个同步方法时,它们之间会形成互斥,达到同步的效果。但是这个对象所属的Class所产生的另一对象P2却可以任意调用这个被加了synchronized关键字的方法。