​笔记汇总:《Java面向对象程序设计》学习笔记

第 15 章 Java 多线程机制

Java 语言的一大特点就是内置了对多线程的支持。

多线程是指同时存在几个执行体,按几条不同的执行线索共同工作的情况。

我们感觉线程正在同步执行,但并不是真的同时执行多个线程,只是Java 快速地从一个线程切换到另一个线程。

Java 虚拟机( JVM )负责管理线程。

(个人理解是,你可以把多线程当作:一个线程运行一下然后等待一会,另一个线程运行一下然后等待一会,继续下去。但因为速度快,看着感觉好像是很多线程一起运行)

15.1 Java 中的线程

15.1.1 程序、进程与线程

程序

  • 程序是一段静态的代码。

进程

  • 进程是程序的一次动态执行过程。

  • os 可以管理多个进程,即让多个进程都机会使用系统的资源,比如 CPU 资源。

    (一个 OS 里可以多个进程)

线程

  • 一个进程在其执行过程中可以产生多个线程。
  • 有自己独特的运行机制。
  • 线程是比进程更小的执行单位,也有产生、存在和消亡的过程。

其他

  • 每个进程都有一段专用的内存区域,但,线程间可以共享相同的内存单元(包括代码与数据)
  • 操作系统分时管理各个进程,按时间片轮流执行每个进程。
  • Java 的多线程就是在操作系统每次分时给 Java 程序一个时间片的 CPU 时间内,在若干个独立的可控制的线程之间进行切换
  • 每个 Java 程序都有一个默认的主线程。

15.1.2 线程的状态与生命周期

Java 语言使用 Thread 类及其子类的对象来表示线程。

使用 Thread 类及其子类的对象来表示线程。

Thread 提供 getstate() 方法返回枚举类型 Thread.State 的下列枚举常量之一:

NEW , RUNNABLE , BLOCKED , WAITING , TIMED_WAITING , TERMINATED

新建状态( NEW )

  • 当一个 Thread 类或其子类的对象被声明并创建时,新生的线程对象处于NEW状态,称作新建状态。

  • 即尚未启动(没有调用 start() 方法)的线程处于此状态。

可运行状态 (RUNNABLE)

  • 处于 NEW 状态的线程,调用 Thread 类提供的 start() 方法,进入 RUNNABLE状态。

  • NEW 状态线程调用 start() ,让自己进入 RUNNABLE状态。

  • JVM 就会知道又有一个新一个线程排队等候切换了。

  • 当 JVM 将 CPU 使用权切换给 RUNNABLE 状态的线程时,如果线程是 Thread 的子类创建的,该类中的 run() 方法就立刻执行。

    如果线程是 Thread 的子类创建的,该类中的 run () 方法就立刻执行。所以我们必须在子类中重写父类的 run() 方法。

中断状态 (BLOCKED 、 WAITING 、 TIMED_WAITING)阻塞、等待、有限时间等待

  • BLOCKED 、 WAITING 、 TIMED_WAITING 状态都属于中断状态。
  • 当中断的线程重新进入 RUNNABLE 状态后,一旦 JVM 将 CPU 使用权切换给该线程, run() 方法将从中断处继续执行。

死亡状态 (TERMINATED)终止

  • 线程完成了它的全部工作,即执行完 run() 方法,该线程进入 TERMINATED 状态。

  • 只有处于NEW状态的线程可以调用 start() 方法,处于其他状态的线程都不可以调用 start() 方法,否则将触发 ILLegalThreadStateException 异常。

在主线程中用 Thread 的子类创建了两个线程,这两个线程在命令行窗口分别输出 5 句“老虎”和“小猫”;主线程在命令行窗口输出 6 句“主人”。

注意,程序在不同的计算机上运行或在同一台计算机上反复运行的结果不尽相同。

public class Tiger extends Thread {
	public void run() {
		for (int i = 1; i <= 5; i++) {
			System.out.print("|老虎" + i);
			try {
				sleep(1000); // 状态:TIMED_WAITING
			} catch (Exception exp) {
			}
		}
	}
}


public class Cat extends Thread {
	public void run() {
		for (int i = 1; i <= 5; i++) {
			// System.out.print("|小猫"+i+"状态:"+getState()+"|");
			System.out.print("|小猫" + i);
		}
	}
}


public class Example15_1 { 
	public static void main(String args[]) { // 主线程
		Tiger tiger;
		Cat cat;
		tiger = new Tiger(); // 创建线程
		cat = new Cat(); // 创建线程
		System.out.println("tiger的状态:" + tiger.getState());
		System.out.println("cat状态:" + cat.getState());
		tiger.start(); // 启动线程
		cat.start(); // 启动线程
		for (int i = 1; i <= 6; i++) {
			System.out.printf("\n%s", "tiger状态:" + tiger.getState());
			System.out.printf("\n%s", "cat状态:" + cat.getState());
			System.out.printf("\n%s", "主人" + i);
		}
		System.out.printf("\n%s", "|tiger的状态:" + tiger.getState());
		System.out.printf("\n%s", "|cat状态:" + cat.getState());
	}
}

