第八章 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的多线程知识,具体内容包括进程与线程的概念、线程的创建与启动方法、线程的调度方法以及线程的同步机制等。其中,线程的同步机制是本章的难点,其关键是掌握同步方法的使用。