J2SE综合--有关 JAVA 的多线程浅析
一 JAVA 语言的来源、及特点
在这个高速信息的时代,商家们纷纷把信息、产品做到Internet国际互连网页上
。再这些不平常网页的背后,要属功能齐全、安全可靠的编程语言,Java是当之无
愧的。Java是由Sun Microsystem开发的一种功能强大的新型程序设计语言。是与
平台无关的编程语言。它是一种简单的、面象对象的、分布式的、解释的、键壮的
、安全的、结构的中立的、可移植的、性能很优异的、多线程的、动态的、语言。
Java自问世以后,以其编程简单、代码高效、可移植性强,很快受到了广大计算机编程人士的青睐。Java语言是Internet上具有革命性的编程语言,它具有强大的动画、多媒体和交互功能,他使World Web进入了一个全新的时代。Java语言与C 极为类似,可用它来创建安全的、可移植的、多线程的交互式程序。另外用Java开发出来的程序与平台无关,可在多种平台上运行。后台开发,是一种高效、实用的编程方法。人们在屏幕前只能看到例如图案、计算的结果等。实际上操作系统往往在后台来调度一些事件、治理程序的流向等。例如操作系统中的堆栈,线程间的资源分配与治理,内存的创建、访问、治理等。可谓举不盛举。下面就多线程来谈一谈。
二 JAVA的多线程理论
2.1引入
Java提供的多线程功能使得在一个程序里可同时执行多个小任务。线程有时也称
小进程是一个大进程里分出来的小的独立的进程。因为Java实现的多线程技术,所
以比C和C 更键壮。多线程带来的更大的好处是更好的交互性能和实时控制性能。
当然实时控制性能还取决于系统本身(UNIX,Windows,Macintosh等),在开发难易程
度和性能上都比单线程要好。传统编程环境通常是单线程的,由于JAVA是多线程的
。尽管多线程是强大而灵巧的编程工具,但要用好却不轻易,且有许多陷阱,即使
编程老手也难免误用。为了更好的了解线程,用办公室工作人员作比喻。办公室工
作人员就象CPU,根据上级指示做工作,就象执行一个线程。在单线程环境中,每
个程序编写和执行的方式是任何时候程序只考虑一个处理顺序。用我们的比喻,就
象办公室工作人员从头到尾不受打搅和分心,只安排做一个工作。当然,实际生活
中工作人员很难一次只有一个任务,更常见的是工作人员要同时做几件事。老板将
工作交给工作人员,希望工作人员做一这个工作,再做点那个工作,等等。假如一
个任务无法做下去了,比如工作人员等待另一部门的信息,则工作人员将这个工作
放在一边,转入另一个工作。一般来说,老板希望工作人员手头的各个任务每一天
都有一些进展。这样就引入了多线程的概念。多线程编程环境与这个典型的办公室
非常相似,同时给CPU分配了几个任务或线程。和办公室人员一样,计算机CPU实际
上不可能同一时间做几件事,而是把时间分配到不同的线程,使每个线程都有点进
展。假如一个线程无法进行,比如线程要求的键盘输入尚未取得,则转入另一线程
的工作。通常,CPU在线程间的切换非常迅速,使人们感觉到好象所有线程是同时
进行的。
任何处理环境,无论是单线程还是多线程,都有三个要害方面。第一个是CPU,它
实际上进行计算机活动;第二个是执行的程序的代码;第三个是程序操作的数据.
。
在多线程编程中,每个线程都用编码提供线程的行为,用数据供给编码操作。多
个线程可以同时处理同一编码和数据,不同的线程也可能各有不同的编码和数据。
事实上编码和数据部分是相当独立的,需要时即可向线程提供。因此经常是几个线
程使用同一编码和不同的数据。这个思想也可以用办公室工作人员来比喻。会计可
能要做一个部门的帐或几个或几个部门的帐。任何情况的做帐的任务是相同的程序
代码,但每个部门的数据是不同的。会计可能要做整个公司的帐,这时有几个任务
,但有些数据是共享的,因为公司帐需要来自各个部门的数据。
多线程编程环境用方便的模型隐藏CPU在任务切换间的事实。模型答应假装成有多
个可用的CPU。为了建立另一个任务,编程人员要求另一个虚拟CPU,指示它开始用
某个数据组执行某个程序段。下面我们来建立线程。
建立线程
在JAVA中建立线程并不困难,所需要的三件事:执行的代码、代码所操作的数据
和执行代码的虚拟CPU。虚拟CPU包装在Thread类的实例中。建立Thread对象时,必
须提供执行的代码和代码所处理的数据。JAVA的面向对象模型要求程序代码只能写
成类的成员方法。数据只能作为方法中的自动(或本地)变量或类的成员存在。这
些规则要求为线程提供的代码和数据应以类的实例的形式出现。
Public class SimpleRunnable implemants Runable{
Private String message;
Public static void main(String args[]){
SimpleRunnable r1=new SimpleRunnable(“Hello”);
Thread t1=new Thread(r1);
t1.start();
}
public SimpleRunnable(String message){
this.message=message;
}
public void run(){
for(;;){
System.out.println(message);
}
}
}
线程开始执行时,它在public void run()方法中执行。这种方法是定义的线程执
行的起点,就象应用程序从main()开始、小程序从init()开始一样。线程操作的本
地数据是传入线程的对象的成员。
首先,main()方法构造SimpleRunnable类的实例。注重,实例有自己的数据,这
里是一个String,初始化为”Hello”.由于实例r1传入Thread类构造器,这是线程
运行时处理的数据。执行的代码是实例方法run()。
2.2 线程的治理
单线程的程序都有一个main执行体,它运行一些代码,当程序结束执行后,它正
好退出,程序同时结束运行。在JAVA中我们要得到相同的应答,必须稍微进行改动
。只有当所有的线程退出后,程序才能结束。只要有一个线程一直在运行,程序就
无法退出。线程包括四个状态:new(开始),running(运行),wait(等候)和done(结
束)。第一次创建线程时,都位于new状态,在这个状态下,不能运行线程,只能等
待。然后,线程或者由方法start开始或者送往done状态,位于done中的线程已经
结束执行,这是线程的最后一个状态。一旦线程位于这个状态,就不能再次出现,
而且当JAVA虚拟机中的所有线程都位于done状态时,程序就强行中止。当前正在执
行的所有线程都位于running状态,在程序之间用某种方法把处理器的执行时间分
成时间片,位于running状态的每个线程都是能运行的,但在一个给定的时间内,
每个系统处理器只能运行一个线程。与位于running状态的线程不同,由于某种原
因,可以把已经位于waiting状态的线程从一组可执行线程中删除。假如线程的执
行被中断,就回到waiting状态。用多种方法能中断一个线程。线程能被挂起,在
系统资源上等候,或者被告知进入休眠状态。该状态的线程可以返回到running状
态,也能由方法stop送入done状态,
方法
描述
有效状态
目的状态
Start()
开始执行一个线程
New
Running
Stop()
结束执行一个线程
New或running
Done
Sleep(long)
暂停一段时间,这个时间为给定的毫秒
Running
Wait
Sleep(long,int)
暂停片刻,可以精确到纳秒
Running
Wait
Suspend()
挂起执行
Running
Wait
Resume()
恢复执行
Wait
Running
Yield()
明确放弃执行
Running
Running
2.3线程的调度
线程运行的顺序以及从处理器中获得的时间数量主要取决于开发者,处理器给每
个线程分配一个时间片,而且线程的运行不能影响整个系统。处理器线程的系统或
者是抢占式的,或者是非抢占式的。抢占式系统在任何给定的时间内将运行最高优
先级的线程,系统中的所有线程都有自己的优先级。Thread.NORM_PRIORITY是线程
的缺省值,Thread类提供了setPriority和getPriority方法来设置和读取优先权,
使用setPriority方法能改变Java虚拟机中的线程的重要性,它调用一个整数,类
变量Thread.MIN_PRIORITY和Thread.MAX_PRIORITY决定这个整数的有效范围。Java
虚拟机是抢占式的,它能保证运行优先级最高的线程。在JAVA虚拟机中我们把一个
线程的优先级改为最高,那么他将取代当前正在运行的线程,除非这个线程结束运
行或者被一条休眠命令放入waiting状态,否者将一直占用所有的处理器的时间。
假如碰到两个优先级相同的线程,操作系统可能影响线程的执行顺序。而且这个区
别取决于时间片(time slicing)的概念。
治理几个线程并不是真正的难题,对于上百个线程它是怎样治理的呢?当然可以
通过循环,来执行每一个线程,但是这显然是冗长、乏味。JAVA创建了线程组。线
程组是线程的一个谱系组,每个组包含的线程数不受限制,能对每个线程命名并能
在整个线程组中执行(Suspend)和停止(Stop)这样的操作。
2.4信号标志:保护其它共享资源
这种类型的保护被称为互斥锁。某个时间只能有一个线程读取或修改这个数据值
。在对文件尤其是信息数据库进行处理时,读取的数据总是多于写数据,根据这个
情况,可以简化程序。下面举一例,假设有一个雇员信息的数据库,其中包括雇员
的地址和电话号码等信息,有时要进行修改,但要更多的还是读数据,因此要尽可
能防止数据被破坏或任意删改。我们引入前面互斥锁的概念,答应一个读取锁(
red lock)和写入锁(write lock),可根据需要确定有权读取数据的人员,而且
当某人要写数据时,必须有互斥锁,这就是信号标志的概念。信号标志有两种状态
,首先是empty()状态,表示没有任何线程正在读或写,可以接受读和写的请求,
并且立即提供服务;第二种状态是reading()状态,表示有线程正在从数据库中读
信息,并记录进行读操作的线程数,当它为0时,返回empty状态,一个写请求将导
致这个线程进入等待状态。
只能从empty状态进入writing状态,一旦进入writing状态后,其它线程都不能写
操作,任何写或读请求都必须等到这个线程完成写操作为止,而且waiting状态中
的进程也必须一直等到写操作结束。完成操作后,返回到empty状态,发送一个通
知信号,等待的线程将得到服务。
下面实现了这个信号标志
class Semaphore{
final static int EMPTY=0;
final static int READING=1;
final static int WRITING=2;
protected int state=EMPTY;
protected int readCnt=0;
public synchronized void readLock(){
if(state==EMPTY){
state=READING;
}
else if(state==READING){
}
else if(state==WRITING){
while(state==WRITING){
try {wait();}
catch(InterruptedException e){;}
}
state=READING;
}
readCnt ;
return;
}
public synchronized void writeLock(){
if(state==EMPTY){
state=WRITING;
}
else{
while(state!=EMPTY){
try {wait();}
catch(InterruptedException e) {;}
}
}
}
public synchronized void readUnlock(){
readCnt--;
if(readCnt==0){
state=EMPTY;
notify();
}
}
public synchronized void writeUnlock(){
state=EMPTY;
notify();
}
}
现在是测试信号标志的程序:
class Process extends Thread{
String op;
Semaphore sem;
Process(String name,String op,Semaphore sem){
super(name);
this.op=op;
this.sem=sem;
start();
}
public void run(){
if(op
catch(InterruptedException e){;}
System.out.println("Unlocking readLock:" getName());
sem.readUnlock();
}
else if(op
catch(InterruptedException e){;}
System.out.println("Unlocking writeLock:" getName());
sem.writeUnlock();
}
}
}
public class testSem{
public static void main(String argv[]){
Semaphore lock = new Semaphore();
new Process("1","read",lock);
new Process("2","read",lock);
new Process("3","write",lock);
new Process("4","read",lock);
}
}
testSem 类从process类的四个实例开始,它是个线程,用来读或写一个共享文件
。Semaphore类保证访问不会破坏文件,执行程序,输出结果如下:
Trying to get readLock:1
Read op:1
Trying to get readLock:2
Read op:2
Trying to get writeLock:3
Trying to get readLock:4
Read op:4
Unlocking readLock:1
Unlocking readLock:2
Unlocking readLock:4
Write op:3
Unlocking writeLock:3
从这可看到,
2.5死锁以及怎样避免死锁:
为了防止数据项目的并发访问,应将数据项目标为专用,只有通过类本身的实例
方法的同步区访问。为了进入要害区,线程必须取得对象的锁。假设线程要独占访
问两个不同对象的数据,则必须从每个对象各取一个不同的锁。现在假设另一个线
程也要独占访问这两个对象,则该进程必须得到这两把锁之后才能进入。由于需要
两把锁,编程假如不小心就可能出现死锁。假设第一个线程取得对象A的锁,预备
取对象B的锁,而第二个线程取得了对象B的锁,预备取对象A的锁,两个线程都不
能进入,因为两者都不能离开进入的同步块,既两者都不能放弃目前持有的锁。避
免死锁要认真设计。线程因为某个先决条件而受阻时,如需要锁标记时,不能让线
程的停止本身禁止条件的变化。假如要取得多个资源,如两个不同对象的锁,必须
定义取得资源的顺序。假如对象A和B的锁总是按字母顺序取得,则不会出现前面说
道的饿死条件。
三Java多线程的优缺点
由于JAVA的多线程功能齐全,各种情况面面具到,它带来的好处也是显然易见的
。多线程带来的更大的好处是更好的交互性能和实时控制性能。当然实时控制性能
还取决于系统本身(UNIX,Windows,Macintosh 等),在开发难易程度和性能上都比
单线程要好。当然一个好的程序设计语言肯定也难免有不足之处。由于多线程还没
有充分利用基本OS的这一功能。这点我在前面已经提到,对于不同的系统,上面的
程序可能会出现截然不同的结果,这使编程者偶会感到迷惑不解。希望在不久的将
来JAVA的多线程能充分利用到操作系统,减少对编程者的困惑。我期待着JAVA会更
好