输出(不唯一,每次线程顺序都可能不一样)

tiger的状态:NEW
cat状态:NEW

tiger状态:RUNNABLE|小猫1|老虎1|小猫2
cat状态:BLOCKED|小猫3
主人1|小猫4
tiger状态:TIMED_WAITING|小猫5
cat状态:RUNNABLE
主人2
tiger状态:TIMED_WAITING
cat状态:TERMINATED
主人3
tiger状态:TIMED_WAITING
cat状态:TERMINATED
主人4
tiger状态:TIMED_WAITING
cat状态:TERMINATED
主人5
tiger状态:TIMED_WAITING
cat状态:TERMINATED
主人6
|tiger的状态:TIMED_WAITING
|cat状态:TERMINATED|老虎2|老虎3|老虎4|老虎5

15.1.3 线程的调度与优先级

处于就绪状态的线程首先进入就绪队列排队等候 CPU 资源,同一时刻在就绪队列中的线程可能有多个。

Java 虚拟机中的线程调度器负责管理线程。

在采用时间片的系统中,每个线程都有机会获得 CPU 的使用权,以便使用 CPU 资源执行线程中的操作。

当线程使用 CPU 资源的时间到了后,即使线程没有完成自己的全部操作, Java 调度器也会中断当前线程的执行,把 CPU 的使用权切换给下一个排队等待的线程,当前线程将等待 CPU 资源的下一次轮回,然后从中断处继续执行。

Java 调度器的任务是使高优先级的线程能始终运行,一旦时间片有空闲,则使具有同等优先级的线程以轮流的方式顺序使用时间片。

在实际编程时,不提倡使用线程的优先级来保证算法的正确执行。

15.2 用 Thread 的子类创建线程

用 Thread 类或子类创建线程对象。

在编写 Thread 类的子类时,需要重写父类的 run() 方法,其目的是规定线程的具体操作,否则线程就什么也不做,因为父类的 run() 方法中没有任何操作语句。

例子

  • 除主线程外还有两个线程,这两个线程共享一个 StringBuffer 对象两个线程在运行期间将修改 StringBuffer 对象中的字符。
  • 为了使结果尽量不依赖于当前 CPU 资源的使用情况,我们应当让线程主动调用 sleep (int n ) 方法让出 CPU 的使用权进入中断状态,
  • sleep(int n) 方法是 Thread 类的静态方法,线程在占有 CPU 资源期间,通过调用 sleep(int n) 方法来使自己放弃 CPU 资源,休眠一段时间
public class People extends Thread {
   StringBuffer str;
   People(String s,StringBuffer str) { 
      setName(s); //调用从Thread类继承的setName方法为线程起名字
      this.str=str;
   }
   public void run() {
      for(int i=1;i<=3;i++) {
         str.append(getName()+",");   //将当前线程的名字尾加到str 这里有可能被中断
         System.out.println("我是"+getName()+",字符串为:"+str);
         try {  sleep(1000); // 中断状态 (TIMED_WAITING)
         }
         catch(InterruptedException e){}
      }
   } 
}


public class Example15_2 {
   public  static void main(String args[]) {
      People personOne,personTwo;
      StringBuffer str=new StringBuffer();
      personOne=new People("张三",str); 
      personTwo =new People("李四",str); 
      personOne.start();  
      personTwo.start();
   }
}

输出(不唯一,每次线程顺序都可能不一样)

我是张三,字符串为:张三,李四,
我是李四,字符串为:张三,李四,
我是李四,字符串为:张三,李四,李四,张三,
我是张三,字符串为:张三,李四,李四,张三,
我是李四,字符串为:张三,李四,李四,张三,李四,张三,
我是张三,字符串为:张三,李四,李四,张三,李四,张三,

15.3 使用 Runnable 接口

使用 Thread 子类创建线程的优点是:我们可以在子类中增加新的成员变量,使线程具有某种属性,也可以在子类中新增加方法,使线程具有某种功能。

但是, Java 不支持多继承, Thread 类的子类不能再扩展其他的类。

15.3.1 Runnable 接口与目标对象

使用 Thread 创建线程对象时,通常使用的构造方法是:

