Loading

Java基础-多线程

概述

  • 线程就是独立的执行路径
  • 在程序运行时,即使没有自己创建线程,后台也会有多个线程,如主线程,gc线程;
  • main()称之为主线程,为系统的入口,用于执行整个程序;
  • 在一个进程中,如果开辟了多个线程,线程的运行由调度器安排调度,调度器是与操作系统紧密相关的,先后顺序是不能认为的干预的。
  • 对同一份资源操作时,会存在资源抢夺的问题,需要加入并发控制
  • 线程会带来额外的开销,如cpu调度时间,并发控制开销。
  • 每个线程在自己的工作内存交互,内存控制不当会造成数据不一致

三种创建进程的方式

  • 继承thread类
  • 实现Runnable接口
  • 实现Callable接口

继承Thread

  1. 继承Thread类
  2. 重写run方法
  3. 调用start
package thread.demo1;

public class TestThread1 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("thread"+i);
        };
    }

    public static void main(String[] args) {
        TestThread1 t1 = new TestThread1();
        //t1.run();
        t1.start();

        for (int i = 0; i < 100; i++) {
            System.out.println("main"+i);
        }
    }
}

调用run方法时先执行子进程再执行主进程

调用start方法时子进程和主进程交替运行,线程不一定立即执行由CPU调度

image-20210712084908490

实现Runnable

另一种方法来创建一个线程是声明实现类Runnable接口实现了run方法。然后可以分配类的实例,在创建Thread时作为参数传递,并启动。

  1. 定义MyRunnable类实现Runnable接口
  2. 实现run()方法,编写线程执行体
  3. 创建线程对象,调用start()方法启动线程
package thread.demo1;

public class TestThread2 implements Runnable{

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println("thread"+i);
        }
    }

    public static void main(String[] args) {

        TestThread2 testThread = new TestThread2();
        //创建线程对象,通过线程对象开启线程、代理
        new Thread(new TestThread2()).start();

        /**
        Thread t1 = new Thread(testThread);
        t1.start();
         **/

        for (int i = 0; i < 100; i++) {
            System.out.println("main"+i);
        }
    }
}

对比

继承Thread类

  • 子类继承Thread类具备多线程能力
  • 启动线程:子类对象.start()
  • 不建议使用:避免OOP单继承局限性

实现Runnable接口

  • 实现接口Runnable具有多线程能力
  • 启动线程:传入目标对象+Thread对象.start()
  • 推荐使用:避免单继承局限性,灵活方便,方便同一个对象被多个线程使用。

实现Callable(了解)

  1. 实现Callable接口,需要返回值类型
  2. 重写call方法,需要抛出异常
  3. 创建目标对象
  4. 创建执行服务:ExecutorService ser = Executors.newFixedThreadPool(1);
  5. 提交执行: Future<Boolean> result1 = ser.submit(t1);
  6. 获取结果: boolean r1 = result1.get()
  7. 关闭服务: ser.shutdownNow();
package thread.demo1;

import java.util.concurrent.*;

public class TestThread3 implements Callable<Boolean> {

    int nums = 10;
    @Override
    public Boolean call() throws Exception {
        while (nums > 0) {
            System.out.println("还有"+nums--+"步到终点");
        }
        return true;
    }


    public static void main(String[] args) throws ExecutionException, InterruptedException {
        TestThread3 t1 = new TestThread3();

        //创建执行服务
        ExecutorService ser = Executors.newFixedThreadPool(1);
        //提交执行
        Future<Boolean> r1 = ser.submit(t1);
        //获取结果
        boolean rs1 = r1.get();
        //关闭服务
        ser.shutdown();
    }

}

Lamda表达式

  • 避免匿名内部类定义过多
  • 让代码看起来很简洁
  • 去掉了没有意义的代码,只留下核心的逻辑。

不同方式去实现方法

package thread.demo2;

public class Test1 {

