[TOC]
*****
# 23.1 基础知识
## 23.1.1 进程
**一个进程**就是一个执行中的程序,而每一个进程都有自己独立的一块内存空间、一组系统资源。在进程的概念中,每一个进程的内部数据和状态都是完全独立的。
在Windows操作系统下可以通过Ctrl+Alt+Del组合键查看进程,在UNIX和Linux操作系统下是通过ps命令查看进程的。打开Windows当前运行的进程,如图23-1所示。
![](https://box.kancloud.cn/40a27aef4506c63db7aacc6db1ea2d43_506x462.png)
在Windows操作系统中一个进程就是一个exe或者dll程序,它们相互独立,互相也可以通信,在Android操作系统中进程间也可以通信。
## 23.1.2 线程
一个进程中可以包含多个线程,线程是一段完成某个特定功能的代码 ,是程序中单个顺序控制的流程,同类的多个线程是共享一块内存空间和一组系统资源。所以系统在各个线程之间切换时,开销要比进程小的多,线程被称为轻量级进程。
## 23.1.3 主线程
Java程序至少会有一个线程,这就是主线程,程序启动后是由JVM创建主线程,程序结束时由JVM停止主线程。主线程它负责管理子线程,即子线程的启动、挂起、停止等等操作。图23-2所示是进程、主线程和子线程的关系,其中主线程负责管理子线程,即子线程的启动、挂起、停止等操作。
![](https://box.kancloud.cn/973ff90bf460cb2fac34a10688f730bb_991x479.png)
**图23-2 进程、主线程和子线程关系**
获取主线程示例代码如下:
```
//HelloWorld.java文件
package lianl;
public class HelloWorld {
public static void main(String[] args) {
//获取主线程,在main方法中
Thread mainThread = Thread.currentThread();
System.out.println("主线程名:" + mainThread.getName());
}
}
```
Java中创建一个子线程涉及到:java.lang.Thread类和java.lang.Runnable接口。
**Thread线程类:** 创建一个Thread对象就会产生一个新的线程。
**线程执行对象:** 实现Runnable接口的对象,线程执行的代码是实现Runnable接口对象重写run()方法中的代码。
**提示**
**主线程中执行入口**是main(String\[\] args)方法,主线程可以控制程序的流程,管理其他的子线程。
**子线程执行入口**是线程执行对象的run()方法
# 23.2.1 实现Runnable接口
创建线程Thread对象时,可以将线程执行对象传递给它,这需要是使用Thread类如下两个构造方法:
* Thread(Runnable target, String name):target是线程执行对象,实现Runnable接口。name为线程指定一个名字。
* Thread(Runnable target):target是线程执行对象,实现Runnable接口。线程名字是由JVM分配的。
*****
**下面看一个具体示例,实现Runnable接口的线程执行对象Runner代码如下:**
```
//Runner.java文件
package com.a51work6;
//线程执行对象
public class Runner implements Runnable { ①
// 编写执行线程代码
@Override
public void run() { ②
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n", i,
Thread.currentThread().getName()); ③
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
Thread.sleep(sleepTime); ④
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
}
```
上述代码第①行声明实现Runnable接口,这要覆盖代码第②行的run()方法。run()方法是线程体,在该方法中编写你自己的线程处理代码。
本例中线程体中进行了十次循环,每次让当前线程休眠一段时间。其中代码第③行是打印次数和线程的名字,Thread.currentThread()可以获得当前线程对象,getName()是Thread类的实例方法,可以获得线程的名。代码第④行Thread.sleep(sleepTime)是休眠当前线程, sleep是静态方法它有两个版本:
* static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠。
* static void sleep(long millis, int nanos) 在指定的毫秒数加指定的纳秒数内让当前正在执行的线程休眠。
**测试程序HelloWorld代码如下:**
```
//Runner.java文件
package com.a51work6;
//线程执行对象
public class Runner implements Runnable { ①
// 编写执行线程代码
@Override
public void run() { ②
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n", i,
Thread.currentThread().getName()); ③
try {
// 随机生成休眠时间
long sleepTime = (long) (1000 * Math.random());
// 线程休眠
Thread.sleep(sleepTime); ④
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
}
```
一台PC通常就只有一颗CPU,在某个时刻只能是一个线程在运行,而Java语言在设计时就充分考虑到线程的并发调度执行。对于程序员来说,在编程时要注意给每个线程执行的时间和机会,主要是通过让线程休眠的办法(调用sleep()方法)来让当前线程暂停执行,然后由其他线程来争夺执行的机会。如果上面的程序中没有用到sleep()方法,则就是第一个线程先执行完毕,然后第二个线程再执行完毕。所以用活sleep()方法是多线程编程的关键。
# 23.3 线程的状态
* 新建状态
新建状态(New)是通过new等方式创建线程对象,它仅仅是一个空的线程对象。
* 就绪状态
当主线程调用新建线程的start()方法后,它就进入就绪状态(Runnable)。此时的线程尚未真正开始执行run()方法,它必须等待CPU的调度。
* 运行状态
CPU的调度就绪状态的线程,线程进入运行状态(Running),处于运行状态的线程独占CPU,执行run()方法。
* 阻塞状态
因为某种原因运行状态的线程会进入不可运行状态,即阻塞状态(Blocked),处于阻塞状态的线程JVM系统不能执行该线程,即使CPU空闲,也不能执行该线程。如下几个原因会导致线程进入阻塞状态:
* 当前线程调用sleep()方法,进入休眠状态。
* 被其他线程调用了join()方法,等待其他线程结束。
* 发出I/O请求,等待I/O操作完成期间。
* 当前线程调用wait()方法。
处于阻塞状态可以重新回到就绪状态,如:休眠结束、其他线程加入、I/O操作完成和调用notify或notifyAll唤醒wait线程。
* 死亡状态
线程退出run()方法后,就会进入死亡状态(Dead),线程进入死亡状态有可以是正常实现完成run()方法进入,也可能是由于发生异常而进入的。
![](https://box.kancloud.cn/2bfa11ca505f381bfb0c52d8712bf781_1020x298.png)
# 23.4 线程管理
## 23.4.1 线程优先级
线程的调度程序根据线程决定每次线程应当何时运行,Java提供了10种优先级,分别用1~10整数表示,最高优先级是10用常量MAX\_PRIORITY表示;最低优先级是1用常量MIN\_PRIORITY;默认优先级是5用常量NORM\_PRIORITY表示。
Thread类提供了setPriority(int newPriority)方法可以设置线程优先级,通过getPriority()方法获得线程优先级。
**代码:**
![](https://box.kancloud.cn/893525eaf954020c0c71c5601c9c46e8_825x487.png)
## 23.4.2 等待线程结束
在介绍现在状态时提到过join()方法,当前主线程调用t1线程的join()方法,则阻塞当前主线程,等待t1线程,如果t1线程结束或等待超时,则当前线程回到就绪状态。
Thread类提供了多个版本的join(),它们定义如下:
* void join():等待该线程结束。
* void join(long millis):等待该线程结束的时间最长为millis毫秒。如果超时为0意味着要一直等下去。
* void join(long millis, int nanos):等待该线程结束的时间最长为millis毫秒加nanos纳秒。
使用join()方法示例代码如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
//共享变量
static int value = 0; ①
public static void main(String[] args) throws InterruptedException {
System.out.println("主线程 开始...");
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(() -> { ②
System.out.println("ThreadA 开始...");
for (int i = 0; i < 2; i++) {
System.out.println("ThreadA 执行...");
value++; ③
}
System.out.println("ThreadA 结束...");
}, "ThreadA");
// 开始线程t1
t1.start();
// 主线程被阻塞,等待t1线程结束
t1.join(); ④
System.out.println("value = " + value); ⑤
System.out.println("主线程 结束...");
}
}
~~~
运行结果如下:
~~~
主线程 开始...
ThreadA 开始...
ThreadA 执行...
ThreadA 执行...
ThreadA 结束...
value = 2
主线程 结束...
~~~
上述代码第①行是声明了一个共享变量value,这个变量在子线程中修改,然后主线程访问它。代码第②行是采用Lambda表达式创建线程,指定线程名为ThreadA。代码第③行是在子线程ThreadA中修改共享变量value。
代码第④行是在当前线程(主线程)中调用t1的join()方法,因此会导致主线程阻塞,等待t1线程结束。t1结束后,再执行主线程。代码第⑤行是打印共享变量value,从运行结果可见value = 2。
如果尝试将t1.join()语句注释掉,输出结果如下,因为此时两个线程交替运:
~~~
主线程 开始...
value = 0
主线程 结束...
ThreadA 开始...
ThreadA 执行...
ThreadA 执行...
ThreadA 结束...
~~~
> **提示** 使用join()方法的场景是,一个线程依赖于另外一个线程的运行结果,所以调用另一个线程的join()方法等它运行完成。
### 23.4.3 线程让步
线程类Thread提供静态方法yield(),调用yield()方法能够使当前线程给其他线程让步。它类似于sleep()方法,能够使运行状态的线程放弃CPU使用权,暂停片刻,然后重新回到就绪状态。
与sleep()方法不同的是,sleep()方法是线程进行休眠,能够给其他线程运行的机会,无论线程优先级高低都有机会运行。而yield()方法只给相同优先级或更高优先级线程机会。
示例代码如下:
~~~
//Runner.java文件
package com.a51work6;
//线程执行对象
public class Runner implements Runnable {
// 编写执行线程代码
@Override
public void run() {
for (int i = 0; i < 10; i++) {
// 打印次数和线程的名字
System.out.printf("第 %d次执行 - %s\n", i,
Thread.currentThread().getName());
Thread.yield(); ①
}
// 线程执行结束
System.out.println("执行完成! " + Thread.currentThread().getName());
}
}
~~~
代码第①行Thread.yield()能够使当前线程让步。
> **提示** yield()方法只能给相同优先级或更高优先级的线程让步,yield()方法在实际开发中很少使用,
> 因为不可以控制时间,而sleep()方法可以。
### 23.4.4 线程停止
线程体中的run()方法结束,线程进入死亡状态,线程就停止了。但是有些业务比较复杂,例如想开发一个下载程序,每隔一段执行一次下载任务,下载任务一般会在由子线程执行的,休眠一段时间再执行。这个下载子线程中会有一个死循环,但是为了能够停止子线程,设置一个结束变量。
示例下面如下:
~~~
//HelloWorld.java文件
package com.a51work6;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class HelloWorld {
private static String command = ""; ①
public static void main(String[] args) {
// 创建线程t1,参数是一个线程执行对象Runner
Thread t1 = new Thread(() -> {
// 一直循环,直到满足条件在停止线程
while (!command.equalsIgnoreCase("exit")) { ②
// 线程开始工作
// TODO
System.out.println("下载中...");
try {
// 线程休眠
Thread.sleep(10000);
} catch (InterruptedException e) {
}
}
// 线程执行结束
System.out.println("执行完成!");
});
// 开始线程t1
t1.start();
try (InputStreamReader ir = new InputStreamReader(System.in); ③
BufferedReader in = new BufferedReader(ir)) {
// 从键盘接收了一个字符串的输入
command = in.readLine(); ④
} catch (IOException e) {
}
}
}
~~~
上述代码第①行是设置一个结束变量。代码第②行是在子线程的线程体中判断,用户输入的是否为exit字符串,如果不是则进行循环,否则结束循环,结束循环就结束了run()方法,线程就停止了。
代码第③行中的System.in是一个很特殊的输入流,能够从控制台(键盘)读取字符。代码第④行是通过流System.in读取键盘输入的字符串。测试是需要注意:在控制台输入exit,然后敲Enter键,如图23-6所示。
*****
## 23.5 线程安全
在多线程环境下,访问相同的资源,有可以会引发线程不安全问题。本节讨论引发这些问题的根源和解决方法。
### 23.5.1 临界资源问题
多一个线程同时运行,有时线程之间需要共享数据,一个线程需要其他线程的数据,否则就不能保证程序运行结果的正确性。
例如有一个航空公司的机票销售,每一天机票数量是有限的,很多售票点同时销售这些机票。下面是一个模拟销售机票系统,示例代码如下:
~~~
//TicketDB.java文件
package com.a51work6;
//机票数据库
public class TicketDB {
// 机票的数量
private int ticketCount = 5; ①
// 获得当前机票数量
public int getTicketCount() { ②
return ticketCount;
}
// 销售机票
public void sellTicket() { ③
try {
// 等于用户付款
// 线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000); ④
} catch (InterruptedException e) {
}
System.out.printf("第%d号票,已经售出\n", ticketCount);
ticketCount--; ⑤
}
}
~~~
上述代码模拟机票销售过程,代码第①行是声明机票数量成员变量ticketCount,这是模拟当天可供销售的机票数,为了测试方便初始值设置为5。代码第②行是定义了获取当前机票数的getTicketCount()方法。代码第③行是销售机票方法,售票网点查询出有没有票可以销售,那么会调用sellTicket()方法销售机票,这个过程中需要等待用户付款,付款成功后,会将机票数减一,见代码第⑤行。为模拟等待用户付款,在代码第④行使用了sleep()方法让当前线程阻塞。
调用代码如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 创建线程t1
Thread t1 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount(); ①
// 查询是否有票
if (currTicketCount > 0) { ②
db.sellTicket(); ③
} else {
// 无票退出
break;
}
}
});
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
});
// 开始线程t2
t2.start();
}
}
~~~
在HelloWorld中创建了两个线程,模拟两个售票网点,没有线程所做的事情类似。首先获得当前机票数量(见代码第①行),然后判断机票数量是否大于零(见代码第②行),如果有票则出票(见代码第②行),否则退出循环,结束线程。
一次运行结果如下:
~~~
第5号票,已经售出
第5号票,已经售出
第3号票,已经售出
第3号票,已经售出
第1号票,已经售出
第0号票,已经售出
~~~
虽然可以能每次运行的结果都不一样,但是从结果看还是能发现一些问题:同一张票重复销售了两次。这些问题的原因是多个线程间共享的数据导致数据的不一致性。
> **提示** 多个线程间共享的数据称为共享资源或临界资源,由于是CPU负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
## 23.5 线程安全
在多线程环境下,访问相同的资源,有可以会引发线程不安全问题。
### 23.5.1 临界资源问题
多个线程同时运行,有时线程之间需要共享数据,一个线程需要其他线程的数据,否则就不能保证程序运行结果的正确性。
例如有一个航空公司的机票销售,每一天机票数量是有限的,很多售票点同时销售这些机票。下面是一个模拟销售机票系统,示例代码如下:
~~~
//TicketDB.java文件
package com.a51work6;
//机票数据库
public class TicketDB {
// 机票的数量
private int ticketCount = 5; ①
// 获得当前机票数量
public int getTicketCount() { ②
return ticketCount;
}
// 销售机票
public void sellTicket() { ③
try {
// 等于用户付款
// 线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000); ④
} catch (InterruptedException e) {
}
System.out.printf("第%d号票,已经售出\n", ticketCount);
ticketCount--; ⑤
}
}
~~~
上述代码模拟机票销售过程,代码第①行是声明机票数量成员变量ticketCount,这是模拟当天可供销售的机票数,为了测试方便初始值设置为5。代码第②行是定义了获取当前机票数的getTicketCount()方法。代码第③行是销售机票方法,售票网点查询出有没有票可以销售,那么会调用sellTicket()方法销售机票,这个过程中需要等待用户付款,付款成功后,会将机票数减一,见代码第⑤行。为模拟等待用户付款,在代码第④行使用了sleep()方法让当前线程阻塞。
调用代码如下:
~~~
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 创建线程t1
Thread t1 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount(); ①
// 查询是否有票
if (currTicketCount > 0) { ②
db.sellTicket(); ③
} else {
// 无票退出
break;
}
}
});
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new Thread(() -> {
while (true) {
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
});
// 开始线程t2
t2.start();
}
}
~~~
在HelloWorld中创建了两个线程,模拟两个售票网点,没有线程所做的事情类似。首先获得当前机票数量(见代码第①行),然后判断机票数量是否大于零(见代码第②行),如果有票则出票(见代码第②行),否则退出循环,结束线程。
一次运行结果如下:
~~~
第5号票,已经售出
第5号票,已经售出
第3号票,已经售出
第3号票,已经售出
第1号票,已经售出
第0号票,已经售出
~~~
虽然可以能每次运行的结果都不一样,但是从结果看还是能发现一些问题:同一张票重复销售了两次。这些问题的根本原因是多个线程间共享的数据导致数据的不一致性。
> **提示** 多个线程间共享的数据称为共享资源或临界资源,由于是CPU负责线程的调度,程序员无法精确控制多线程的交替顺序。这种情况下,多线程对临界资源的访问有时会导致数据的不一致性。
*****
### 23.5.2 多线程同步
为了防止多线程对临界资源的访问有时会导致数据的不一致性,Java提供了“互斥”机制,可以**为这些资源对象加上一把“互斥锁”**,在任一时刻只能由一个线程访问。
即使该线程出现阻塞,该对象的被锁定状态也不会解除,其他线程仍不能访问该对象,这就多线程同步。
线程同步保证线程安全的重要手段,但是线程同步客观上会导致性能下降。
**两种方式实现线程同步**
* 一种是synchronized方法,使用synchronized关键字修饰方法,对方法进行同步
* 另一种是synchronized语句,使用synchronized关键字放在对象前面限制一段代码的执行。
**1\. synchronized方法**
synchronized关键字修饰方法实现线程同步,方法所在的对象被锁定,修改23.5.1节售票系统示例, TicketDB.java文件代码如下:
```
~~~
//TicketDB.java文件
package com.a51work6.method;
//机票数据库
public class TicketDB {
// 机票的数量
private int ticketCount = 5;
// 获得当前机票数量
public synchronized int getTicketCount() { ①
return ticketCount;
}
// 销售机票
public synchronized void sellTicket() { ②
try {
// 等于用户付款
// 线程休眠,阻塞当前线程,模拟等待用户付款
Thread.sleep(1000);
} catch (InterruptedException e) {
}
System.out.printf("第%d号票,已经售出\n", ticketCount);
ticketCount--;
}
}
~~~
上述代码第①行和第②行的方法前都使用了synchronized关键字,表明这两个方法是同步的,被锁定的,每一个时刻只能由一个线程访问。并不是每一个方法都有必要加锁的,要仔细研究加上的必要性,上述代码第①行加锁可以防止出现第0号票情况和5张票卖出2次的情况;代码第②行加锁是防止出现销售两种一样的票,读者可以自己测试一下。
采用synchronized方法修改示例,调用代码HelloWorld.java不需要任何修改。
```
**2. synchronized语句**
synchronized语句方式主要用于第三方类,不方便修改它的代码情况。同样是23.5.1节售票系统示例,可以不用修改TicketDB.java类,只修改调用代码HelloWorld.java实现同步。
```
HelloWorld.java代码如下:
//HelloWorld.java文件
package com.a51work6.statement;
public class HelloWorld {
public static void main(String[] args) {
TicketDB db = new TicketDB();
// 创建线程t1
Thread t1 = new Thread(() -> {
while (true) {
synchronized (db) { ①
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
}
});
// 开始线程t1
t1.start();
// 创建线程t2
Thread t2 = new Thread(() -> {
while (true) {
synchronized (db) { ②
int currTicketCount = db.getTicketCount();
// 查询是否有票
if (currTicketCount > 0) {
db.sellTicket();
} else {
// 无票退出
break;
}
}
}
});
// 开始线程t2
t2.start();
}
}
```
代码第①行和第②行是使用synchronized语句,将需要同步的代码用大括号括起来。synchronized后有小括号,将需要同步的对象括起来。
*****
## 23.6 线程间通信
第23.5节的示例只是简单地为特定对象或方法加锁,但有时情况会更加复杂,如果两个线程之间有依赖关系,线程之间必须进行通信,互相协调才能完成工作。
例如有一个经典的堆栈问题,一个线程生成了一些数据,将数据压栈;另一个线程消费了这些数据,将数据出栈。这两个线程互相依赖,当堆栈为空时,消费线程无法取出数据时,应该通知生成线程添加数据;当堆栈已满时,生产线程无法添加数据时,应该通知消费线程取出数据。
为了实现线程间通信,需要使用Object类中声明的5个方法:
* void wait():使当前线程释放对象锁,然后当前线程处于对象等待队列中阻塞状态,如图23-7所示,等待其他线程唤醒。
* void wait(long timeout):同wait()方法,等待timeout毫秒时间。
* void wait(long timeout, int nanos):同wait()方法,等待timeout毫秒加nanos纳秒时间。
* void notify():当前线程唤醒此对象等待队列中的一个线程,如图23-7所示该线程将进入就绪状态。
* void notifyAll():当前线程唤醒此对象等待队列中的所有线程,如图23-7所示这些线程将进入就绪状态。
![](http://www.ituring.com.cn/figures/2017/Javarookietomaster/24.d23z.007.png)
**图23-7 线程间通信**
> **提示** 图23-7是图23-5补充,从图23-7可见,线程有多种方式进入阻塞状态,除了通过wait()外还有,加锁的方式和其他方式,加锁方式是23.5节介绍的使用synchronized加互斥锁;其他方式事实上是23.3节线程状态时介绍的方式,这里不再赘述。
下面看看消费和生产示例中堆栈类代码:
~~~
//Stack.java文件
package com.a51work6;
//堆栈类
class Stack {
// 堆栈指针初始值为0
private int pointer = 0;
// 堆栈有5个字符的空间
private char[] data = new char[5];
// 压栈方法,加上互斥锁
public synchronized void push(char c) { ①
// 堆栈已满,不能压栈
while (pointer == data.length) {
try {
// 等待,直到有数据出栈
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他线程把数据出栈
this.notify();
// 数据压栈
data[pointer] = c;
// 指针向上移动
pointer++;
}
// 出栈方法,加上互斥锁
public synchronized char pop() { ②
// 堆栈无数据,不能出栈
while (pointer == 0) {
try {
// 等待其他线程把数据压栈
this.wait();
} catch (InterruptedException e) {
}
}
// 通知其他线程压栈
this.notify();
// 指针向下移动
pointer--;
// 数据出栈
return data[pointer];
}
}
~~~
上述代码实现了同步堆栈类,该堆栈有最多5个元素的空间,代码第①行声明了压栈方法push(),该方法是一个同步方法,在该方法中首先判断是否堆栈已满,如果已满不能压栈,调用this.wait()让当前线程进入对象等待状态中。如果堆栈未满,程序会往下运行调用this.notify()唤醒对象等待队列中的一个线程。代码第②行声明了出栈方法pop()方法,与push()方法类似,这里不再赘述。
调用代码如下:
```
//HelloWorld.java文件
package com.a51work6;
public class HelloWorld {
public static void main(String args[]) {
Stack stack = new Stack(); ①
// 下面的消费者和生产者所操作的是同一个堆栈对象stack
// 生产者线程
Thread producer = new Thread(() -> { ②
char c;
for (int i = 0; i < 10; i++) {
// 随机产生10个字符
c = (char) (Math.random() * 26 + 'A');
// 把字符压栈
stack.push(c);
// 打印字符
System.out.println("生产: " + c);
try {
// 每产生一个字符线程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
// 消费者线程
Thread consumer = new Thread(() -> { ③
char c;
for (int i = 0; i < 10; i++) {
// 从堆栈中读取字符
c = stack.pop();
// 打印字符
System.out.println("消费: " + c);
try {
// 每读取一个字符线程就睡眠
Thread.sleep((int) (Math.random() * 1000));
} catch (InterruptedException e) {
}
}
});
producer.start(); // 启动生产者线程
consumer.start(); // 启动消费者线程
}
}
```
上述代码第①行创建堆栈对象。代码第②行创建生产者线程,代码第③行创建消费者线程。