多应用+插件架构,代码干净,二开方便,首家独创一键云编译技术,文档视频完善,免费商用码云13.8K 广告
[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(); // 启动消费者线程 } } ``` 上述代码第①行创建堆栈对象。代码第②行创建生产者线程,代码第③行创建消费者线程。