1、第8章 多线程,8.1 多线程的概念 8.2 线程类 8.3 资源的协调与同步 8.4 线程间通信,8.1 多线程的概念,程序是一段静态的代码,它是应用软件执行的蓝本。进程就是程序的运行时的一个实例。 线程可以看作单独地占有CPU时间来执行相应的代码的。 线程是共享地址空间的,也就是说多线程可以同时读取相同的地址空间,并且利用这个空间进行交换数据。,8.1 多线程的概念,多线程具有以下特点: (1)多个线程在运行时,系统自动在线程之间进行切换; (2)由于多个线程共存于同一块内存,线程之间的通信非常容易; (3)Java将线程视为一个对象。线程要么是Thread类的对象,要么是接口Runnab
2、le的对象。 (4)当多个线程并行执行时,具有较高优先级的线程将获得较多的CPU时间片; (5)优先级是从0到10的整数,并且它仅表示线程之间的相对关系; (6)多个线程共享一组资源,有可能在运行时产生冲突。必须采用synchronized关键字协调资源,实现线程同步。,8.2 线程类,8.2.1 多线程编程中常用的常量和方法 8.2.2 线程的生命周期 8.2.3 创建多线程的方法,8.2.1 多线程编程中常用的常量和方法,Thread类包含的常量有: 1. public static final int MAX_PRIORITY: 最大优先级,值是10。 2. public static
3、final int MIN_PRIORITY: 最小优先级,值是1。 3. public static final int NORM_PRIORITY:缺省优先级,值是5。,8.2.1 多线程编程中常用的常量和方法,常用方法: currentThread( ):返回当前运行的线程对象,是一个静态的方法。 sleep(int n) : 使当前运行的线程睡n个毫秒,然后继续执行,也是静态方法。 yield( ) :使当前运行的线程放弃执行,切换到其它线程,是一个静态方法。 isAlive( ) : 判断线程是否处于执行的状态,返回值true表示处于运行状态,false表示已停止。 start( )
4、 :使调用该方法的线程开始执行。 run( ) :该方法由start( )方法自动调用。,8.2.1 多线程编程中常用的常量和方法,常用方法: stop( ) :使线程停止执行,并退出可执行状态。 suspend(): 使线程暂停执行,不退出可执行态。 resume( ) : 将暂停的线程继续执行。 setName(String s) :赋予线程一个名字。 getName( ) :获得调用线程的名字。 getPriority( ) :获得调用线程的优先级。 setPriority(int p) :设置线程的优先级。 join( ) :等待线程死亡,若中断了该线程, 将抛出异常。,【实例8-1】
5、 class getThreadInfo public static void main(String args ) Thread curr; int num=7;curr=Thread.currentThread( );curr.setPriority(num);System.out.println(“当前线程: “+curr);System.out.println(“线程名: “+ curr.getName( );System.out.println(“优先级 :“+ curr.getPriority( ); 程序输出结果: 当前线程: Threadmain,7,main 线程名 : ma
6、in 优先级 :7,8.2.2 线程的生命周期,Java支持一种“抢占式”(preemptive)调度方式 “Newborn”(新建)状态: 线程在己被创建但未执行这段时间内,处于一个特殊的“Newborn“状态,这时,线程对象己被分配内存空间,其私有数据己被初始化,但该线程还未被调度。此时线程对象可通过start()方法调度,或者利用stop()方法杀死.新创建的线程一旦被调度,就将切换到“Runnable“状态。,8.2.2 线程的生命周期,“Runnable“(就绪)状态: 表示线程正等待处理器资源,随时可被调用执行。处于就绪状态的线程事实上己被调度,也就是说,它们己经被放到某一队列等待
7、执行。处于就绪状态的线程何时可真正执行,取决于线程优先级以及队列的当前状况。线程的优先级如果相同,将遵循“先来先服务“的调度原则。,8.2.2 线程的生命周期,“Running”(运行)状态: 表明线程正在运行,该线己经拥有了对处理器的控制权,其代码目前正在运行。这个线程将一直运行直到运行完毕,除非运行过程的控制权被一优先级更高的线程强占。,8.2.2 线程的生命周期,“Blocked”(堵塞)状态: 一个线程如果处于“Blocked“(堵塞)状态,那么暂时这个线程将无法进入就绪队列。处于堵塞状态的线程通常必须由某些事件才能唤醒。至于是何种事件,则取决于堵塞发生的原因:处于睡眠中的线程必须被堵
8、塞一段固定的时间;被挂起、或处于消息等待状态的线程则必须由一外来事件唤醒。 “Dead”(死亡)状态: Dead表示线程巳退出运行状态,并且不再进入就绪队列。其中原因可能是线程巳执行完毕(正常结束),也可能是该线程被另一线程所强行中断(kill)。,图8-1 线程生命周期示意图,start,时间片结束,分配时间片,睡眠时 间结束,notify,notify All,sleep,wait,stop,I/O请求,suspend,resume,I/O请求结束,8.2.3 创建多线程的方法,Java中编程实现多线程应有两种途径 一种是创建Thread线程的子类 实现一个接口Runnable 无论是哪种
9、途径最终都需要使用Thread类及其方法。,8.2.3 创建多线程的方法,1通过继承Thread类实现多线程 (1) 定义Thread类的一个子类。 (2) 定义子类中的方法run( ),覆盖父类中的方法run( )。 (3)创建该子类的一个线程对象。 (4) 通过start( )方法启动线程。,【实例8-2】 class myThread extends Threadint sleeptime;public myThread(String id) / 构造函数super(id);sleeptime=(int)(Math.random( )*100);System.out.println(“T
10、he Thread Name=“+getName( )+“,Sleeping: “+sleeptime);public void run()try / 通过线程睡眠模拟程序的执行 Thread.sleep(sleeptime); catch(InterruptedException e)System.err.println(“Exception:“ +e.toString();System.out.println(“The running Thread=“+getName(); ,【实例8-2】 public class fourThreadspublic static void main(S
11、tring args ) myThread t1,t2,t3,t4;t1=new myThread(“Thread 1“);t2=new myThread(“Thread 2“); t3=new myThread(“Thread 3“); t4=new myThread(“Thread 4“); t1.start( ); t2.start( );t3.start( ); t4.start( ); ,【实例8-2】 程序某次的运行结果: he Thread Name=Thread 1,Sleeping: 6 The Thread Name=Thread 2,Sleeping: 49 The Th
12、read Name=Thread 3,Sleeping: 19 The Thread Name=Thread 4,Sleeping: 69 The running Thread=Thread 1 The running Thread=Thread 3 The running Thread=Thread 2 The running Thread=Thread 4 注意:Thread类中的run( )方法具有public属性,覆盖该方法时,前面必须带上public。,8.2.3 创建多线程的方法,2通过实现Runnable接口实现多线程 (1)定义一个实现Runnable接口的类。 (2)定义方法
13、run( )。Runnable接口中有一个空的方法run( ),实现它的类必须覆盖此方法。 (3)创建该类的一个线程对象,并将该对象作参数,传递给Thread类的构造函数,从而生成Thread类的一个对象。 (4)通过start( )方法启动线程。,【实例8-3】 class myThread implements Runnableint count=1,number;public myThread(int num)number=num;System.out.println(“创建线程:“ +number);public void run()while(true)System.out.prin
14、tln(“线程 “ + number + “:计数 “ + count);if(+count=6) return; public class runnableThreadspublic static void main(String args)for(int i = 0; i5; i+) new Thread(new myThread(i+1).start(); ,【实例8-3】 程序运行某次的输出结果: 创建线程:1 创建线程:2 线程 1:计数 1 线程 1:计数 2 线程 1:计数 3 线程 2:计数 1 线程 2:计数 2 线程 2:计数 3 创建线程:3 线程 3:计数 1 线程 3
15、:计数 2 线程 3:计数 3 值得指出的是同一个实现了Runnable接口的对象作为参数产生的所有Thread对象是同一对象下的线程。,8.2.3 创建多线程的方法,3两种方法的简单比较 使用Thread方法简单明了,符合大家的习惯,但是,它也有一个很大的缺点,那就是如果我们的类已经从一个类继承(如小程序必须继承自 Applet 类),则无法再继承 Thread 类。 使用 Runnable 接口来实现多线程使得我们能够在一个类中包容所有的代码,有利于封装,它的缺点在于,我们只能使用一套代码,若想创建多个线程并使各个线程执行不同的代码,则仍必须额外创建类,如果这样的话,在大多数情况下也许还不
16、如直接用多个类分别继承 Thread 来得紧凑。,8.3 资源的协调与同步,8.3.1 线程调度模型 8.3.2 资源冲突 8.3.3 同步方法,8.3.1 线程调度模型,当计算机中只有一个CPU时,同一时刻正在运行的线程只能有一个,当一个新的线程通过new()创建并通过start()方法启动后,线程只是进入就绪状态,是否能运行要看调度的结果。 线程调度程序挑选线程时,将选择处于就绪状态且优先级最高的线程。优先级别主要分高、中、低三个级别,分别代表的数字是10、5、1。最小优先级的常量是MIN_PRIORITY,普通的优先级的常量是NORM_PRIORITY,最高的优先级的常量是MAX_PRI
17、ORITY。一般主线程的优先级是普通。另外可以通过Thread类的setPriority(int a)方法来修改系统自动设置的线程优先级。 如果多个线程具有相同的优先级,它们将被轮流调度。,图8-1 线程优先级调度示意图,新建线程y,就绪线程,O的等锁池,正在运行线程x,y.start(),执行到synchronized(O) 标记不存在则进入O等锁池,O标记返还了 线程可运行,按优先级排队,高优先级占先优先级,【实例8-5】验证了Java对多线程的调度方法。 class myThread extends Thread myThread(String str) super(str); publ
18、ic void run( )try Thread.sleep(2);catch(InterruptedException e) System.err.println(e.toString();System.out.println(“the Thread Name=“+getName( )+“,Priority=“ +getPriority( ); ,【实例8-5】验证了Java对多线程的调度方法。 public class testTheadPrioritypublic static void main(String agrs)Thread minThread=new myThread(“Mi
19、nPriorityThread“ );Thread normThread=new myThread(“NormPriorityThread“ );Thread maxThread=new myThread(“MaxPriorityThread“ );minThread.setPriority(Thread.MIN_PRIORITY);normThread.setPriority(Thread.NORM_PRIORITY);maxThread.setPriority(Thread.MAX_PRIORITY);minThread.start( );normThread.start( );maxTh
20、read.start( ); ,【实例8-5】 程序输出结果: the Thread Name=MaxPriorityThread,Priority=10 the Thread Name=NormPriorityThread,Priority=5 the Thread Name=MinPriorityThread,Priority=1,8.3.2 资源冲突,多个线程同时运行虽然可以提高程序的执行效率,但由于共享一组资源,可能会产生冲突,例如【实例8-6】。,【实例8-6】 class myThread void Test(int n) System.out.println(“运行线程 NO:“
21、+n);tryThread.sleep(3);catch(InterruptedException e) System.out.println(“线程异常, NO:“+n);System.out.println(“结束线程 NO:“+n); ,【实例8-6】 class rThread implements Runnable myThread Obj;int num;rThread(myThread t,int n) Obj=t;num=n;public void run( ) Obj.Test(num); ,【实例8-6】 public class ThreadClash public st
22、atic void main(String args ) myThread Obj=new myThread( );Thread t1=new Thread(new rThread(Obj,1);Thread t2=new Thread(new rThread(Obj,2);Thread t3=new Thread(new rThread(Obj,3);t1.start( );t2.start( );t3.start( ); ,【实例8-6】 程序运行结果: 运行线程 NO:1 运行线程 NO:2 运行线程 NO:3 结束线程 NO:1 结束线程 NO:2 结束线程 NO:3,8.3.3 同步
23、方法,同一进程的多个线程共享同一片存储空间,在带来方便的同时,也带来了访问冲突这个严重的问题。 可以通过 private 关键字来保证数据对象只能被方法访问 synchronized 关键字:锁定冲突的方法和锁定冲突的对象。,8.3.3 同步方法,1锁标志 每个对象都有一个标志锁。当对象的一个线程访问了对象的某个synchronized数据时,这个对象就将被“上锁” 被声明为synchronized的数据都不能被调用 只有当前线程访问完它要访问的synchronized数据者抛出了没有在 synchronized 块中捕获的异常时,释放“锁标志”后,同一个对象的其它线程才能访问synchron
24、ized数据。 这样,每次只有一个线程可以执行给定监控器保护的代码块。从其它线程的角度看,该代码块可以看作是原子的,它要么全部执行,要么根本不执行。,8.3.3 同步方法,1锁标志 锁用于保护代码块或整个方法,必须记住是锁的身份保护了代码块,而不是代码块本身,这一点很重要。一个锁可以保护许多代码块或方法。 反之,仅仅因为代码块由锁保护并不表示两个线程不能同时执行该代码块。它只表示如果两个线程正在等待相同的锁,则它们不能同时执行该代码。 此外 non-static的synchronized数据只能在同一个对象的纯种实现同步访问,不同对象的线程仍可同时访问。 每个class也有一个“锁标志”。对于
25、synchronized static数据可以在整个class下进行锁定,避免static数据的同时访问。对象的“锁标志”和class的“锁标志”是相互独立的。这点在前面已举例说明,在此不在赘述。,8.3.3 同步方法,2. 锁定冲突的方法 即在待锁定方法声明中加入 synchronized关键字。这种加上synchronized关键字的方法也称synchronized方法。 格式为:public synchronized void accessVal(int newVal); synchronized方法控制对类成员变量的访问:每个类实例对应一把锁,每个synchronized 方法都必须获
26、得调用该方法的类实例的锁方能执行,否则所属线程阻塞,方法一旦执行,就独占该锁,直到从该方法返回时才将锁释放,此后被阻塞的线程方能获得该锁,重新进入可执行状态。这种机制确保了同一时刻对于每一个类实例,其所有声明为 synchronized 的成员函数中至多只有一个处于可执行状态(因为至多只有一个能够获得该类实例对应的锁),从而有效避免了类成员变量的访问冲突(只要所有可能访问类成员变量的方法均被声明为 synchronized)。,8.3.3 同步方法,2. 锁定冲突的方法 synchronized 方法的缺陷:若将一个大的方法声明为synchronized 将会大大影响效率,典型地,若将线程类的
27、方法 run() 声明为 synchronized ,由于在线程的整个生命期内它一直在运行,因此将导致它对本类任何 synchronized 方法的调用都永远不会成功。当然我们可以通过将访问类成员变量的代码放到专门的方法中,将其声明为 synchronized ,并在主方法中调用来解决这一问题,但是 Java 为我们提供了更好的解决办法,那就是 synchronized 块。,【实例8-7】 public class SyncThreads1private static int x,y;private static class Thread1 extends Thread public syn
28、chronized void run() x=y=0;System.out.println(x);private static class Thread2 extends Thread public synchronized void run() x= y =1;System.out.println(y);public static void main(String args) new Thread1().run();new Thread2().run(); ,8.3.3 同步方法,3锁定冲突块(synchronized 块): 通过 synchronized关键字来声明synchronize
29、d 块。语法如下:synchronized(syncObject) /允许访问控制的代码 synchronized 块是这样一个代码块,其中的代码必须获得对象 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。,8.3.3 同步方法,3锁定冲突块(synchronized 块): 通过 synchronized关键字来声明synchronized 块。语法如下:synchronized(syncObject) /允许访问控制的代码 synchronized 块是这样一个代码块,其中的代码必须获得对象
30、 syncObject (如前所述,可以是类实例或类)的锁方能执行,具体机制同前所述。由于可以针对任意代码块,且可任意指定上锁的对象,故灵活性较高。,【实例8-8】 public class SyncThreads2private static int x,y;private static Object lockObject=new Object();private static class Thread1 extends Thread public void run() synchronized (lockObject)x=y=0;System.out.println(x);,【实例8-8】
31、private static class Thread2 extends Thread public void run() synchronized (lockObject) x= y =1;System.out.println(y);public static void main(String args) new Thread1().run();new Thread2().run(); ,8.4 线程间通信,多线程通信的方法有两种: 1. 把共享变量和方法封装在一个类中实现; 2. 通过wait( )和notify( )方法实现。,8.4 线程间通信,8.4.1 共享变量和方法封装在一个类中
32、 8.4.2 通过系统方法实现线程通信,8.4.1 共享变量和方法封装在一个类中,【实例8-9】。 class shareClass /共享类private int n;private boolean flag=false;void produce(int i) while(flag) n=i;flag=true;System.out.println(“n 产生数据:“+n); void get( ) while(!flag) flag=false;System.out.println(“ 读取数据:“+n); ,【实例8-9】 / 读取数据类 class Producer implement
33、s RunnableshareClass shc;Producer(shareClass c) shc=c; public void run( ) / 生产者产生5个随机数for(int i=0;i5;i+)shc.produce(int)(Math.random( )*100); class Consumer implements RunnableshareClass shc;Consumer(shareClass c) shc=c;public void run( ) for(int i=0;i5;i+) shc.get( ); / 消费者读取数据 ,【实例8-9】 public clas
34、s TheadsCommunication public static void main(String args ) shareClass shc=new shareClass( );Thread t1=new Thread(new Producer(shc);Thread t2=new Thread(new Consumer(shc);t1.start( );t2.start( ); ,【实例8-9】 程序的某次运行结果: 产生数据:47 读取数据:47产生数据:73读取数据:73产生数据:69读取数据:69产生数据:98读取数据:98产生数据:68读取数据:68,8.4.2 通过系统方法
35、实现线程通信,多线程通过把任务分成分散的逻辑单元取代了事件循环,消除了循环检测。循环检测通常通过重复检查一个特定条件的循环,一旦条件为真,就采取相应的行动,它浪费了CPU时间。例如,经典的队列问题,一个线程产生一些数据,而另一些线程取走数据。假设生产者生产更多的数据之前必须等到消费者结束。它才开始循环检测,浪费许多的CPU周期来等待消费者结束。很明显,这种情况并不是我们所期望的。,8.4.2 通过系统方法实现线程通信,为了避免循环检测,Java通过wait()、notify()和notifyAll()方法实现了一个巧妙的进程内通信的机制。这些方法作为Object的final方法来实现,因此所有
36、类都包含有它们。这三个方法只可以在一个同步的上下文访问,而且从计算机科学的角度上来说,它在概念上是很先进的,但是使用这些方法的规则却很简单。,8.4.2 通过系统方法实现线程通信,wait notify调度机制是几个线程对同一对象进行操作,其中某些线程在一定条件下自动挂起,等待其他线程在一定条件下通知其继续运行。这和join()不同,join()是等其他线程运行完,而wait()是等其他线程向其发出通知。 wait()和notify()是java中每个对象都有的方法,当一个线程执行到对象O的wait()方法时,java虚拟机会自动挂起,进入该对象的wait池等待。当其他线程执行到对象O的not
37、ify()方法时,java虚拟机会从对象O的wait池随机取出一个线程放入O的等锁池中,一旦O的标记被其他线程返回,即可运行。也可以执行对象O的notifyALL(),对象O的wait池中所有线程放入O的等锁池中。,图8-3 线程调度示意图,新建线程y,就绪线程,O的等锁池,O的wait池,正在运行线程x,x.sleep() x.yield() z.join(),y.start(),执行到synchronized(O) 标记不存在则进入O等锁池,O标记返还了 线程可运行,有线程执行O.notify()或O.interrupt(),则终止等待,有O的标记且执行到O.wait(),则释放标记,进入
38、O的wait池,按优先级排队,高优先级占先优先级,等待条件满足,8.4.2 通过系统方法实现线程通信,(1)wait()函数有两种形式:第一种形式接受一个毫秒值,用于在指定时间长度内暂停线程,使线程进入阻塞状态。第二种形式为不带参数,代表wait()在notify()或notifyAll()之前会持续阻塞。 (2)当对一个对象执行notify()时,会从线程等待池中随机取出一个线程放入O的等锁池中;当对一个对象执行notifyAll()时,会从线程等待池中移走所有该对象的所有线程,并把它们放到锁标志等待池中。 (3) 当调用wait()后,线程会释放掉它所占有的“锁标志”,从而使线程所在对象中
39、的其它synchronized数据可被别的线程使用。,8.4.2 通过系统方法实现线程通信,下面的程序错误地实现了生产者/消费者问题的简化形式。它包含了四个类:Queue,试图同步的队列;Producer,产生队列输入的线程对象;Consumer,使用队列数据的线程对象;PC,一个创建单个Queue、Producer和Consumer的小型类。,【实例8-10】 class Queue int n;synchronized int get() System.out.println(“Get:“+n); return n;synchronized void put(int n) this.n=n
40、;System.out.print(“Put:“+n); ,【实例8-10】 class Producer implements RunnableQueue q;Producer(Queue q) this.q=q;new Thread(this,“Producer“).start();public void run()int i=0;while(true) q.put(i+); ,【实例8-10】 class Consumer implements RunnableQueue q;Consumer(Queue q) this.q=q;new Thread(this,“Consumer“).s
41、tart(); public void run() while(true)q.get(); ,【实例8-10】 class PCpublic static void main(String args) Queue q=new Queue ();new Producer(q);new Consumer(q);System.out.println(“Press control-C to stop.“); ,【实例8-10】 class PCpublic static void main(String args) Queue q=new Queue ();new Producer(q);new Consumer(q);System.out.println(“Press control-C to stop.“); ,