Thread ( Runnable target )
  • 在创建线程对象时必须向构造方法的参数传递一个实现 Runnable 接口类的实例,该实例对象称作所创线程的目标对象,
  • 当线程调用 start() 方法后,一旦轮到它来享用 CPU 资源,目标对象就会自动调用接口中的 run() 方法(接口回调),这一过程是自动实现的,用户程序只需要让线程调用 start 方法即可。
  • 对于使用同一目标对象的线程,目标对象的成员变量自然就是这些线程共享的数据单元。另外,创建目标对象类在必要时还可以是某个特定类的子类,因此,使用 Runnable 接口比使用 Thread 的子类更具有灵活性。

例子

  • threadOne 和 threadTwo 两个线程使用同一个目标对象,两个线程共享目标对象的成员变量 number。
  • threadOne 负责递增number , threadTwo 负责递减 number ,而且递减的速度大于递增的速度。
  • 当 number 的值小于 150 时,线程 threadOne 结束自己的 run() 方法进入死亡状态;当 number 的值小于 0 时,线程threadTwo 结束自己的 run() 方法进入死亡状态。
public class Bank implements Runnable {
   private int number=0;
   public void setMoney(int m) {
      number=m;
   }
   public void run() {      //重写Runnable接口中的方法
      while(true) {
         String name=Thread.currentThread().getName();
         if(name.equals("One")) {
            if(number<=160) {
               System.out.println(name+"进入死亡状态");
               return;     // threadOne的run方法结束
            }    
            number=number+10; // 可能被中断
            System.out.println("我是"+name+"现在number="+number);
         } 
         if(Thread.currentThread().getName().equals("Two")) {  
            if(number<=0) {
               System.out.println(name+"进入死亡状态");
               return;     // threadTwo的run方法结束  
            } 
            number=number-100; // 可能被中断
            System.out.println("我是"+name+"现在number="+number);
         } 
         try{  Thread.sleep(800);
         }
         catch(InterruptedException e){}
      }
   }
}


public class Example15_3 {
   public static void main(String args[ ]) {
      Bank bank=new Bank();
      bank.setMoney(300);
      Thread threadOne,threadTwo;
      threadOne=new Thread(bank); 
      threadOne.setName("One");
      threadTwo=new Thread(bank); //threadTwo和 threadOne的目标对象相同  
      threadTwo.setName("Two");
      threadOne.start();
      threadTwo.start();
   }
}

输出(不唯一,每次线程顺序都可能不一样)

我是One现在number=310
我是Two现在number=210
我是One现在number=220
我是Two现在number=220
我是Two现在number=120
One进入死亡状态
我是Two现在number=20
我是Two现在number=-80
Two进入死亡状态

15.3.2 run() 方法中的局部变量

  • 具有相同目标对象的线程,当其中一个线程享用 CPU 资源时,目标对象自动调用接口中的 run 方法,这时, run 方法中的局部变量被分配内存空间

  • 当轮到另一个线程享用 CPU 资源时,目标对象会再次调用接口中的 run 方法,那么, run() 方法中的局部变量会再次分配内存空间。

    也就是说 run() 方法己经启动运行了两次,分别运行在不同的线程中,即运行在不同的时间片内。

  • 不同线程的 run() 方法中的局部变量互不干扰,一个线程改变了自己的 run() 方法中局部变量的值不会影响其他线程的 run() 方法中的局部变量。

在线程中启动其它线程

public class Move implements Runnable {
   public void run() {      
      String name=Thread.currentThread().getName();    //局部变量name
      StringBuffer str=new StringBuffer();            //局部变量str
      for(int i=1;i<=3;i++) {                         //局部变量i
         if(name.equals("张三")) {
            str.append(name);
            System.out.println(name+"线程的局部变量i="+i+",str="+str);
         } 
         else if(name.equals("李四")) {
            str.append(name);
            System.out.println(name+"线程的局部变量i="+i+",str="+str);
         } 
         try{  Thread.sleep(800);
         }
         catch(InterruptedException e){}
     }
   }
}


public class Example15_4 {
   public static void main(String args[]) {
      Move move=new Move();
      Thread zhangsan,lisi;
      zhangsan=new Thread(move); 
      zhangsan.setName("张三");
      lisi=new Thread(move);
      lisi.setName("李四");
      zhangsan.start();
      lisi.start(); 
   }
}

输出(唯一,不同线程的 run() 方法中的局部变量互不干扰)

李四线程的局部变量i=1,str=李四
张三线程的局部变量i=1,str=张三
李四线程的局部变量i=2,str=李四李四
张三线程的局部变量i=2,str=张三张三
李四线程的局部变量i=3,str=李四李四李四
张三线程的局部变量i=3,str=张三张三张三

15.3.3 在线程中启动其他线程

线程通过调用 start() 方法启动,使其从新建状态进入就绪队列排队,一旦轮到它来享用CPU 资源,就可以脱离创建它的主线程独立开始自己的生命周期了。

前面的例子都是在主线程中启动其他线程。实际上也可以在任何一个线程中启动另外一个线程。

