Java线程.docx
《Java线程.docx》由会员分享,可在线阅读,更多相关《Java线程.docx(55页珍藏版)》请在冰豆网上搜索。
Java线程
第六章Java线程
【本章导读】
本章首先介绍了线程的基本概念,区别了线程和进程,接着介绍了线程的挂起方式,详细结实了线程的生命周期,然后介绍了线程的同步与死锁,详细解释了线程同步和死锁的概念及原因,并总结了多线程编程的一般规则,最后用案例一说明多线程的服务器编程,用案例二设计了一个时钟日历。
【本章要点】
线程的基本概念:
进程与线程
创建线程的方式:
继承Thread类
实现Runnable接口
线程的生命周期
线程的同步与死锁
6.1线程的基本概念
1进程
进程是并发执行的程序在一个数据集合上的执行过程。
对于多任务的操作系统Windows,我们可以同时打开和运行多哥应用程序。
每个独立运行的应用程序即为一个进程,同时运行的多个应用程序则为多进程。
对应用程序来说,进程就像一个大容器。
在应用程序被运行后,就相当于将应用程序装进容器里了,你可以往容器里加其他东西(如:
应用程序在运行时所需的变量数据、需要引用的DLL文件等),当应用程序被运行两次时,容器里的东西并不会被倒掉,系统会找一个新的进程容器来容纳它。
图6-1显示了Windows任务管理器中的进程。
图6-1Windows任务管理器中的进程
进程是由进程控制块、程序段、数据段三部分组成。
一个进程可以包含若干线程(Thread),线程可以帮助应用程序同时做几件事(比如一个线程向磁盘写入文件,另一个则接收用户的按键操作并及时做出反应,互相不干扰),在程序被运行后中,系统首先要做的就是为该程序进程建立一个默认线程,然后程序可以根据需要自行添加或删除相关的线程。
是可并发执行的程序。
在一个数据集合上的运行过程,是系统进行资源分配和调度的一个独立单位,也是称活动、路径或任务,它有两方面性质:
活动性、并发性。
进程可以划分为运行、阻塞、就绪三种状态,并随一定条件而相互转化:
就绪--运行,运行--阻塞,阻塞--就绪。
2线程
线程又称为轻量级进程,它和进程一样拥有独立的执行控制,由操作系统负责调度,区别在于线程没有独立的存储空间,而是和所属进程中的其它线程共享一个存储空间,这使得线程间的通信远较进程简单。
图6-2给出了进程和线程之间的关系。
图6-2进程和线程之间的关系
作为进程中的一个实体,是被系统独立调度和分派的基本单位,线程自己不拥有系统资源,只拥有一点在运行中必不可少的资源,但它可与同属一个进程的其它线程共享进程所拥有的全部资源。
一个线程可以创建和撤消另一个线程,同一进程中的多个线程之间可以并发执行。
由于线程之间的相互制约,致使线程在运行中呈现出间断性。
线程也有就绪、阻塞和运行三种基本状态。
线程是程序中一个单一的顺序控制流程,在单个程序中同时运行多个线程完成不同的工作,称为多线程。
线程和进程的区别在于,子进程和父进程有不同的代码和数据空间,而多个线程则共享数据空间,每个线程有自己的执行堆栈和程序计数器为其执行上下文。
具体而言,
1)多个进程的内部数据和状态都是完全独立的,而多线程是共享一块内存空间和一组系统资源,有可能互相影响。
2)线程本身的数据通常只有寄存器数据,以及一个程序执行时使用的堆栈,所以线程的切换比进程切换的负担要小。
3)对线程的综合支持是Java技术的一个重要特色.它提供了thread类、监视器和条件变量的技术。
4)虽然Macintosh,WindowsNT,Windows9等操作系统支持多线程,但若要用C或C++编写多线程程序是十分困难的,因为它们对数据同步的支持不充分。
多线程主要是为了节约CPU时间,发挥利用,根据具体情况而定。
线程的运行中需要使用计算机的内存资源和CPU。
如图6-3所示,进程中多线程同时运行。
图6-3进程中的多线程同时运行
例如,我们使用浏览器时,可以在由浏览器打开的毕竟中打印页面,同时可滚动操作该页面,还可同时听音频文件和观赏动画图象。
这些任务中的每一个都是由线程完成的。
Java对线程有内部的支持。
Java体系结构的主要部分是多线程的,而且,Java程序中的线程大多允许applet同时接受用户的输入和显示另一动画,完成的这些任务就是采用了多线程。
使用多线程的优势:
1)减轻编写交互频繁、涉及面多的程序的困难.
2)程序的吞吐量会得到改善.
3)由多个处理器的系统,可以并发运行不同的线程.(否则,任何时刻只有一个线程在运行)
6.2创建线程的方式
Java中实现线程的方式有两种,一是生成Thread类的子类,并定义该子类自己的run方法,线程的操作在方法run中实现。
但我们定义的类一般是其他类的子类,而Java又不允许多重继承,因此第二种实现线程的方法是实现Runnable接口。
通过覆盖Runnable接口中的run方法实现该线程的功能。
基本的格式如下:
①继承Thread类,比如
classMyThreadextendsThread{
publicvoidrun(){
//这里写上线程的内容
}
publicstaticvoidmain(String[]args){
//使用这个方法启动一个线程
newMyThread().start();
}
}
②实现Runnable接口
classMyThreadimplementsRunnable{
publicvoidrun(){
//这里写上线程的内容
}
publicstaticvoidmain(String[]args){
//使用这个方法启动一个线程
newThread(newMyThread()).start();
}
}
说明:
一般鼓励使用第二种方法,应为Java里面只允许单一继承,但允许实现多个接口。
第二个方法更加灵活。
6.2.1Thread类
Java是通过java.lang包中的Thread类来实现的,可以构造和访问多线程中的各个线程。
该类支持许多种方法,得到关于线程的活动、集合的信息,并检查线程的性质,引起线程等待、中断和撤消。
通过扩展Thread类可使应用程序和类在单独的线程中运行。
类Thread在包java.lang中定义,它的构造方法如下:
publicThread();
publicThread(Runnabletarget);
publicThread(Runnabletarget,Stringname);
publicThread(Stringname);
publicThread(ThreadGroupgroup,Runnabletarget);
publicThread(ThreadGroupgroup,Stringname);
主要方法
isActive()判断是否处于执行状态
Suspend()暂停执行
reSume恢复执行
start()开始执行
Stop()停止执行
sleep()睡眠
run()程序体
yield()向其他线程退让运行权
例6-1继承Thread类,覆盖方法run(),我们在创建的Thread类的子类中重写run(),加入线程所要执行的代码即可。
publicclassMyThreadextendsThread
{
intcount=1,number;
publicMyThread(intnum)
{
number=num;
System.out.println("Createthethread!
"+number);
}
publicvoidrun()
{
while(true)
{
System.out.println("Threads"+number+":
Count"+count);
if(++count==6)
return;
}
}
publicstaticvoidmain(String[]args)
{
inti;
for(i=0;i<5;i++)
newMyThread(i+1).start();
}
}
程序运行结果如图6-3:
图6-3程序6-1的运行结果
例6-2小应用程序中不用Runnable接口仍然可以使用线程(不调用主类的方法和调用主类的方法)。
---------------------------
//不调用主类的方法
importjava.applet.*;
publicclassthreadextendsApplet
{
mythreadt1=newmythread();
publicvoidinit()
{
t1.start();
}
classmythreadextendsThread
{
publicvoidrun()
{
for(inti=0;i<4;i++)
System.out.println(""+i);
{
try{
sleep(400);
}
catch(Exceptione)
{}
}
}
}
}
程序的运行结果入图6-4所示:
图6-4不调用主类的方法不用Runnable接口的示例结果
//调用主类的方法
importjava.awt.*;
importjavax.swing.*;
publicclassmainclassextendsJApplet
{
Ct1=newC(this);
publicvoidinit()
{
t1.start();
}
publicvoidpaint(Graphicsg)
{
g.drawString("Hello,java",10,50);
}
}
classCextendsThread
{
mainclassa;
C(mainclassb)
{
a=b;
}
publicvoidrun()
{
while(true)
{
a.repaint();
try{
sleep(400);
}
catch(InterruptedExceptione)
{}
}
}
}
程序运行结果如图6-5:
图6-5调用主类的方法不用Runnable接口的示例结果
6.2.2Runnable接口
在网上创建applet时,需要继承JApplet,因为Java不支持多重继承,不可同时继承JApplet类和Thread类。
因此,需要使用Runnable接口来解决这个问题。
Runnable由单个的run()方法组成,在线程激活时执行,可扩展JApplet类并实现Runnable接口和重新编码run()方法。
Runnable为非Thread子类的类也提供了一种激活方式。
通过实例化某个Thread实例并将自身作为运行目标,就可以运行实现Runnable的类而无需创建Thread的子类。
大多数情况下,如果只想重写run()方法,而不重写其他Thread方法,那么应使用Runnable接口。
Runnable接口只有一个方法run(),我们声明自己的类实现Runnable接口并提供这一方法,将我们的线程代码写入其中,就完成了这一部分的任务。
但是Runnable接口并没有任何对线程的支持,我们还必须创建Thread类的实例,这一点通过Thread类的构造函数
publicThread(Runnabletarget);
来实现。
例6-3Runnable接口运用的例子。
publicclassMyThreadimplementsRunnable
{
intcount=1,number;
publicMyThread(intnum)
{
number=num;
System.out.println("创建线程"+number);
}
publicvoidrun()
{
while(true)
{
System.out.println
("线程"+number+":
计数"+count);
if(++count==6)return;
}
}
publicstaticvoidmain(Stringargs[])
{
for(inti=0;i〈5;i++)newThread(newMyThread(i+1)).start();
}
}
程序运行结果如图6-6所示。
图6-6程序6-3的运行结果
严格地说,创建Thread子类的实例也是可行的,但是必须注意的是,该子类必须没有覆盖Thread类的run方法,否则该线程执行的将是子类的run方法,而不是我们用以实现Runnable接口的类的run方法,对此读者不妨试验一下。
使用Runnable接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不如直接用多个类分别继承Thread来得紧凑。
6.2.3线程的生命周期
一个线程产生后,就处于整个线程的某个状态。
图6-7描述了线程的生命周期。
图6-7线程的生命周期
线程的生命周周期有四个状态:
1、创建状态(NewThread)
已被创建但尚未执行(start()尚未被调用)。
根据实现线程的方法不同,创建新的线程对象有不同方法:
如果一个类继承了Thread类,那么这个类本身就是一个线程类,创建该类的对象即可。
创建一个新的线程对象之后,系统并不会为之分配资源,必须调用Thread类的start()方法来启动线程。
例如:
classsubThreadextendsThread
{
//重写run()方法的线程体
}
publicclassThreadDemo
{
publicstaticvoidmain(String[]args)
{
subThreadthread=newsubThread(“线程1”);
thread.start();
}
}
以Thread的常用构造函数为例来说明线程:
publicThread(Runnabletarget,Stringname);其中的target是实现线程体的目标对象,必须是Runnable接口类的对象,name是线程的名称。
2、可执行状态(Ruunable)
线程可以执行,虽然不一定正在执行。
CPU时间随时可能被分配给该线程,从而使得它执行。
start()方法负责启动线程,用于分配线程所必须的系统资源,调度线程运行,调用线程的run()。
当调用start()方法时,线程进入可运行转台。
线程处于可运行状态,并不就是正在运行。
因为单处理机不可能依次执行一条以上的线程,处理器维持一个线程队列。
一旦启动,线程排队等候处理机时间,等待论到其执行,在任何时刻,线程可能在等待处理机的处理。
这就是线程的状态是可运行而非正在运行的原因。
对于优先级相同的线程,线程等待处理的概率是相同。
说明:
线程的优先级
线程的优先级代表该线程的重要程度,当有多个线程同时处于可执行状态并等待获得CPU时间时,线程调度系统根据各个线程的优先级来决定给谁分配CPU时间,优先级高的线程有更大的机会获得CPU时间,优先级低的线程也不是没有机会,只是机会要小一些罢了。
可以调用Thread类的方法getPriority()和setPriority()来存取线程的优先级,线程的优先级界于1(MIN_PRIORITY)和10(MAX_PRIORITY)之间,缺省是5(NORM_PRIORITY)。
一旦启动线程,线程进入可运行状态,调用run()方法。
该方法一般包含一个循环,否则,线程会失去其存在的意义。
3、不可运行状态(NotRunnable):
正常情况下run()返回使得线程死亡。
调用stop()或destroy()亦有同样效果,但是不被推荐,前者会产生异常,后者是强制终止,不会释放锁。
如果线程处于下列状态之一,我们称之为不可运行状态:
正在睡眠sleep();
正在等待wait();
被另一个线程所阻塞。
用sleep()方法可使线程进入睡眠状态,一个睡眠状态的线程在指定时间过去后,可进入运行状态。
在指定时间里,线程是不可运行的。
publicstaticvoidsleep(longmillis)throwsInterruptedException
参数millis是线程睡眠的时间,为毫秒数。
如例6-2程序中的sleep(400);该方法是静态方法,属于在当前的线程上操作,而且会抛出InterruptedException异常,在调用该方法时,必须处理这种异常。
4、阻塞状态(Dead):
线程不会被分配CPU时间,无法执行,也称为死亡状态。
一个线程可以是自然死亡,也可以是被杀死。
当线程完成run()方法中的循环时,则线程是自然死亡。
例如程序6-1中:
publicvoidrun()
{
while(true)
{
System.out.println("Threads"+number+":
Count"+count);
if(++count==6)
return;
}
}
run()方法中的循环变量有6次自增,此线程的生命周期为循环的6次自增。
因此,当一个线程执行完所有语句后就自动终止,我们可以调用线程的stop()方法,也可以强制终止线程。
如果希望线程正常终止,可采用标记来使线程中的run()方法退出。
在Thread类中,方法isAlive()可用来确定线程是否启动或停止。
根据线程的生命周期,我们可以总结出线程在运用使的操作步骤如下:
1.publicclassmythreadextendsAppletimplementsRunnable
(小应用或已经是某个类的子类时)
2.继承类Thread
publicclassmythreadextendsThread
3.上述两种方法中都可用类Thread产生线程的对象Threadnewthread;
4.创建并启动线程
newthread=newThread(this);
newthread.start();
5.run方法是运行线程的主体,启动线程时,由java直接调用publicvoidrun()
6.停止线程,由小应用程序的stop调用线程的stopnewthread.stop()
7.sleep方法的作用,暂停线程的执行,让其它线程得到机会,sleep要丢出异常,必须抓住.
try{
sleep(1000);
}
catch(InterruptedExceptione)
{
…
}
6.3线程的同步与死锁
6.3.1同步问题的提出
线程的同步是为了防止多个线程访问一个数据对象时,对数据造成的破坏。
例6-4:
两个线程ThreadA、ThreadB都操作同一个对象Foo对象,并修改Foo对象上的数据。
classFoo
{
privateintx=100;
publicintgetX()
{
returnx;
}
publicintfix(inty)
{
x=x-y;
returnx;
}
}
publicclassMyRunnableimplementsRunnable
{
privateFoofoo=newFoo();
publicstaticvoidmain(String[]args)
{
MyRunnabler=newMyRunnable();
Threadta=newThread(r,"Thread-A");
Threadtb=newThread(r,"Thread-B");
ta.start();
tb.start();
}
publicvoidrun()
{
for(inti=0;i<3;i++)
{
this.fix(30);
try{
Thread.sleep
(1);
}
catch(InterruptedExceptione)
{
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName()+":
当前foo对象的x值="+foo.getX());
}
}
publicintfix(inty)
{
returnfoo.fix(y);
}
}
程序运行结果如图6-8所示:
图6-8程序6-4的运行结果
说明:
从结果发现,这样的输出值明显是不合理的。
原因是两个线程不加控制的访问Foo对象并修改其数据所致。
如果要保持结果的合理性,只需要达到一个目的,就是将对Foo的访问加以限制,每次只能有一个线程在访问。
这样就能保证Foo对象中数据的合理性了。
在具体的Java代码中需要完成一下两个操作:
把竞争访问的资源类Foo变量x标识为private;
同步那些修改变量的代码,使用synchronized关键字同步方法或代码。
我们可以从一个经典的银行取款问题了解同步与死锁的含义。
有一个银行账户,还有余额1100元,现在A通过银行卡从中取1000元,而同时另外一个人B通过存折也从这个账户中取1000元。
取钱之前,要首先进行判断:
如果账户中的余额大于要取的金额,则可以执行取款操作,否则,将拒绝取款。
我们假定有两个线程来分别从银行卡和存折进行取款操作,当A线程执行完判断语句后,获得了当前账户中的余额数(1000元),因为余额大于取款金额,所以准备执行取钱操作(从账户中减去1000元),但此时它被线程B打断,然后,线程B根据余额,从中取出1000元,然后,将账户里面的余额减去1000元,然后,返回执行线程A的动作,这个线程将从上次中断的地方开始执行:
也就是说,它将不再判断账户中的余额,而是直接将上次中断之前获得的余额减去1000。
此时,经过两次的取款操作,账户中的余额为100元,从账面上来看,银行支出了1000元,但实际上,银行支出了2000元