    //3.静态内部类
    static class Like2 implements ILike{
        @Override
        public void lambda(){
            System.out.println("i like lambda2");
        }
    }

    public static void main(String[] args) {
        ILike like = new Like();
        like.lambda();

        ILike like2 = new Like2();
        like2.lambda();

        //4.局部内部类
        class Like3 implements ILike{
            @Override
            public void lambda(){
                System.out.println("i like lambda3");
            }
        }

        ILike like3 = new Like3();
        like3.lambda();

        //5.匿名内部类
        like = new ILike() {
            @Override
            public void lambda() {
                System.out.println("i like lambda4");
            }
        };
        like.lambda();

        //6.用lambda简化
        like = ()->{
            System.out.println("i like lambda5");
        };
        like.lambda();
    }
}

//1.定义接口
interface ILike{
    void lambda();
}

//2.实现类
class Like implements ILike{

    @Override
    public void lambda() {
        System.out.println("i like lambda");
    }
}

/**
i like lambda
i like lambda2
i like lambda3
i like lambda4
i like lambda5
**/

简化过程

package thread.demo2;

public class Test2 {
    public static void main(String[] args) {
        ILove l = (int i)-> {
            System.out.println("love you " + i + " times");
        };
        l.love(0000);

        //1.简化类型
        ILove l1 = (i)-> {
            System.out.println("love you " + i + " times");
        };
        l1.love(1000);

        //2.简化括号
        ILove l2 = i-> {
            System.out.println("love you " + i + " times");
        };
        l2.love(2000);

        //3.简化花括号(代码只能有一行)
        ILove l3 = i-> System.out.println("love you " + i + " times");
        l3.love(3000);
    }
}

interface ILove{
    void love(int i);
}

/**
love you 0 times
love you 1000 times
love you 2000 times
love you 3000 times
**/

总结

lambda的前提是接口为函数式接口 表达式只有在一行的时候才能简化为一行如果有多行需要用花括号包裹

静态代理

  • 真实对象和代理对象都要实现同一个接口、代理对象要代理真实角色
  • 代理对象可以做很多真实对象做不了的事情、真实对象专注做门己的事情

婚庆公司代理结婚需要处理的事

package thread.demo3;

public class StaticProxy {
    public static void main(String[] args) {
        WeddingCom wc = new WeddingCom(new You());
        wc.HappyMarry();
        new Thread(()-> System.out.println("proxy thread")).start();
    }
}

interface Marry{
    void HappyMarry();
}

//真实角色
class You implements Marry{
    @Override
    public void HappyMarry() {
        System.out.println("Bob get married");
    }
}

//代理
class WeddingCom implements Marry{
    private Marry target;

    public WeddingCom(Marry target) {
        this.target = target;
    }

    @Override
    public void HappyMarry() {
        Before();
        this.target.HappyMarry();
        After();
    }

    private void Before() {
        System.out.println("Prepare for the wedding");
    }

    private void After() {
        System.out.println("Pay for Wedding company");
    }
}

/**
Prepare for the wedding
Bob get married
Pay for Wedding company
proxy thread
**/

进程状态

image-20210801212436079

image-20210801212710525

线程方法

  • setPriority(int newPriority) 更改线程的优先级
  • static void sleep(long millis) 在指定的毫秒数内让当前正在执行的线程休眠
  • void join() 等待该线程终止
  • static void yield() 暂停当前正在执行的线程对象,并执行其他线程
  • void interrupt() 中断线程,别用这个方式
  • boolean isAlive() 测试线程是否处于活动状态

停止线程

  • 不推荐使用JDK提供的stop()destroy()
  • 推荐线程自己停下来
  • 建议使用标志位来终止变量 当flag=false终止进程
package thread.demo3;

import basic.demo7.Test;

public class Test1 implements Runnable{
    private boolean flag = true;

    @Override
    public void run() {
        int i = 0;
        while (flag){
            System.out.println("run thread" + i++);
        }
    }

