nullnull JavaEE基础教程 第9章 多线程9.1 线程概述9.2 线程的创建9.3 线程的调度null9.1 线程概述9.1.1 进程的概念
进程是程序的一次执行过程,对应了从代码的加载、执行到执行结束这样一个完整的过程,也是进程从产生、发展到消亡的过程。每个进程在计算机的内存中都对应一段专有的内存空间。现在的操作系统都支持多进程操作,比如:计算机可以同时播放视频、声音,同时还可以上网、聊天等。本章介绍的多线程技术与多进程技术是不一样的。
9.1.2 线程的概念
线程是比进程更小的执行单元,单个进程的执行可以产生多个线程。每个线程都有独立的生命周期,同一个进程中的线程共享同样的内存空间,并通过共享的内存空间来达到数据交换、通信和同步等工作。在基于线程的多任务处理环境中,线程是执行特定任务的最小单位。一个程序可分为多个任务,每个任务都可分配给一个线程来实现。在Java程序启动时,一个进程马上启动,同时该进程会自动启动一个线程的运行,这个线程称为程序的主线程。因为它是在程序启动后就执行的。
该主线程是多线程编程的核心,它是产生其他子线程的线程。在多线程运行时,它是第一个启动的线程。由该线程控制其他线程的启动,执行各种关闭操作。
返回null9.2 线程的创建 Java在类和接口方面提供了对线程的内置支持,任何类如果希望能够以线程的形式运行,都需要实现接口java.lang.Runnable;或者继承java.lang.Thread类。Runnable接口只有一个run()方法,实现该接口的类必须重写该方法。而Thread类也实现了Runnable接口,但该类有更丰富的方法。Thread类的常用方法包括start()方法、run()方法和join()、interrupt()方法、join()方法等。start()方法用于启动线程,而run()方法是线程的主体方法,线程完成的功能代码都写在该方法体内。
Thread类定义了8个常用的构造方法。下面的两个是较为常用的:
Thread() //创建一个具有默认
参数
转速和进给参数表a氧化沟运行参数高温蒸汽处理医疗废物pid参数自整定算法口腔医院集中消毒供应
值的Thread对象 Thread(String name) //创建一个线程名为name的Thread对象
返回null9.2.1 继承Thread类
该类具有创建和运行线程的所有功能,通过重写该类的run()方法,实现用
户所需的功能。通过实例化自定义的Thread类,使用start()方法启动线程。
例9-1 继承Thread类创建MyThread1类,显示主线程的信息,创建子线程并启动它
程序清单:ch09\MyThread1.java
public class MyThread1 extends Thread {
public static void main(String[] args) {
Thread t=Thread.currentThread(); //A
System.out.println("当前主线程是:"+t); //B
t.setName("MyThread1"); //C
System.out.println("当前主线程是:"+t); //D
MyThread1 mt=new MyThread1(); //E
mt.start(); //F
}null public void run() { //G
int sum=0;
for(int i=0;i<101;i++) sum+=i;
System.out.println("1+2+...+100="+sum);
} }
程序的执行结果如下所示:
当前主线程是:Thread[main,5,main]
当前主线程是:Thread[MyThread1,5,main]
1+2+...+100=5050
分析上面的程序代码可知,A行代码通过调用Thread类的currentThread()静态方法获得
当前主线程的引用。然后在B行代码输出主线程的信息;输出结果“Thread[main,5,main]”
的第一个main代
表
关于同志近三年现实表现材料材料类招标技术评分表图表与交易pdf视力表打印pdf用图表说话 pdf
主线程的名称,5代表它的优先级,第二个main代表线程组。通过代码行C
修改主线程的名称,代码行D再次输出主线程的信息:第一个参数值改为修改后的值
“MyThread1”。代码行E创建了子线程mt,代码行F启动了该子线程,执行线程类的run()
方法。从代码行G开始,重写了Thread类的run()方法,实现自定义的功能,本例是实现了
“1到100的累加和”的功能。注意:在本例中,如果不重写run()方法,程序正常运行,
只是此时的子线程不完成任何功能;子线程mt的启动是在主线程中实现的,而子线程在运行
完run()方法后也自动终止了。null9.2.2 实现Runnable接口
在Java中,不仅可以通过继承Thread类实现多线程的功能,也可以通过实现Runnable接口
来实现同样的功能。由于Java语言规定的单一继承原则,所以如果希望用户自定义的类继承其他类,此时可以通过实现Runnable接口的方式使用线程。
例9-2 创建SimpleThread类,实现Runnable接口,并在run()方法中实现规定的输出功能:在控制台输出字符“*”。
程序清单:ch09\SimpleThread.java
public class SimpleThread implements Runnable {
public static void main(String[] args) {
Thread t=new Thread(new SimpleThread(),"线程1"); //A
t.start();
System.out.println("主线程运行结束"); //B
}
public void run() {
int i=1;
while(i<=10){
try {
System.out.print("*");
Thread.sleep(1000); //C
}nullcatch (InterruptedException e){
e.printStackTrace();
}
i++;
}
}
}
类SimpleThread实现了线程接口Runnable,重写了接口中的方法run()。在代码行A中,
通过调用Thread类的构造函数:
public Thread(Runnable target)
创建了一个线程对象t;然后启动该线程。在run()方法中的代码行C,调用了Thread类的静态
方法sleep(long millis),该方法的参数代表线程休眠的毫秒数,本例中的1000代表1秒钟。
可见,run()方法实现的功能是每个1秒钟输出一个字符“*”,一共输出10个字符。在运行该
程序时,主线程在运行到代码行B时已经结束了,而子线程的run()方法此时还在运行。显
然,两个线程的运行是相互独立的,主线程只能启动子线程的运行,而不能终止它的运行。
程序的输出结果如下所示:
主线程运行结束
**********
null9.3 线程的调度9.3.1 线程的生命周期
线程从创建到死亡的整个过程称为线程的一个“生命周期”。在某个时间点上,线程具有不同的状态,主要的状态有如下几种:
创建状态
可执行状态
非执行状态
终止状态
线程的各个状态间的关系如图9-1所示:
返回null图9-1线程的状态转换图 null下面根据图9-1分别介绍线程生命周期的各个状态:
(1) 创建状态
当使用线程类的构造函数创建某个线程类的对象时,线程处于“创建状态”,在调用了对
象的start()方法后,线程进入了“可执行”状态。
(2) 可执行状态
在线程进入“可执行”状态后,如果系统的CPU空闲,则线程就可以直接投入运行了。在
线程运行时,如果调用线程的wait()方法或者sleep()方法,则线程进入了“非可执行”状
态,此时系统的CPU不再分配时间片给该线程。
(3) 非可执行状态
在线程进入“非可执行”状态后,可以通过调用线程的notify()方法或者notifyall()方
法、interrupt()方法再次进入“可执行”状态。
(4) 终止状态
当线程的run()方法执行完毕后,线程自动消亡,该线程占用的系统资源会自动释放。该
线程的整个生命周期就此结束。
null9.3.2 线程的优先级
根据前面介绍的生命周期,线程创建后调用了start()方法就进入了“可执行”状态。
如果同时有多个线程进入了“可执行”状态,而系统只有一个CPU时,或者CPU的个数少于进入
“可执行”状态的线程的个数时,如何调度线程的运行呢?此时可以通过设定线程的优先级来
决定首先执行哪个线程。线程的优先级是通过Thread类中定义的常量来实现的,Thread类中定
义了三个此类常量:
MAX_PRIORITY 线程的最高优先级,代表常量值10
NORM_PRIORITY 线程的默认优先级,代表常量值5
MIN_PRIORITY 线程的最低优先级,代表常量值1
在Java中的每个线程都有一个优先级。在缺省情况下,线程的优先级为NORM_PRIORITY或
5。设置和获取线程优先级的方法有:
void setPriority(int newPriority)
int getPriority()
在线程运行时,一旦高优先级的线程要运行,则低优先级的线程将进入“非可执行”状
态,CPU的控制权交给高优先级的线程。null9.3.3 线程的同步
通过线程的优先级可以设置线程占用CPU时间的策略,保证多个线程能合理地、
顺序地占用CPU时间,执行自己的run()方法。但是,如果出现多个线程同时操作
一个共享资源,比如打印机、文件等,如何分配多个线程对打印机的操作和控制?
为了处理这种对共享资源的竞争,Java语言提供了线程的同步机制。所谓同步机制
是指两个或多个线程同时访问一个对象时,应该保存对象数据的统一性和完整性。
同步是基于“监视器”的概念,类似于平时说的“黑盒子”,一旦某个线程获得控
制权后,其他线程只能等待,直到原先的线程放弃对“黑盒子”的控制,其他线程
才能获得对“黑盒子”的控制权。Java语言提供了对线程同步的内置支持,通过使
用Java语言提供的synchronized关键字,可以采用同步方法或同步代码块的方法实
现线程的同步。
下面分别介绍如何使用同步方法和同步代码块实现线程的同步。
(1)同步方法
同步方法是指将访问共享资源的方法都标记为synchronized,这样当某个线程调
用了该方法后,其他调用该方法的线程将进入阻塞状态,直到原线程完成对
synchronized方法的调用为止。
null例9-3 创建两个线程,同时调用某个类的print()方法,把print()方法定义为同步和
非同步两种方法,分析执行结果的差异。
程序清单:ch09\SyncExample.java
class PrintCH{
public static /*synchronized*/ void print(char c){
for(int i=0;i<4;i++){
System.out.print(c);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SyncExample extends Thread {
private char ch;
public SyncExample(char ch) {
this.ch = ch;
}null public void run(){
PrintCH.print(ch);
}
public static void main(String[] args) {
SyncExample t1=new SyncExample('A');
SyncExample t2=new SyncExample('B');
t1.start();
t2.start();
}
}
如果去掉print()方法前的synchronized关键字,print()方法为非同步方法,则
执行结果如下所示:
ABBABAAB
如果加上synchronized关键字,该方法为同步方法,则结果如下所示:
AAAABBBB
null(2) 同步代码块
尽管可以在创建类的时候,把访问共享资源的方法定义为同步方法,实现线程的同步,
但是这种方法并不是一直有效的。比如:程序中调用了一个第三方类库中某个类的方法,
无法获得该类库的源代码。这样,无法在相关方法前添加synchronized关键字。那么,怎
么才能解决这类问
题
快递公司问题件快递公司问题件货款处理关于圆的周长面积重点题型关于解方程组的题及答案关于南海问题
呢?通过使用Java语言的同步代码块机制可以解决这个问题。同步代
码块的一般格式如下:
synchronized(object){
//要同步的语句
}
其中,object是需要被同步的对象的引用。一个同步块在调用object对象的某个方法之前,
必须确保所在线程能够访问object对象,获得该对象的控制权。在某一时刻,只能有一个线
程获得该对象的控制权,从而保证一次只能有一个线程执行该同步块。
例如:假定线程A和B都希望访问同步块中的代码,线程A已经进入同步块内,那么线程B
就必须等待。因为此时线程A获得了对象object的控制权,而线程B只能等待线程A执行完同步
块中的代码,从同步块中退出,然后放弃对object的控制权,线程B才能进入同步块,执行其
中的代码。null例9-4 修改例9-3,用代码块实现线程同步的功能
程序清单:ch09\SyncMassExample.java
class PrintCH2{
public void print(char c){
for(int i=0;i<4;i++){
System.out.print(c);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
public class SyncMassExample extends Thread {
private char ch;
private static PrintCH2 myprint=new PrintCH2(); //A
public SyncMassExample(char ch) {
this.ch = ch;
}
nullpublic void run(){
synchronized(myprint){ //B
myprint.print(ch);
}
}
public static void main(String[] args) {
SyncMassExample t1=new SyncMassExample('A');
SyncMassExample t2=new SyncMassExample('B');
t1.start();
t2.start();
}
}
这段代码与例9-3相比,添加了代码行A和B,其中A行创建了一个静态对象,代码行B采
用了同步块的语法结构,只有获得对象myprint的控制权,才能运行大括号里的代码。通过
采用这种方法,不用修改原始类PrintCH2,就可以实现同步功能了。
以上两种同步线程的方法,可以根据实际情况灵活运用。
null9.3.4 wait-notify机制
在前面的实例中,两个线程都试图访问某个共享资源,通过采用同步方法或
者代码块可以解决多个线程访问共享资源的问题。那么,假如有两个线程A和B,
A线程需要首先访问共享资源M,然后访问共享资源N;线程B也需要访问共享资源
M和N。现在,A线程已经拥有资源M的控制权,需要资源N,线程才能正常运行;
而线程B已经获得资源N的控制权,需要资源M。此时,线程A在等待线程B释放资
源N,而线程B在等待线程A释放资源M。这样两个线程互相等待,永远不会结束,
程序进入“死锁”状态。
为了解决这类问题,Java语言提供了“wait-notify机制”。Java语言通过
使用wait()、notify()和notifyAll()方法实现线程间的通信,从而尽量避
免多线程运行时出现“死锁”的情况。
wait()方法通知被调用的线程放弃对共享资源的控制,进入等待状态,直
到其他线程释放了共享资源并调用notify()方法。 notify()方法唤醒同一
对象上第一次调用了wait()方法的线程。notifyAll()方法唤醒所有调用了
wait()方法的线程,此时退出睡眠状态的、优先级最高的线程将恢复执行。null在使用这三个方法时要注意以下几点:
(1)线程调用wait()方法并进入等待状态时,会释放已经控制的共享资源,必须
由当前线程自己调用wait()方法,即:是线程本身在得不到需要的资源时,主动放
弃对已有资源的控制,进入等待状态。
(2)线程调用notify()和notifyAll()方法时,是在当前线程已经使用完所控制
的共享资源,并且已经放弃了对共享资源的控制时,通知其他线程恢复执行。
(3)线程不能自己调用notify()或者notifyAll()方法唤醒自己;线程不能调用
wait()方法要求其他线程进入等待状态。
null例9-5 4位哲学家用4根筷子吃饭的问题
程序清单:ch09\WaitNotiExample.java
本程序是Java语言使用wait-notify机制的典型例题。4位哲学家一边就餐,一边思考,
每个人之间只有一根筷子,而哲学家要就餐必须有两个筷子,所以任一时刻,只能有两个
人就餐,两个人思考。
本程序创建了三个类,其中ChopStick类中定义了一个变量available,指明是否有筷
子,方法takeup()和putdown()均被定义为同步方法,takeup()方法表示已经有一根
筷子,需要等待第二根筷子,因而在方法体中调用了wait()等待另一个筷子;putdown
()方法表示放下筷子,将筷子让给别人,并调用了notify()方法通知其他被阻塞的线
程。类Philosopher是一个自定义线程类,方法eat()模拟了就餐的过程,先拿两根筷
子,然后就餐;方法think()模拟了思考的过程,放下两根筷子,然后思考;run()方
法先就餐,然后睡眠1秒钟,再思考、睡眠,一直循环下去。类WaitNotiExample是测试
类,初始化两个数组:chopsticks和philos,模拟筷子和哲学家。其中四位哲学家对应了
四个线程,它们的优先级是一样的。
null 通过使用wait()和notify()方法,四个线程间实现了通信功能,避免出现死锁的
情况。在程序运行时,由于所有的筷子都空着,所以第一个线程对应的哲学家能顺利拿到
两根筷子开始就餐,而第二个线程对应的哲学家就必须等待了,此时调用了wait()方法
进入等待状态;当第一位哲学家用餐后,放下筷子,调用了notify()方法,通知其他等
待的线程,可以访问共享资源:筷子。不断循环下去,每位哲学家都能顺利地获得就餐的
机会。也就是说,每个线程都能正常地运行,不会发生死锁的情况。程序的运行结果如下
所示:
哲学家 1 在用餐
哲学家等待另一根筷子
哲学家等待另一根筷子
哲学家等待另一根筷子
哲学家等待另一根筷子
哲学家 4 在用餐
哲学家 1 在思考
……
以上的输出结果在每次运行后可能会有所不同,但是每一位哲学家都有机会用餐、思
考,而且每一位获得的机会都是均等的。这就是线程间的wait-notify机制。
nullThe End