第八章 Java的多线程机制

本文详细介绍了Java中的多线程概念,包括进程与线程的区别、线程的生命周期、线程创建与启动、线程调度方法以及线程同步机制。通过实例展示了如何创建线程、设置线程优先级、休眠、让步、等待,以及同步方法的使用。重点讨论了线程同步中的wait()、notifyAll()方法,以及在生产者-消费者问题中的应用。
摘要由CSDN通过智能技术生成

《Java程序设计实例与操作》(丁永卫)目录

8.1 了解Java中的进程与线程

一、进程与线程

   对于一般程序而言,其结构大致可以划分为一个入口、一个出口和一个顺序执行的语句序列。程序开始运行时,系统从程序入口开始,按照语句的执行顺序(包括顺序、分支和循环)完成相应指令,然后从出口退出,同时整个程序结束。这样的结构称为进程,或者说进程就是程序的一次动态执行过程。一个进程既包括程序的代码,同时也包括了系统的资源,如CPU、内存空间等,但不同的进程所占用的系统资源都是独立的。
  线程是比进程更小的执行单位。一个进程在执行过程中,为了同时完成多个操作,可以产生多个线程。与进程不同的是,线程没有入口,也没有出口,其自身不能自动运行,而必须存在于某一进程中,由进程触发执行。在系统资源的使用上,属于同一进程的所有线程共享该进程的系统资源。

二、线程的生命周期

   每个Java程序都有一个默认的主线程。对于应用程序,主线程是main()方法执行的线索,要想实现多线程,必须在主线程中创建新的线程对象。新建的线程在一个完整的生命周期中通常需要经历创建、就绪、运行、阻塞、死亡五种状态 。
在这里插入图片描述

1.新建状态

  当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状态。
  例如,下面的语句可以创建一个新的线程:

myThread myThread1=new myThread1();

myThread线程类有两种实现方式,一种是继承Thread类;另一种是实现Runnable接口。

2.就绪状态

   一个线程对象调用start()方法,即可使其处于就绪状态。处于就绪状态的线程具备了除CPU资源之外的运行线程所需的所有资源。也就是说,就绪状态的线程排队等候CPU资源,而这将由系统进行调度。

3.运行状态

   处于就绪状态的线程获得CPU资源后即处于运行状态。每个Thread类及其子类的对象都有一个run()方法,当线程处于运行状态时,它将自动调用自身的run()方法,并开始执行run()方法中的内容。

4.阻塞状态

  处于运行状态的线程如果因为某种原因不能继续执行,则进入阻塞状态。阻塞状态与就绪状态的区别是:就绪状态只是因为缺少CPU资源不能执行,而阻塞状态可能会由于各种原因使得线程不能执行,而不仅仅是CPU资源。引起阻塞的原因解除以后,线程再次转为就绪状态,等待分配CPU资源。

5.死亡状态

   当线程执行完run()方法的内容或被强制终止时,则处于死亡状态。至此,线程的生命周期结束。

8.2 掌握线程的创建与启动方法

一、创建线程

  在Java中,创建线程有两种方式:一种是继承java.lang.Thread类,另一种是实现Runnable接口。

1.通过继承Thread类创建线程类

  Java中定义了线程类Thread,用户可以通过继承Thread类,覆盖其run()方法创建线程类。
  通过继承Thread类创建线程的语法格式如下:

class <ClassName> extends Thread{ 
	public void run(){
	……//线程执行代码
             }
}

2.通过实现Runnable接口创建线程类

   另一种方式是通过实现Runnable接口创建线程类,进而实现Runnable接口中的run()方法。其语法格式如下:

      class <ClassName> implements Runnable{
	public void run(){
	……//线程执行代码
	}
      }
//实现Runnable接口创建线程类
class MyThread implements Runnable{	MyThread
	public void run(){		//实现Runnable接口的run()方法
		for(int i=0;i<9;i++){
		System.out.println(i);
		}
	}
}

二、启动线程

1.通过继承Thread类线程的启动

   继承Thread类方式的线程的启动非常简单,只要在创建线程类对象后,调用类的start()方法即可。

// ThreadExample1.java
package Chapter8;

class MyThread extends Thread { // 继承Thread类创建线程类MyThread
	public void run() { // 重写Thread类的run()方法
		for (int i = 0; i < 10; i++) {
			System.out.print(i+"  "); // 打印0~9之间的数字
		}
	}
}

public class ThreadExample1 {
	public static void main(String args[]) {
		MyThread t = new MyThread(); // 创建线程类MyThread的实例t
		t.start(); // 启动线程
	}
}