    //设置公开停止方法
    public void stop(){
        this.flag = false;
    }

    public static void main(String[] args) {
        Test1 t =new Test1();

        new Thread(t).start();

        for (int i = 0; i < 1000; i++) {
            System.out.println("main" + i);
            if (i==900){
                t.stop();
                System.out.println("Stop!!!!!!!!");
            }
        }
    }
}

image-20210801230200971

线程休眠

  • sleep(时间)指定当前线程阻塞的毫秒数
  • sleep存在异常 InterruptedException
  • sleep时间达到后线程进入就绪状态
  • sleep可以模拟网络延时,倒计时等。
  • 每一个对象都有一个锁,sleep不会释放锁;

每秒打印一次时间

package thread.demo3;

import java.text.SimpleDateFormat;
import java.util.Date;

public class Test2 {
    public static void main(String[] args) throws InterruptedException {
        //打印系统时间
        Date sTime = new Date(System.currentTimeMillis());
        while (true){
            Thread.sleep(1000);
            System.out.println(new SimpleDateFormat("hh:mm:ss").format(sTime));
            sTime = new Date(System.currentTimeMillis());
        }
    }
}
/**
10:08:27
10:08:28
10:08:29
10:08:30
10:08:31
10:08:32
**/

sleep的作用是放大问题的发生性

线程礼让

  • 礼让线程,让当前正在执行的线程暂停,但不阻塞
  • 将线程从运行状态转为就绪状态
  • 让cpu重新调度,礼让不一定成功
package thread.demo3;

public class Test3 {
    public static void main(String[] args) {
        new Thread(new MyYield(), "a").start();
        new Thread(new MyYield(), "b").start();
    }
}

class MyYield implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + ":开始线程");
        Thread.yield();
        System.out.println(Thread.currentThread().getName() + ":停止线程");
    }
}
/**
a:开始线程
b:开始线程
a:停止线程
b:停止线程
**/

强制执行

Join合并线程,待此线程执行完成后,再执行其他线程,其他线程阻塞。

package thread.demo3;

public class Test4 {
    public static void main(String[] args) throws InterruptedException{
        Thread t =  new Thread(new TestJoin());
        t.start();

        for (int i = 0; i < 500; i++) {
            if (i==200){
                t.join();
            }
            System.out.println("main:" + i);
        }
    }
}

class TestJoin implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 300; i++) {
            System.out.println("join:" + i);
        }
    }
}

image-20210804095844858

线程状态

线程处于以下状态:

  • NEW 尚未启动的线程处于此状态
  • RUNNABLE 在Java虚拟机中执行的线程处于此状态
  • BLOCKED 被阻塞等待监视器锁定的线程处于此状态
  • WAITING 正在等待另一个线程执行特定动作的线程处于此状态
  • TIME_WAITING 正在等待另一个线程执行动作达到指定等待时间的线程处于此状态
  • TERMINATED 已退出的线程处于此状态
package thread.demo3;

public class Test5 {
    public static void main(String[] args) throws InterruptedException {

        Thread t= new Thread(()->{
            for (int i = 0; i < 5; i++) {
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("!");
            }
        });

        //观察状态
        Thread.State s =  t.getState();
        System.out.println(s);

        //观察后启动
        t.start();
        s = t.getState();
        System.out.println(s);

        while (s != Thread.State.TERMINATED){
            Thread.sleep(100);
            s = t.getState();
            System.out.println(s);
        }
    }

}

/**
NEW
RUNNABLE
TIMED_WAITING
!
TIMED_WAITING
TIMED_WAITING
!
TIMED_WAITING
TIMED_WAITING
!
TERMINATED
**/

线程优先级

Java提供一个线程调度器来监控程序中启动后进入就绪状态的所有线程,线程调度器按照优先级决定应该调度哪个线程来执行。

线程的优先级用数字来表示

  • Thread.MIN_PRIORITY =1;
  • Thread.MAX_PRIORITY= 10;
  • Thread.NORM_PRIORITY = 5;

