目的:为了开启一条执行路径,去运行指定的代码和其他代码实现同时运行
1. 多线程相关概念
1.1 程序、进程、线程
1)程序
为完成特定任务,用某种语言编写的一组指令集合。(静态代码)
2)进程
一个正在执行的程序;(程序加载到内存,动态)
3)线程
进程的一部分,控制进程的执行,执行功能的最小单位。
※ 一个 java 应用程序(java.exe),至少有三个线程,main() 主线程,gc() 垃圾回收线程,异常处理线程(会影响主线程)
1.2 内存分配
内存区域:方法区、堆、虚拟机栈、程序计数器、本地方法栈
每个线程各自有一套虚拟机栈和一套程序计数器;
一个进程一份方法区(static)和堆(new 对象及对象的实例变量),即各线程共享方法区和堆;
1.3 CPU单核和多核
单核CPU:同一时间只能执行一个线程任务,通过高频率不断切换执行各个线程任务,是一种假的多线程;
多核CPU:同一时间执行多个线程任务,真正的多线程;
1.4 并行与并发
并行:多个CPU 同时执行多个任务,好比多个人同时做不同的事;
并发:一个CPU “同时”执行多个任务,好比多个人做同一件事(秒杀)
1.5 多线程的优点
单核CPU “同时”执行多个线程要比分别执行各个线程(一个完成后再执行另一个)的速度要快,因为CPU 要不断的切换也需要耗费时间。
那么问题来了,既然多线程速度慢,为什么还要费劲整多线程呢?
优点:
1)提高应用程序的响应,对图形化界面更有意义,提高用户体验;
2)提高计算机系统CPU的利用率;
3)改善程序结构,将又长有复杂的进程分为多个线程,独立运行,利于理解和修改
1.6 多线程应用场景
1)程序需要同时执行多个任务
主线程执行时,伴随一个垃圾回收线程,随时回收垃圾,不然容易内存溢出;
2)程序需要实现一些需要等待的任务
用户输入、文件读写操作、网络操作、搜索等;
3)需要一些后台运行的程序
2. 代码实现
2.1 继承Thread 类(第一种)
2.1.1 实现步骤
- 1.继承Thread 类
- 2.重现 run(),具体要执行的代码
- 3.创建子类
- 4.子类对象调用 start(),启动当前线程,调用当前线程的 run()
1)如果不调用start(),而直接调用run(),则线程不被启用,仍旧为单线程;
2)对于一个线程来说,不可以让已经start()的线程再次start(),这是需要新建对象 Mythread mythread2 = new Mythread();,再去调用mythread2.start()
/***
* 多线程创建方法 - 继承 Thread类
*/
public class ThreadTest {
public static void main(String[] args) {
// 3.创建子类
Mythread mythread = new Mythread();
// 4.子类对象调用 start()
mythread.start();
// 主线程执行代码
for (int i = 0; i < 100; i++) {
if (i%2 == 0){
System.out.println("===Hello===" + i);
}
}
}
}
// 1.继承Thread 类
class Mythread extends Thread{
// 2.重现 run(),具体要执行的代码
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0){
System.out.println(i);
}
}
}
}
2.1.2 Thread 常用方法
1)start():启动当前线程,调用当前线程的 run();
2)run():通常需要重写Thread 类中的此方法,将创建的线程要执行的操作写在此方法中;
3)currentThread():当前线程,静态方法;
4)setName()/getName():设置/获取线程名称;
5)yield():一时释放当前CPU执行权;
6)join():在线程A中调用B线程的join(),线程A进入阻塞状态,B执行完之后,A结束阻塞状态(礼让B先执行);
7)stop():已过时,强制结束当前线程;
8)sleep(1000):让当前线程强制“睡眠”指定时间;
9)isAlive():判断当前线程是否存活
public class ThreadMethodTest {
public static void main(String[] args) {
MyThreadMethod myThread = new MyThreadMethod("Thread1");
// 设置线程名
// myThread.setName("Thread1");
myThread.start();
// 设置主线程名
Thread.currentThread().setName("MainThread");
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + ":" + i);
}
if (i == 20) {
try {
// 在线程A中调用B线程的join(),线程A进入阻塞状态,B执行完之后,A结束阻塞状态
// 礼让B先执行
myThread.join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
System.out.println(myThread.isAlive());
}
}
class MyThreadMethod extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
try {
// 强制阻塞线程
sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + ":" + i);
}
// if (i%20 == 0) {
// // 一时释放CPU执行权
// yield();
// }
}
}
public MyThreadMethod(String name){
super(name);
}
}
2.1.3 线程调度
优先级从低到高:1 ~ 10
// 设定优先级
setPriority(Thread.MAX_PRIORITY);
// 获取优先级
getPriority()
注意:
设置高优先级,只是提高执行概率,并不是一定优先执行
public class ThreadMethodTest {
public static void main(String[] args) {
MyThreadMethod myThread = new MyThreadMethod();
myThread.setName("Thread1");
// 设置分线程的优先级(只是提高执行概率,并不是一定优先执行)
myThread.setPriority(Thread.MAX_PRIORITY);
myThread.start();
// 设置主线程名
Thread.currentThread().setName("MainThread");
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":"+i);
}
}
}
}
class MyThreadMethod extends Thread{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
// 获取当前线程名称
System.out.println(Thread.currentThread().getName() + ":" + Thread.currentThread().getPriority() + ":"+i);
}
}
}
}
2.1.4 案例
三个卖票窗口,总票数100张
/***
* 三个卖票窗口,总票数100张
*/
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(getName() + "卖票,票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
存在线程安全问题,保留
若不想使用static ,引出下一节Rumable 接口
private static int ticket = 100;
2.2 实现Runnable 接口(第二种)
2.2.1 实现步骤
- 1.定义类实现Runable 接口;
- 2 覆盖Runable中的run();
- 3 通过Thread 类创建线程对象;// Thread t1 = new Thread(Runable r);
- 4 将Runable 接口子类对象作为实际参数传递给Thread 类的构造方法;
- 5 调用Thread 类的Start 方法开启线程,并调用Runable 接口子类的run 方法
/***
* 多线程创建方法 - 实现Runnable 接口
*/
public class RunnableTest {
public static void main(String[] args) {
// 3.创建实现接口类的对象
MyRunnable runnable = new MyRunnable();
// 4.将此对象作为参数传递到Thread 类的构造器中,创建Thread 对象
Thread t1 = new Thread(runnable);
t1.setName("线程1");
// 5.通过Thread 类的对象调用start()
t1.start();
// 再启动一个线程
Thread t2 = new Thread(runnable);
t2.setName("线程2");
t2.start();
}
}
// 1.创建实现Runnable接口的类
class MyRunnable implements Runnable{
// 2.实现抽象方法
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.2.2 案例
三个卖票窗口,总票数100张,Runnable方式
因为只创建了一个 Window1 对象,所以ticket 不用static 定义
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
private int ticket = 100;
@Override
public void run() {
while (true){
if (ticket > 0){
System.out.println(Thread.currentThread().getName() + "票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
2.3 实现Callable 接口(第三种)(JDK5.0新增)
public class ThreadCallableTest {
public static void main(String[] args) {
// 3.创建Callable 接口实现例的对象
CallableThread callableThread = new CallableThread();
// 4.接口实现例的对象作为参数传递给FutureTask构造器中,创造FutureTask对象
FutureTask<Integer> futureTask = new FutureTask<Integer>(callableThread);
// 5.将FutureTask对象作为参数,传递给Thread类中,创建Thread对象,并调用start()
new Thread(futureTask).start();
try {
// 返回值即为FutureTask 参数CallableThread 重写的call() 的返回值
// 6.获取Callable中call() 的返回值,可以用于线程间传值(参考)
Integer sum = futureTask.get();
System.out.println("总和为:" + sum);
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
}
}
}
// 1.创建Callable 接口的实现类
class CallableThread implements Callable<Integer> {
// 2.实现call(),子线程需要执行的操作声明在此处
@Override
public Integer call() throws Exception {
int sum = 0;
for (int i = 1; i <= 100; i++) {
if(i%2 == 0){
sum += i;
}
}
return sum;
}
}
2.4 线程池-(第四种)(JDK5.0新增)
1)背景
线程经常创建和销毁、使用量特别打的资源,比如并发情况下的线程,对性能影响打
2)思路
提前创建好多个线程,放入线程池中,使用时直接获取,使用完放回池中,可以避免频繁创建销毁、实现重复利用。类似生活中的公共交通工具,而不用自己造车。
3)好处
① 提高相应速度(减少了创建新线程的时间);
② 降低资源消耗(重复利用线程池中线程,不需要每次都创建);
③ 便于线程管理
corePoolSize:核心池大小
maximunPoolSize:最大线程数
keepAliveTime:线程没有任务时最多保持多长时间后会终止
…
public class ThreadPoolTest {
public static void main(String[] args) {
// 1.提供指定线程数量的线程池
ExecutorService executorService = Executors.newFixedThreadPool(10);
// 设置线程池的属性
ThreadPoolExecutor executorService1 = (ThreadPoolExecutor) executorService;
executorService1.setCorePoolSize(15);
// 2.执行指定的线程操作,需要提供实现Runnable 或Callable接口
executorService1.execute(new RunnableThread1());// 适合适用于Runnable
executorService1.execute(new RunnableThread2());// 适合适用于Runnable
// 3.关闭连接池
executorService1.shutdown();
// Callable接口
// executorService.submit(Callable callable);// 适合适用于Callable
}
}
class RunnableThread1 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 == 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
class RunnableThread2 implements Runnable{
@Override
public void run() {
for (int i = 0; i < 100; i++) {
if (i%2 != 0) {
System.out.println(Thread.currentThread().getName() + ":" + i);
}
}
}
}
2.5 Thread 和Runnable 比较
开发中优先选择实现Runnable 接口的方式
1)实现的方法没有类的单继承的局限性;
2)更适合处理多个线程有共享数据的情况;
两种方式都要重写run()
2.6 Callable 和 Runnable比较
Callable 要比Runnable 更强大些
Callable 的Call() 可以有返回值
可以抛异常;
支持泛型;
需要借助FutureTask 类,比如获取返回结果
3. 线程的生命周期
3.1 线程的状态
新建、运行、冻结(sleep()、wait())、阻塞、消灭
cpu的执行资格:可以被cpu处理,在处理队列中排队
cpu的执行权:正在被cpu处理
3.2 生命周期
4. 线程安全
线程的同步(只有一个线程可以访问)
解决线程安全问题,多线程访问共同变量时,为保证当前变量的完整性,Synchronized 关键字修饰操作共享变量的方法时,其他线程对共享变量不可操作;
4.1 同步代码块(synchronized)
1)确认共享数据
共享数据:多个线程共同操作的变量(数据)
2)确认操作共享数据的代码
操作共享数据的代码,即为需要被同步的代码,要放入同步代码块中;
3)确认同步监视器
同步监视器,俗称“锁”,要求多个线程必须要公用一把锁。
4.1.1 解决Runnable 安全问题
1)多窗口卖票
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
// 共享数据
private int ticket = 100;
// 锁:多个线程必须要公用一把锁,因为Window1对象只被创建一次
Object obj = new Object();
@Override
public void run() {
while (true){
// 同步代码块,obj也可以换成this,因为该对象只被创建一次
synchronized (obj){
if (ticket > 0){
// 提升出现错票概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为:" + ticket);
ticket--;
}else {
break;
}
}
}
}
}
2)银行同账户存钱
/***
* 两个储户分别向同一账户存3000元,每次存1000
* 分析:
* 属于多线程(两个储户),存在共享数据(账户余额)
*/
public class AccountTest {
public static void main(String[] args) {
// 两客户公用账户对象
Account acc = new Account(0);
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
// 共享账户类
/***
* 两个储户分别向同一账户存3000元,每次存1000
* 分析:
* 属于多线程(两个储户),存在共享数据(账户余额)
*/
public class AccountTest {
public static void main(String[] args) {
// 两客户公用账户对象
Account acc = new Account(0);
Customer c1 = new Customer(acc);
Customer c2 = new Customer(acc);
c1.setName("甲");
c2.setName("乙");
c1.start();
c2.start();
}
}
// 共享账户类
class Account{
private double balance;
public Account(double balance) {
this.balance = balance;
}
// 存钱(synchronized 解决线程安全问题)
// 继承Thread 默认锁为this,而Account 对象唯一,没有死锁问题
public synchronized void deposit(double amt){
if (amt > 0) {
// 添加阻塞(测试用)
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
balance += amt;
System.out.println(Thread.currentThread().getName() + "客户存钱成功!余额为:"+balance);
}
}
}
// 用户类
class Customer extends Thread{
private Account acc;
public Customer(Account acc) {
this.acc = acc;
}
@Override
public void run() {
for (int i = 0; i < 3; i++) {
acc.deposit(1000);
}
}
}
输出:
甲客户存钱成功!余额为:1000.0
甲客户存钱成功!余额为:2000.0
乙客户存钱成功!余额为:3000.0
甲客户存钱成功!余额为:4000.0
乙客户存钱成功!余额为:5000.0
乙客户存钱成功!余额为:6000.0
4.1.2 解决Thread 安全问题
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
private static Object obj = new Object();
@Override
public void run() {
while (true){
// obj 不可以用this 替代,因为Window 对象被创建多次,不唯一,而可以用Window.class,类只加载一次
synchronized (obj) {
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(getName() + "卖票,票号为:" + ticket);
ticket--;
} else {
break;
}
}
}
}
}
4.2 同步方法(synchronized)
如果操作共享数据的代码完整的声明在一个方法中,可以将该方法声明为同步的方法;
仍然需要同步监视器,只是不需要显示声明,非静态的同步方法是this,静态的同步方法是当前类本身
4.2.1 解决Runnable 安全问题
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
// 共享数据
private int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
// 提取操作共享数据的代码
private synchronized void show(){
if (ticket > 0){
// 提升出现错票概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为:" + ticket);
ticket--;
}
}
}
4.2.2 解决Thread 安全问题
public class WindowTest {
public static void main(String[] args) {
Window w1 = new Window();
Window w2 = new Window();
Window w3 = new Window();
w1.setName("窗口1");
w2.setName("窗口2");
w3.setName("窗口3");
w1.start();
w2.start();
w3.start();
}
}
class Window extends Thread{
private static int ticket = 100;
@Override
public void run() {
while (true){
show();
}
}
// 方法要定义为 static
private static synchronized void show(){
if (ticket > 0) {
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "卖票,票号为:" + ticket);
ticket--;
}
}
}
4.2.3 解决懒汉模式线程安全问题
// 懒汉式
class Bank{
private Bank(){}
private static Bank instance = null;
public static synchronized Bank getInstance(){
if (instance == null){
instance = new Bank();
}
return instance;
}
}
4.3 解决线程安全问题(Lock锁)
JDK5.0 新增
步骤:
1.实例话lock
2.加锁
3.解锁
public class WindowTest1 {
public static void main(String[] args) {
Window1 w1 = new Window1();
Thread t1 = new Thread(w1);
Thread t2 = new Thread(w1);
Thread t3 = new Thread(w1);
t1.setName("窗口1");
t2.setName("窗口2");
t3.setName("窗口3");
t1.start();
t2.start();
t3.start();
}
}
class Window1 implements Runnable{
// 共享数据
private int ticket = 100;
// 1.实例话lock,如果用继承Thread 方式的话,需要将lock 声明为静态(锁唯一)
// true:先来后到
private ReentrantLock lock = new ReentrantLock(true);
@Override
public void run() {
while (true){
try {
// 2.加锁
lock.lock();
if (ticket > 0){
// 提升出现错票概率
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread().getName() + "票号为:" + ticket);
ticket--;
}
}finally{
// 3.解锁
lock.unlock();
}
}
}
}
4.4 synchronized 和 Lock锁 异同
4.4.1 相同点
二者都可以解决线程安全问题
4.4.2 不同点
synchronized 在执行完相应的同步代码后,自动的释放同步监视器
Lock锁 需要手动的启动同步和结束同步
4.4.3 优先使用顺序
开发中推荐使用优先顺序:
Lock锁 ⇒ 同步代码块(进入方法体,再分配资源) ⇒ 同步方法(在方法体之外)
4.5 线程死锁
解决线程安全时,添加锁的情况下,容易造成死锁问题,要注意
不同的线程分别占用对方需要的同步资源不放弃,都在等对方放弃自己需要的同步资源,就形成了线程死锁;
出现死锁后,不会出现异常,也不会提示,只是所有的线程都处于阻塞状态,无法继续
死锁代码例
public class ThreadLockTest {
public static void main(String[] args) {
StringBuffer s1 = new StringBuffer();
StringBuffer s2 = new StringBuffer();
// 继承Thread 线程
new Thread(){
@Override
public void run() {
synchronized (s1){
s1.append("a");
s2.append("1");
// 阻塞代码(测试用),增加死锁发生概率
// 拥有s1锁,等待s2锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s2){
s1.append("b");
s2.append("2");
System.out.println(s1);
System.out.println(s2);
}
}
}
}.start();
// 实现Runnable 线程
new Thread(new Runnable() {
@Override
public void run() {
synchronized (s2){
s1.append("c");
s2.append("3");
// 阻塞代码(测试用),增加死锁发生概率
// 拥有s2锁,等待s1锁
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (s1){
s1.append("d");
s2.append("4");
System.out.println(s1);
System.out.println(s2);
}
}
}
}).start();
}
}
5. 同步线程间通信
多线程在处理同一资源,但是任务却不同
5.1 用到的方法
wait():一旦执行,当前线程进入阻塞状态,并释放监视器(锁);
notify():一旦执行,将会唤醒被 wait的一个线程,多个线程被wait 时,唤醒优先级高的线程;
notifyAll():一旦执行,将会唤醒所有被 wait的线程。
注意:
1)只能用于同步代码块或同步方法中;
2)三个方法的调用者必须是同步代码块或同步方法中的同步监视器(锁);
3)三个方法是Object 类的方法;
5.2 案例
5.2.1 交替打印
/***
* 使用两个线程交替打印 1~100
*/
public class ThreadCommunicationTest {
public static void main(String[] args) {
Number number = new Number();
Thread t1 = new Thread(number);
Thread t2 = new Thread(number);
t1.setName("线程1");
t2.setName("线程2");
t1.start();
t2.start();
}
}
class Number implements Runnable{
private int number = 1;
@Override
public void run() {
while (true){
synchronized (this){
// 唤醒当前阻塞线程
notify();
if(number <= 100){
System.out.println(Thread.currentThread().getName() + ":" + number);
number++;
// 当前线程阻塞,执行wait() 会释放锁
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}else {
break;
}
}
}
}
}
5.2.2 生产者与消费者
生产者将产品交给店员,消费者从店员处取走产品,店员最多持有20个产品,超出则要求生产者暂停生产,低于20再通知生产;没有产品,告诉消费者等一等,有产品后通知消费者取走产品。
/***
* 生产者与消费者
* 分析:
* 线程:生产者、消费者
* 共享数据:产品
*/
public class ProductTest {
public static void main(String[] args) {
Clerk clerk = new Clerk();
Producer producer = new Producer(clerk);
Consumer customer1 = new Consumer(clerk);
Consumer customer2 = new Consumer(clerk);
producer.setName("生产者");
customer1.setName("消费者1");
customer2.setName("消费者2");
producer.start();
customer1.start();
customer2.start();
}
}
// 店员
class Clerk{
private int productCount = 0;
// 生产产品
public synchronized void produceProduct() {
if(productCount < 20){
productCount++;
System.out.println(Thread.currentThread().getName() + ":开始生产第"+productCount +"个产品");
// 生产完产品,唤醒阻塞的消费者
notify();
}else {
// 等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
// 消费产品
public synchronized void consumeProduct() {
if (productCount > 0){
System.out.println(Thread.currentThread().getName() + ":开始消费第"+productCount +"个产品");
productCount--;
// 消费者买走一个产品,就可以唤醒生产者继续生产
notify();
}else {
// 等待
try {
wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
// 生产者
class Producer extends Thread{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println(getName() + ": 开始生产产品...");
try {
Thread.sleep(20);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.produceProduct();
}
}
}
// 消费者
class Consumer extends Thread{
private Clerk clerk;
public Consumer(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
System.out.println(getName() + ": 开始消费...");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
clerk.consumeProduct();
}
}
}