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