getPriority() 获取优先级 setPriority(int x) 设置优先级

package thread.demo4;

public class Test {
    public static void main(String[] args) {
        MyPriority m = new MyPriority();

        Thread t1 = new Thread(m);
        Thread t2 = new Thread(m);
        Thread t3 = new Thread(m);
        Thread t4 = new Thread(m);
        Thread t5 = new Thread(m);

        t1.start();

        t2.setPriority(2);
        t2.start();

        t3.setPriority(4);
        t3.start();

        t4.setPriority(6);
        t4.start();

        t2.setPriority(8);
        t5.start();

    }
}

class MyPriority implements Runnable{
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "-->" + Thread.currentThread().getPriority());
    }
}
/**
Thread-0-->5
Thread-1-->8
Thread-3-->6
Thread-4-->5
Thread-2-->4
**/

优先级低只是意味着获得调度的概率低.并不是优先级低就不会被调用了.取决于CPU的调度

守护(daemon)线程

  • 线程分为用户线程守护线程
  • 虚拟机必须确保用户进程执行完毕
  • 虚拟机不用等待守护进程执行完毕
package thread.demo4;

public class Test2 {
    public static void main(String[] args) {
        Thread t1 = new Thread(new TestDaemon());
        t1.setDaemon(true);  //设置为守护进程
        t1.start();

        new Thread(new User()).start();
    }
}

class User implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println("user process");
        }
    }
}

class TestDaemon implements Runnable{
    @Override
    public void run() {
        while (true){
            System.out.println("daemon process");
        }
    }
}

image-20210804111252908

线程同步

多个线程访问同个对象(并发),多个线程修改对象(同步)。

线程同步其实就是一种等待机制,多个需要同时访问此对象的线程进入这个对象的等待池形成队列,等待前面线程使用完毕,下一个线程再使用

由于同一进程的多个线程共享同一块存储空间,在带来方便的同时,也带来了访问冲突问题﹐为了保证数据在方法中被访问时的正确性,在访问时加入锁机制Synchronized ,当一个线程获得对象的排它锁,独占资源﹐其他线程必须等待,使用后释放锁即可.存在以下问题︰

  • 一个线程持有锁会导致其他所有需要此锁的线程挂起
  • 在多线程竞争下﹐加锁,释放锁会导致比较多的上下文切换和调度延时,引起性能问题
  • 如果一个优先级高的线程等待一个优先级低的线程释放锁会导致优先级倒置,引起性能问题.

不安全案例

案例1

多个用户抢票

package thread.demo5;

public class Test1 {
    public static void main(String[] args) {
        BuyTicket b = new BuyTicket();

        new Thread(b, "张三").start();
        new Thread(b, "李四").start();
        new Thread(b, "王五").start();
    }
}

class BuyTicket implements Runnable{
    private int nums = 5;
    boolean flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void buy(){
        if (nums <=0){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "买到了第" + nums-- + "张票");
    }
}

/**
李四买到了第4张票
王五买到了第3张票
张三买到了第5张票
李四买到了第2张票
王五买到了第2张票
张三买到了第1张票
**/

可能会有拿到第0张票及以后的票 以及两个人买了同一张票

案例2

多个用户同时取钱

package thread.demo5;

public class Test2 {
    public static void main(String[] args) {
        Account card = new Account("工资卡", 100);
        Draw user1 = new Draw(card, 50, "user1");
        Draw user2 = new Draw(card, 100, "user2");
        user1.start();
        user2.start();
    }
    

}

class Account{
    String name;
    int money;

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
}

class Draw extends Thread{
    Account account ;
    int drawMoney;
    int havingMoney;

    public Draw(Account account, int drawMoney, String name){
        super(name);
        this.account = account;
        this.drawMoney = drawMoney;
    }