2.实现Runnable接口线程的启动

  对于通过实现Runnable接口创建的线程类,应首先基于此类创建对象,然后再将该对象作为Thread类构造方法的参数,创建Thread类对象,最后通过Thread类对象调用Thread类的start()方法启动线程。

// ThreadExample2.java
package Chapter8;

class MyThread1 implements Runnable { // 实现Runnable接口创建线程类MyThread
	public void run() { // 实现Runnable接口的run()方法
		for (int i = 0; i < 9; i++) {
			System.out.print(i + "  ");
		}
	}
}

public class ThreadExample2 {
	public static void main(String args[]) {
		MyThread1 mt = new MyThread1(); // 创建线程类MyThread的实例t
		Thread t = new Thread(mt); // 创建Thread类的实例t
		t.start(); // 启动线程
	}
}

8.3 了解线程的优先级设置与调度方法

一、线程的优先级

 线程的优先级是指线程在被系统调度执行时的优先级级别。在多线程程序中,往往是多个线程同时在就绪队列中等待执行。优先级越高,越先执行;优先级越低,越晚执行;优先级相同时,则遵循队列的“先进先出”原则。
  Thread类有三个与线程优先级有关的静态变量,其意义如下:
MIN_PRIORITY:线程能够具有的最小优先级(1)。
MAX_PRIORITY:线程能够具有的最大优先级(10)。
NORM_PRIORITY:线程的普通优先级,默认值是5。

提示:当创建线程时,优先级默认为由NORM_PRIORITY标识的整数(5)。可以通过setPriority()方法设置线程的优先级,也可以通过getPriority()方法获得线程的优先级。

// ThreadExample3.java
package Chapter8;

class MyThread2 extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(i + " " + getName() + "优先级是:" + getPriority());
		}
	}
}

public class ThreadExample3 {
	public static void main(String args[]) {
		MyThread2 t1 = new MyThread2(); // 创建线程类MyThread2的实例t1
		MyThread2 t2 = new MyThread2(); // 创建线程类MyThread2的实例t2
		t1.setPriority(1); // 设置线程t1的优先级为1
		t2.setPriority(10); // 设置线程t2的优先级为10
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}
}

程序可能运行的结果:
0 Thread-0优先级是:1
0 Thread-1优先级是:10
1 Thread-1优先级是:10
1 Thread-0优先级是:1
2 Thread-1优先级是:10
2 Thread-0优先级是:1
3 Thread-1优先级是:10
3 Thread-0优先级是:1
4 Thread-1优先级是:10
4 Thread-0优先级是:1

二、线程休眠

   对于正在运行的线程,可以调用sleep()方法使其放弃CPU资源进行休眠,此线程转为阻塞状态。
  sleep()方法包含long型的参数,用于指定线程休眠的时间,单位为毫秒。sleep()方法会抛出非运行时异常InterruptedException,程序需要对此异常进行处理。

// ThreadExample3.java
package Chapter8;

class MyThread2 extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(i + " " + getName() + "优先级是:" + getPriority());
		}
	}
}

public class ThreadExample3 {
	public static void main(String args[]) {
		MyThread2 t1 = new MyThread2(); // 创建线程类MyThread2的实例t1
		MyThread2 t2 = new MyThread2(); // 创建线程类MyThread2的实例t2
		t1.setPriority(1); // 设置线程t1的优先级为1
		t2.setPriority(10); // 设置线程t2的优先级为10
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}
}

三、线程让步

   对于正在运行的线程,可以调用yield()方法使其重新在就绪队列中排队,并将CPU资源让给排在队列后面的线程,此线程转为就绪状态。
  另外,yield()方法只让步给高优先级或同等优先级的线程,如果就绪队列后面是低优先级线程,则继续执行此线程。yield()方法没有参数,也没有抛出任何异常。

// ThreadExample4.java
package Chapter8;

class MyThread3 extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.print(i+"  ");
			try {
				sleep(1000); // 线程休眠1秒,即每隔1秒打印一个数字
			} catch (InterruptedException e) {
				System.out.print("error:" + e);
			}
		}
	}
}

public class ThreadExample4 {
	public static void main(String[] args) {
		MyThread3 t = new MyThread3();
		t.start(); // 启动线程t
	}
}

// ThreadExample5.java
package Chapter8;

class MyThread4 extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.print(i);
			yield(); // 线程让步
		}
	}
}

public class ThreadExample5 {
	public static void main(String[] args) {
		MyThread4 t1 = new MyThread4();
		MyThread4 t2 = new MyThread4();
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}
}

四、线程等待

  对于正在运行的线程,可以调用join()方法等待其结束,然后才执行其他线程。join()方法有几种重载形式。其中,不带任何参数的join()方法表示等待线程执行结束为止。
  另外,join()方法也会抛出非运行时异常InterruptedException,程序需要对此异常进行处理。

