多任务操作有两种不同的方式,这取决于操作系统在中断程序运行之前是否首先与程序进行了查询,还是只有当程序愿意产生控制时才被中断这些情况而定,前者称为抢占式多任务操作,后者称为共用式(非抢占式)多任务操作.
多线程程序扩展了多任务操作的概念,它将多任务操作降低一级来运行,各个程序似乎式同一个时间内执行多个任务.每个任务通常成为一个线程,它是控件的线程的简称.能够同时运行多个线程的程序称为多线程程序.可将每个线程视为在不同环境中运行的,也就是说这些环境看上去好像每个线程都有它自己的CPU一样.多线程与多进程的基本差别是,每个线程共享相同的数据,而进程则有它自己的一组完整的变量.
Java语言本身使用一个线程,以便在后台进行无用数据的回收,这样就不会遇到内存管理的问题.图形用户界面(GUI)程序有一个专门的线程,用于收集来自主机操作环境的用户界面事件.
1.1 什么是线程
1. 使用线程为其他任务提供机会
在Thread派生类的run方法中,用于实现在独立的线程中运行某段代码.
2. 运行和启动线程
当构建一个从Thread派生而来的对象时,并不会自动调用run方法,必须调用对象中的start方法,以便真正地启动一个线程;sleep(long mills)方法让线程睡眠规定的毫秒数.
3. 运行多个线程
Java语言中线程结构的巨大优点是非常易于建立任何数据的看上去像是并行运行的自主对象.
4. Runnable接口
当实现线程的类需要访问其超类的属性时,只能选择实现Runnable接口;仍然得用一个线程对象去激活该线程,在此线程对象的构造函数中为该线程赋予一个对Runnable对象的引用,然后该线程便调用该对象的run方法,如下:
class Test implements Runnable{
private Thread t;
public void start(){
t = new Thread(this);
t.start();
}
public void run(){
….
}
}
Thread构造函数的this参数用于指定当该线程执行的是Test对象的一个实例时应该调用其run方法的对象.
如果线程的run方法需要专门访问另一个类,通常可以使用一个内部类,如下:
class Test implements Runnable{
private Thread t;
public void start(){
t = new Thread(){
public void run(){
//thread action goes here
}
};
t.start();
}
}
Runnable接口的另一个用途是作为线程池,在这个池子里保留着预先产生的各个线程,以便在需要时运行;线程池有时可以用在执行大量线程的环境之中,以便降低建立线程对象和收集线程对象的无用信息而付出的代价.
1.2 线程的中断
当线程的run方法返回时,该线程便终止运行;没有任何方法能够强制某个线程终止运行,可以使用interrupt方法来请求终止某个线程的运行;当线程进入睡眠状态时,它就无法主动检查它是否应该终止运行.这时,InterruptedException就可以发挥作用了.当对当前被中断运行的线程对象调用Interrupt方法时,中断调用将被InterruptedException终止.代码如下:
public void run(){
try{
while(!interrupted()&&more work to do){
do more work
}
}catch(InterruptedException exception){
//thread was interrupted during sleep or wait
}finally{
cleanup, if required
}
//exit run method and terminate thread
}
其中interrupted()方法是个静态方法,用于检查当前线程是否已经被中断,还能够清除线程的”中断”状态.
1.3 线程的属性
1. 线程的状态
线程有下面4种状态:
new (新)
runnable (可运行)
blocked (被中断运行)
dead (死)
新线程
当用new运算符创建一个线程时,该线程尚未运行,处于new(新)状态;当一个线程处于新状态时,该线程尚未开始执行它里面的代码.
可运行的线程
一旦调用了start方法,该线程便是个可运行的线程;可运行的线程可以是尚未运行的线程;这是,需要由操作系统为该线程赋予运行的时间;当线程内的代码开始执行时,该线程便开始运行.
被中断运行的线程
当出现下列操作之一时,线程便进入中断状态:
调用该线程的sleep()方法;
该线程调用了一个在输入/输出时中断的操作;
该线程调用了wait()方法;
该线程试图锁定一个当前被另一个线程锁定了的对象;
调用了该线程的suspend()方法;
2. 退出中断状态
线程必须退出中断状态,并且返回可运行状态,方法是使用与进入中断状态相反的过程,情况如下:
如果线程已经被置于睡眠状态,就必须经过规定的毫秒数;
如果线程正在等待输入或输出操作完成,那么必须等待该操作完成;
如果线程调用了wait方法,那么另一个线程必须调用notifyAll或notify;
如果线程正在等待另一个线程拥有的对象锁,那么另一个线程必须放弃该锁的所有权;
如果线程已经暂停运行,那么必须有人调用它的resume方法.但是,由于suspend方法已经被废除,因此resume方法也被作废,并且你在你自己的代码中不应该调用它.
如果调用了一个与线程上的状态不一致的方法,那么虚拟机就会抛出一个IllegalThreadStateException事件.
3. 死线程
原因有如下两个:
由于run方法的正常退出而自然死亡;
没有抓取到的异常事件终止了run方法的执行,从而导致线程突然死亡.
可使用isAlive方法确定某个线程当前是否活着.
4. 守护线程
通过调用setDaemon(true)方法可以把线程变成一个守护线程;守护线程不过是为其他线程提供服务的一种线程,例如定时器线程.
5. 线程组
使用Java编程语言可以创建一个所谓的线程组,就可以同时对一组线程进行操作,如下:
String groupName = …;
ThreadGroup g = new ThreadGroup(groupName);
将各个线程添加给该线程组,如下:
Thread t = new Thread(g,threadName);
确定某个线程组中的任一线程是否仍处于可运行状态,如下:
if(g.activeCount()==0){
//all threads in the group g have stopped
}
若要中断某个线程组中所有线程的运行,只需要在线程组对象上调用interrupt方法,如下:
g. interrupt(); //interrupt all threads in group g
线程组可以下设子线程组;在默认条件下,新创建的线程组将成为当前线程组的子线程组;线程组有个很好的特性,就是如果某个线程由于异常事件而成为死线程,将会得到通知.
1.4 线程的优先级
Java编程语言中,每个线程都有一个优先级;在默认条件下,一个线程将继承其父线程的优先级;可使用setPriority方法来提高或者降低线程的优先级,优先级可设置为MIN_PRIORITY(值为1)与MAX_ PRIORITY(值为10)之间的任何一个值,NORM_ PRIORITY(值为5).
最高优先级的可运行线程将始终保持运行状态,直到:
它通过调用yield方法放弃运行为止;
它不再是个可运行线程;
一个高优先级线程已经变成可运行线程.
1.5 利己线程
当某个线程正在执行一个很长的循环时,它应该始终调用sleep或yield方法,以确保它不会独占整个系统,不遵循这个原则的线程称为利己线程.
1.6 同步
1. 不同步的线程通信
2. 对共享资源的访问实施同步
Java编程语言配有一个更好的机制,只需要给应该被中断的所有操作加上synchronized(异步)标志即可;如果一个线程调用了synchronized方法,它就能够保证在另一个线程将对同一个对象执行任何synchronized方法之前,该synchronized方法必须先执行完.
3. 对象锁
当线程调用synchronized方法时,对象将转为”锁定”状态;如果某个线程拥有一个对象的锁,并且它调用了同一个对象上的另一个synchronized方法,那么该线程将自动被赋予对该方法的访问权;只有当该线程退出上一个synchronized方法时,它才会释放该锁;一个对象的锁只能被一个线程拥有;一个线程可以同时拥有多个对象的锁,只需要在执行一个对象上的synchronized方法的同时,又执行另一个对象的synchronized方法即可.
4. wait和notify方法
当synchronized方法中的wait方法被调用时,当前线程将被中断运行,并且放弃该对象的锁,进入对象的等待列表,在该线程没有从等待列表中被删除之前,调度程序将忽略该线程,并且该线程将没有机会继续运行; notifyAll方法用于将对象等待列表中的所有线程全部删除, notify方法只是用于删除一个任意选择的线程.
如果其他所有线程都中断运行,并且最后一个活动线程在不打开其他线程的锁的情况下调用wait方法,它也会中断运行;结果将没有任何剩余的线程能够使其他线程退出中断状态,从而导致整个程序挂起.
下面简要说明一下同步机制是如何运行的:
1) 若要调用synchronized方法,隐含函数不应该被锁定.调用该方法便可锁定该对象.而从该调用返回则可撤销对隐含参数对象的锁定.因此,每次只有一个线程能够在特定对象上执行synchronized方法.
2) 当一个线程执行对wait方法的调用时,它将释放对象锁,并且进入该对象的等待列表.
3) 要从等待列表中删除一个线程,另外的某个线程必须调用同一个对象上的notifyAll或notify方法.
调度原则操作如下:
1) 如果两个或多个线程修改一个对象,请将执行修改的方法声明为synchronized方法.受到对象修改影响的只读方法也必须实现同步.
2) 如果一个线程必须等待某个对象的状态出现变更,那么它应该在对象的内部等待,而不是在外边等待,这可通过输入一个synchronized方法,并且调用wait方法来实现.
3) 不要在synchronized方法中花费大量的时间.大多数操作只是更新数据结构,然而很快返回.如果你不能立即完成synchronized方法的操作,那么请调用wait方法,这样你就可以在等待时释放该对象锁.
4) 每当一个方法改变某个对象的状态时,它就应该调用notifyAll方法.这样可以给等待线程一个机会,以便查看环境有没有发生变化.
5) 请记住,wait和notifyAll/notify方法都属于Object类的方法,而不是Thread类的方法.请反复检查你对wait方法的调用与同一个对象上的通知是否匹配.
5. 同步块
使用同步块可以锁定一个对象,并且获取对该对象少数几个指令的独占访问权,而不编写新的同步方法;同步块包含一系列的语句,语句封装在{…}中,并且配有前缀synchronized(obj),其中obj是被锁定的对象.例子如下:
public void run(){
synchronized(bank){//lock the bank object
if(bank.getBalance(from) >= amount)
bank.transfer(from,to,amount);
}
}
6. 同步静态方法
调用一个静态方法将会锁定类对象Singleton.class.因此,如果一个线程调用一个类的静态synchronized方法,那么该类的所有静态synchronized方法均被锁定,直到第一个调用返回为止.
1.7 死锁
Java编程语言中没有任何东西能够避免或者打破这种死锁现象;必须在设计线程时确保死锁条件不能出现;必须分析程序,确保每个被封锁的线程最终都能得到通知,并且至少要有一个线程总是在继续不断地运行.
当对具备封锁功能的方法进行调用时,比如调用wait方法或者调用I/O操作时,线程就会失去对程序运行的控制,并且会受到另一个线程的制约,或者在调用I/O操作时,受到外部环境的制约.可使用超时特性来防止这种问题的产生.
有两个wait方法带有超时参数:
void wait (long mills)
void wait (long mills,int nanos)
使当前线程被封锁,直到线程t已经运行结束,或者经过了规定的毫秒数为止,以首先发生的条件为准,方法是t.join(millis);封锁操作既可以在规定的毫秒数内取得成功,也可以在join方法将控制返回给当前线程时取得成功.
1.8 用线程进行用户界面编程
1.9 将管道用于线程间的通信
生产者(producer)线程负责生成一个字节流.消费者(consumer)线程则负责读取和处理该字节流.如果没有字节可供读取,消费者线程便中断运行.如果生产者线程生产的数据量超过了消费者线程的处理能力,那么生产者线程的写操作便中断运行.
Java编程语言配有一组使用方便的类,即PipedInputStream和PipedOutputStream,用于实现这种通信方式(如果生产者线程产生一个Unicode字符串流,而不是字节流,那么还可以使用另外一对类,即PipedReader和PipedWriter).
使用管道的主要原因是为了使每个线程始终能够保持简单.生产者线程只负责将其结果发送给一个数据流,然后就不管它们了.消费者线程则负责读取来自数据流的数据,而不关心数据究竟来自什么地方.通过使用管道,可以将多个线程相互连接起来,而不必担心线程的同步问题.
管道式数据流只适用于线程间在低层次上的通信;在其他情况下,可使用队列;生产者线程负责将对象插入该队列,而消费者线程则负责删除这些对象.
2007-08-27