    @Override
    public void run() {
        if (account.money - drawMoney < 0){
            System.out.println(account.name+"没钱了");
            return;
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        account.money = account.money - drawMoney;
        havingMoney += drawMoney;

        System.out.println(account.name+"余额为"+account.money);
        System.out.println(this.getName()+"手里有"+havingMoney);
    }

}

/**
工资卡余额为-50
工资卡余额为-50
user1手里有50
user2手里有100
**/

同时取的时候并没有验证余额是否足够 导致多取了

案例3

写入数据到列表

package thread.demo5;

import java.util.ArrayList;
import java.util.List;

public class Test3 {
    public static void main(String[] args) {
        List<String> list = new ArrayList<String>();
        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }
        System.out.println(list.size());
    }
}

/**
9989
**/

有同时写入同一位置的情况,导致最终有未写入的数据

同步方法

由于我们可以通过private关键字来保证数据对象只能被方法访问﹐所以我们只需要针对方法提出一套机制。这套机制就是synchronized关键字,它包括两种用法︰synchronized方法和synchronized 块。

public synchronized void method(int args){}

synchronized方法控制对“对象”的访问,每个对象对应一把锁 每个synchronized方法都必须获得调用该方法的对象的锁才能执行。否则线程会阻塞,方法一旦执行,就独占该锁。直到该方法返回才释放锁﹐后面被阻塞的线程才能获得这个锁,继续执行。

方法里面需要修改的内容才需要锁,锁的太多,浪费资源

  • 对于普通同步方法,锁是当前实例对象。 如果有多个实例 那么锁对象必然不同无法实现同步。
  • 对于静态同步方法,锁是当前类的Class对象。有多个实例 但是锁对象是相同的 可以完成同步。
  • 对于同步方法块,锁是Synchonized括号里配置的对象。对象最好是只有一个的 如当前类的 class 是只有一个的 锁对象相同 也能实现同步。

案列1

package thread.demo6;

public class Test1 {
    public static void main(String[] args) {
        BuyTicket b = new BuyTicket();

        new Thread(b, "张三").start();
        new Thread(b, "李四").start();
        new Thread(b, "王五").start();
    }
}