在线程中启动其它线程

public class ComputerSum implements Runnable {
   int i=1,sum=0;             //线程共享的数据
   public void run() {      
      Thread  thread=Thread.currentThread();
      System.out.println(thread.getName()+"开始计算");
      while(i<=10) {
        sum=sum+i;
        System.out.print(" "+sum);
        if(i==5) {
           System.out.println(thread.getName()+"完成任务了!i="+i);
           Thread threadTwo=new Thread(this);//threadTwo与 threadOne的目标对象相同
           threadTwo.setName("李四");
           threadTwo.start();  //启动threadTwo
           i++;      //死亡之前将i变成6
           return;   //threadOne死亡  
        }
        i++;
        try{  Thread.sleep(300);
        }
        catch(InterruptedException e){}
      }
   }
}


public class Example15_5 {
   public static void main(String args[]) {
      ComputerSum computer=new ComputerSum();
      Thread threadOne;
      threadOne=new Thread(computer); 
      threadOne.setName("张三");
      threadOne.start();
   }
}

输出

张三开始计算
 1 3 6 10 15张三完成任务了!i=5
李四开始计算
 21 28 36 45 55

15.4 线程的常用方法

start()

  • 线程调用该方法将启动线程,使之从新建状态进入就绪队列排队,

  • 一旦轮到它来享用 CPU 资源时,就可以脱离创建它的线程独立开始自己的生命周期了。

run()

  • Thread 类的 run() 方法与 Runnab1e 接口中的 run() 方法的功能和作用相同,都用来定义线程对象获得 CPU 资源后所执行的操作,都是系统自动调用而无需要用户调用的方法。

  • 用户程序需要重写 run() 方法,定义线程需要完成的任务。

例子

public class E {
	public static void main(String[] args) {
		Target target = new Target();
//		target.run();		
		Thread thread = new Thread(target);
		thread.start(); // 如果注释掉,就是没有调用start() 并没有成为真正的线程,只是新建状态
		for (int i = 0; i < 3; i++) {
			System.out.println("yes");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException exp) {
			}
		}
	}
}

class Target implements Runnable {
	public void run() {
		for (int i = 0; i < 3; i++) {
			System.out.println("ok");
			try {
				Thread.sleep(1000);
			} catch (InterruptedException exp) {
			}
		}
	}
}

输出

yes
ok
yes
ok
yes
ok

sleep(int millsccond)

  • 主线程在 main 方法或用户线程在它的 run() 方法中调用 sleep 方法 (Thread 类提供的 static 方法)放弃 CPU 资源,休眠一段时间。

  • 如果线程在休眠时被打断, JVM 就抛出 InterruptedException 异常。

    因此,必须在 try~ catch 语句块中调用 sleep 方法。

isAIive()

  • 线程处于NEW状态时,线程调用 isAlive() 方法返回 false 。
  • 当一个线程调用 start() 方法后,没有进入死亡状态之前,线程调用 isAIive() 方法返回 true。
  • 当线程进入 TERMINATED( 死亡)状态后,线程仍可以调用方法 isAlive () ,这时返回的值是 false 。

(NEW和TERMINATED时返回false,否则返回true)

垃圾 “实体”

需要注意的是,一个己经运行的线程在没有进入 TERMINATED (死亡)状态时,不要再给线程分配实体,

由于线程只能引用最后分配的实体,先前的实体就会成为“垃圾”并且不会被垃圾收集机收集掉

线程每隔 1 s 在命令行窗口输出本地机器的时间,

该线程又被分配了实体,新实体又开始运行。

因为垃圾实体仍然在工作,因此,在命令行每秒钟能看见两行同样的本地机器时间。

import java.time.*;
public class Target implements Runnable {
    public void run() {     
       while(true) {
           LocalTime time = LocalTime.now();
           System.out.printf("%d:%d:%d\n",
           time.getHour(),time.getMinute(),time.getSecond());
           try{ Thread.sleep(1000);
           }
           catch(InterruptedException e){}
       }
    }
}


public class Example15_6 {
   public static void main(String args[]) {
      Target target = new Target();
      Thread thread = new Thread(target); 
      thread.start();
      thread = new Thread(target); // 新实体,老实体成了垃圾实体
      thread.start();
   }
}

输出(不唯一,程序运行时时间不一样)

17:37:3
17:37:3
17:37:4
17:37:4
17:37:5
17:37:5
17:37:6
17:37:6
··· // 无限输出下去

currentThread()

currentThread() 方法是 Thread 类中的 static 方法,可以用类名调用,该方法返回当前正在使用 CPU 资源的线程的引用。

interrupt()

interrupt 方法经常用来“吵醒”休眠的线程。

