1.什么是进程?什么是线程?
进程是:一个应用程序(1个进程是一个软件)。
线程是:一个进程中的执行场景/执行单元。
注意:一个进程可以启动多个线程。
我们在启动java程序的时候,会先启动JVM,而JVM就是一个进程。
JVM再启动一个主线程调用main方法
(main方法就是主线程)。
同时再启动一个垃圾回收线程(GC)
负责看护,回收垃圾。
在java程序中至少有两个线程并发
,一个是 垃圾回收线程
,一个是 执行main方法的主线程
。
2.进程和线程是什么关系?
进程: 可以看做是现实生活当中的公司。
线程: 可以看做是公司当中的某个员工。
注意:
进程A和进程B的 内存独立不共享。
例如:
QQ是一个进程,微信是一个进程,这两个进程是独立的,不共享资源。
线程A和线程B是什么关系?
在java语言中:
线程A和线程B,堆内存
和 方法区
内存共享。但是 栈内存
独立,一个线程一个栈。
eg.
假设启动10个线程,会有10个栈空间,每个栈和每个栈之间,互不干扰,各自执行各自的,这就是多线程并发。
eg.
火车站,可以看做是一个进程。
火车站中的每一个售票窗口可以看做是一个线程。
我在窗口1购票,你可以在窗口2购票,你不需要等我,我也不需要等你。所以多线程并发可以提高效率。
java中之所以有多线程机制,目的就是为了 提高程序的处理效率。
3.什么是真正的多线程并发?
1线程执行t1的。
t2线程执行t2的。
t1不会影响t2,t2也不会影响t1
。这叫做真正的多线程并发。
4.使用了多线程机制之后,main方法结束,是不是有可能程序也不会结束?
main方法结束只是主线程结束了,主栈空了,其它的线程可能还在压栈弹栈。
5.对于单核的CPU来说,真的可以做到真正的多线程并发吗?
对于多核的CPU电脑来说,真正的多线程并发是没问题的。4核CPU表示同一个时间点上,可以真正的有4个进程并发执行。
单核的CPU表示只有一个大脑:
不能够做到真正的多线程并发,但是可以做到给人一种“多线程并发”的感觉。
对于单核的CPU来说,在某一个时间点上实际上只能处理一件事情,但是由于CPU的处理速度极快,多个线程之间频繁切换执行,给别人的感觉是:多个事情同时在做!!!
eg.
线程A:播放音乐
线程B:运行魔兽游戏
线程A和线程B频繁切换执行,人类会感觉音乐一直在播放,游戏一直在运行,
给我们的感觉是同时并发的。(因为计算机的速度很快,我们人的眼睛很慢,所以才会感觉是多线程!)
6.线程的生命周期(附图)
- 新建状态
- 就绪状态
- 运行状态
- 阻塞状态
- 死亡状态
7.多线程的创建方法
在java中,如果要实现多线程,就必须依靠线程主体类,而java.lang.Thread是java中负责多线程操作类,只需继承Thread类,就能成为线程主体类,为满足一些特殊要求,也可以通过实现Runnable接口或者Callable接口来完成定义。
具体方式如下:
1.继承Thread类,重写run方法(无返回值)
2.实现Runnable接口,重写run方法(无返回值)
3.实现Callable接口,重写call方法(有返回值且可以抛出异常)
7.1Thread类实现多线程
通过继承Thread类,并重写父类的run()方法实现
public void run()
定义线程类:
public class MyThread extends Thread{ @Override public void run() { for (int i = 0; i < 50; i++) { System.out.println(Thread.currentThread().getName()+"-"+i); try { Thread.sleep(500); } catch (InterruptedException e) { throw new RuntimeException(e); } } } }
多线程启动:
public class ThreadDemo { public static void main(String[] args) { MyThread myThread = new MyThread(); Thread t1 = new Thread(myThread); Thread t2 = new Thread(myThread); t1.start(); t2.start(); } }
注意:
- t.run() 不会启动线程,只是普通的调用方法而已。不会分配新的分支栈。(这种方式就是单线程。)
- t.start() 方法的作用是:启动一个分支线程,在JVM中开辟一个新的栈空间,这段代码任务完成之后,瞬间就结束了。这段代码的任务只是为了开启一个新的栈空间,只要新的栈空间开出来,start()方法就结束了。线程就启动成功了。启动成功的线程会自动调用run方法,并且run方法在分支栈的栈底部(压栈)。run方法在分支栈的栈底部,main方法在主栈的栈底部。run和main是平级的。
7.2Runnable接口实现多线程【比较常用】
编写一个类,实现 java.lang.Runnable
接口,实现run
方法。
一个类实现了接口,它还可以去继承其它的类,更灵活。
public class MyRunnable implements Runnable{ public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t = new Thread(myRunnable); //设置线程名称 t.setName("myRunnable"); t.start(); for (int i = 0; i < 50; i++) { try { //线程睡眠 Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println("main"+"-"+i); } } @Override public void run() { for (int i = 0; i < 50; i++) { try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } System.out.println(Thread.currentThread().getName()+"-"+i); } } }
采用匿名内部类创建:
public class ThreadTest04 { public static void main(String[] args) { // 创建线程对象,采用匿名内部类方式。 Thread t = new Thread(new Runnable(){ @Override public void run() { for(int i = 0; i < 100; i++){ System.out.println("t线程---> " + i); } } }); // 启动线程 t.start(); for(int i = 0; i < 100; i++){ System.out.println("main线程---> " + i); } } }
7.3Callable接口实现多线程
使用Runnable接口实现的多线程可以避免单继承的局限,但是还有一个问题就是run方法没有返回值,为了解决这个问题,所以提供了一个Callable接口java.util.concurrent.Callable
FutureTask类常用方法:
import java.util.concurrent.ExecutionException; // 导入ExecutionException异常包 public FutureTask(Callable<T> callable) // 构造函数:接收Callable接口实例 public FutureTask(Runable runnable,T result) // 构造函数:接收Runnable接口实例,同时指定返回结果类型 public T get() throws InterruptedException,ExecutionException // 取得线程操作返回结果
Thread类的一个构造方法:
public Thread(FutureTask<T> futuretask) //构造方法:接收FutureTask实例化对象
定义线程主体类:
public class CallableDemo implements Callable <Integer> { @Override public Integer call() throws Exception { int sum = 0; for (int i = 0; i < 10; i++) { System.out.println(Thread.currentThread().getName()+"--"+i); sum+=i; } return sum; } }
多线程启动:
public class ThreadDemo { public static void main(String[] args) throws ExecutionException, InterruptedException { CallableDemo callableDemo1 = new CallableDemo(); CallableDemo callableDemo2 = new CallableDemo(); FutureTask<Integer> ft1 = new FutureTask<Integer>(callableDemo1); FutureTask<Integer> ft2 = new FutureTask<Integer>(callableDemo2); new Thread(ft1).start(); new Thread(ft2).start(); System.out.println(ft1.get()); System.out.println(ft2.get()); } }
Callable接口实现采用泛型技术实现,继承需要重写call方法,再通过FutureTask包装器包装,传入后实例化Thread类实现多线程。
其中FutureTask
类是Runnable
接口的子类,所以才可以利用Thread类的start方法启动多线程,读者可以将call方法假设为有返回值的run方法。
8. 多线程常用操作方法
8.1 获取线程对象名字、修改线程对象名字
线程是不确定的运行状态,名称就是线程的主要标记。因此,需要注意的是,对于线程的名字一定要在启动之前设置进程名称,不建议对已经启动的线程,进行更改名称,或者为不同线程设置重名。
public class MyRunnable implements Runnable { public static void main(String[] args) { MyRunnable myRunnable = new MyRunnable(); Thread t = new Thread(myRunnable); //修改线程对象名字 t.setName("myRunnable"); t.start(); } @Override public void run() { for (int i = 0; i < 50; i++) { //获取线程对象名字 System.out.println(Thread.currentThread().getName() + "-" + i); } } }
当线程没有设置名字的时候,默认的名字是什么?
- Thread-0
- Thread-1
- Thread-2
8.2 线程休眠sleep方法
1.静态方法:Thread.sleep(1000);
2.参数是毫秒
3.作用: 让当前线程进入休眠,进入“阻塞状态”,放弃占有CPU时间片,让给其它线程使用。
这行代码出现在A线程中,A线程就会进入休眠。
这行代码出现在B线程中,B线程就会进入休眠。
4.Thread.sleep()方法,可以做到这种效果:
间隔特定的时间,去执行一段特定的代码,每隔多久执行一次。
public class MyThread implements Runnable{ @Override public void run() { for(int i = 0 ; i<90 ; i++) { System.out.println(Thread.currentThread().getName() + " 正在工作中……" + i ); if(i == 20) { try { System.out.println(Thread.currentThread().getName() + " 等一会儿就要休息五秒钟了……"); Thread.sleep(5000); // 当前线程休眠五秒钟 System.out.println(Thread.currentThread().getName() + " 已经休息五秒钟了……"); }catch (InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 休眠被打扰了……"); } } } } }
线程中断
interrupt方法定义在java.lang.Thread中,由Thread.interrupt()调用实现。该方法将会设置该线程的中断状态位,即设置为true,中断的结果线程是终止状态、还是阻塞状态或是继续运行至下一步,就取决于该程序本身。线程会不时地检测这个中断标示位,以判断线程是否应该被中断(即中断标示值是否为true)。它并不像stop方法那样会中断一个正在运行的线程。
定义线程主体类:
public class MyThread implements Runnable { @Override public void run() { int i = 0; while(true) { System.out.println(Thread.currentThread().getName() + " 正在努力工作中……" + i++); try { System.out.println(Thread.currentThread().getName() + " 准备休息5秒钟了……"); Thread.sleep(5000); // 休眠5秒钟 System.out.println(Thread.currentThread().getName() + " 已经休息5秒钟了……"); }catch(InterruptedException e) { System.out.println(Thread.currentThread().getName() + " 被打扰了,不想工作了……"); break; } } } }
测试中断:
public class ThreadDemo { public static void main(String[] args) throws Exception{ Runnable mt1 = new MyThread(); Runnable mt2 = new MyThread(); Runnable mt3 = new MyThread(); Thread thread1 = new Thread(mt1,"线程一"); //线程一就绪 Thread thread2 = new Thread(mt2,"线程二"); //线程二就绪 Thread thread3 = new Thread(mt3,"线程三"); //线程三就绪 thread1.start(); //线程一启动 thread2.start(); //线程二启动 thread3.start(); //线程三启动 // 以下通过利用main线程控制 线程一 中断 Thread.sleep(6000); //使main方法先休眠6秒钟,即让子线程先运行6秒钟 if(!thread1.isInterrupted()) { System.out.println("吵闹~~~"); thread1.interrupt(); //中断线程一的执行 } } }
线程一在休眠期间被中断,然后直接break,也就是说以后就只有线程二和三工作了。这里每当中断执行都会产生InterruptedException异常。
8.3 线程让出yield
yield方法定义在java.lang.Thread中,由Thread.yield()调用实现。多线程在彼此交替执行的时候往往需要进行资源的轮流抢占,如果某些不是很重要的线程抢占到资源但是又不急于执行时,就可以将当前的资源暂时让步出去,交给其它资源先执行。但是,因为yeild是将线程由“运行状态”转别为“就绪状态”,这样并不能保证在当前线程调用yield方法之后,其它具有相同优先级的线程就一定能获得执行权,也有可能是当前线程又进入到“运行状态”继续运行,因为还是要依靠CPU调度才可以.
定义线程主体类:
public class MyThread implements Runnable{ private Thread thread = null; public MyThread () {} public MyThread(Thread thread) { this.thread = thread; } @Override public void run() { for(int i = 0 ; i<50 ; i++) { System.out.println(Thread.currentThread().getName() + " 正在工作中……" + i); if( i == 30) { System.out.println(Thread.currentThread().getName() + " 打算将工作交给 "+thread.getName() + "了……"); Thread.yield(); // 当前线程让步出去 System.out.println(Thread.currentThread().getName() + " 又想自己工作了……"); } } } }
观察线程让出操作:
public class ThreadDemo { public static void main(String[] args) { Thread mainThread = Thread.currentThread(); Runnable mt1 = new MyThread(mainThread); Thread thread1 = new Thread(mt1,"子线程"); thread1.start(); for(int i = 0 ; i < 40 ; i++) { System.out.println(Thread.currentThread().getName() + " 正在工作中……" + i); } } }
由以上结果容易了解到,会短暂地将资源调度让给其它的线程,当然这并不是严格的,因为还是要CPU调度的。
8.4 线程优先级
所有创造的线程都是子线程,所有的子线程在启动时都会保持同样的优先级权限,但是如果现在某些重要的线程希望可以优先抢占到资源并且先执行,就可以修改优先级权限来实现。
记住当线程的优先级没有指定时,所有线程都携带普通优先级。
需要理解的是:
优先级用从1到10的范围的整数指定。10表示最高优先级,1表示最低优先级,5是普通优先级,也就是默认优先级。
优先级相对最高的线程在执行时被给予优先权限。但是不能保证线程在启动时就进入运行状态。
优先级越高越有可能先执行。
其主要方法以及常量:
public static final int MAX_PRIORITY // 静态常量:最高优先级,数值为10 public static final int NORM_PRIORITY //静态常量:普通优先级,数值为5 public static final int MIN_PRIORITY // 静态常量:最低优先级,数值为1 public final void setPriority(int newPriority) // 普通函数:设置优先级 public final int getPriority() //普通函数:获取优先级
定义线程主体类:
public class MyThread implements Runnable{ @Override public void run() { for(int i = 0 ; i<20 ; i++) { System.out.println(Thread.currentThread().getName() + " 正在工作中……" + i); } } }
观察线程优先级操作:
public class ThreadDemo { public static void main(String[] args) { Runnable mt1 = new MyThread(); Runnable mt2 = new MyThread(); Thread thread1 = new Thread(mt1,"线程一"); Thread thread2 = new Thread(mt2,"线程二"); // 设置优先级 thread2.setPriority(Thread.MAX_PRIORITY); thread1.setPriority(Thread.MIN_PRIORITY); // 启动 thread1.start(); thread2.start(); } }
线程二执行的概率高于线程一的执行概率,前面的执行情况大致为交替执行,是因为它们的优先级均相等,默认都等于5。
9. 线程的同步和死锁
当这样也导致了一个问题:在某一时刻,这一份资源在某一个线程发生改变时,其它线程正在执行的操作也会受到其影响。
如下程序为售票员售票代码以及结果:
定义线程主体类:
public class MyThread implements Runnable { private static int ticket = 10; @Override public void run() { for (int i = 0; i < 300; i++) { if (ticket > 0) { ticket--; try { Thread.sleep(300); // 延迟1秒,使得ticket可以被其它线程充分改变(可能此时的ticket小于等于0了) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在售票剩余票数--" + ticket + "张"); } } } }
观察售票状态:
public class ThreadDemo { public static void main(String[] args) { MyThread s1 = new MyThread (); Thread t1 = new Thread(s1,"售票员1"); Thread t2 = new Thread(s1,"售票员2"); Thread t3 = new Thread(s1,"售票员3"); t1.start(); t2.start(); t3.start(); } }
以上结果就很好地说明了多线程的不安全问题,提现了数据丢失问题
9.1线程同步
解决数据共享问题必须使用同步,所谓的同步就是指多个线程在同一个时间段内只能有一个线程执行指定的代码,其他线程要等待此线程完成之后才可以继续进行执行,在Java中提供有synchronized关键字以实现同步处理,同步的关键是要为代码加上“锁”。
而锁的操作有三种:
1.同步代码块
2.同步方法
3.Lock实现
9.1.1 同步代码块实现
使用方式:
synchronized(需要同步的对象){ 需要同步的操作 }
定义线程主体类:
package thread; /** * 同步锁方法 */ public class MyThread implements Runnable { private static int ticket = 10; @Override public void run() { for (int i = 0; i < 300; i++) { synchronized (this) { if (ticket > 0) { ticket--; try { Thread.sleep(300); // 延迟1秒,使得ticket可以被其它线程充分改变(可能此时的ticket小于等于0了) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在售票剩余票数--" + ticket + "张"); } } } } }
观察售票状态:
public class ThreadDemo { public static void main(String[] args) { MyThread s1 = new MyThread (); Thread t1 = new Thread(s1,"售票员1"); Thread t2 = new Thread(s1,"售票员2"); Thread t3 = new Thread(s1,"售票员3"); t1.start(); t2.start(); t3.start(); } }
虽然它的确起到了安全的作用,但是执行的效率却下降了,因为每次都只有一个线程才能访问同步代码块。
9.1.2 同步方法实现
使用方式:
利用函数包装的形式实现,如下: 修饰符 synchronized 返回类型 函数名()
定义线程主体类:
public class MyThread implements Runnable { private static int ticket = 10; @Override public void run() { for (int i = 0; i < 300; i++) { method(); } } private synchronized static void method() { if (ticket > 0) { ticket--; try { Thread.sleep(300); // 延迟1秒,使得ticket可以被其它线程充分改变(可能此时的ticket小于等于0了) } catch (InterruptedException e) { e.printStackTrace(); } System.out.println(Thread.currentThread().getName() + " 正在售票剩余票数--" + ticket + "张"); } } }
观察售票状态:
public class ThreadDemo { public static void main(String[] args) { MyThread s1 = new MyThread (); Thread t1 = new Thread(s1,"售票员1"); Thread t2 = new Thread(s1,"售票员2"); Thread t3 = new Thread(s1,"售票员3"); t1.start(); t2.start(); t3.start(); } }
9.1.3Lock锁实现
定义线程主体类:
public class ThreadLock implements Runnable { ReentrantLock lock = new ReentrantLock(); private static int ticket = 10; @Override public void run() { for (int i = 0; i < 300; i++) { method(); } } private void method() { lock.lock(); try { if (ticket > 0) { try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } ticket--; System.out.println(Thread.currentThread().getName() + " 正在售票剩余票数--" + ticket + "张"); } }catch (Exception e) { e.printStackTrace(); } finally { lock.unlock(); } } }
观察售票状态:
public class ThreadDemo { public static void main(String[] args) { ThreadLock s1 = new ThreadLock(); Thread t1 = new Thread(s1,"售票员1"); Thread t2 = new Thread(s1,"售票员2"); Thread t3 = new Thread(s1,"售票员3"); t1.start(); t2.start(); t3.start(); } }
9.2 我们以后开发中应该怎么解决线程安全问题?
是一上来就选择线程同步吗?synchronized
不是,synchronized会让程序的执行效率降低,用户体验不好。
系统的用户吞吐量降低。用户体验差。在不得已的情况下再选择线程同步机制。
第一种方案:尽量使用局部变量 代替 “实例变量和静态变量”。
第二种方案:如果必须是实例变量,那么可以考虑创建多个对象,这样实例变量的内存就不共享了。
(一个线程对应1个对象,100个线程对应100个对象,对象不共享,就没有数据安全问题了。)
第三种方案:如果不能使用局部变量,对象也不能创建多个,这个时候就只能选择synchronized了。线程同步机制。
9.3 线程死锁
所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去,死锁的操作一般是在程序运行时候才有可能出现,死锁是在多线程开发中较为常见的一种问题,过多的同步就有可能出现死锁。
观察死锁状态:
class firstCorssBridge{ public synchronized void tell(secondCorssBridge scb) { System.out.println("张三告诉王五:我先过,你后过,否则你别想过这桥!"); scb.cross(); } // 以下函数不会执行 public synchronized void cross() { System.out.println("张三快快乐乐地过桥了……"); } } class secondCorssBridge{ public synchronized void tell(firstCorssBridge fcb) { System.out.println("王五告诉张三:我先过,你后过,否则你别想过这桥!"); fcb.cross(); } // 以下函数不会执行 public synchronized void cross() { System.out.println("王五快快乐乐地过桥了……"); } } public class DeadLock implements Runnable{ private firstCorssBridge fcb = new firstCorssBridge(); private secondCorssBridge scb = new secondCorssBridge(); public DeadLock() { // 启动线程 并执行以下语句 new Thread(this).start(); // 会运行run函数 fcb.tell(scb); // 运行到里面时 fcb会等待scb } @Override public void run() { scb.tell(fcb); // 运行到里面时 scb会等待fcb } public static void main(String[] args) { new DeadLock(); } }
运行情况:
两者已经处于相互等待状态,后续代码并不会执行。
如果读者不太理解上面的代码,可以在new Thread(this).start(); // 会运行run函数
语句的下面加上:
try { Thread.sleep(100); // 休眠0.1秒钟 }catch(InterruptedException e) { e.printStackTrace(); }
结果就为:
这实际上是“锁中有锁”的情况。
10. 守护线程
Java中的线程分为两类,用户线程和守护线程。守护线程(Daemon)是一种运行在后台的线程服务线程,当用户线程存在时,守护线程可以同时存在,但是,当用户线程不存在时,守护线程会全部消失。
其中具有代表性的就是:垃圾回收线程(守护线程)。
注意:主线程main方法是一个用户线程。
主要操作方法:
public final setDaemon(boolean on) throws Exception // 普通函数:是否设置为守护线程 public final boolean isDaemon() //普通函数: 判断是否为
观察守护线程操作:
class MyThread implements Runnable{ private int times; public MyThread(int times) { this.times = times; } @Override public void run() { for(int i = 0 ; i<times;i++) { if(Thread.currentThread().isDaemon()) { try { Thread.sleep(10); // 如果是守护线程,则休眠0.01秒钟 }catch(InterruptedException e) { e.printStackTrace(); } } System.out.println(Thread.currentThread().getName() + " 正在工作中……"+i); } } } public class testDemo { public static void main(String[] args) { MyThread mt1 = new MyThread(4); MyThread mt2 = new MyThread(100); //守护线程的循环次数远多于用户线程 Thread thread1 = new Thread(mt1,"用户线程"); Thread thread2 = new Thread(mt2,"守护线程"); thread2.setDaemon(true); //thread2设置为守护线程 thread1.start(); thread2.start(); } }
由以上可以了解到,守护线程已经提前结束了,原因是main线程等用户线程全部消失了。
守护线程的应用场景
在主线程关闭后无需手动关闭守护线程,因为会自动关闭,避免了麻烦,Java垃圾回收线程就是一个典型的守护线程,简单粗暴地可以理解为所有为线程服务而不涉及资源的线程都能设置为守护线程。
11. Object类的wait()、notify()、notifyAll()方法
主要方法:
void wait() 让活动在当前对象的线程无限等待(释放之前占有的锁) void notify() 唤醒当前对象正在等待的线程(只提示唤醒,不会释放锁) void notifyAll() 唤醒当前对象全部正在等待的线程(只提示唤醒,不会释放锁)
1、wait和notify方法不是线程对象的方法,是普通java对象都有的方法。
2、wait方法和notify方法建立在 线程同步 的基础之上。因为多线程要同时操作一个仓库。有线程安全问题。
3、wait方法作用:o.wait() 让正在o对象上活动的线程t进入等待状态,并且释放掉t线程之前占有的o对象的锁。
4、notify方法作用:o.notify() 让正在o对象上等待的线程唤醒,只是通知,不会释放o对象上之前占有的锁。
12. 生产者消费者模式(wait()和notify())
12.1 什么是“生产者和消费者模式”?
生产线程负责生产,消费线程负责消费。
生产线程和消费线程要达到均衡。
这是一种特殊的业务需求,在这种特殊的情况下需要使用wait方法和notify方法。
12.2模拟一个业务需求
当前我们在一家餐厅中,厨师生产菜,服务员端菜,但是我们只有一个盘子,只能是厨师生产好菜后,服务员在能端菜。
必须做到这种效果:生产1个消费1个。
使用wait方法和notify方法实现“生产者和消费者模式”
public class ProductionConsume { public static void main(String[] args) { Foot foot = new Foot(); //服务员和厨师共用一个对象 Production production = new Production(foot); Consume consume = new Consume(foot); Thread t1 = new Thread(production); Thread t2 = new Thread(consume); t1.start(); t2.start(); } } class Production implements Runnable { private Foot foot; public Production(Foot foot) { this.foot = foot; } @Override public void run() { //生产20个菜 for (int i = 0; i < 20; i++) { if (i % 2 == 0) { foot.set("好吃的", "很好吃", i); } else { foot.set("锅包肉", "酸甜口", i); } } } } class Consume implements Runnable { private Foot foot; public Consume(Foot foot) { this.foot = foot; } @Override public void run() { //服务员获取菜的方法 for (int i = 0; i < 20; i++) { try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } foot.get(); } } } class Foot { private String name; private String desc; private int num; private boolean flag = true; public synchronized void set(String name, String desc, int num) { if (!flag) { try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } this.name = name; try { Thread.sleep(300); } catch (InterruptedException e) { throw new RuntimeException(e); } this.desc = desc; this.num = num; flag = false; this.notify(); } public synchronized void get() { if (flag) { try { this.wait(); } catch (InterruptedException e) { throw new RuntimeException(e); } } System.out.println(this.name + "----" + this.desc + "----" + this.num); flag = true; this.notify(); } }