class BuyTicket implements Runnable{
    private int nums = 5;
    boolean flag = true;
    @Override
    public void run() {
        while (flag){
            buy();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private synchronized void buy(){
        if (nums <=0){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "买到了第" + nums-- + "张票");
    }
}


/**
张三买到了第5张票
王五买到了第4张票
李四买到了第3张票
王五买到了第2张票
张三买到了第1张票
**/

同步块

案列2

package thread.demo6;

public class Test2 {
    public static void main(String[] args) {
        Account card = new Account("工资卡", 100);
        Draw user1 = new Draw(card, 50, "user1");
        Draw user2 = new Draw(card, 100, "user2");
        user1.start();
        user2.start();
    }


}

class Account{
    String name;
    int money;

    public Account(String name, int money) {
        this.name = name;
        this.money = money;
    }
}

class Draw extends Thread{
    Account account;
    private int drawMoney;
    private int havingMoney;

    public Draw(Account account, int drawMoney, String name){
        super(name);
        this.account = account;
        this.drawMoney = drawMoney;
    }

    @Override
    public void run() {
        synchronized (account){
            if (account.money - drawMoney < 0){
                System.out.println(account.name+"没钱了");
                return;
            }
            account.money = account.money - drawMoney;
            havingMoney = havingMoney + drawMoney;

            System.out.println(account.name+"余额为"+account.money);
            System.out.println(this.getName()+"手里有"+havingMoney);
        }
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

}

/**
工资卡余额为50
user1手里有50
工资卡没钱了
**/

Lock锁

虽然我们可以理解同步代码块和同步方法的锁对象问题,但是我们并没有直接看到在哪里加上了锁,在哪里释放了锁,为了更清晰的表达如何加锁和释放锁,JDK5以后提供了一个新的锁对象Lock

Lock实现提供比使用synchronized方法和语句可以获得更广泛的锁定操作

  • void lock():获得锁
  • void unlock():释放锁

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

案例2

package thread.demo6;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class Test1 {
    public static void main(String[] args) {
        BuyTicket b = new BuyTicket();

        new Thread(b, "张三").start();
        new Thread(b, "李四").start();
        new Thread(b, "王五").start();
    }
}

class BuyTicket implements Runnable{
    private int nums = 5;
    boolean flag = true;
    private Lock lock = new ReentrantLock();
    @Override
    public void run() {
        while (flag){
            lock.lock();
            buy();
            lock.unlock();
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    private void buy(){
        if (nums <=0){
            flag = false;
            return;
        }
        System.out.println(Thread.currentThread().getName() + "买到了第" + nums-- + "张票");
    }
}

/**
张三买到了第5张票
李四买到了第4张票
王五买到了第3张票
李四买到了第2张票
张三买到了第1张票
**/

线程安全的类

static <T>list<T> synchronizedList (List<T> list 返回由指定列表支持的同步(线程安全)列表

package thread.demo6;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

public class Test3 {
    public static void main(String[] args) throws InterruptedException {
        List<String> list = Collections.synchronizedList(new ArrayList<String>());

        for (int i = 0; i < 10000; i++) {
            new Thread(()->{
                list.add(Thread.currentThread().getName());
            }).start();
        }

        Thread.sleep(1);
        System.out.println(list.size());
    }
}

/**
10000
**/

生产者消费者

生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻所谓生产者消费者问题,实际上主要是包含了两类线程:

  • 一类是生产者线程用于生产数据
  • 一类是消费者线程用于消费数据
  • 为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库
  • 生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为

为了体现生产和消费过程中的等待和唤醒,Java就提供了几个方法供几个方法在Object类中等待和唤醒方法:

  • void wait() 导致当前线程等待,直到另一个线程调用该对象的notify()方法或notifyAll()方法
  • void notify() 唤醒正在等待对象监视器的单个线程
  • void notifyAll() 唤醒正在等待对象监视器的所有线程

奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作

package thread.demo7;

public class Box {
    private int milk;
    private boolean state = false;

    public synchronized void put(int milk){
        // 有牛奶 等待放入
        if (state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //没牛奶 放入牛奶
        this.milk = milk;
        System.out.println("将第" + this.milk + "瓶奶送入奶箱");
        state = true;

        //唤醒其他等待线程
        notifyAll();
    }

    public synchronized void get(){
        //没牛奶 等待放入
        if (!state){
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        //有牛奶 拿牛奶
        System.out.println("用户拿到第" + this.milk + "瓶奶");
        state = false;

        //唤醒其他等待线程
        notifyAll();
    }
}

生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作

package thread.demo7;

public class Producer implements Runnable{
    private Box b;

    public Producer(Box b) {
        this.b = b;
    }

    @Override
    public void run() {
        while (true){
            b.get();
        }
    }
}

消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作

package thread.demo7;

public class Customer implements Runnable{
    private Box b;
    public Customer(Box b){
        this.b = b;
    }
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            b.put(i);
        }
    }
}

测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下

package thread.demo7;

public class Test {
    public static void main(String[] args) {
        Box b = new Box();

        Producer p = new Producer(b);
        Customer c = new Customer(b);

        Thread t1 = new Thread(p);
        Thread t2 = new Thread(c);

        t1.start();
        t2.start();
    }
}

输出

将第0瓶奶送入奶箱
用户拿到第0瓶奶
将第1瓶奶送入奶箱
用户拿到第1瓶奶
将第2瓶奶送入奶箱
用户拿到第2瓶奶
将第3瓶奶送入奶箱
用户拿到第3瓶奶
将第4瓶奶送入奶箱
用户拿到第4瓶奶

Process finished with exit code -1
posted @ 2021-08-05 13:41  Th0r  阅读(35)  评论(0编辑  收藏  举报