当一些线程调用 sleep 方法处于休眠状态时,一个占有 CPU 资源的线程可以让休眠的线程调用 interrupt() 方法“吵醒”自己,

即导致休眠的线程发生 InterruptedException 异常,从而结束休眠,进人RUNNABLE 状态,重新排队等待 CPU 资源。

有两个线程: student 和 teacher ,

其中,student 准备睡一小时后再开始上课, teacher 在输出 3 句“上课”后,吵醒休眠的线程 student。

public class ClassRoom implements Runnable {
   Thread student,teacher;
   ClassRoom() {
     teacher=new Thread(this); 
     student=new Thread(this); 
     teacher.setName("雷老师");
     student.setName("张爱睡");
   } 
   public void run() {     
      if(Thread.currentThread()==student) {
        try{ System.out.println(student.getName()+"正在睡觉,不听课");
             Thread.sleep(1000*60*60);
        }
        catch(InterruptedException e) {
             System.out.println(student.getName()+"被老师叫醒了");
        }
        System.out.println(student.getName()+"开始听课");
      }
      else if(Thread.currentThread()==teacher) {
        for(int i=1;i<=3;i++) {
            System.out.println("上课!");
            try { Thread.sleep(500);
            }
            catch(InterruptedException e){} 
        }
        student.interrupt();   //吵醒student
      }
   }
}


public class Example15_7 {
   public static void main(String args[]) {
      ClassRoom room=new ClassRoom();
      room.student.start();
      room.teacher.start();

   }
}

输出(不唯一,每次线程顺序都可能不一样)

上课!
张爱睡正在睡觉,不听课
上课!
上课!
张爱睡被老师叫醒了
张爱睡开始听课

15.5 GUI 线程

应该不考

15.6 线程的同步

问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量。我们应对这样的问题作出处理,否则可能发生混乱

  • 可以把修改数据的方法用关键字: synchronized 来修饰。
  • 一个方法使用关键字 synchronized 修饰后,当一个线程 A 使用这个方法时,其他线程想使用这个方法时就必须等待,直到线程 A 使用完该方法。
  • 所谓线程同步就是若千个线程都需要使用一个 synchronized 修饰的方法。

有两个线程,即会计和出纳,它们共同拥有一个账本。

它们都可以使用存取方法对账本进行访问,在会计使用存取方法时,向账本上写入存钱记录;在出纳使用存取方法时,向账本写入取钱记录。

因此,当会计正在使用账本时,出纳被禁止使用,反之亦然。

例如,

会计使用账本时,在账本上存入 270 万元,但在存入这笔钱时,每存入 90 万元就喝口茶,那么会计喝茶休息时(注意,这时存钱这件事还没结束,即会计还没有使用完存取方法),出纳仍不能使用账本;

出纳使用账本时,在账本上取出 60 万元,但在取出这笔钱时,每取出 30 万元就喝口茶,那么出纳喝茶休息时,会计不能使用账本。

也就是说,程序要保证其中一个人使用账本时,另一个人必须等待。

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
public class WindowMoney extends JFrame implements ActionListener {
   JTextArea text1,text2;
   Bank bank;
   Thread 会计,出纳;
   JButton startShowing=new JButton("开始演示");
   WindowMoney() {
     bank=new Bank();
     会计=new Thread(bank); 
     出纳=new Thread(bank);
     会计.setName("会计");
     出纳.setName("出纳");
     text1=new JTextArea(5,16); 
     text2=new JTextArea(5,16);
     bank.setShowText(text1,text2);
     bank.setMoney(100); 
     setLayout(new FlowLayout());
     add(startShowing);
     add(new JScrollPane(text1));
     add(new JScrollPane(text2));
     setVisible(true);
     setSize(570,300);
     startShowing.addActionListener(this);
     setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   }
   public void actionPerformed(ActionEvent e) {
      if(!(出纳.isAlive())&&!(会计.isAlive())) {   
          会计=new Thread(bank); 
          出纳=new Thread(bank);
          会计.setName("会计");
          出纳.setName("出纳");
          bank.setMoney(100);
          text1.setText(null);
          text2.setText(null);
      }
      try{
         会计.start();
         出纳.start();
      }
      catch(Exception exp){}
   }
} 