// ThreadExample6.java
package Chapter8;

class MyThread5 extends Thread {
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.print(i);
		}
	}
}

public class ThreadExample6 {
	public static void main(String[] args) throws InterruptedException {
		MyThread5 t1 = new MyThread5();
		MyThread5 t2 = new MyThread5();
		t1.start(); // 创建线程t1
		t1.join(); // 等待t1执行结束
		t2.start();
	}
}

实例8-1 模拟左右手轮流写字

【实例描述】
   利用多线程的调度机制实现左右手轮流写字。
【技术要点】
  利用继承Thread类的方法创建线程类LeftHand与RightHand,并在其中重写run()方法。然后调用休眠方法sleep()让当前线程让出CPU资源。最后,线程对象调用start()方法启动线程。

// ThreadTest.java
package Chapter8;

public class ThreadTest {
	public static void main(String[] args) {
		LeftHand left = new LeftHand(); // 创建线程left
		RightHand right = new RightHand(); // 创建线程right
		left.start(); // 线程启动后,LeftHand类中的run()方法将被执行
		right.start();
	}
}

// 左手线程类LeftHand
class LeftHand extends Thread {
	public void run() {
		for (int i = 0; i <= 5; i++) {
			System.out.print("A");
			try {
				sleep(500); // left线程休眠500毫秒
			} catch (InterruptedException e) {
			}
		}
	}
}

// 右手线程类RightHand
class RightHand extends Thread {
	public void run() {
		for (int i = 0; i <= 5; i++) {
			System.out.print("B");
			try {
				sleep(300); // right线程休眠300毫秒
			} catch (InterruptedException e) {
			}
		}
	}
}

8.4 掌握多线程的同步机制——同步方法的使用

  在程序中运行多个线程时,可能会发生以下问题:当两个或多个线程同时访问同一个变量,并且一个线程需要修改这个变量时,程序中可能会出现预想不到的结果。
  例如,一个工资管理人员正在修改雇员的工资表,而其他雇员正在复制工资表。如果这样做,就会出现混乱。因此,工资管理人员在修改工资表时,应该不允许任何雇员操作工资表。也就是说,这些雇员必须等待。

// ThreadExample7.java
package Chapter8;

class MyThread6 implements Runnable {
	private int count = 0; // 定义共享变量count

	public void run() {
		test();
	}

	private void test() {
		for (int i = 0; i < 5; i++) {
			count++;
			Thread.yield(); // 线程让步
			count--;
			System.out.print(count + "  "); // 输出count的值
		}
	}
}

public class ThreadExample7 {
	public static void main(String[] args) throws InterruptedException {
		MyThread6 t = new MyThread6();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}
}

程序可能运行的结果:
0 0 0 0 0 0 0 0 0 0
1 0 0 0 1 1 0 0 0 0

   要解决共享资源问题,需要使用synchronized关键字对共享资源进行加锁控制,进而实现线程的同步。
   synchronized关键字可以作为方法的修饰符,也可以修饰一个代码块。使用synchronized修饰的方法称为同步方法。当一个线程A执行这个同步方法时,试图调用该同步方法的其他线程都必须等待,直到线程A退出该同步方法。

// ThreadExample8.java
package Chapter8;

class MyThread7 implements Runnable {
	private int count = 0; // 定义共享变量count

	public void run() {
		test();
	}

	private synchronized void test() {
		for (int i = 0; i < 5; i++) {
			count++;
			Thread.yield(); // 线程让步
			count--;
			System.out.print(count + "  "); // 输出count的值
		}
	}
}

public class ThreadExample8 {
	public static void main(String[] args) throws InterruptedException {
		MyThread7 t = new MyThread7();
		Thread t1 = new Thread(t);
		Thread t2 = new Thread(t);
		t1.start(); // 启动线程t1
		t2.start(); // 启动线程t2
	}
}

程序可能运行的结果:
0 0 0 0 0 0 0 0 0 0

  提示:当一个线程使用的同步方法用到某个变量,而此变量又需要其他线程修改后才能符合本线程的需要,那么可以在同步方法中使用wait()方法。使用wait()方法可以中断方法的执行,使本线程等待,暂时让出CPU资源的使用权,并允许其他线程使用这个同步方法。如果其他线程使用这个同步方法时不需要等待,那么它使用完这个同步方法时,应当使用notifyAll()方法通知所有由于使用这个同步方法而处于等待的线程结束等待。曾中断的线程就会从刚才的中断处继续执行这个同步方法,并遵循“先中断先继续”的原则。

实例8-2 模拟排队买票

