750 likes | 910 Views
第 6 章 Java 多线程. 主要内容 线程基本概念 线程状态及生命周期 Java 线程的创建 Thread 类和 Runnable 接口 线程调度与优先级 线程的控制和消息传递 线程同步和死锁 守护线程 ( Daemon ). 程序、进程与线程. 程序与进程 程序( program ) 是一段静态的代码文件,它是应用软件执行的 ( 描述 ) 蓝本。 进程( Process ) 是程序的一次动态执行过程,它依赖于操作系统,从分配内存、加载代码、执行相应操作至完成任务的一个完整过程。 这个过程本身也是动态的:从产生、运行、发展至死亡的过程。. 线程
E N D
第6章 Java多线程 主要内容 • 线程基本概念 • 线程状态及生命周期 • Java线程的创建 • Thread类和Runnable接口 • 线程调度与优先级 • 线程的控制和消息传递 • 线程同步和死锁 • 守护线程 (Daemon)
程序、进程与线程 • 程序与进程 • 程序(program) • 是一段静态的代码文件,它是应用软件执行的(描述)蓝本。 • 进程(Process) • 是程序的一次动态执行过程,它依赖于操作系统,从分配内存、加载代码、执行相应操作至完成任务的一个完整过程。 • 这个过程本身也是动态的:从产生、运行、发展至死亡的过程。
线程 • 线程是程序内的控制流,与进程相似,但执行单位比进程更小。 • 线程也称为轻型进程,不同线程间允许任务协作和数据交换。 • 一个程序在其运行过程中,可以产生多个线程,形成多条执行线索。每条线索,即每个线程也有它自身的产生、存在和消亡的过程。
多线程 • 多线程是实现程序并发的一种有效手段。 • 一个进程可以通过运行多个线程来并发地执行多项任务。 • 多个线程如何调度执行由系统来实现。 • 线程是程序中的单个执行流,多线程是一个程序 中包含的多个同时运行的执行流
线程与进程比较 • 进程:内核级的实体。 每个进程有自己的状态、有自己的专用数据段(独立内存资源),包含虚存映象、文件指示符用户ID等。这些结构都在内核空间中,用户程序只有通过系统调用才能访问与改变。 • 线程:用户级的实体。 线程结构驻留在用户空间中,能够被普通的用户级函数组成的线程库直接访问。寄存器(栈指针,程序计数器)是线程专有的成分。线程间共享数据段 • 一个进程中的所有线程共享该进程的状态。
线程并发 • 并发性(Concurrency)是两个或多个线程(或传统的进程)可以同时在执行代码之中;可以是相同的代码,也可以是不同的代码。这些线程可以一次执行,也可以多次执行,即一个已开始执行但被中断,而另外一个已开始了。 • 但在给定的时间点上,只有一个在CPU在处理一个线程 。
线程并行 • 并行性(Parallelism)是针对多处理器环境而言的,是指两个或多个线程真正同时运行在不同的CPU上。 • 在多处理器机上,很多不同的线程可以并行运行,或者说是同时运行。
Benefits of Threads • Takes less time to create a new thread than a process • Less time to terminate a thread than a process • Less time to switch between two threads within the same process • Since threads within the same process share memory and files, they can communicate with each other without invoking the kernel
Multithreading Operating system supports multiple threads of execution within a single process
How to Configure Threads • Kernel-Level Threads (KLTs) • User-Level Threads (ULTs) • Hybrid
KLTs • Kernel maintains context information for both the process and the threads • Kernel provides thread control API for applications • Scheduling is done on a thread basis • Examples of this kind of thread include Win XP/Server, Linux, and OS/2
ULTs • All thread management is done on the application level • It is supported by a thread libraryoutside the kernel e.g. pthread(POSIX thread) libraries, with all of the threadsmapping into a single kernellevel process • The kernel is not aware of the existence of threads
Combine KLT and ULT Both KLT and ULT have their pro and con • By using hybrid strategy, the configuration can take advantages of both • If the parallelism of application is logical and needs no hardware support, then use the ULT • E.g. In a windowing system, all but one are idle • If it is a real parallelism, then use multiple KLT for performance gain • SMP can further enhance the performance by dispatch correlating threads to multiple processors simultaneously
Hybrid Approaches • Thread creation done in the user space • Bulk of scheduling and synchronization of threads done in the user space • User’s threads are supported by a set of kernel threads • An good example is Solaris
Solaris Thread And SMP Management Solaris takes the hybrid approach It makes use of four separate thread-related concepts • Processnormal UNIX process • User-level threads(ULTs) are implemented through a threads library in the address space of a process, these are invisible to the operating system. ULTs are the interface for application parallelism • Lightweight processes(LWP) a mapping between ULTs and kernel threads Each LWP supports one or more ULTs and can be visible within a process, hence LWP data structures exist within their respective process address space Each LWP is bound to a single dispatchable kernel thread, and the data structure for that kernel thread is maintained within the kernel's address space • Kernel threadsthe fundamental entities that can be scheduled and dispatched to run on one of the system processors
CPU Code Data Java中的线程 • Java 中线程被认为是一个CPU、程序代码、和数据的封装体 一个虚拟的CPU, 该CPU执行的代码: 代码与数据是相互独立的,代 码可以与其它线程共享。 代码所操作的数据:数据也可以 被多个线程共享。
Java 线程的作用 • Java 线程 • 从语言级提供对多线程的支持 (包括用户线程和系统线程),并提供对共享数据管理功能和同步机制。 • 应用Java多线程机制可以有效处理动画程序、游戏程序和大型Web服务程序等。 • Java线程分为: • 主线程:通过JVM启动的第一个线程。 • 子线程(Thread) • 通过Java应用程序创建的线程。 • 守护线程(Daemon) • 一种用于监视其他线程的服务线程。
线程的生命周期 • 一个线程从创建→工作→死亡的过程称为线程的生命周期 • 一个线程周期有四种状态: ⑴ 新建状态 ⑵ 可运行状态 ⑶ 中断(阻塞)状态 ⑷ 死亡状态
创建状态 • 新建状态 • 指创建了一个线程还没有启动它(此时它已经有了相应的内存空间和其他资源)。 • 创建新的线程通过Thread类及其子类说明 线程类名 线程对象名 = new 线程类名(线程名);
可运行状态 • 就绪状态 • 指具备了运行条件,进入可运行队列排队等待CPU服务,一旦轮到它来享用CPU资源时,就可以脱离创建它的主线程独立开始自己的生命周期。 • 当新建状态的线程调用了start( )方法进入就绪状态。 • 运行状态 • 当就绪状态的线程获得CPU处理器资源时,便进入工作运行状态。 • 每一个Thread类及其子类的对象都有一个重要的run()方法,当线程对象被调度执行时,它将自动调用该对象的run()方法,从方法体的第一句开始顺序执行,直到完成这个线程操作的所有功能。
中断状态 • 当正在运行的线程遇到某些特殊情况(如:延迟、挂起、等待和I/O设备等原因)时, 该线程将让出CPU并暂时中止自己的执行,进入中断(阻塞)状态。 • 线程进入中断状态,它不再参加排队,只有当引起阻塞的原因被消除后,线程又可以转入就绪状态,重新进入排队等待CPU资源,以便从原来终止处开始继续运行。 • 恢复运行态通常有三种途径: • 自动恢复(如:sleep、I/O操作) • 用resume()方法恢复 • 用notify()或notifyAll()方法恢复
死亡状态 • 有两种情况使一个线程终止,进入死亡状态 • 自然撤消(线程完成了全部工作,正常退出) • 被提前强制性地终止: • 调用stop()方法 • 调用interrupt()方法 • 注意:线程进入死亡状态,就不具有继续运行的能力,也不能再转到其他状态
阻塞 新建 死亡 run()出口 Interrupt() stop() stop( ) 可运行 线程的生命周期图 yield( ) 线程的状态 wait() suspend() sleep() I/O操作 new Thread( ) start( ) resume() notify() stop( )
线程调度与优先级 • Java 线程调度器 • 监视控制就绪状态的所有线程 • 线程调度策略(采用抢占式 ) • 优先级高的线程比优先级低的线程先执行; 优先级相同的情况下,按“先到先服务”原则 。 • 线程的优先级用常数表示。 • 每个线程根据继承特性自动获得一个线程的优先级,也可在程序中设置。 • 对于任务较紧急的重要线程,可安排优先级较高的;相反则较低。
设置/获取线程优先级方法 • getPriority()方法 • 获得线程的优先级 • setPriority(int 优先级) • 改变线程的优先级方法。创建时,继承父进程的优先级 • 优先级 • 数值越大优先级越高(范围 1-10,缺省是5 ) • 最低:Thread.MIN-PRIORITY • 最高: Thread.MAX-PRIORITY • 标准:Thread.NORM-PRIORITY • 举例 setPriority(Thread.MIN-PRIORITY) • 例:PriorityTest.java
Java编程中实现多线程的办法 • Java的线程实现主要通过Java.lang包中的Thread类和Runnable接口。 • 两种方式 • 通过继承Thread类定义一个线程类,对线程的操作要求可在该类的run( )方法中定义,再用此类创建对象来实现一个线程。 • 通过一个类去继承接口Runnable来实现线程的创建,这个类必须提供Runnable接口中的run()方法的实现。
Thread类 • Thread类 • 专门用来创建线程和对线程进行操作的类。 • Thread类的构造方法 • public Thread( ); • 创建一个线程 • public Thread(String m); • 创建一个以m命名的线程 • public Thread(Runnable target); • 创建线程,参数target称为被创建线程的目标对象。 • public Thread(ThreadGroup g, String m); • 创建一个以m命名的线程类,该类属于指定的线程组g • 例:ThreadGroupTest.java
线程的启动、操作和延迟方法 • start(); • 启动,由新建态到就绪态 • run(); • 实现线程行为(操作)的方法 • sleep(延迟时间); sleep(int millsecond); 或 sleep(int millsecond,int nanosecond); • 令线程在指定时间段内放弃对CPU控制,使同优先级其他线程有机会被执行,时间到重新排队 • 产生例外Interrupted Exception • 用try块调用sleep(),用catch块处理例外 • static方法,不可重载
创建线程 (Thread类) • Thread类创建线程的例子 import java.lang.* class MyThread extends Thread{ public void run() { //子类run方法覆盖父类run …… } } • 用new 使线程进入创建状态。 MyThread t = new MyThread(); t.start(); //用start()方法使线程进入可运行状态
获取线程名字和判断状态方法 • currentThread() • 判断当前正在占有CPU的那个线程。 • 举例:MainThread.java • getName() • 获取线程的名字 • setName() • 设置线程的名字 • isAlive() • 返回boolean, 表明是否还活跃
停止线程执行方法 • stop()与destroy() • 强制线程生命期结束 • stop()还完成一些清理工作,并抛出例外 • suspend() • 挂起线程,处于不可运行(阻塞)状态 • resume() • 恢复挂起的线程,重新进入就绪队列排队 • yield()//对正在执行的线程 • 若就绪队列中有与当前线程同优先级的排队线程, 则当前线程让出CPU控制权,移到队尾 • 若队列中没有同优先级的线程,忽略此方法
Thread类创建方法举例 • 例 • 在main主线程中创建了两个新的线程lefthand和righthand。当lefthand调用start()开始运行时,类Lefthand中的run()将自动被执行。 … • 分析程序的输出结果。 • Left线程先执行,这时run方法输出 “伸出左手”后,left主动“休息500毫秒,让出CPU。这时正在等待CUP的right线程获得运行资源,输出 “换右手”,right线程主动让出CPU300 毫秒后又来排队等待CPU服务,过了300毫秒后,发现left线程还没有“醒来”,因此有轮到right。又输出“换右手”
public class ExampleHand{ static Lefthand left; static Righthand right; public static void main(String args[ ]){ left=new Lefthand(); right=new Righthand(); left.start(); right.start(); } }
class Lefthandextends Thread { public void run(){ for(int i=0; i<=5; i++){ System.out.println("伸出左手。"); try{sleep(500);} catch(InterruptedExceptione){} } } } • 例:BidThread.java
Runnable接口 • 方法run() • 线程的所有活动都是通过线程体的方法run来实现 • run 方法体定义该线程的具体的执行内容。 • 当一个线程启动后,Java系统就自动地调用run()方法。 • 通常run()方法的工作是一种循环。 • 一个实现了Runnable接口的类实际上定义了一个主线程之外的新线程的操作,而定义新线程的操作和执行流程,是实现多线程应用的最主要和最基本的工作
创建线程 (实现Runnable接口) • 接口Runnable创建线程的例子 public class xyz implement Runnable { //定义一个接口连接 int i; public void run( ) { //定义一个抽象方法run()的实现 while (true) { System.out.println(“Hello” + i++); } } } • 先建一个对象,再创建一个线程 Runnable r = new xyz( ); Thread t = new Thread( r ); //这种方法比较灵活, *对于一个类既需要继承父类又要由此创建一个线程时,可用接口解决 • 例:BidRun.java Auction.java
采用间接创建线程的原因 • 第一个理由是我们并不改变线程本身的性质,仅覆盖run方法,并没有增加新的功能,因此将Thread扩展子类并不恰当,这不太符合类扩展规范。 • 第二个理由是:如果实现Runnable接口,它可能使我们所设计的类扩展其它类型而变得更为有用。
Daemon线程 • Daemon线程 • 称为系统守护线程,一种专门为其他线程提供服务的线程(如:内存垃圾收集) • 通常在一个较低的优先级上运行 • 守护线程的特点:无限循环运行 • Thread类提供的方法 setDaemon( true );//将线程设置为守护线程 isDaemon( );//检测是否守护线程
Daemon线程举例 Daemon 线程的框架是: class DaemonThread extend Thread { DaemonThread( ) { setDaemon( true ); start( ); } public void run( ) { while ( true ) {... // 等待服务请求和处理 } } } 例:DaemonTest.java
与线程所处状态有关的方法举例 • join方法:等待指定的线程运行结束. • 例子:JoinTest.java • yield方法:让当前线程主动放弃对CPU的占用. • 例子:YieldTest.java • interrupt方法的例子: InterruptTest.java
多线程并发执行中的问题 • 多个线程相对执行的顺序是不确定的。 • 线程执行顺序的不确定性会产生执行结果的不确定性。 • 在多线程对共享数据 操作时常常会产生这种不确定性。 • 举例: ThreadSharedData.java
银行业务中共享资源冲突示意 举例: AccountSimulator.java
线程间的同步 • 线程同步 • 解决多线程争夺同一变量 (共享资源) • 线程同步技术,用于协调多线程中对某个方法或者某个代码的执行秩序 • 避免多线程在并发运行时对共享数据处理过程中,发生的数据不一致问题 • 如何进行多线程中的同步控制 • 同步控制原理、编程方法和处理原则 • wait()、notify()和notifyAII()方法的使用
同步控制原理 • 管程(monitor) • 实现加锁,解决多线程中争夺共享资源 • 管程作用 • 控制资源使用,一个资源在同一时刻只能供一个线程使用 • 当资源未被占用,线程可以进入(这个资源的)管程,从而得到该资源的使用权;当线程执行完毕,便退出管程 • 如果一个线程已进入某资源的管程,其它的线程必须等待 • 管程与Java对象 • 每一个Java对象都与一个管程联系 • 在同一类中的所有静态同步方法都使用同类的管程 • 所有实例同步方法使用同一个实例对象管程
Java编程中的同步控制 • 关键字synchronized定义临界区 • 锁住共享资源 • 在Java中可以是某个方法或者某个代码块 • 同步的协调方法 • 方法 wait( ) notify( ) notifyAll( )
同步控制举例 • 例有两个线程:会计和出纳,他俩共同拥有一个账本。 • 他俩都可以使用存取方法对账本进行访问,会计使用存取方法时,向账本上写入存钱记录;出纳使用存取方法时, 向账本写入取钱记录。 • 因此,当会计正在使用账本时,出纳被禁止使用,反之也是这样。比如, 会计每次使用账本时,在账本上存入90元钱,但在存入这笔钱时,每写入30元,就喝口茶,那么他喝茶休息时(注意,这时存钱这件事还没结束,即会计还没有使用完存取方法),出纳仍不能使用账本。从周一到周三会计和出纳都要使用账本,我们要保证其中一人使用账本时,另一个人必须等待。
import java.applet.*; import java.awt.*; import java.awt.event.*; public class Example19_7 extends Applet implements Runnable{ int money=100; TextArea text1,text2; Thread 会计,出纳; public void init(){ 会计=new Thread(this); 出纳=new Thread(this); text1=new TextArea(20,8); text2=new TextArea(20,8); add(text1);add(text2); } public void start(){ 会计.start(); 出纳.start(); //线程开始。 }
//存取方法 public synchronized void 存取(int number) { if(Thread.currentThread() == 会计){ for(int i=1; i<=3; i++) { //会计使用存取方法存入90元,存入30元,稍歇一下, money=money+number; //这时出纳仍不能使用存取方法, try{Thread.sleep(1000);} //因为会计还没使用完存取方法。 catch(InterruptedExceptione){} text1.append("\n"+money);}} if(Thread.currentThread() == 出纳){ for(int i=1; i<=2; i++) { //出纳使用存取方法取出30元,取出15元,稍歇一下, money=money-number/2; //这时会计仍不能使用存取方法, try{Thread.sleep(1000);} //因为出纳还没使用完存取方法。 catch(InterruptedExceptione){} text2.append("\n"+money); } } }
public void run(){ if(Thread.currentThread()==会计||Thread.currentThread()==出纳){ for(int i=1;i<=3;i++) //从周一到周三会计和出纳都要使用账本。 { 存取(30); } } } } <HTML> <APPLET CODE="Example.class" WIDTH=400 HEIGHT=200> </APPLET> </HTML>