java多线程

tech2024-11-09  13

线程

进程与线程

1.1进程

几乎所有操作系统都支持进程的概念,所有运行中的任务通常对应一条进程(Process)。当一个程序进入内存运行,即变成一个进程。进程是处于运行过程中的程序,并且具有一定独立功能,进程是操作系统进行资源分配和调度的一个独立单位。

1.2线程

(1)多线程则扩展了多进程的概念,使得通一个进程可以同时并发处理多个任务。线程(Thread)也被称为轻量级进程。就像进程在操作系统中地位一样,线程在进程中也是独立的、并发的执行流。当进程被初始化后,主线程就被创建了,对于Java程序来说,main线程就是主线程,但我们可以在该进程内创建多条顺序执行路径,这些独立的顺序执行路径就是线程。

(2)进程中的每一个线程可以完成一定的任务,并且是独立的,线程可以拥有自己独立的堆栈、自己的程序计数器和自己的局部变量,但不再拥有系统资源,它与父进程的其他线程共享该进程所拥有的全部资源。由于线程间的通信是在同一个地址空间上进行的,所以不需要额外的通信机制,这就使得通信更简便而且信息传递的速度也更快,因此可以通过简单编程实现多线程相互协同来完成进程所要完成的任务。但是也存在安全问题,因为其中一个线程对共享的系统资源的操作都会给其他线程带来影响,由此可知,多线程中的同步是非常重要的问题。

2.线程的创建和启动

Java使用Thread类代表线程,所有的线程对象都必须是Thread类或其子类的对象。每个线程的作用是完成一定的任务,实际上就是执行一段代码,我们称之为线程执行体。Java使用run方法来封装这段代码,即run方法的方法体就是线程执行体。

2.1继承Thread类创建线程类

通过继承Thread类来创建并启动多线程的步骤如下: (1)定义继承Thread的子类,并重写该类的run()。查看Thread类的源代码,发现当我们没有传target时,那么Thread类的run()就是一个什么也没干的方法,因此我们必须重写run()来完成线程的任务。 (2)创建Thread子类的实例,一个实例对象就是一个线程。 (3)调用线程对象的start()来启动线程。

public class TestMyThread { public static void main(String[] args) { MyThread my1 = new MyThread(); my1.start(); MyThread my2 = new MyThread(); my2.start(); for (int i = 10; i >= 1; i--) { System.out.println(Thread.currentThread().getName() + "线程:" + i); } } } class MyThread extends Thread{ public void run(){ for (int i = 1; i <= 10; i++) { System.out.println(super.getName() + "线程:" + i); } } }

说明: (1)Thread.currentThread()方法总是返回当前在执行的线程对象; (2)getName()方法是Thread的实例方法,该方法返回当前线程对象的名字,可以通过setName(String name)方法设置线程名称,否则依次为Thread-0,Thread-1…等 提示: (1)JavaSE的程序至少有一个main主线程,它的方法体即是线程体。 (2)Thread-0和Thread-1线程虽然都是MyThread类的线程对象,但是各自调用自己的run(),互相之间是独立的,因此各打印1-10。 (3)启动线程用start()方法,而不是run()!调用start()方法来启动线程,系统会把该run()方法当成线程执行体来处理。但是如果直接调用run(),系统会把线程对象当成一个普通对象,而run()就是一个普通方法。

2.2实现Runnable接口

实现Runnable接口来创建并启动多线程的步骤: (1)定义Runnable接口的实现类,并重写该接口的run()方法 (2)创建Runnable实现类的对象 (3)创建Thread类的对象,并将Runnable实现类的对象作为target。其实该Thread类的对象才是真正的线程对象。当JVM调用线程对象的run()时,如果target不为空,就会调用target的run()。 (4)调用线程对象的start()启动线程。

package com.atguigu.part01.thread; public class TestMyRunnable { public static void main(String[] args) { MyRunnable my = new MyRunnable(); new Thread(my).start(); new Thread(my).start(); for (int i = 10; i >= 1; i--) { System.out.println(Thread.currentThread().getName() + "线程:" + i); } } } class MyRunnable implements Runnable{ public void run(){ for (int i = 1; i <= 10; i++) { System.out.println(Thread.currentThread().getName() + "线程:" + i); } } }