import javax.swing.*;
public class Bank implements Runnable {
   int money=100;
   String name; 
   JTextArea text1,text2;
   public void setShowText(JTextArea t1,JTextArea t2) {
      text1=t1;
      text2=t2;
   }
   public void setMoney(int n) {
      money=n;
   }
   public synchronized void 存取(int number) { //存取方法
      if(name.equals("会计")) {
         for(int i=1;i<=3;i++) { //会计使用存取方法存入270,存入90,稍歇一下
             money=money+number;         
             text1.append("帐上有"+money+"万,休息一会再存\n");
             try { Thread.sleep(1000);     //这时出纳仍不能使用存取方法 
             }                             //因为会计还没使用完存取方法
             catch(InterruptedException e){}
         }
      }
      else if(name.equals("出纳")) {
         for(int i=1;i<=2;i++) { //出纳使用存取方法取出60,取出30,稍歇一下
             money=money-number;   
             text2.append("帐上有"+money+"万,休息一会再取\n");
             try { Thread.sleep(1000);       //这时会计仍不能使用存取方法
             }                               //因为出纳还没使用完存取方法
             catch(InterruptedException e){}
         }
      }
   }
   public void run() {
      name=Thread.currentThread().getName();
      if(name.equals("会计")) 
         存取(90);
      else if(name.equals("出纳"))
         存取(30);
   }
}


public class Example15_10 {
   public static void main(String args[]) {
      WindowMoney win=new WindowMoney();
      win.setTitle("会计与出纳");
   }
}

程序运行效果(假装下面是个窗体)

帐上有190万,休息一会再存
帐上有280万,休息一会再存
帐上有370万,休息一会再存
帐上有340万,休息一会再取
帐上有310万,休息一会再取

15.7 在同步方法中使用 wait() 、notify() 和notifyAll()方法

当一个线程正在使用一个同步方法时(用 synchronized 修饰的法) ,其它线程就不能使用这个同步方法。

对于同步方法,有时涉及到某些特殊情况,

比如当你在一个售票窗口排队购买电影票时,

如果你给售票员的钱不是零售票员又没有零钱找给你,那么你就必须等待,允许你后面的人买票,以便售票员获得零钱给你。

如果第 2 个人仍没有零钱,那么你俩必须等待,并允许后面的人买票。

wait() 方法

一个线程使用的同步方法中用到某个变量,此变量又需要其它线程修改后才能符合本线程的需要,那么可以在同步方法中使用 wait() 方法。

  • 使用 wait 方法可以中断方法的执行,使本线程等待,暂时让出 CPU 的使用权,并允许其它线程使用这个同步方法。
  • 其它线程如果在使用这个同步方法时不需要等待,
  • 那么它使用完这个同步方法的同时,应当用 notifyAll() 方法通知所有的由于使用这个同步方法而处于等待的线程结束等待。
  • 曾中断的线程就会从刚才的中断处继续执行这个同步方法,并遵循“先中断先继续”的原则。
  • 如果使用 notify() 方法,那么只是通知处于等待中的线程的某一个结束等待。

wait() 、 notify() 和 notifyAll() 都是 Object 类中的 final 方法,被所有的类继承、且不允许重写的方法。

为了避免复杂的数学算法,我们模拟张平和李明两个人买电影票,

售票员只有两张 5 元的人民币,电影票 5 元一张。

张平拿一张 20 元的人民币排在李明的前面买票,李明拿一张 5 元的人民币买票,因此张平必须等待。

import java.awt.*;
import java.awt.event.*; 
import javax.swing.*;
public class WindowTicket extends JFrame implements Runnable,ActionListener {
   SellTicket ticketAgent;
   Thread 张平,李明;  
   static JTextArea text;
   JButton startBuy=new JButton("开始买票"); 
   WindowTicket() {
      ticketAgent=new SellTicket(); //售票员;
      张平=new Thread(this);
      张平.setName("张平");
      李明=new Thread(this);
      李明.setName("李明"); 
      text=new JTextArea(10,30);
      startBuy.addActionListener(this); 
      add(text,BorderLayout.CENTER);
      add(startBuy,BorderLayout.NORTH);
      setVisible(true);
      setSize(360,300);
      setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   } 
   public void actionPerformed(ActionEvent e) {
      try{  张平.start();
            李明.start();
      }
      catch(Exception exp) {} 
   }
   public void run() {
      if(Thread.currentThread()==张平) {
          ticketAgent.售票规则(20);
      }
      else if(Thread.currentThread()==李明) {
          ticketAgent.售票规则(5);
      }
   }   
}


public class SellTicket {
   int 五元钱的个数=2,十元钱的个数=0,二十元钱的个数=0; 
   String s=null;
   public synchronized void  售票规则(int money) {
      String name=Thread.currentThread().getName();
      if(money==5) {  //如果使用该方法的线程传递的参数是5,就不用等待
        五元钱的个数=五元钱的个数+1; 
         s= "给"+name+"入场卷,"+name+"的钱正好";
         WindowTicket.text.append("\n"+s);
      }
      else if(money==20) {           
         while(五元钱的个数<3) {
            try { WindowTicket.text.append("\n"+name+"靠边等...");
                  wait();    //如果使用该方法的线程传递的参数是20须等待
            }
            catch(InterruptedException e){}
         }
         五元钱的个数=五元钱的个数-3;
         二十元钱的个数=二十元钱的个数+1;
         s="给"+name+"入场卷,"+name+"给20,找赎15元";
         WindowTicket.text.append("\n"+s);
      }
      notifyAll();
   }
}


