你必定知道多任务处理,由于它实际上被全部的现代操做系统所支持。然而,多任务处理有两种大相径庭的类型:基于进程的和基于线程的。认识二者的不一样是十分重要的。对不少读者,基于进程的多任务处理是更熟悉的形式。进程(process)本质上是一个执行的程序。所以,基于进程(process-based)的多任务处理的特色是容许你的计算机同时运行两个或更多的程序。举例来讲,基于进程的多任务处理使你在运用文本编辑器的时候能够同时运行Java编译器。在基于进程的多任务处理中,程序是调度程序所分派的最小代码单位。在基于线程(thread-based)的多任务处理环境中,线程是最小的执行单位。这意味着一个程序能够同时执行两个或者多个任务的功能。例如,一个文本编辑器能够在打印的同时格式化文本。因此,多进程程序处理“大图片”,而多线程程序处理细节问题。 程序员
多线程程序比多进程程序须要更少的管理费用。进程是重量级的任务,须要分配它们本身独立的地址空间。进程间通讯是昂贵和受限的。进程间的转换也是很须要花费的。另外一方面,线程是轻量级的选手。它们共享相同的地址空间而且共同分享同一个进程。线程间通讯是便宜的,线程间的转换也是低成本的。当Java程序使用多进程任务处理环境时,多进程程序不受Java的控制,而多线程则受Java控制。 编程
多线程帮助你写出CPU最大利用率的高效程序,由于空闲时间保持最低。这对Java运行的交互式的网络互连环境是相当重要的,由于空闲时间是公共的。举个例子来讲,网络的数据传输速率远低于计算机处理能力,本地文件系统资源的读写速度远低于CPU的处理能力,固然,用户输入也比计算机慢不少。在传统的单线程环境中,你的程序必须等待每个这样的任务完成之后才能执行下一步——尽管CPU有不少空闲时间。多线程使你可以得到并充分利用这些空闲时间。若是你在Windows98 或Windows 2000这样的操做系统下有编程经验,那么你已经熟悉了多线程。然而,Java管理线程使多线程处理尤为方便,由于不少细节对你来讲是易于处理的。安全
|
Java运行系统在不少方面依赖于线程,全部的类库设计都考虑到多线程。实际上,Java使用线程来使整个环境异步。这有利于经过防止CPU循环的浪费来减小无效部分。 数据结构
为更好的理解多线程环境的优点能够将它与它的对照物相比较。单线程系统的处理途径是使用一种叫做轮询的事件循环方法。在该模型中,单线程控制在一无限循环中运行,轮询一个事件序列来决定下一步作什么。一旦轮询装置返回信号代表,已准备好读取网络文件,事件循环调度控制管理到适当的事件处理程序。直到事件处理程序返回,系统中没有其余事件发生。这就浪费了CPU时间。这致使了程序的一部分独占了系统,阻止了其余事件的执行。 总的来讲, 单线程环境, 当一个线程由于等待资源时阻塞(block,挂起执行),整个程序中止运行。 多线程
Java多线程的优势在于取消了主循环/轮询机制。一个线程能够暂停而不影响程序的其余部分。例如,当一个线程从网络读取数据或等待用户输入时产生的空闲时间能够被利用到其余地方。多线程容许活的循环在每一帧间隙中沉睡一秒而不暂停整个系统。在Java程序中出现线程阻塞,仅有一个线程暂停,其余线程继续运行。 并发
线程存在于好几种状态。线程能够正在运行(running)。只要得到CPU时间它就能够运行。运行的线程能够被挂起(suspend),并临时中断它的执行。一个挂起的线程能够被恢复(resume), 容许它从中止的地方继续运行。一个线程能够在等待资源时被阻塞(block)。异步
在任什么时候候,线程能够终止(terminate),这当即中断了它的运行。一旦终止,线程不能被恢复。编辑器
|
Java给每一个线程安排优先级以决定与其余线程比较时该如何对待该线程。线程优先级是详细说明线程间优先关系的整数。做为绝对值,优先级是毫无心义的;当只有一个线程时,优先级高的线程并不比优先权低的线程运行的快。相反,线程的优先级是用来决定什么时候从一个运行的线程切换到另外一个。这叫“上下文转换”(context switch)。决定上下文转换发生的规则很简单:
· 线程能够自动放弃控制。在I/O未决定的状况下,睡眠或阻塞由明确的让步来完成。在这种假定下,全部其余的线程被检测,准备运行的最高优先级线程被授予CPU。
· 线程能够被高优先级的线程抢占。在这种状况下,低优先级线程不主动放弃,处理器只是被先占——不管它正在干什么——处理器被高优先级的线程占据。基本上,一旦高优先级线程要运行,它就执行。这叫作有优先权的多任务处理。
当两个相同优先级的线程竞争CPU周期时,情形有一点复杂。对于Windows98这样的操做系统,等优先级的线程是在循环模式下自动划分时间的。对于其余操做系统,例如Solaris 2.x,等优先级线程相对于它们的对等体自动放弃。若是不这样,其余的线程就不会运行。
警告:不一样的操做系统下等优先级线程的上下文转换可能会产生错误。
|
由于多线程在你的程序中引入了一个异步行为,因此在你须要的时候必须有增强同步性的方法。举例来讲,若是你但愿两个线程相互通讯并共享一个复杂的数据结构,例如链表序列,你须要某些方法来确保它们没有相互冲突。也就是说,你必须防止一个线程写入数据而另外一个线程正在读取链表中的数据。为此目的,Java在进程间同步性的老模式基础上实行了另外一种方法:管程(monitor)。管程是一种由C.A.R.Hoare首先定义的控制机制。
你能够把管程想象成一个仅控制一个线程的小盒子。一旦线程进入管程,全部线程必须等待直到该线程退出了管程。用这种方法,管程能够用来防止共享的资源被多个线程操纵。
不少多线程系统把管程做为程序必须明确的引用和操做的对象。Java提供一个清晰的解决方案。没有“Monitor”类;相反,每一个对象都拥有本身的隐式管程,当对象的同步方法被调用时管程自动载入。一旦一个线程包含在一个同步方法中,没有其余线程能够调用相同对象的同步方法。这就使你能够编写很是清晰和简洁的多线程代码,由于同步支持是语言内置的。
|
在你把程序分红若干线程后,你就要定义各线程之间的联系。用大多数其余语言规划时,你必须依赖于操做系统来确立线程间通讯。这样固然增长花费。然而,Java提供了多线程间谈话清洁的、低成本的途径——经过调用全部对象都有的预先肯定的方法。Java的消息传递系统容许一个线程进入一个对象的一个同步方法,而后在那里等待,直到其余线程明确通知它出来。
|
Java的多线程系统创建于Thread类,它的方法,它的共伴接口Runnable基础上。Thread类封装了线程的执行。既然你不能直接引用运行着的线程的状态,你要经过它的代理处理它,因而Thread 实例产生了。为建立一个新的线程,你的程序必须扩展Thread 或实现Runnable接口。
Thread类定义了好几种方法来帮助管理线程。本章用到的方法如表13-1所示:
到目前为止,本书所应用的例子都是用单线程的。本章剩余部分解释如何用Thread 和Runnable 来建立、管理线程。让咱们从全部Java程序都有的线程:主线程开始。
|
当Java程序启动时,一个线程马上运行,该线程一般叫作程序的主线程(main thread),由于它是程序开始时就执行的。主线程的重要性体如今两方面:
· 它是产生其余子线程的线程
· 一般它必须最后完成执行,由于它执行各类关闭动做。
尽管主线程在程序启动时自动建立,但它能够由一个Thread对象控制。为此,你必须调用方法currentThread()得到它的一个引用,currentThread()是Thread类的公有的静态成员。
它的一般形式以下:
static Thread currentThread( )
该方法返回一个调用它的线程的引用。一旦你得到主线程的引用,你就能够像控制其余线程那样控制主线程。
让咱们从复习下面例题开始:
// Controlling the mainThread.
class CurrentThreadDemo {
public static void main(String args[]) {
Thread t = Thread.currentThread();
System.out.println("Current thread:" + t);
// change the name of the thread
t.setName("My Thread");
System.out.println("After name change:" + t);
try {
for(int n = 5; n > 0; n--) {
System.out.println(n);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println("Main threadinterrupted");
}
}
}
在本程序中,当前线程(天然是主线程)的引用经过调用currentThread()得到,该引用保存在局部变量t中。而后,程序显示了线程的信息。接着程序调用setName()改变线程的内部名称。线程信息又被显示。而后,一个循环数从5开始递减,每数一次暂停一秒。暂停是由sleep()方法来完成的。Sleep()语句明确规定延迟时间是1毫秒。注意循环外的try/catch块。
Thread类的sleep()方法可能引起一个InterruptedException异常。这种情形会在其余线程想要打搅沉睡线程时发生。本例只是打印了它是否被打断的消息。在实际的程序中,你必须灵活处理此类问题。下面是本程序的输出:
Current thread:Thread[main,5,main]
After name change: Thread[MyThread,5,main]
5
4
3
2
1
注意t做为语句println()中参数运用时输出的产生。该显示顺序:线程名称,优先级以及组的名称。默认状况下,主线程的名称是main。它的优先级是5,这也是默认值,main也是所属线程组的名称。一个线程组(thread group)是一种将线程做为一个总体集合的状态控制的数据结构。这个过程由专有的运行时环境来处理,在此就不赘述了。线程名改变后,又被输出。此次,显示了新的线程名。
让咱们更仔细的研究程序中Thread类定义的方法。 sleep()方法按照毫秒级的时间指示使线程从被调用到挂起。它的一般形式以下:
static void sleep(longmilliseconds) throws InterruptedException
挂起的时间被明肯定义为毫秒。该方法可能引起InterruptedException异常。
sleep()方法还有第二种形式,显示以下,该方法容许你指定时间是以毫秒仍是以纳秒为周期。
static void sleep(long milliseconds,int nanoseconds) throws InterruptedException
第二种形式仅当容许以纳秒为时间周期时可用。
如上述程序所示,你能够用setName()设置线程名称,用getName()来得到线程名称(该过程在程序中没有体现)。这些方法都是Thread 类的成员,声明以下:
final void setName(StringthreadName)
final String getName( )
这里,threadName 特指线程名称。
|
大多数状况,经过实例化一个Thread对象来建立一个线程。Java定义了两种方式:
· 实现Runnable 接口。
· 能够继承Thread类。
下面的两小节依次介绍了每一种方式。
|
建立线程的最简单的方法就是建立一个实现Runnable接口的类。Runnable抽象了一个执行代码单元。你能够经过实现Runnable接口的方法建立每个对象的线程。为实现 Runnable 接口,一个类仅需实现一个run()的简单方法,该方法声明以下:
public void run( )
在run()中能够定义代码来构建新的线程。理解下面内容是相当重要的:run()方法可以像主线程那样调用其余方法,引用其余类,声明变量。仅有的不一样是run()在程序中确立另外一个并发的线程执行入口。当run()返回时,该线程结束。
在你已经建立了实现Runnable接口的类之后,你要在类内部实例化一个Thread类的对象。Thread 类定义了好几种构造函数。咱们会用到的以下:
Thread(Runnable threadOb, String threadName)
该构造函数中,threadOb是一个实现Runnable接口类的实例。 这定义了线程执行的起点。
新线程的名称由threadName定义。创建新的线程后,它并不运行直到调用了它的start()方法,该方法在Thread 类中定义。本质上,start() 执行的是一个对run()的调用。 Start()方法声明以下:
void start( )
下面的例子是建立一个新的线程并启动它运行:
// Create a second thread.
class NewThread implements Runnable {
Thread t;
NewThread(){
// Createa new, second thread
t = newThread(this, "Demo Thread");
System.out.println("Child thread: " + t);
t.start(); // Start the thread
}
// This isthe entry point for the second thread.
public voidrun() {
try {
for(inti = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
Thread.sleep(500);
}
} catch(InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ThreadDemo {
publicstatic void main(String args[]) {
newNewThread(); // create a new thread
try {
for(inti = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch(InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
在NewThread 构造函数中,新的Thread对象由下面的语句建立:
t = new Thread(this, "Demo Thread");
经过前面的语句this 代表在this对象中你想要新的线程调用run()方法。而后,start() 被调用,以run()方法为开始启动了线程的执行。这使子线程for循环开始执行。调用start()以后,NewThread 的构造函数返回到main()。当主线程被恢复,它到达for 循环。两个线程继续运行,共享CPU,直到它们的循环结束。该程序的输出以下:
Child thread: Thread[Demo Thread,5,main]
Main Thread: 5
Child Thread: 5
Child Thread: 4
Main Thread: 4
Child Thread: 3
Child Thread: 2
Main Thread: 3
Child Thread: 1
Exiting child thread.
Main Thread: 2
Main Thread: 1
Main thread exiting.
如前面提到的,在多线程程序中,一般主线程必须是结束运行的最后一个线程。实际上,一些老的JVM,若是主线程先于子线程结束,Java的运行时间系统就可能“挂起”。
前述程序保证了主线程最后结束,由于主线程沉睡周期1000毫秒,而子线程仅为500毫秒。
这就使子线程在主线程结束以前先结束。简而言之,你将看到等待线程结束的更好途径。
|
建立线程的另外一个途径是建立一个新类来扩展Thread类,而后建立该类的实例。当一个类继承Thread时,它必须重载run()方法,这个run()方法是新线程的入口。它也必须调用start()方法去启动新线程执行。下面用扩展thread类重写前面的程序:
// Create a second thread by extending Thread
class NewThread extends Thread {
NewThread(){
// Createa new, second thread
super("Demo Thread");
System.out.println("Child thread: " + this);
start();// Start the thread
}
// This isthe entry point for the second thread.
public voidrun() {
try {
for(inti = 5; i > 0; i--) {
System.out.println("Child Thread: " + i);
Thread.sleep(500);
}
} catch(InterruptedException e) {
System.out.println("Child interrupted.");
}
System.out.println("Exiting child thread.");
}
}
class ExtendThread {
publicstatic void main(String args[]) {
newNewThread(); // create a new thread
try {
for(inti = 5; i > 0; i--) {
System.out.println("Main Thread: " + i);
Thread.sleep(1000);
}
} catch(InterruptedException e) {
System.out.println("Main thread interrupted.");
}
System.out.println("Main thread exiting.");
}
}
该程序生成和前述版本相同的输出。子线程是由实例化NewThread对象生成的,该对象从Thread类派生。注意NewThread 中super()的调用。该方法调用了下列形式的Thread构造函数:
public Thread(String threadName)
这里,threadName指定线程名称。
|
到这里,你必定会奇怪为何Java有两种建立子线程的方法,哪种更好呢。全部的问题都归于一点。Thread类定义了多种方法能够被派生类重载。对于全部的方法,唯一的必须被重载的是run()方法。这固然是实现Runnable接口所需的一样的方法。不少Java程序员认为类仅在它们被增强或修改时应该被扩展。所以,若是你不重载Thread的其余方法时,最好只实现Runnable 接口。这固然由你决定。然而,在本章的其余部分,咱们应用实现runnable接口的类来建立线程。
|
到目前为止,咱们仅用到两个线程:主线程和一个子线程。然而,你的程序能够建立所需的更多线程。例如,下面的程序建立了三个子线程:
// Create multiple threads.
class NewThread implementsRunnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: "+ t);
t.start(); // Start the thread
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ":" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name +"Interrupted");
}
System.out.println(name + "exiting.");
}
}
class MultiThreadDemo {
public static void main(String args[]) {
newNewThread("One"); // start threads
new NewThread("Two");
new NewThread("Three");
try {
// wait for other threads to end
Thread.sleep(10000);
} catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main threadexiting.");
}
}
程序输出以下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread:Thread[Three,5,main]
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Three: 3
Two: 3
One: 2
Three: 2
Two: 2
One: 1
Three: 1
Two: 1
One exiting.
Two exiting.
Three exiting.
Main thread exiting.
如你所见,一旦启动,全部三个子线程共享CPU。注意main()中对sleep(10000)的调用。这使主线程沉睡十秒确保它最后结束。
|
如前所述,一般你但愿主线程最后结束。在前面的例子中,这点是经过在main()中调用sleep()来实现的,通过足够长时间的延迟以确保全部子线程都先于主线程结束。然而,这不是一个使人满意的解决方法,它也带来一个大问题:一个线程如何知道另外一线程已经结束?幸运的是,Thread类提供了回答此问题的方法。
有两种方法能够断定一个线程是否结束。第一,能够在线程中调用isAlive()。这种方法由Thread定义,它的一般形式以下:
final boolean isAlive( )
若是所调用线程仍在运行,isAlive()方法返回true,若是不是则返回false。 但isAlive()不多用到,等待线程结束的更经常使用的方法是调用join(),描述以下:
final void join( ) throwsInterruptedException
该方法等待所调用线程结束。该名字来自于要求线程等待直到指定线程参与的概念。
join()的附加形式容许给等待指定线程结束定义一个最大时间。下面是前面例子的改进版本。运用join()以确保主线程最后结束。一样,它也演示了isAlive()方法。
// Using join() to wait forthreads to finish.
class NewThread implementsRunnable {
String name; // name of thread
Thread t;
NewThread(String threadname) {
name = threadname;
t = new Thread(this, name);
System.out.println("New thread: "+ t);
t.start(); // Start the thread
}
// This is the entry point for thread.
public void run() {
try {
for(int i = 5; i > 0; i--) {
System.out.println(name + ":" + i);
Thread.sleep(1000);
}
} catch (InterruptedException e) {
System.out.println(name + "interrupted.");
}
System.out.println(name + "exiting.");
}
}
class DemoJoin {
public static void main(String args[]) {
NewThread ob1 = newNewThread("One");
NewThread ob2 = newNewThread("Two");
NewThread ob3 = new NewThread("Three");
System.out.println("Thread One isalive: "
+ ob1.t.isAlive());
System.out.println("Thread Two isalive: "
+ ob2.t.isAlive());
System.out.println("Thread Three isalive: "
+ ob3.t.isAlive());
// wait for threads to finish
try {
System.out.println("Waiting forthreads to finish.");
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch (InterruptedException e) {
System.out.println("Main threadInterrupted");
}
System.out.println("Thread One isalive: "
+ ob1.t.isAlive());
System.out.println("Thread Two isalive: "
+ ob2.t.isAlive());
System.out.println("Thread Three isalive: "
+ ob3.t.isAlive());
System.out.println("Main threadexiting.");
}
}
程序输出以下所示:
New thread: Thread[One,5,main]
New thread: Thread[Two,5,main]
New thread:Thread[Three,5,main]
Thread One is alive: true
Thread Two is alive: true
Thread Three is alive: true
Waiting for threads to finish.
One: 5
Two: 5
Three: 5
One: 4
Two: 4
Three: 4
One: 3
Two: 3
Three: 3
One: 2
Two: 2
Three: 2
One: 1
Two: 1
Three: 1
Two exiting.
Three exiting.
One exiting.
Thread One is alive: false
Thread Two is alive: false
Thread Three is alive: false
Main thread exiting.
如你所见,调用join()后返回,线程终止执行。
|
线程优先级被线程调度用来断定什么时候每一个线程容许运行。理论上,优先级高的线程比优先级低的线程得到更多的CPU时间。实际上,线程得到的CPU时间一般由包括优先级在内的多个因素决定(例如,一个实行多任务处理的操做系统如何更有效的利用CPU时间)。
一个优先级高的线程天然比优先级低的线程优先。举例来讲,当低优先级线程正在运行,而一个高优先级的线程被恢复(例如从沉睡中或等待I/O中),它将抢占低优先级线程所使用的CPU。
理论上,等优先级线程有同等的权利使用CPU。但你必须当心了。记住,Java是被设计成能在不少环境下工做的。一些环境下实现多任务处理从本质上与其余环境不一样。为安全起见,等优先级线程偶尔也受控制。这保证了全部线程在无优先级的操做系统下都有机会运行。实际上,在无优先级的环境下,多数线程仍然有机会运行,由于不少线程不可避免的会遭遇阻塞,例如等待输入输出。遇到这种情形,阻塞的线程挂起,其余线程运行。
可是若是你但愿多线程执行的顺利的话,最好不要采用这种方法。一样,有些类型的任务是占CPU的。对于这些支配CPU类型的线程,有时你但愿可以支配它们,以便使其余线程能够运行。
设置线程的优先级,用setPriority()方法,该方法也是Tread 的成员。它的一般形式为:
final void setPriority(int level)
这里,level指定了对所调用的线程的新的优先权的设置。Level的值必须在MIN_PRIORITY到MAX_PRIORITY范围内。一般,它们的值分别是1和10。要返回一个线程为默认的优先级,指定NORM_PRIORITY,一般值为5。这些优先级在Thread中都被定义为final型变量。
你能够经过调用Thread的getPriority()方法来得到当前的优先级设置。该方法以下:
final int getPriority( )
当涉及调度时,Java的执行能够有本质上不一样的行为。Windows 95/98/NT/2000 的工做或多或少如你所愿。但其余版本可能工做的彻底不一样。大多数矛盾发生在你使用有优先级行为的线程,而不是协同的腾出CPU时间。最安全的办法是得到可预先性的优先权,Java得到跨平台的线程行为的方法是自动放弃对CPU的控制。
下面的例子阐述了两个不一样优先级的线程,运行于具备优先权的平台,这与运行于无优先级的平台不一样。一个线程经过Thread.NORM_PRIORITY设置了高于普通优先级两级的级数,另外一线程设置的优先级则低于普通级两级。两线程被启动并容许运行10秒。每一个线程执行一个循环,记录反复的次数。10秒后,主线程终止了两线程。每一个线程通过循环的次数被显示。
// Demonstrate thread priorities.
class clicker implements Runnable {
int click =0;
Thread t;
privatevolatile boolean running = true;
publicclicker(int p) {
t = newThread(this);
t.setPriority(p);
}
public void run() {
while(running) {
click++;
}
}
public voidstop() {
running =false;
}
public voidstart() {
t.start();
}
}
class HiLoPri {
publicstatic void main(String args[]) {
Thread.currentThread().setPriority(Thread.MAX_PRIORITY);
clickerhi = new clicker(Thread.NORM_PRIORITY + 2);
clickerlo = new clicker(Thread.NORM_PRIORITY - 2);
lo.start();
hi.start();
try { Thread.sleep(10000);
} catch(InterruptedException e) {
System.out.println("Main thread interrupted.");
}
lo.stop();
hi.stop();
// Waitfor child threads to terminate.
try {
hi.t.join();
lo.t.join();
} catch(InterruptedException e) {
System.out.println("InterruptedExceptioncaught");
}
System.out.println("Low-priority thread: " + lo.click);
System.out.println("High-priority thread: " + hi.click);
}
}
该程序在Windows 98下运行的输出,代表线程确实上下转换,甚至既不屈从于CPU,也不被输入输出阻塞。优先级高的线程得到大约90%的CPU时间。
Low-priority thread: 4408112
High-priority thread: 589626904
固然,该程序的精确的输出结果依赖于你的CPU的速度和运行的其余任务的数量。当一样的程序运行于无优先级的系统,将会有不一样的结果。
上述程序还有个值得注意的地方。注意running前的关键字volatile。尽管volatile 在下章会被很仔细的讨论,用在此处以确保running的值在下面的循环中每次都获得验证。
while (running) {
click++;
}
若是不用volatile,Java能够自由的优化循环:running的值被存在CPU的一个寄存器中,每次重复不必定须要复检。volatile的运用阻止了该优化,告知Java running能够改变,改变方式并不以直接代码形式显示。
|
当两个或两个以上的线程须要共享资源,它们须要某种方法来肯定资源在某一刻仅被一个线程占用。达到此目的的过程叫作同步(synchronization)。像你所看到的,Java为此提供了独特的,语言水平上的支持。
同步的关键是管程(也叫信号量semaphore)的概念。管程是一个互斥独占锁定的对象,或称互斥体(mutex)。在给定的时间,仅有一个线程能够得到管程。当一个线程须要锁定,它必须进入管程。全部其余的试图进入已经锁定的管程的线程必须挂起直到第一个线程退出管程。这些其余的线程被称为等待管程。一个拥有管程的线程若是愿意的话能够再次进入相同的管程。
若是你用其余语言例如C或C++时用到过同步,你会知道它用起来有一点诡异。这是由于不少语言它们本身不支持同步。相反,对同步线程,程序必须利用操做系统源语。幸运的是Java经过语言元素实现同步,大多数的与同步相关的复杂性都被消除。
你能够用两种方法同步化代码。二者都包括synchronized关键字的运用,下面分别说明这两种方法。
|
Java中同步是简单的,由于全部对象都有它们与之对应的隐式管程。进入某一对象的管程,就是调用被synchronized关键字修饰的方法。当一个线程在一个同步方法内部,全部试图调用该方法(或其余同步方法)的同实例的其余线程必须等待。为了退出管程,并放弃对对象的控制权给其余等待的线程,拥有管程的线程仅需从同步方法中返回。
为理解同步的必要性,让咱们从一个应该使用同步却没有用的简单例子开始。下面的程序有三个简单类。首先是Callme,它有一个简单的方法call( )。call( )方法有一个名为msg的String参数。该方法试图在方括号内打印msg 字符串。有趣的事是在调用call( ) 打印左括号和msg字符串后,调用Thread.sleep(1000),该方法使当前线程暂停1秒。
下一个类的构造函数Caller,引用了Callme的一个实例以及一个String,它们被分别存在target 和 msg 中。构造函数也建立了一个调用该对象的run( )方法的新线程。该线程当即启动。 Caller类的run( )方法经过参数msg字符串调用Callme实例target的call( ) 方法。 最后,Synch类由建立Callme的一个简单实例和Caller的三个具备不一样消息字符串的实例开始。Callme的同一实例传给每一个Caller实例。
// This program is not synchronized.
class Callme {
voidcall(String msg) {
System.out.print("[" + msg);
try {
Thread.sleep(1000);
}catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callmetarget;
Thread t;
publicCaller(Callme targ, String s) {
target =targ;
msg = s;
t = newThread(this);
t.start();
}
public voidrun() {
target.call(msg);
}
}
class Synch {
publicstatic void main(String args[]) {
Callmetarget = new Callme();
Callerob1 = new Caller(target, "Hello");
Callerob2 = new Caller(target, "Synchronized");
Caller ob3= new Caller(target, "World");
// waitfor threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
该程序的输出以下:
Hello[Synchronized[World]
]
]
在本例中,经过调用sleep( ),call( )方法容许执行转换到另外一个线程。该结果是三个消息字符串的混合输出。该程序中,没有阻止三个线程同时调用同一对象的同一方法的方法存在。这是一种竞争,由于三个线程争着完成方法。例题用sleep( )使该影响重复和明显。
在大多数状况,竞争是更为复杂和不可预知的,由于你不能肯定什么时候上下文转换会发生。这使程序时而运行正常时而出错。为达到上例所想达到的目的,必须有权连续的使用call( )。也就是说,在某一时刻,必须限制只有一个线程能够支配它。为此,你只需在call() 定义前加上关键字synchronized,以下:
class Callme {
synchronizedvoid call(String msg) {
...
这防止了在一个线程使用call( )时其余线程进入call( )。 在synchronized加到call( )前面之后,程序输出以下:
[Hello]
[Synchronized]
[World]
任什么时候候在多线程状况下,你有一个方法或多个方法操纵对象的内部状态,都必须用synchronized关键字来防止状态出现竞争。记住,一旦线程进入实例的同步方法,没有其余线程能够进入相同实例的同步方法。然而,该实例的其余不一样步方法却仍然能够被调用。
|
尽管在建立的类的内部建立同步方法是得到同步的简单和有效的方法,但它并不是在任什么时候候都有效。这其中的缘由,请跟着思考。假设你想得到不为多线程访问设计的类对象的同步访问,也就是,该类没有用到synchronized方法。并且,该类不是你本身,而是第三方建立的,你不能得到它的源代码。这样,你不能在相关方法前加synchronized修饰符。怎样才能使该类的一个对象同步化呢?很幸运,解决方法很简单:你只需将对这个类定义的方法的调用放入一个synchronized块内就能够了。
下面是synchronized语句的普通形式:
synchronized(object) {
// statements tobe synchronized
}
其中,object是被同步对象的引用。若是你想要同步的只是一个语句,那么不须要花括号。一个同步块确保对object成员方法的调用仅在当前线程成功进入object管程后发生。
下面是前面程序的修改版本,在run( )方法内用了同步块:
// This program uses a synchronized block.
class Callme {
voidcall(String msg) {
System.out.print("["+ msg);
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
System.out.println("]");
}
}
class Caller implements Runnable {
String msg;
Callmetarget;
Thread t;
publicCaller(Callme targ, String s) {
target =targ;
msg = s;
t = newThread(this);
t.start();
}
//synchronize calls to call()
public voidrun() {
synchronized(target) { // synchronized block
target.call(msg);
}
}
}
class Synch1 {
publicstatic void main(String args[]) {
Callmetarget = new Callme();
Callerob1 = new Caller(target, "Hello");
Callerob2 = new Caller(target, "Synchronized");
Callerob3 = new Caller(target, "World");
// waitfor threads to end
try {
ob1.t.join();
ob2.t.join();
ob3.t.join();
} catch(InterruptedException e) {
System.out.println("Interrupted");
}
}
}
这里,call( )方法没有被synchronized修饰。而synchronized是在Caller类的run( )方法中声明的。这能够获得上例中一样正确的结果,由于每一个线程运行前都等待先前的一个线程结束。
|
上述例题无条件的阻塞了其余线程异步访问某个方法。Java对象中隐式管程的应用是很强大的,可是你能够经过进程间通讯达到更微妙的境界。这在Java中是尤其简单的。
像前面所讨论过的,多线程经过把任务分红离散的和合乎逻辑的单元代替了事件循环程序。线程还有第二优势:它远离了轮询。轮询一般由重复监测条件的循环实现。一旦条件成立,就要采起适当的行动。这浪费了CPU时间。举例来讲,考虑经典的序列问题,当一个线程正在产生数据而另外一个程序正在消费它。为使问题变得更有趣,假设数据产生器必须等待消费者完成工做才能产生新的数据。在轮询系统,消费者在等待生产者产生数据时会浪费不少CPU周期。一旦生产者完成工做,它将启动轮询,浪费更多的CPU时间等待消费者的工做结束,如此下去。很明显,这种情形不受欢迎。
为避免轮询,Java包含了经过wait(),notify( )和notifyAll( )方法实现的一个进程间通讯机制。这些方法在对象中是用final方法实现的,因此全部的类都含有它们。这三个方法仅在synchronized方法中才能被调用。尽管这些方法从计算机科学远景方向上来讲具备概念的高度先进性,实际中用起来是很简单的:
· wait( ) 告知被调用的线程放弃管程进入睡眠直到其余线程进入相同管程而且调用notify( )。
· notify( ) 恢复相同对象中第一个调用 wait( ) 的线程。
· notifyAll( ) 恢复相同对象中全部调用 wait( ) 的线程。具备最高优先级的线程最早运行。
这些方法在Object中被声明,以下所示:
final void wait( ) throws InterruptedException
final void notify( )
final void notifyAll( )
wait( )存在的另外的形式容许你定义等待时间。
下面的例子程序错误的实行了一个简单生产者/消费者的问题。它由四个类组成:Q,设法得到同步的序列;Producer,产生排队的线程对象;Consumer,消费序列的线程对象;以及PC,建立单个Q,Producer,和Consumer的小类。
// An incorrect implementation of a producer andconsumer.
class Q {
int n;
synchronized int get() {
System.out.println("Got: " + n);
return n;
}
synchronized void put(int n) {
this.n =n;
System.out.println("Put: " + n);
}
}
class Producer implements Runnable {
Q q;
Producer(Qq) {
this.q =q;
newThread(this, "Producer").start();
}
public voidrun() {
int i =0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Qq) {
this.q =q;
newThread(this, "Consumer").start();
}
public voidrun() {
while(true) {
q.get();
}
}
}
class PC {
publicstatic void main(String args[]) {
Q q = newQ();
new Producer(q);
newConsumer(q);
System.out.println("Press Control-C to stop.");
}
}
尽管Q类中的put( )和get( )方法是同步的,没有东西阻止生产者超越消费者,也没有东西阻止消费者消费一样的序列两次。这样,你就获得下面的错误输出(输出将随处理器速度和装载的任务而改变):
Put: 1
Got: 1
Got: 1
Got: 1
Got: 1
Got: 1
Put: 2
Put: 3
Put: 4
Put: 5
Put: 6
Put: 7
Got: 7
生产者生成1后,消费者依次得到一样的1五次。生产者在继续生成2到7,消费者没有机会得到它们。
用Java正确的编写该程序是用wait()和notify( )来对两个方向进行标志,以下所示:
// A correct implementation of a producer andconsumer.
class Q {
int n;
booleanvalueSet = false;
synchronized int get() {
if(!valueSet)
try {
wait();
}catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
System.out.println("Got: " + n);
valueSet = false;
notify();
returnn;
}
synchronized void put(int n) {
if(valueSet)
try {
wait();
}catch(InterruptedException e) {
System.out.println("InterruptedException caught");
}
this.n= n;
valueSet = true;
System.out.println("Put: " + n);
notify();
}
}
class Producer implements Runnable {
Q q;
Producer(Qq) {
this.q =q;
newThread(this, "Producer").start();
}
public voidrun() {
int i =0;
while(true) {
q.put(i++);
}
}
}
class Consumer implements Runnable {
Q q;
Consumer(Q q) {
this.q = q;
newThread(this, "Consumer").start();
}
public voidrun() {
while(true) {
q.get();
}
}
}
class PCFixed {
publicstatic void main(String args[]) {
Q q = newQ();
newProducer(q);
newConsumer(q);
System.out.println("Press Control-C to stop.");
}
}
内部get( ), wait( )被调用。这使执行挂起直到Producer 告知数据已经预备好。这时,内部get( ) 被恢复执行。获取数据后,get( )调用notify( )。这告诉Producer能够向序列中输入更多数据。在put( )内,wait( )挂起执行直到Consumer取走了序列中的项目。当执行再继续,下一个数据项目被放入序列,notify( )被调用,这通知Consumer它应该移走该数据。
下面是该程序的输出,它清楚的显示了同步行为:
Put: 1
Got: 1
Put: 2
Got: 2
Put: 3
Got: 3
Put: 4
Got: 4
Put: 5
Got: 5
|
须要避免的与多任务处理有关的特殊错误类型是死锁(deadlock)。死锁发生在当两个线程对一对同步对象有循环依赖关系时。例如,假定一个线程进入了对象X的管程而另外一个线程进入了对象Y的管程。若是X的线程试图调用Y的同步方法,它将像预料的同样被锁定。而Y的线程一样但愿调用X的一些同步方法,线程永远等待,由于为到达X,必须释放本身的Y的锁定以使第一个线程能够完成。死锁是很难调试的错误,由于:
· 一般,它极少发生,只有到两线程的时间段恰好符合时才能发生。
· 它可能包含多于两个的线程和同步对象(也就是说,死锁在比刚讲述的例子有更多复杂的事件序列的时候能够发生)。
为充分理解死锁,观察它的行为是颇有用的。下面的例子生成了两个类,A和B,分别有foo( )和bar( )方法。这两种方法在调用其余类的方法前有一个短暂的停顿。主类,名为Deadlock,建立了A和B的实例,而后启动第二个线程去设置死锁环境。foo( )和bar( )方法使用sleep()强迫死锁现象发生。
// An example of deadlock.
class A {
synchronized void foo(B b) {
Stringname = Thread.currentThread().getName();
System.out.println(name + " entered A.foo");
try {
Thread.sleep(1000);
}catch(Exception e) {
System.out.println("A Interrupted");
}
System.out.println(name + " trying to call B.last()");
b.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class B {
synchronized void bar(A a) {
Stringname = Thread.currentThread().getName();
System.out.println(name + " entered B.bar");
try {
Thread.sleep(1000);
}catch(Exception e) {
System.out.println("B Interrupted");
}
System.out.println(name + " trying to call A.last()");
a.last();
}
synchronized void last() {
System.out.println("Inside A.last");
}
}
class Deadlock implements Runnable {
A a = newA();
B b = newB();
Deadlock(){
Thread.currentThread().setName("MainThread");
Thread t= new Thread(this, "RacingThread");
t.start();
a.foo(b);// get lock on a in this thread.
System.out.println("Back in main thread");
}
public voidrun() {
b.bar(a);// get lock on b in other thread.
System.out.println("Back in other thread");
}
public staticvoid main(String args[]) {
newDeadlock();
}
}
运行程序后,输出以下:
MainThread entered A.foo
RacingThread entered B.bar
MainThread trying to call B.last()
RacingThread trying to call A.last()
由于程序死锁,你须要按CTRL-C来结束程序。在PC机上按CTRL-BREAK (或在Solaris下按CTRL-\)你能够看到全线程和管程缓冲堆。你会看到RacingThread在等待管程a时占用管程b,同时,MainThread占用a等待b。该程序永远都不会结束。像该例阐明的,你的多线程程序常常被锁定,死锁是你首先应检查的问题。
|
有时,线程的挂起是颇有用的。例如,一个独立的线程能够用来显示当日的时间。若是用户不但愿用时钟,线程被挂起。在任何情形下,挂起线程是很简单的,一旦挂起,从新启动线程也是一件简单的事。
挂起,终止和恢复线程机制在Java 2和早期版本中有所不一样。尽管你运用Java 2的途径编写代码,你仍需了解这些操做在早期Java环境下是如何完成的。例如,你也许须要更新或维护老的代码。你也须要了解为何Java 2会有这样的变化。由于这些缘由,下面内容描述了执行线程控制的原始方法,接着是Java2的方法。
|
先于Java2的版本,程序用Thread定义的suspend() 和 resume() 来暂停和再启动线程。
它们的形式以下:
final void suspend( )
final void resume( )
下面的程序描述了这些方法:
// Using suspend() and resume().
class NewThread implements Runnable {
Stringname; // name of thread
Thread t;
NewThread(String threadname) {
name =threadname;
t = newThread(this, name);
System.out.println("New thread: " + t);
t.start(); // Start the thread
}
// This isthe entry point for thread.
public voidrun() {
try {
for(inti = 15; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(200);
}
} catch(InterruptedException e) {
System.out.println(name + " interrupted.");
}
System.out.println(name + " exiting.");
}
}
class SuspendResume {
publicstatic void main(String args[]) {
NewThreadob1 = new NewThread("One");
NewThreadob2 = new NewThread("Two");
try {
Thread.sleep(1000);
ob1.t.suspend();
System.out.println("Suspending thread One");
Thread.sleep(1000);
ob1.t.resume();
System.out.println("Resuming thread One");
ob2.t.suspend();
System.out.println("Suspending thread Two");
Thread.sleep(1000);
ob2.t.resume();
System.out.println("Resuming thread Two");
} catch(InterruptedException e) {
System.out.println("Main thread Interrupted");
}
// waitfor threads to finish
try {
System.out.println("Waiting for threads to finish.");
ob1.t.join();
ob2.t.join();
}catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Mainthread exiting.");
}
}
程序的部分输出以下:
New thread: Thread[One,5,main]
One: 15
New thread: Thread[Two,5,main]
Two: 15
One: 14
Two: 14
One: 13
Two: 13
One: 12
Two: 12
One: 11
Two: 11
Suspending thread One
Two: 10
Two: 9
Two: 8
Two: 7
Two: 6
Resuming thread One
Suspending thread Two
One: 10
One: 9
One: 8
One: 7
One: 6
Resuming thread Two
Waiting for threads to finish.
Two: 5
One: 5
Two: 4
One: 4
Two: 3
One: 3
Two: 2
One: 2
Two: 1
One: 1
Two exiting.
One exiting.
Main thread exiting.
Thread类一样定义了stop() 来终止线程。它的形式以下:
void stop( )
一旦线程被终止,它不能被resume() 恢复继续运行。
|
Thread定义的suspend(),resume()和stop()方法看起来是管理线程的完美的和方便的方法,它们不能用于新Java版本的程序。下面是其中的缘由。Thread类的suspend()方法在Java 2中不被同意,由于suspend()有时会形成严重的系统故障。假定对关键的数据结构的一个线程被锁定的状况,若是该线程在那里挂起,这些锁定的线程并无放弃对资源的控制。其余的等待这些资源的线程可能死锁。
Resume()方法一样不被赞同。它不引发问题,但不能离开suspend()方法而独立使用。
Thread类的stop()方法一样在Java 2中受到反对。这是由于该方法可能致使严重的系统故障。设想一个线程正在写一个精密的重要的数据结构且仅完成一个零头。若是该线程在此刻终止,则数据结构可能会停留在崩溃状态。
由于在Java 2中不能使用suspend(),resume()和stop()方法来控制线程,你也许会想那就没有办法来中止,恢复和结束线程。其实否则。相反,线程必须被设计以使run()方法按期检查以来断定线程是否应该被挂起,恢复或终止它本身的执行。有表明性的,这由创建一个指示线程状态的标志变量来完成。只要该标志设为“running”,run()方法必须继续让线程执行。若是标志设为“suspend”,线程必须暂停。若设为“stop”,线程必须终止。
固然,编写这样的代码有不少方法,但中心主题对全部的程序应该是相同的。 下面的例题阐述了从Object继承的wait()和notify()方法怎样控制线程的执行。该例与前面讲过的程序很像。然而,不被赞同的方法都没有用到。让咱们思考程序的执行。
NewTread 类包含了用来控制线程执行的布尔型的实例变量suspendFlag。它被构造函数初始化为false。Run()方法包含一个监测suspendFlag的同步声明的块。若是变量是true,wait()方法被调用以挂起线程。Mysuspend()方法设置suspendFlag为true。Myresume()方法设置suspendFlag为false而且调用notify()方法来唤起线程。最后,main()方法被修改以调用mysuspend()和myresume()方法。
// Suspending and resuming a thread for Java2
class NewThread implements Runnable {
Stringname; // name of thread
Thread t;
booleansuspendFlag;
NewThread(String threadname) {
name =threadname;
t = newThread(this, name);
System.out.println("New thread: " + t);
suspendFlag = false;
t.start(); // Start the thread
}
// This isthe entry point for thread.
public voidrun() {
try {
for(inti = 15; i > 0; i--) {
System.out.println(name + ": " + i);
Thread.sleep(200);
synchronized(this) {
while(suspendFlag) {
wait();
}
}
}
} catch(InterruptedException e) {
System.out.println(name+ " interrupted.");
}
System.out.println(name + " exiting.");
}
voidmysuspend() {
suspendFlag = true;
}
synchronized void myresume() {
suspendFlag = false;
notify();
}
}
class SuspendResume {
publicstatic void main(String args[]) {
NewThreadob1 = new NewThread("One");
NewThreadob2 = new NewThread("Two");
try {
Thread.sleep(1000);
ob1.mysuspend();
System.out.println("Suspending thread One");
Thread.sleep(1000);
ob1.myresume();
System.out.println("Resuming thread One");
ob2.mysuspend();
System.out.println("Suspending thread Two");
Thread.sleep(1000);
ob2.myresume();
System.out.println("Resuming thread Two");
} catch(InterruptedException e) {
System.out.println("Main thread Interrupted");
}
// waitfor threads to finish
try {
System.out.println("Waiting for threads to finish.");
ob1.t.join();
ob2.t.join();
}catch (InterruptedException e) {
System.out.println("Main thread Interrupted");
}
System.out.println("Main thread exiting.");
}
}
该程序的输出与前面的程序相同。此书的后面部分,你将看到用Java 2机制控制线程的更多例子。尽管这种机制不像老方法那样“干净”,然而,它是确保运行时不发生错误的方法。它是全部新的代码必须采用的方法。
|
若是你和大多数程序员同样, 那么在语言中加入多线程支持对你来讲是很新鲜的事物。
有效运用这种支持的关键是并发思考而不是连续思考。例如,当你的程序有两个能够并行执行的子系统,建立它们各自的线程。仔细的运用多线程,你能编写出很是有效的程序。
然而要注意:若是你建立太多的线程,你可能会减弱而不是增强程序的性能。记住,上下文转换是须要开销的。若是你建立了太多的线程,更多的CPU时间会用于上下文转换而不是用来执行程序。
|
1. 思考现实生活中,咱们有没有同时处理两件事情的状况?
2. 在同时处理两件事情时,每次作一件仍是两件事情?
|
在本章中,咱们主要学习了:
u 线程的概念;
u 线程创建的运行;
u 线程的同步及死锁;
|
英文 全文 中文
Thread Thread 线程
Runnable Runnable 可运行接口
Deadlock Deadlock 死锁
|