第七章 多线程机制
【课前思考】
什么是线程?它和进程有什么区别?适用方向是什么? Java的线程是如何实现的? Java的线程是如何调度的?
Java中的多线程有什么特点?同步和互斥的原理是如何实现的? 【学习目标】
学习java中线程的使用,掌握线程的调度和控制方法,清楚地理解多线程的互斥和同步的实现原理,以及多线程的应用。 【学习指南】
掌握线程之间的相互调度关系,尤其是通过线程睡眠来使其它线程获得执行机会的机制,以及互斥和同步的实现机制。 【难 重 点】
多线程的调度和控制。 多线程的互斥和同步。
【知 识 点】 线程简介
多线程的互斥与同步 【内 容】
第一节 线程的概念
随着计算机的飞速发展,个人计算机上的操作系统也纷纷采用多任务和分时设计,将早期只有大型计算机才具有的系统特性带到了个人计算机系统中。一般可以在同一时间内执行多个程序的操作系统都有进程的概念。一个进程就是一个执行中的程序,而每一个进程都有自己的一块内存空间、一组系统资源。在进程概念中,每一个进程的内部数据和状态都是完全的。Java程序通过流控制来执行程序流,程序中单个顺序的流控制称为线程,多线程则指的是在单个程序中可以同时运行多个不同的线程,执行不同的任务。多线程意味着一个程序的多行语句可以看上去几乎在同一时间内同时运行。
线程与进程相似,是一段完成某个特定功能的代码,是程序中单个顺序的流控制;但与进程不同的是,同类的多个线程是共享一块内存空间和一组系统资源,而线程本身的数据通常只有微处理器的寄存器数据,以及一个供程序执行时使用的堆栈。所以系统在产生一个线程,或者在各个线程之间切换时,负担要比进程小的多,正因如此,线程被称为轻负荷进程(light-weight process)。一个进程中可以包含多个线程。
程序(program)是对数据描述与操作的代码的集合,是应用程序执行的脚本 进程(process)是程序的一次执行过程,是系统运行程序的基本单位
多任务(multitask)是指在一个系统中可以同时运行多个程序,即有多个运行的任务,每个任务对应一个进程
同进程一样,一个线程也有从创建、运行到消亡的过程,称为线程的生命周期。用线程的状态(state)表明线程处在生命周期的哪个阶段。线程有创建、可运行、运行中、阻塞、死亡五中状态。通过线程的控制与调度可使线程在这几种状态间转化
每个程序至少自动拥有一个线程,称为主线程。当程序加载到内存时,启动主线程。要加载其他线程,程序就要使用Runnable接口和Thread类
第1页共12页
Java2程序设计实用教程(电子教案)
第二节 Runnable接口与Thread类
Runnable接口
Runnable接口中声明了一个run方法 public void run()
Runnable接口中的run方法只是一个未实现的方法。一个线程对象必须实现run方法完成线程的所有活动,已实现的run方法称为该对象的线程体。
任何实现Runnable接口的对象都可以作为一个线程的目标对象. 为:
public class Thread extends Object implements Runnable 构造方法:
public Thread( ) public Thread(String name) public Thread(Runnable target)
public Thread(Runnable target,String name) public Thread(ThreadGroup group,Runnable target) public Thread(ThreadGroup group,String name)
public Thread(ThreadGroup group,Runnable target,String name) Thread类的静态方法:
public static Thread currentThread( ) //返回当前执行线程的引用对象 public static intactiveCount() //返回当前线程组中活动线程个数 public static enumerate(Thread[] tarray) //将当前线程组中的活动线程拷贝到tarray数组中,包括子线程 Thread类的实例方法:
public final String getName() //返回线程名 public final void setName(String name) //设置线程的名字为name
public void start() //启动已创建的线程对象 public final boolean isAlive() //返回线程是否启动的状态 public final ThreadGroup getThreadGroup() //返回当前线程所属的线程组名
public String toString() //返回线程的字符穿信息 例 7.1 继承Thread类创建线程
本例淙通过继承Thread类来创建线程的方法。类Thread1声明为Thread的子类,它的构造方法定义线程名和起始参数。
程序如下:
public class Thread1 extends Thread {
int k=0;
public Thread1(String name,int k) {
super(name); this.k = k; }
public void run() //覆盖run方法的线程体
Thread类
Thread类将Runnable接口中的run方法实现为空方法,并定义许多用于创建和控制线程的方法。格式
第2页共12页
Java2程序设计实用教程(电子教案)
{
int i = k;
System.out.println();
System.out.print(getName()+\": \"); while (i<50) {
System.out.print(i+\" \"); i+=2; }
System.out.println(getName() +\" end!\"); }
public static void main (String args[]) {
Thread1 t1 = new Thread1(\"Thread1\ //创建线程对象 Thread1 t2 = new Thread1(\"Thread2\
t1.start(); //启动执行线程 t2.start();
System.out.println(\"activeCount=\"+t2.activeCount()); } }
程序运行结果: activeCount=3
Thread1: 1 3 5 7 9 11 13 15 17 19 21 23 25 27
Thread2: 2 4 6 8 10 12 14 16 18 20 22 24 26 28 30 32 34 36 38 40 42 44 46 48 Thread2 end!
29 31 33 35 37 39 41 43 45 47 49 Thread1 end!
两种创建线程方法的比较 比较两者的特点和应用领域:
直接继承线程Thread类。该方法编写简单,可以直接操作线程,适用于单重继承情况,因而不能在继承其他类
实现Runnable接口。当一个线程已继承了另一个类时,就只能用实现Runnable接口的方法来创建线程,且便于保持程序风格的一致性。
线程组
每个线程都是一个线程组的成员,线程组把多个线程集成为一个对象,通过线程组可以同时对其中的多个线程进行操作,如启动一个线程组的所有线程等。Java的线程组由java.lang包中的Thread——Group类实现。
ThreadGroup类用来管理一组线程,包括:线程的数目,线程间的关系,线程正在执行的操作,以及线程将要启动或终止时间等。线程组还可以包含线程组。在Java的应用程序中,最高层的线程组是名位main的线程组,在main中还可以加入线程或线程组,在mian的子线程组中也可以加入线程和线程组,形成线程组和线程之间的树状继承关系
第三节 线程的控制与调度
线程的生命周期
第3页共12页
Java2程序设计实用教程(电子教案)
线程的状态表示线程正在进行的活动以及在此时间段内所能完成的任务。线程有创建、可运行、运行中、阻塞、死亡五中状态。一个具有生命的线程,总是处于这五种状态之一。
创建状态
使用new运算符创建一个线程后,该线程仅仅是一个空对象,系统没有分配资源,称该线程处于创建状态(new thread)
可运行状态
使用start()方法启动一个线程后,系统为该线程分配了除CPU外的所需资源,使该线程处于可运行状态(Runnable)
运行中状态
Java运行系统通过调度选中一个Runnable的线程,使其占有CPU并转为运行中状态(Running)。此时,系统真正执行线程的run()方法。
阻塞状态
一个正在运行的线程因某种原因不能继续运行时,进入阻塞状态(Blocked) 死亡状态
线程结束后是死亡状态(Dead)
线程调度与优先级 线程的调度模型
同一时刻如果有多个线程处于可运行状态,则他们需要排队等待CPU资源。此时每个线程自动获得一个线程的优先级(priority),优先级的高低反映线程的重要或紧急程度。可运行状态的线程按优先级排队,线程调度依据优先级基础上的“先到先服务”原则。
线程调度管理器负责线程排队和CPU在线程间的分配,并由线程调度算法进行调度。当线程调度管理器选种某个线程时,该线程获得CPU资源而进入运行状态。
线程调度是先占式调度,即如果在当前线程执行过程中一个更高优先级的线程进入可运行状态,则这个线程立即被调度执行。先占式调度分为:独占式和分时方式。
独占方式下,当前执行线程将一直执行下去,直 到执行完毕或由于某种原因主动放弃CPU,或CPU被一个更高优先级的线程抢占
分时方式下,当前运行线程获得一个时间片,时间到时,即使没有执行完也要让出CPU,进入可运行状态,等待下一个时间片的调度。系统选中其他可运行状态的线程执行
分时方式的系统使每个线程工作若干步,实现多线程同时运行
线程的优先级用1~10表示,1表示优先级最高,默认值是5。每个优先级对应一个Thread类的公用静态常量。如:
public static final int NORM_PRIORITY=5 public static final int MIN_PRIORITY=1 public static final int MAX_PRIORITY=10
改变线程状态 1)
线程睡眠sleep( )
public static void sleep(long millis)throw InterruptedException
当前线程睡眠(停止执行)若干豪秒,线程由运行中的状态进入不可运行状态,睡眠时间过后进程再进入可运行状态。
2)
暂停线程 yield( )
public static void yield( )
yield()暂停当前线程执行,允许其他线程执行。该线程仍处于可运行状态,不转为阻塞状态。此时,系统选择其他同优先级线程执行,若无其他同优先级程,则选中该线程继续执行。
3)
连接线程join()
第4页共12页
Java2程序设计实用教程(电子教案)
join( )方法使当前暂停执行,等待调用该方法的线程结束后再继续执行本线程。它有三种调用方法:
public final void join() throws InterruptedException public final void join(long mills) throws InterruptedException public final void join(long mills,int nanos) throws InterruptedException 等待调用该方法的线程结束,或者最多等待millis毫秒+nanos纳秒后,再继续执行本线程。
如果需要在一个线程中等待,直到另一个线程消失,可以调用join()方法。如果当前线程另一线程中断,join()方法会抛出InterruptedException异常。
4)
中断线程interrupt() public void interrupt( ) public boolean isInterrupted( ) public static boolean interrupted( )
interrupt()方法为线程设置一个中断标记,以便于run()方法运行时使用isInterrupted()方法能够检测到,此时,线程在sleep()方法抛出一个Interr——uptedException异常,然后捕获这个异常以处理超时。 例 7.4 改变线程状态。
本例演示线程对象的生命周期从创建到结束的过程,其间使用new()、start()、sleep()、interrupt()等方法改变线程的状态。本例综合运用内部类、图形界面、线程等多方面技术实现设计思想。程序如下:
import java.awt.*; import java.awt.event.*;
public class Welcome extends WindowAdapter implements ActionListener {
Frame f;
static Welcome.Thread3 wt1,wt2; public static void main(String arg[]) {
Welcome w= new Welcome(); w.display();
wt1=w.new Thread3(\"Welcome!\"); wt2=w.new Thread3(\"How are you?\"); wt2.start();
wt2.setButton(); //设置按钮状态 }
public void display() {
f = new Frame(\"Welcome\"); f.setSize(400,240); f.setLocation(200,140);
f.setBackground(Color.lightGray); f.setLayout(new GridLayout(4,1)); f.addWindowListener(this); f.setVisible(true); }
public class Thread3 extends Thread {
Panel p1;
第5页共12页
Java2程序设计实用教程(电子教案)
Label lb1; TextField tf1,tf2; Button b1,b2;
int sleeptime = (int)(Math.random()*100); public Thread3(String str) {
super(str);
for(int i=0;i<100;i++) str = str + \" \"; tf1 = new TextField(str); f.add(tf1); p1 = new Panel();
p1.setLayout(new FlowLayout(FlowLayout.LEFT)); lb1 = new Label(\"sleep\"); tf2 = new TextField(\"\"+sleeptime); p1.add(lb1); p1.add(tf2);
b1 = new Button(\"启动\"); b2 = new Button(\"中断\"); p1.add(b1); p1.add(b2);
b1.addActionListener(new Welcome()); b2.addActionListener(new Welcome()); f.add(p1); f.setVisible(true); }
public void run() {
String str;
while (this.isAlive() && !this.isInterrupted())
{ //线程活动且没中断时 try {
str = tf1.getText();
str = str.substring(1)+ str.substring(0,1); tf1.setText(str); this.sleep(sleeptime); }
catch(InterruptedException e)
{ //中断时抛出 System.out.println(e);
break; //退出循环 } }
第6页共12页
Java2程序设计实用教程(电子教案)
}
public void setButton() //设置按钮状态 {
if (this.isAlive()) b1.setEnabled(false); if (this.isInterrupted()) b2.setEnabled(false); } }//线程
public void windowClosing(WindowEvent e) {
System.exit(0); }
public void actionPerformed(ActionEvent e)
{ //单击按钮时触发 if ((e.getSource()==wt1.b1) || (e.getSource()==wt1.b2)) actionPerformed(e,wt1);
if ((e.getSource()==wt2.b1) || (e.getSource()==wt2.b2)) actionPerformed(e,wt2); }
public void actionPerformed(ActionEvent e,Thread3 wt1) { //重载 if(e.getSource()==wt1.b1) //启动 {
wt1.sleeptime=Integer.parseInt(wt1.tf2.getText()); wt1.start(); }
if(e.getSource()==wt1.b2) //中断 wt1.interrupt();
wt1.setButton(); //设置按钮状态 } }
第四节 线程的同步机制
前面所提到的线程都是的,而且异步执行,也就是说每个线程都包含了运行时所需要的数据或方法,而不需要外部的资源或方法,也不必关心其它线程的状态或行为。但是经常有一些同时运行的线程需要共享数据,此时就需考虑其他线程的状态和行为,否则就不能保证程序的运行结果的正确性。 1)
共享数据的线程“互斥“锁定 线程间的数据共享
为解决操作的不完整性问题,在Java 语言中,引入了对象互斥锁的概念,来保证共享数据操作的完整性。每个对象都对应于一个可称为\" 互斥锁\" 的标记,这个标记用来保证在任一时刻,只能有一个线程访问该对象。 关键字synchronized 来与对象的互斥锁联系。当某个对象用synchronized 修饰时,表明该对象在任一时刻只能由一个线程访问。 例 7.5 银行帐户的存取款线程设计。
本例设计三个类,银行帐户类Account1、存款线程类Save1和取款线程类Fetch1。
第7页共12页
Java2程序设计实用教程(电子教案)
程序如下:
class Account1 //帐户缓冲区 {
private String name; private int value;
void put(int i) //欲存入金额i {
value = value + i; //存入时,value值增加 }
int get(int i) {
if (value>i)
value = value - i; else {
i = value;
value = 0; }
return i; }
int howmatch() {
return value; } }
class Save1 extends Thread {
private Account1 a1; private int amount;
public Save1(Account1 a1,int amount) {
this.a1 = a1;
this.amount = amount; }
public void run() {
int k = a1.howmatch(); try {
sleep(1); }
catch(InterruptedException e) {
System.out.println(e);
//欲取金额i,返回实际取到金额//取走时,value值减少 //帐户金额不够所取时 //取走全部所余金额 //查看帐户上现有金额 //存款线程 //花费时间 第8页共12页
Java2程序设计实用教程(电子教案)
}
a1.put(amount);
System.out.println(\"现有\"+k+\存入\"+amount+ \余额\"+a1.howmatch()); }
public static void main (String args[]) {
Account1 a1 = new Account1(); (new Save1(a1,100)).start(); (new Save1(a1,200)).start(); (new Fetch1(a1,500)).start(); } }
class Fetch1 extends Thread //取款线程 {
private Account1 a1; private int amount;
public Fetch1(Account1 a1,int amount) {
this.a1 = a1 ; this.amount = amount; }
public void run() {
int k = a1.howmatch(); try {
sleep(1); //花费时间 }
catch(InterruptedException e) {
System.out.println(e); }
System.out.println(\"现有\"+k+\取走\"+a1.get(amount)+ \余额\"+a1.howmatch()); } }
程序运行结果:
现有0, 存入100, 余额100 现有0, 存入200, 余额300 现有0, 取走300, 余额0 2)
Synchronized
Synchronized锁定一段代码,称为创建一个代码临界区,使得线程必须等候特定资源的所有权。 当第一个线程执行这段代码时,它获取特定的对象的所有权,即拥有该对象的锁。此时,如果有第二
第9页共12页
Java2程序设计实用教程(电子教案)
个线程对同一个对象也要执行这段代码时,它试图获取该对象的所有权,但因该对象已被锁定,则第二个线程必须等待,直到锁被释放为止。第一个线程执行完<语句>后,自动释放了锁,接下去第二个线程获得锁并可运行。这样就形成了多个线程对同一个对象的“互斥”使用方式,该对象称为“同步对象”。
这种锁定方式是针对某个特定对象而言的。如果有两个线程同时对两个不同的对象进行操作,则没有锁定,它们可以同时进入代码的临界区 例 7.6 带锁定的存取款线程设计。
class Save2 extends Thread //存款线程 {
private Account1 a1; private int amount;
public Save2(Account1 a1,int amount) {
this.a1 = a1;
this.amount = amount; }
public void run() {
synchronized (a1) //锁定帐户对象 {
int k = a1.howmatch(); try {
sleep(1); //花费时间 }
catch(InterruptedException e) {
System.out.println(e); }
a1.put(amount);
System.out.println(\"现有\"+k+\存入\"+amount+ \余额\"+a1.howmatch()); } }
public static void main (String args[]) {
Account1 a1 = new Account1(); (new Save2(a1,100)).start(); (new Save2(a1,200)).start(); (new Fetch2(a1,500)).start(); } }
class Fetch2 extends Thread //取款线程 {
private Account1 a1;
第10页共12页
Java2程序设计实用教程(电子教案)
private int amount;
public Fetch2(Account1 a1,int amount) {
this.a1 = a1 ; this.amount = amount; }
public void run() {
synchronized (a1) //锁定帐户对象 {
int k = a1.howmatch(); try {
sleep(1); //花费时间 }
catch(InterruptedException e) {
System.out.println(e); }
System.out.println(\"现有\"+k+\取走\"+a1.get(amount)+ \余额\"+a1.howmatch()); } } }
程序运行结果:
现有0, 存入100, 余额100 现有100, 存入200, 余额300 现有300, 取走300, 余额0 1)
传送数据的线程同步运行 线程间传送数据
如果多个线程同时运行,而且相互间需要传送数据,则必须使线程运行的步调一致,才能保证传送的数据及时准确收到,这称为线程同步问题。 2)
synchronized与“互斥锁”标志
如果将Buffer1类中的put和get方法声明为“互斥”方法: synchronized void put(int){ } synchronized int get(){}
则意味着任一时刻只能有一个线程访问put或get方法。但可以有两个线程同时分别访问put和get方法。所以必须增加称为“互斥锁标志”(lock flag)的信号量。这样在任一时刻只有一个线程可以更改共享数据,从而保证数据的完整性和一致性。
死锁问题
多线程在使用互斥机制实现同步的同时,存在“死锁”的潜在危险。如果多个线程都处于等待状态而无法被唤醒时,就构成死锁(deallock)此时处于等待状态的多个线程占用系统资源,但无法运行,因此不会释放自己的资源,由于系统资源有限程序停止运行。
Java技术即不能发现死锁也不能避免死锁。所以程序员编程时应注意死锁问题,尽量避免。避免死锁
第11页共12页
Java2程序设计实用教程(电子教案)
的有效方法是:
线程因为某个条件未满足而受阻,不能让其继续占有资源
如果有多个对象需要互斥访问,应确定线程获得锁的顺序,并保证整个程序以相反的顺序释放锁
第12页共12页
因篇幅问题不能全部显示,请点此查看更多更全内容
Copyright © 2019- huatuo0.cn 版权所有 湘ICP备2023017654号-2
违法及侵权请联系:TEL:199 18 7713 E-MAIL:2724546146@qq.com
本站由北京市万商天勤律师事务所王兴未律师提供法律服务