public class Example15_11 {
   public static void main(String args[]) {
      WindowTicket win=new WindowTicket();
      win.setTitle("请排队买票");
   }
}

程序运行效果(假装下面是个窗体)

张平靠边等…
给李明入场卷,李明的钱正好
给张平入场卷,张平给20,找赎15元

用两个线程来实现猜数字游戏,

第一个线程负责随机给出 0 ~ 99 的一个整数,第二个线程负责猜出这个数,

每当第二个线程给出自己的猜测后,第一个线程会提示“你猜小了”“你猜大了”或“恭喜,你猜对了”。

第二个线程首先要等待第一个线程设置好要猜测的数。

在第一个线程设置好猜测数之后,两个线程还要互相等待,其原则是第二个线程给出自己的猜测后,等待第一个线程给出的提示;

第一个线程给出提示后,等待第二个线程给出猜测,

如此进行,直到第二个线程给出正确的猜测,两个线程进入死亡状态。程序运行效

public class Number implements Runnable {
   final int SMALLER=-1,LARGER=1,SUCCESS=8;
   int realNumber,guessNumber,min=0,max=100,message=SMALLER;
   boolean pleaseGuess=false,isGiveNumber=false;
   Thread giveNumberThread,guessNumberThread;
   Number() {
      giveNumberThread=new Thread(this); 
      guessNumberThread=new Thread(this);
   }
   public void run() {
      for(int count=1;true;count++) {
         setMessage(count);
         if( message==SUCCESS)
            return;
      }
   }
   public synchronized void setMessage(int count) {
      if(Thread.currentThread()==giveNumberThread&&isGiveNumber==false) {
          realNumber=(int)(Math.random()*100);
          System.out.println("随机给你一个0至99之间的数,猜猜是多少?");
          isGiveNumber=true;
          pleaseGuess=true;
      }
      if(Thread.currentThread()==giveNumberThread) {
          while(pleaseGuess==true)             
             try  { wait();  //让出CPU使用权,让另一个线程开始猜数
             }
             catch(InterruptedException e){}
             if(realNumber>guessNumber)  { //结束等待后,根据另一个线程的猜测给出提示
                message=SMALLER;
                System.out.println("你猜小了");
             }
             else if(realNumber<guessNumber) {
                message=LARGER;
                System.out.println("你猜大了");
             } 
             else {
                message=SUCCESS;
                System.out.println("恭喜,你猜对了");
             }  
             pleaseGuess=true;
          }
      if(Thread.currentThread()==guessNumberThread&&isGiveNumber==true) {
             while(pleaseGuess==false)
                try { wait();  //让出CPU使用权,让另一个线程给出提示
                }
                catch(InterruptedException e){}
                if(message==SMALLER) {
                   min=guessNumber;
                   guessNumber=(min+max)/2; 
                   System.out.println("我第"+count+"次猜这个数是:"+guessNumber);
                }
                else if(message==LARGER) {
                   max=guessNumber;
                   guessNumber=(min+max)/2; 
                   System.out.println("我第"+count+"次猜这个数是:"+guessNumber);
                }
                pleaseGuess=false; 
      }
      notifyAll();
   }
}


public class Example15_12 {
   public static void main(String args[]) {
      Number number=new Number();
      number.giveNumberThread.start();
      number.guessNumberThread.start();

   }
}

输出(不唯一,每次的数都可能不一样)

随机给你一个0至99之间的数,猜猜是多少?
我第1次猜这个数是:50
你猜大了
我第2次猜这个数是:25
你猜大了
我第3次猜这个数是:12
你猜大了
我第4次猜这个数是:6
你猜小了
我第5次猜这个数是:9
恭喜,你猜对了

15.8 计时器线程 Timer

应该不考

15.9 线程的联合

一个线程 A 在占有 CPU 资源期间,可以让其他线程调用 join() 和本线程联合,

例如:

b.join();

称 A 在运行期间联合了 B。

  • 如果线程 A 在占有 CPU 资源期间联合了线程 B ,

    那么线程 A 将立刻中断执行,一直等到它联合的线程 B 执行完毕,

    线程 A 再重新排队等待 CPU 资源,以便恢复执行。

  • 如果 A 准备联合的线程 B 已经结束,那么 B.join() 不会产生任何效果

一个线程在运行期间联合了另外个线程