【实例描述】
  张先生和李先生买电影票,售票员只有两张5元的钱,电影票5元一张。张先生用一张20元的人民币排在李先生的前面买票,而李先生用一张5元的人民币买票。请通过编程模拟排队买票的情形。
【技术要点】
  ① 如果售票员5元钱的个数少于3,当“张先生线程”用20元钱去买票时,则“张先生线程”应调用wait()方法等待并允许“李先生线程”买票。“李先生线程”执行完毕后应调用notifyAll()方法通知“张先生线程”继续进行买票。
   ② Thread类的currentThread()方法返回正在运行的线程。

// TicketSeller.java
package Chapter8;

public class TicketSeller {
	int sumFive = 2, sumTwenty = 0; // 定义5元钱与20元钱的个数

	public synchronized void sellRegulate(int money) {
		if (money == 5) {
			System.out.println("李先生,您的钱数正好。");
		} else if (money == 20) {
			while (sumFive < 3) {
				try {
					wait(); // 如果5元的个数少于3张,则线程等待
				} catch (InterruptedException e) {
				}
				sumFive = sumFive - 3;
				sumTwenty = sumTwenty + 1;
				System.out.println("张先生,您给我20元,找您15元。");
			}
		}
		notifyAll(); // 通知等待的线程
	}
}

// TicketSellerTest.java
package Chapter8;

public class TicketSellerTest implements Runnable {
	static Thread MrZhang, MrLi;
	static TicketSeller MissWang;

	public void run() {
		if (Thread.currentThread() == MrZhang) { // 判断当前的线程

			MissWang.sellRegulate(20); // 调用买票的方法
		} else if (Thread.currentThread() == MrLi) {

			MissWang.sellRegulate(5);
		}
	}

	public static void main(String[] args) {
		TicketSellerTest t = new TicketSellerTest();
		MrZhang = new Thread(t);
		MrLi = new Thread(t);
		MissWang = new TicketSeller();
		MrZhang.start(); // 启动张先生的线程
		MrLi.start(); // 启动李先生的线程
	}
}

综合实例——生产者与消费者的同步

// ProducerConsumerSyn.java
package Chapter8;

// 生产者线程
class Producer extends Thread {
	private Monitor s;

	Producer(Monitor s) {
		this.s = s;
	}

	public void run() {
		for (char ch = 'A'; ch <= 'E'; ch++) {
			try {
				Thread.sleep((int) Math.random() * 400); // 线程休眠
			} catch (InterruptedException e) {
			}
			s.recordProduct(ch); // 记录生产的产品
			System.out.println(ch + " product has been produced by producer.");
		}
	}
}

// 消费者线程类
class Consumer extends Thread {
	private Monitor s;

	Consumer(Monitor s) {
		this.s = s;
	}

	public void run() {
		char ch;
		do {
			try {
				Thread.sleep((int) Math.random() * 400); // 线程休眠
			} catch (InterruptedException e) {
			}
			ch = s.getProduct(); // 获取生产的产品
			System.out.println(ch + " product has been consumed by consumer!");
		} while (ch != 'E');
	}
}

// 监视器类
class Monitor {
	private char c;
	// 生产消费标记。true: 表示产品已生产,但未消费
	// flase:表示产品已消费,但新的产品尚未生产出来
	private boolean flag = true;

	// 记录生产的产品。如果产品未消费,则等待,即flag由false变为true	
	public synchronized void recordProduct(char c) {
		// 如果新的产品尚未生产出来,则让消费者等待
		if (!flag) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}
		this.c = c; // 记录生产的产品
		flag = false;// 产品尚未消费
		notify(); // 通知消费者线程,产品已经可以消费
	}

	// 获取生产的产品。如果产品已消费,则等待新的产品生产出来
	// 即flag由true变为flase
	public synchronized char getProduct() {
		// 产品已生产出来,等待消费
		if (flag) {
			try {
				wait();
			} catch (InterruptedException e) {
			}
		}
		flag = true;// 产品已消费
		notify(); // 通知生产者需要生产新的产品
		return this.c; // 返回生产的产品
	}
}

// 公共测试类
public class ProducerConsumerSyn {
	public static void main(String args[]) {
		Monitor s = new Monitor();
		new Producer(s).start(); // 启动生产者进程
		new Consumer(s).start(); // 启动消费者进程
	}
}

程序运行的结果:
李先生,您的钱数正好。
张先生,您给我20元,找您15元。

本章小结

  本章介绍了Java的多线程知识,具体内容包括进程与线程的概念、线程的创建与启动方法、线程的调度方法以及线程的同步机制等。其中,线程的同步机制是本章的难点,其关键是掌握同步方法的使用。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值