2.3实现Callable接口

1. (1)编写类实现Callable接口 , 实现call方法 class XXX implements Callable { @Override public call() throws Exception { return T; } } (2) 创建FutureTask对象 , 并传入第一步编写的Callable类对象 FutureTask future = new FutureTask<>(callable); (3)通过Thread,启动线程 new Thread(future).start();

2. (1)Runnable 与 Callable的相同点 都是接口 都可以编写多线程程序 都采用Thread.start()启动线程 (2)Runnable 与 Callable的不同点 Runnable没有返回值;Callable可以返回执行结果 Callable接口的call()允许抛出异常;Runnable run()不能抛出 (3)Callable获取返回值 Callalble接口支持返回执行结果,需要调用FutureTask.get()得到,此方法会阻塞主进程的继续往下执行,如果不调用不会阻塞.

public class Demo5 { public static void main(String[] args) throws ExecutionException, InterruptedException { Callable<Integer> c=new MyCallable(); FutureTask<Integer>task =new FutureTask<>(c); new Thread(task).start(); //得到返回值,必须获得完返回值,才会执行后面的线程 //task.isDone();判断线程是否执行完成 task.cancel(true);//取消线程 Integer j=task.get(); System.out.println("返回值为:"+j); } static class MyCallable implements Callable<Integer>{ @Override public Integer call() throws Exception{ return 100; } } }

2.4线程的生命周期

一个完整的生命周期中通常要经历如下的五种状态:新建(New)、就绪(Runnable)、运行(Running)、阻塞(Blocked)、死亡(Dead)。CPU需要在多条线程之间切换,于是线程状态会多次在运行、阻塞、就绪之间切换。 (1)新建 当一个Thread类或其子类的对象被声明并创建时,新生的线程对象处于新建状。此时它和其他Java对象一样,仅仅由JVM为其分配了内存,并初始化了实例变量的值。此时的线程对象并没有任何线程的动态特征,程序也不会执行它的线程体run()。 (2) 就绪 但是当线程对象调用了start()方法之后,就不一样了,线程就从新建状态转为就绪状态。JVM会为其创建方法调用栈和程序计数器,当然,处于这个状态中的线程并没有开始运行,只是表示已具备了运行的条件,随时可以被调度。至于什么时候被调度,取决于JVM里线程调度器的调度。 (3)运行 如果处于就绪状态的线程获得了CPU,开始执行run()方法的线程体代码,则该线程处于运行状态。如果计算机只有一个CPU,在任何时刻只有一个线程处于运行状态,如果计算机有多个处理器,将会有多个线程并行(Parallel)执行。 当然,美好的时光总是短暂的,而且CPU讲究雨露均沾。对于抢占式策略的系统而言,系统会给每个可执行的线程一个小时间段来处理任务,当该时间用完,系统会剥夺该线程所占用的资源,让其回到就绪状态等待下一次被调度。此时其他线程将获得执行机会,而在选择下一个线程时,系统会适当考虑线程的优先级。 (4)阻塞 当在运行过程中的线程遇到如下情况时,线程会进入阻塞状态:

线程调用了sleep()方法,主动放弃所占用的CPU资源;

线程调用了一个阻塞式IO方法,在该方法返回之前,该线程被阻塞;

线程试图获取一个同步监视器,但该同步监视器正被其他线程持有;

线程执行过程中,同步监视器调用了wait(),让它等待某个通知(notify);

线程执行过程中,遇到了其他线程对象的加塞(join); 当前正在执行的线程被阻塞后,其他线程就有机会执行了。针对如上情况,当发生如下情况时会解除阻塞,让该线程重新进入就绪状态,等待线程调度器再次调度它: 线程的sleep()时间到; 线程调用的阻塞式IO方法已经返回; 线程成功获得了同步监视器; 线程等到了通知(notify); 加塞的线程结束了; (5)死亡 线程会以以下三种方式之一结束,结束后的线程就处于死亡状态: run()方法执行完成,线程正常结束 线程执行过程中抛出了一个未捕获的异常(Exception)或错误(Error)直接调用该线程的stop()来结束该线程(已过时,因为容易发生死锁)可以调用线程的isAlive()方法判断该线程是否死亡,当线程处于就绪、运行、阻塞三种状态时,该方法返回true,当线程处于新建、死亡两种状态时,该方法将返回false。

3.Thread类的方法

3.1 创建线程对象

. 构造器: ① Thread():创建新的Thread对象 ② Thread(String threadname):创建线程并指定线程实例名 ③ Thread(Runnable target):指定创建线程的目标对象,它实现了Runnable接口中的run方法 ④ Thread(Runnable target, String name):创建新的Thread对象 编写线程体和启动线程: ① public void run():子类必须重写run()以编写线程体 ② public void start():启动线程 2 获取和设置线程信息 (1) public static Thread currentThread():这是一个静态方法,总是返回当前在执行的线程对象 (2) public final boolean isAlive():测试线程是否处于活动状态。如果线程已经启动且尚未终止,则为活动状态。 (3) public final String getName():getName()方法是Thread的实例方法,该方法返回当前线程对象的名字,可以通过 (4) public final void setName(String name):设置该线程名称。除了主线程main之外,其他线程可以在创建时指定线程名称或通过setName(String name)方法设置线程名称,否则依次为Thread-0,Thread-1…等。 (5) public final int getPriority() :返回线程优先值 (6) public final void setPriority(int newPriority) :改变线程的优先级.每个线程都有一定的优先级,优先级高的线程将获得较多的执行机会。每个线程默认的优先级都与创建它的父线程具有相同的优先级。Thread类提供setPriority(int newPriority)和getPriority()方法类设置和获取线程的优先级,其中setPriority方法需要一个整数,并且范围在[1,10]之间,通常推荐设置Thread类的三个优先级常量:  MAX_PRIORITY(10):最高优先级  MIN _PRIORITY (1):最低优先级  NORM_PRIORITY (5):普通优先级,默认情况下main线程具有普通优先级

3.2 控制线程

1.线程睡眠:sleep 如果当我们需要让当前正在执行的线程暂停一段时间,则可以通过调用Thread类的静态sleep()方法,它会导致当前线程进入阻塞状态: (1)public static void sleep(long millis):在指定的毫秒数内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 (2)public static void sleep(long millis,int nanos):在指定的毫秒加纳秒内让当前正在执行的线程休眠(暂停执行),此操作受到系统计时器和调度程序精度和准确性的影响。 代码示例:倒计时

public class TestSleep { public static void main(String[] args) { for (int i = 10; i>=0; i--) { System.out.println(i); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } System.out.println("新年快乐!"); } }

2.线程让步:yield yield()也是一个静态方法,它可以让当前正在执行的线程暂停,但它不会阻塞该线程,它只是将该线程转入就绪状态。 yield只是让当前线程暂停一下,让系统的线程调度器重新调度一次,希望优先级与当前线程相同或更高的其他线程能够获得执行机会,但是这个不能保证,完全有可能的情况是,当某个线程调用了yield方法暂停之后,线程调度器又将其调度出来重新执行。

public class TestYield { public static void main(String[] args) { MyYieldThread m1 = new MyYieldThread("低"); m1.setPriority(Thread.MIN_PRIORITY); MyYieldThread m2 = new MyYieldThread("高"); m2.setPriority(Thread.MAX_PRIORITY); m1.start(); m2.start(); } } class MyYieldThread extends Thread{ public MyYieldThread(String name) { super(name); } public void run(){ for (int i = 1; i <= 10; i++) { System.out.println(getName() + ":" +i); Thread.yield(); } } }

3.线程加塞:join 当在某个线程的线程体中调用了另一个线程的join()方法,当前线程将被阻塞,直到join进来的线程执行完它才能继续。 void join() :等待该线程终止。 void join(long millis) :等待该线程终止的时间最长为 millis 毫秒。如果millis时间到,将不再等待。 void join(long millis, int nanos) :等待该线程终止的时间最长为 millis 毫秒 + nanos 纳秒。 案例:编写龟兔赛跑多线程程序,设赛跑长度为30米 兔子的速度是10米每秒,兔子每跑完10米休眠的时间10秒 乌龟的速度是1米每秒,乌龟每跑完10米的休眠时间是1秒 要求:要等兔子和乌龟的线程结束,主线程(裁判)才能公布最后的结

package com.atguigu.part02; public class Racer extends Thread { private String name;//运动员名字 private long runTime;//每米需要时间,单位毫秒 private long restTime;//每10米的休息时间,单位毫秒 private long distance;//全程距离,单位米 private long time;//跑完全程的总时间 public Racer(String name, long distance, long runTime, long restTime) { super(); this.name = name; this.distance = distance; this.runTime = runTime; this.restTime = restTime; } @Override public void run() { long sum = 0; long start = System.currentTimeMillis(); while (sum < distance) { System.out.println(name + "正在跑..."); try { Thread.sleep(runTime);// 每米距离,该运动员需要的时间 } catch (InterruptedException e) { return ; } sum++; try { if (sum % 10 == 0 && sum < distance) { // 每10米休息一下 System.out.println(name+"已经跑了"+sum+"米正在休息...."); Thread.sleep(restTime); } } catch (InterruptedException e) { return ; } } long end = System.currentTimeMillis(); time = end - start; System.out.println(name+"跑了"+sum+"米,已到达终点,共用时"+time/1000.0+"秒"); } public long getTime() { return time; } } package com.atguigu.part02; public class TestJoin2 { public static void main(String[] args) { Racer rabbit = new Racer("兔子", 30, 100, 10000); Racer turtoise = new Racer("乌龟", 30, 1000, 1000); rabbit.start(); turtoise.start(); try { rabbit.join();//main线程加塞 } catch (InterruptedException e) { e.printStackTrace(); } try { turtoise.join(); } catch (InterruptedException e) { e.printStackTrace(); } //因为要兔子和乌龟都跑完,才能公布结果 System.out.println("比赛结束"); System.out.println(rabbit.getTime()<turtoise.getTime()?"兔子赢":"乌龟赢"); } }

4.守护线程 有一种线程,它是在后台运行的,它的任务是为其他线程提供服务的,这种线程被称为“守护线程”。JVM的垃圾回收线程就是典型的守护线程。 守护线程有个特点,就是如果所有非守护线程都死亡,那么守护线程自动死亡。 调用setDaemon(true)方法可将指定线程设置为守护线程。必须在线程启动之前设置,否则会报IllegalThreadStateException异常。 调用isDaemon()可以判断线程是否是守护线程。

4.线程安全

多线程编程是有趣且复杂的事情,它常常容易突然出现“错误情况”,这是由于系统的线程调度具有一定的随机性。即使程序在运行过程中偶尔会出现问题,那也是由于我们的代码有问题导致的。当多个线程访问同一个数据时,非常容易出现线程安全问题。

4.2 线程安全问题

关于线程安全问题,有一个经典的问题:卖票问题。卖票的基本流程很简单:看是否还有票,如果有就可以卖。 现在开启多个窗口同时卖票:

import java.util.ArrayList; public class TicketService { private ArrayList<String> list; public TicketService(){ list = new ArrayList<String>(); list.add("01车01A"); list.add("01车01B"); list.add("01车01C"); list.add("01车01D"); list.add("01车01E"); list.add("01车02A"); list.add("01车02B"); list.add("01车02C"); list.add("01车02D"); list.add("01车02E"); } public boolean hasTicket(){ return list.size()>0; } public String buy(){ try { return list.remove(0); } catch (IndexOutOfBoundsException e) { throw new RuntimeException("票卖超了"); } } } //现在有两个窗口同时卖票。 public class Saler extends Thread{ private TicketService ts; public Saler(TicketService ts) { super(); this.ts = ts; } public void run(){ while(ts.hasTicket()){ try { Thread.sleep(100); //这里加入休眠时间,是强制让线程切换发生,增大问题出现的概率,好让大家看效果 } catch (InterruptedException e) { e.printStackTrace(); } try { String buy = ts.buy(); System.out.println("购买票:" + buy); } catch (Exception e) { System.err.println(e.getMessage()); } } System.out.println("没有票了"); } } public class TestSaler { public static void main(String[] args) { TicketService ts = new TicketService(); Saler s1 = new Saler(ts); Saler s2 = new Saler(ts); s1.start(); s2.start(); } }

这里就发生了线程安全问题,当多个线程多条语句(size()和remove())访问共享数据(这里TicketService的list存票的集合)时,就会发生线程安全问题。 解释1:“票卖超了”如何发生的,不是先判断是否有票,才买的吗?

4.3同步代码块

如何解决线程安全问题呢? 解决思路:对多条操作共享数据的语句,只能让一个线程都执行完,在执行过程中,其他线程不可以参与执行。

为了解决这个问题,Java的多线程支持引入同步监视器来解决这个问题。使用同步监视器的方式有两种:同步代码块和同步方法。 同步代码块的语法格式如下: synchronized(同步监视器对象){ //… } 上面代码的含义,线程开始执行同步代码块之前,必须先获得对同步监视器的锁定,换句话说没有获得对同步监视器的锁定,就不能进入同步代码块的执行,线程就会进入阻塞状态,直到对方释放了对同步监视器对象的锁定。 任何时刻只能有一个线程可以获得对同步监视器的锁定,当同步代码块执行结束后,该线程自然会释放对同步监视器对象的锁定。 Java程序运行使用任何对象来作为同步监视器对象,只要保证共享资源的这几个线程,锁的是同一个同步监视器对象即可。

public class Saler extends Thread{ private TicketService ts; public Saler(TicketService ts) { super(); this.ts = ts; } public void run(){ while(true){ synchronized (ts) { if(ts.hasTicket()){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } try { String buy = ts.buy(); System.out.println("购买票:" + buy); } catch (Exception e) { System.out.println(e.getMessage()); } }else{ System.out.println("没有票了"); break; } } } } }

(2)选择this对象作为同步监视器对象 如果线程是继承Thread类实现的,那么把同步监视器对象换成this,那么就没有起到作用,仍然会发生线程安全问题。因为两个线程的this对象是不同的。 但是如果线程是实现Runnable接口实现的,那么如果两个线程共用同一个Runnable接口实现类对象作为target的话,就可以把同步监视器对象换成this。

public class Window implements Runnable{ private TicketService ts; public Window(TicketService ts) { super(); this.ts = ts; } public void run(){ while(true){ synchronized (this) { if(ts.hasTicket()){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } try { String buy = ts.buy(); System.out.println("购买票:" + buy); } catch (Exception e) { System.out.println(e.getMessage()); } }else{ System.out.println("没有票了"); break; } } } } } public class TestWindow { public static void main(String[] args) { TicketService ts = new TicketService(); Window w = new Window(ts); Thread t1 = new Thread(w); Thread t2 = new Thread(w); t1.start(); t2.start(); } }

同步方法 与同步代码块对应的,Java的多线程安全支持还提供了同步方法,同步方法就是使用synchronized关键字来修饰某个方法,则该方法称为同步方法。对于同步方法而言,无须显式指定同步监视器,静态方法的同步监视器对象是当前类的Class对象,非静态方法的同步监视器对象是调用当前方法的this对象。

import java.util.ArrayList; public class TicketService { private ArrayList<String> list; public TicketService(){ list = new ArrayList<String>(); list.add("01车01A"); list.add("01车01B"); list.add("01车01C"); list.add("01车01D"); list.add("01车01E"); list.add("01车02A"); list.add("01车02B"); list.add("01车02C"); list.add("01车02D"); list.add("01车02E"); } public synchronized boolean hasTicket(){ return list.size()>0; } public synchronized String buy(){ try { return list.remove(0); } catch (IndexOutOfBoundsException e) { throw new RuntimeException("票卖超了"); } } } public class Saler extends Thread{ private TicketService ts; public Saler(TicketService ts) { super(); this.ts = ts; } public void run(){ while(ts.hasTicket()){ try { Thread.sleep(100); } catch (InterruptedException e) { e.printStackTrace(); } try { String buy = ts.buy(); System.out.println("购买票:" + buy); } catch (Exception e) { System.err.println(e.getMessage()); } } System.out.println("没有票了"); } } public class TestSaler { public static void main(String[] args) { TicketService ts = new TicketService(); Saler s1 = new Saler(ts); Saler s2 = new Saler(ts); s1.start(); s2.start(); } }

释放同步监视器的锁定 任何线程进入同步代码块、同步方法之前,必须先获得对同步监视器的锁定,那么何时会释放对同步监视器的锁定呢? 1、释放锁的操作 当前线程的同步方法、同步代码块执行结束。 当前线程在同步代码块、同步方法中遇到break、return终止了该代码块、该方法的继续执行。 当前线程在同步代码块、同步方法中出现了未处理的Error或Exception,导致当前线程异常结束。 当前线程在同步代码块、同步方法中执行了锁对象的wait()方法,当前线程被挂起,并释放锁。 2、不会释放锁的操作 (1)线程执行同步代码块或同步方法时,程序调用Thread.sleep()、Thread.yield()方法暂停当前线程的执行。 (2)线程执行同步代码块时,其他线程调用了该线程的suspend()方法将该该线程挂起,该线程不会释放锁(同步监视器)。 (3)应尽量避免使用suspend()和resume()这样的过时来控制线程

3、 死锁 不同的线程分别锁住对方需要的同步监视器对象不释放,都在等待对方先放弃时就形成了线程的死锁。一旦出现死锁,整个程序既不会发生异常,也不会给出任何提示,只是所有线程处于阻塞状态,无法继续。

public class TestDeadLock { public static void main(String[] args) { Object g = new Object(); Object m = new Object(); Owner s = new Owner(g,m); Customer c = new Customer(g,m); new Thread(s).start(); new Thread(c).start(); } } class Owner implements Runnable{ private Object goods; private Object money; public Owner(Object goods, Object money) { super(); this.goods = goods; this.money = money; } @Override public void run() { synchronized (goods) { System.out.println("先给钱"); synchronized (money) { System.out.println("发货"); } } } } class Customer implements Runnable{ private Object goods; private Object money; public Customer(Object goods, Object money) { super(); this.goods = goods; this.money = money; } @Override public void run() { synchronized (money) { System.out.println("先发货"); synchronized (goods) { System.out.println("再给钱"); } } } }

5.生产者和消费者

生产者与消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。 该问题描述了两个(多个)共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。 生产者与消费者问题中其实隐含了两个问题: (1)线程安全问题:因为生产者与消费者共享数据缓冲区,不过这个问题可以使用同步解决。 (2)线程的协调工作问题: 要解决该问题,就必须让生产者线程在缓冲区满时等待(wait),暂停进入阻塞状态,等到下次消费者消耗了缓冲区中的数据的时候,通知(notify)正在等待的线程恢复到就绪状态,重新开始往缓冲区添加数据。同样,也可以让消费者线程在缓冲区空时进入等待(wait),暂停进入阻塞状态,等到生产者往缓冲区添加数据之后,再通知(notify)正在等待的线程恢复到就绪状态。通过这样的通信机制来解决此类问题。

案例:有加餐馆的取餐口比较小,只能放10份快餐,厨师做完快餐放在取餐口的工作台上,服务器从这个工作台取出快餐给顾客。现在有多个厨师和多个服务员。

public class Workbench { private static final int MAX_VALUE = 10; private int num; public synchronized void put() { while(num >= MAX_VALUE){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); //加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题 } catch (InterruptedException e) { e.printStackTrace(); } num++; System.out.println("厨师制作了一份快餐,现在工作台上有:" + num + "份快餐"); this.notifyAll(); } public synchronized void take() { while(num <=0){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } } try { Thread.sleep(100); //加入睡眠时间是放大问题现象,去掉同步和wait等,可观察问题 } catch (InterruptedException e) { e.printStackTrace(); } num--; System.out.println("服务员取走了一份快餐,现在工作台上有:" + num + "份快餐"); this.notifyAll(); } package com.atguigu.part04; public class TestManyAndMany { public static void main(String[] args) { Workbench bench = new Workbench(); Cook c1 = new Cook(bench); Cook c2 = new Cook(bench); Waiter w1 = new Waiter(bench); Waiter w2 = new Waiter(bench); c1.start(); c2.start(); w1.start(); w2.start(); } }
最新回复(0)