public class ThreadJoin implements Runnable {
   Car car;
   Thread customer,carMaker;
   ThreadJoin() {
      customer=new Thread(this);
      customer.setName("顾客");
      carMaker=new Thread(this);
      carMaker.setName("汽车制造厂");
   }
   public void run() {
      if(Thread.currentThread()==customer) {
          System.out.println(customer.getName()+"等"+carMaker.getName()+"生产汽车");
          try{  carMaker.start(); 
                carMaker.join();  //线程customer开始等待carMaker结束
          } 
          catch(InterruptedException e){}
          System.out.println(customer.getName()+
                         "买了一辆汽车:"+car.name+" 价钱:"+car.price);
      }
      else if(Thread.currentThread()==carMaker) {
          System.out.println(carMaker.getName()+"开始生产汽车,请等...");
          try { carMaker.sleep(2000);    
          }
          catch(InterruptedException e){}
          car=new Car("红旗轿车",288000) ;
          System.out.println(carMaker.getName()+"生产完毕");
      }
   }    
}


public class Car {
   float price;
   String name;
   Car(String name,float price) {
      this.name=name;
      this.price=price;
   }
}


public class Example15_14 {
   public static void main(String args[]) {
     ThreadJoin  a=new ThreadJoin();
     a.customer.start();
   }
}

输出(不唯一,每次线程顺序都可能不一样)

顾客等汽车制造厂生产汽车
汽车制造厂开始生产汽车,请等...
汽车制造厂生产完毕
顾客买了一辆汽车:红旗轿车 价钱:288000.0

15.10 守护线程

线程默认是非守护线程,非守护线程也称用户(user)线程,

一个线程调用void setDaemon(boolean on)方法可以将自己设置成一个守护(daemon)线程。

例如:

thread.setDaemon(true)

当程序中的所有用户线程都结束运行时,即使守护线程的 run() 方法中还有需要执行的语句,守护线程也立刻结束运行。

可以用守护线程做一些不是很严格的工作,线程的随时结束不会产生什么不良的后果。

注意,一个线程必须在运行之前设置自己是否是守护线程。

守护线程

public class Daemon implements Runnable {
   Thread A,B;
   Daemon() {
       A=new Thread(this);
       B=new Thread(this);
   }
   public void run() {
      if(Thread.currentThread()==A) {
          for(int i=0;i<8;i++) {
             System.out.println("i="+i) ;
             try{  Thread.sleep(1000);    
             }
             catch(InterruptedException e) {}
          } 
      }
      else if(Thread.currentThread()==B) {
          while(true) {
             System.out.println("线程B是守护线程 "); 
             try{  Thread.sleep(1000);    
             }
             catch(InterruptedException e){}
          }
      }
   }    
}

public class Example15_15 {
   public static void main(String args[]) {
      Daemon  a=new Daemon ();
      a.A.start();
      a.B.setDaemon(true);
      a.B.start();
   }
}

输出(不唯一,多出的 线程B是守护线程 可能出现在其他地方)

i=0
线程B是守护线程 
线程B是守护线程 
i=1
线程B是守护线程 
i=2
线程B是守护线程 
i=3
线程B是守护线程 
i=4
线程B是守护线程 
i=5
线程B是守护线程 
i=6
线程B是守护线程 
i=7
线程B是守护线程

15.11 小结

  1. 线程是比进程更小的执行单位。一个进程在其执行过程中可以产生多个线程,形成多条执行线索,每条线索,即每个线程有它自身的产生、存在和消亡的过程,也是一个动态的概念。
  2. Java 虚拟机中的线程调度器负责管理线程,在采用时间片的系统中,每个线程都有机会获得 CPU 的使用权。当线程使用 CPU 资源的时间到了后,即使线程没有完成自己的全部操作, Java 调度器也会中断当前线程的执行,把 CPU 的使用权切换给下一个排队等待的线程,当前线程将等待 CPU 资源的下一次轮回,然后从中断处继续执行。
  3. 线程创建后仅仅是占有了内存资源,在 JVM 管理的线程中还没有这个线程,此线程必须调用 start() 方法(从父类继承的方法)通知 JVM ,这样 JVM 就能知道又有一个新线程排队等候切换了。
  4. 线程同步是指几个线程需要调用同一个同步方法(用 synchronized 修饰的方法个线程在使用同步方法时,可能根据问题的需要,必须使用 wait( )方法暂时让出 CPU 的使用权,以便其他线程使用这个同步方法。如果其他线程在使用这个同步方法时不需要等待,那么它在用完这个同步方法的同时,应当执行 notifyAll() 方法通知所有由于使用这个同步方法处于等待的线程结束等待。

结语

同志们,欢呼吧,《Java面向对象程序设计》到这就结束了!

休息一天准备继续《算法设计与分析》的学习吧!

Logo

华为开发者空间,是为全球开发者打造的专属开发空间,汇聚了华为优质开发资源及工具,致力于让每一位开发者拥有一台云主机,基于华为根生态开发、创新。

更多推荐