Java多线程(3)

7、Java并发知识

Java并发的重要性在于,最大限度的提高计算资源的效率

并发是多线程中交替的反问同一个资源,同一个资源可以是CPU资源,也可以是内存资源,该资源的特点是同一时刻只能一个线程进行访问

临界资源和临界区

  • 临界资源:一般是指内存资源,一个时刻只能有一个线程进行访问,一个线程正在使用临界资源的时候,另一个线程是不能使用,临界资源时非可剥夺资源,即使操作系统(JVM)也无法阻止这种资源的独享行为
  • 临界区:是一个线程中访问临界资源的程序片段,不是内存资源,这个就是临界区和临界资源的区别,临界区的使用原则“空闲让进,忙则等待,有限等待,让权等待”
  • 空闲让进:临界资源空闲是一定要让线程进入, 不发生“互斥礼让”行为
  • 忙则等待:临界资源正在使用时外面的线程就需要等待
  • 有限等待:线程在进入到临界区的时间是有限的,不会发生“饿死”的情况
  • 让权等待:线程进入到临界区应该让出CPU的使用
  • 线程安全:在单线程下执行和多线程下执行的,最终得到的结果是相同到的,这个操作称之为线程安全

并发的特性

原子性

如果一个操作时不可分割的,我们称这个操作为原子性

a++操作就是一个非原子性操作,在并发情况下,操作是非线程安全的,但是可以通过同步技术(Lock)实现安全处理的

可见性

一个变量被多个线程共享,如果一个线程的修改了这个变量的值,其他的线程立即知道这个修改,我们称这个修改具有可见性

有序性

Java中线程的有序性,表现为两个方面
在一个线程内部观察,所有的操作都是有序的,所有的操作的执行按照“串行”(as-if seial "向排了序一样)
在线程间观察,在某一个线程中观察另一个线程,则所有的线程都可以进行交叉并行执行,是正序的

int a=10;
a=a++;
++a;

在单线程下a得到的结果??? 11
在多线程下:a++操作 ,两个线程执行 不确定
首先来获取a原有值:10
a+1
a=a+1(赋值)
a+1 时:另一个线程获取 a=10, 另一个线程操作完值为11
赋值时:另一个线程获取a=11,另一个线程操作完值为12
a++操作是一个非原子性操作

8、Volatile关键字

统计:1秒内count++的次数

public class Count {
    
    
    private static volatile boolean flag = true;//线程共享变量

    public static void main(String[] args) {
    
    
        Thread A = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int count = 0;
                while (flag) {
    
    
                    count++;
                }
                System.out.println("count:" + count);

            }
        });

        Thread B = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                //到1秒钟,设置flag为false
                flag = false;
                System.out.println("1秒钟结束");
            }
        });

        A.start();
        B.start();
    }
}

 
在演示代码中,线程共享变量flag在未加volatile关键字时,t2线程的修改,t1线程未能实时感知到,加了volatile关键字,可以让t2线程中对flag修改t1线程能够立即感知到
在这里插入图片描述
内存模型上,堆内存用来存储对象和基本数据类型的备份,称之为主内存,将栈内存中存储的变量的部分内存,称之为本地内存(工作内存),这个就是 JMM模型的内容

  1. Java线程对于所有的变量操作(读取,赋值)都是在自己的工作内存中进行的,线程是不直接读取主内存中的变量
  2. 不同线程无法直接访问对方的工作内存中的变量
  3. 线程间变量的传递主要是通过主内存来完成

主内存和工作内存具体的交互协议,即一个变量从主内存拷贝到工作内存,如果从工作内存同步到主内存的是有具体的交互操作:
主要分为8种操作:Lock(锁定)、unlock(解锁)、read(读取)、load(载入)、use(使用)、assign(赋值)、store(存储)、write(写入)

分析上面的代码,首先代码及变量都放在主内存变量flag为true,A线程执行要获取flag,通过交互操作将flag的副本拷贝到A线程的本地内存A,B线程执行睡眠1秒,修改flag值,即B线程从主内存中获取变量flag拷贝到本地内存B中,B线程将flag修改为false,注意:修改的是本地内存B中的falg副本,B线程的本地副本未及时写入主内存,或者主动写入主内存A线程本地副本未读取主内存最新的数据时,A线程的本地副本就一直是非最新值

加入volatile之后, 在汇编层面(非字节码 产生的.class )在对应的汇编语句前加了“#Lock”,当B线程修改flag变量操作时,

  1. 本地内存B将flag副本修改为最新值,并立即将最新值回写到主内存上,通过总线将A线程的flag副本的标识置为无效
  2. 当A线程来访问flag的本地副本时,先检测标志位为无效时,A线程会从主内存拷贝数据到本地内存副本上

Volatile特征

保证了内存可见性

volatile修饰的变量(本地内存:java虚拟机栈/寄存器)不会被缓存在寄存器,变量在本地内存(虚拟机栈线程私有的空间),一旦变量修改会立即回写至主内存,每一个线程访问主内存上的数据是最新的变量结果,如果已经有线程存在本地副本,即在回写时有效标志位会失效,从而是存储副本的线程能够进入主内存获取最新数据

禁止指令重排序

Java内存模型不会对volatile指令进行重排序,从而保证对volatile变量的执行顺序,永远按照顺序出现的顺序执行。

重排序是语句happen-before法则,法则之一规定“对volatile字段的写入操作happen-before与每一个后续的同一个字段的读操作”

注意:volatile字段只能够满足并发特征中的可见性、(有序性),不能高正原型性,也不能保证线程安全

注意点:
volatile只能修饰变量,对基本类型的数据起作用

volatile修饰对象是否起作用?
答: 对对象不起作用,只能对对象的地址空间进行可见,即地址如果发生改变,其他线程能够立即感知,但是对象本身的属性发生改变,volatile是不能保证其他下线程立即感知。

Volatile工作原理

《深入理解Java虚拟机》关于volatile描述:
 
“观察加入volatile关键字和没有加入volatile关键字所产生的汇编语言,加入了volatile关键字时,会多出一个Lock前缀指令”

Lock前缀指定实际是相当于一个内存屏障(内存栅栏),内存屏障存在的作用主要有3点:

  • 他保证指定重排序之后内存栅栏之后的指定不会到内存栅栏之前,内存栅栏之前的指定不会进入栅栏之后
  • 它会强制对缓存(工作内存)中的数据立即写回主内存
  • 如果是写操作,他会立即导致其他CPU的对应的缓存立即无效
    在这里插入图片描述

如果volatile当前修饰的是一个变量

  1. 变量值从主内存(在堆中)加载load到本地内存(虚拟机栈的栈帧中)
  2. 之后,线程对该变量的操作就不在和主内存打交道/不在联系,直接使用本地内存的副本数据,如果主内存中或副本的数据发生任何变化,如果不互相联系,则导致主内存和副本数据不一致的的问题
  3. 在volatile修饰的变量在某个线程中发生改变,基于volatile的的特征:立即将该变量的修改写回主内存,并且其他CPU上对应的缓存是会立即失效

volatile Object o = new Object();
volatile a++

9、Synchronized关键字

Synchronized的使用

Synchronized添加到代码块

    //修饰代码块
    public void test1(Object o) {
    
    
        synchronized (o) {
    
    
            //doing
        }
    }

synchronized如果锁的是某一个obj的对象,实际上作用在代码块上

synchronized添加在普通的方法上

    //修饰普通方法
    public synchronized void  test2() {
    
    
        //doing 
    }

Synchronized加在普通方法上,锁的是对象实例

多个线程来竞争时,那个线程获取了该对象实例那个线程调用的方法才能继续执行

Q:加入存在两个线程,同时拥有该对象实例,一个线程调用test2方法,一个线程调用test4方法,两个方法可以同时执行嘛?

A:不能, test2和test4方法都是Synchronized修饰的普通方法,Synchronized加在普通方法上,锁的是对象实例一个对象实例时,一个线程获取,首先占有当前对象实例,然后调用该对象的方法,两个线程同时来要执行,哪一个线程先获取对象实例,则才能调用对应方法,另一个线程只能等占有的线程释放掉锁之后才能继续获取对象实例进行执行方法

例子:假如demo2线程抢先获取对象实例,则可以执行test2方法,同时demo4线程因为竞争对象实例synchronizeDemo失败,而需要等待demo2线程执行结束才能执行

Synchronized添加到静态方法上

//修饰静态方法
    public synchronized static void  test3() {
    
    
       //doing
    }

如果Synchronized加在静态方法上,锁的就是当前的class实例

Synchronized的特点

Synchronized修饰的方法或代码块,在同一时刻JVM只能允许一个线程访问,Synchronized通过锁机制来完成同一时刻只能一个线程访问(临界区)

并发编程中,Synchronized的锁机制可以做到原子性,可见性,有序性。

public class Count {
    
    
    private static  boolean flag = true;//线程共享变量

    public static void main(String[] args) {
    
    
        Thread t1 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                int count = 0;
//                while (flag) {
    
    
                while (Count.isTrue()){
    
    
                    count++;
                }
                System.out.println("count:" + count);

            }
        });

        Thread t2 = new Thread(new Runnable() {
    
    
            @Override
            public void run() {
    
    
                try {
    
    
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
                //到1秒钟,设置flag为false
//                flag = false;
                setFlag(false);
                System.out.println("1秒钟结束");
            }
        });

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

    }

    //判断flag是否修改
    public synchronized static boolean isTrue() {
    
    
        return flag;
    }

    //修改flag
    public synchronized static void setFlag(boolean b) {
    
    
        flag = b;
    }
}

synchronized原理

通过Javac命令将代码编程生成字节码文件.class文件
通过javap -v XXX.class反编译字节码

修饰代码块:
在这里插入图片描述
在这里插入图片描述
通过上面大致可以看到,修饰方法在字节码层面上flag中通过ACC_SYNCHRONIZED标志,修饰代码块时使用monitorenter和monitorexit来完成,无论使用以上两种那种方式,本质上都是对一个对象的监视器(monitor)进行获取,而对于这个监视器的获取是排他的,也就是同一时刻一个线程可以获取到由Synchronized所保护的对象的监视器。
在这里插入图片描述
通过上图可以知道,任何线程的访问对象,都需要首先来获取对象的监视器(monitor),如果获取监视器成功,则可以访问该对象,一旦有线程成功获取monitor对象,其他的线程则获取失败,线程会进入到BLocking状态,将获取不成功的线程放入到队列中,在成功访问的线程在Monitor监视器

Monitor排他性的实现是需要借助操作系统所提供的锁来实现(mutux)

Synchronized也称之为重量级锁,随着java的发展,对于Synchronize做了优化,锁的优化(自旋锁,偏向锁,轻量级锁,重量级锁 实现思路,尽量通过代码层进行加锁操作,减少系统交互).

Synchronize的使用场景

考虑在场景下线程是否安全?为什么?

场景1:两个线程同时访问同一个对象的同步方法

分析:两个线程来访问同一个对象锁,所以会相互等待,是线程安全的

两个线程同时访问同一个对象的同步方法,是线程安全的

场景2:两个线程同时访问两个对象的同步方法

这是一种锁失效的情况,访问两个对象的同步方法,那个线程分别持有两个对象的同步方法,所以线程之间树互不受限,加锁的目的是为了多个线程竞争同一把锁,而这个 不存在多个线程竞争同一把锁,而是分别持有一把锁

两个线程同时访问两个对象的同步方法,是线程不安全的

场景3:两个线程同时访问(一个或者两个)对象的静态同步方法

和上面场景2的锁实现的解决方案是同一个问题

两个线程同时访问(一个或者两个)对象的静态同步方法,是线程安全的

场景4:两个线程分别同时访问(一个或者两个)对象的同步方法和非同步方法

两个线程其中一个线程访问同步方法,另一个访问非同步方法,是否是线程安全的?

是线程不安全的

public class Condition implements Runnable {
    
    
    
    
    
    @Override
    public void run() {
    
    
        if (Thread.currentThread().getName().equals("Thread-0")){
    
    
            //执行同步方法
            test1();
        } 
        if (Thread.currentThread().getName().equals("Thread-1")) {
    
    
            //执行非同步方法
            test2();
        }
    }
    
    
    
    //同步方法
    public synchronized void test1() {
    
    
        System.out.println("线程名:"+Thread.currentThread().getName()+" 线程开始");
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程名:"+Thread.currentThread().getName()+" 线程结束");

    }
    
    //非同步方法
    public void  test2() {
    
    
        System.out.println("线程名:"+Thread.currentThread().getName()+" 线程开始");
        try {
    
    
            Thread.sleep(5000);
        } catch (InterruptedException e) {
    
    
            e.printStackTrace();
        }
        System.out.println("线程名:"+Thread.currentThread().getName()+" 线程结束");

    }

    public static void main(String[] args) {
    
    
        Condition condition = new Condition();
        Thread t1 = new Thread(condition);
        Thread t2 = new Thread(condition);
        t1.start();
        t2.start();
    }
}

代码执行结果:
在这里插入图片描述
两个线程是并行执行的,所以是线程不安全的

作业:模拟3个窗口售票

模拟车票窗口售票问题:存在三个窗口,同时完成A到B地的车票售卖,车票总数为100张,车票编号分别为1,2,3…100

要求:每个窗口卖出的票是随机的,票不能多买,少买,重复买

提示:三个窗口通过三个线程模拟,车票号为三个线程共享,车票的售卖情况和编号三个线程共享

/**
 * 火车票类
 */
public class PicketDemo {
    
    
    private int count = 1;//票编号


    //获取票号,每次一个窗口获取
    public synchronized int getPicketId(){
    
    
        return count++;

    }

    //判断票是否售完
    public synchronized boolean isEnd() {
    
    
        //true  售卖完
        return count == 101;
    }
}


/**
 * 售票窗口
 */
public class SellDemo extends Thread {
    
    
    //获取票信息
    private PicketDemo picketDemo;
    //窗口名
    private String sellName;

    //三个线程共用售票类对象
    public SellDemo(PicketDemo picketDemo, String sellName) {
    
    
        this.picketDemo = picketDemo;
        this.sellName = sellName;
    }


    @Override
    public void run() {
    
    
        Random random = new Random();
        while (!picketDemo.isEnd()) {
    
    
            //票未卖完,获取一张票号
            int picketId = picketDemo.getPicketId();

            //随机睡眠1秒内
            try {
    
    
                Thread.sleep(random.nextInt(1000));
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(sellName+"售卖票:"+picketId);
        }
    }

    public static void main(String[] args) {
    
    
        PicketDemo picketDemo = new PicketDemo();
        SellDemo sell1 = new SellDemo(picketDemo, "窗口1");
        SellDemo sell2 = new SellDemo(picketDemo, "窗口2");
        SellDemo sell3 = new SellDemo(picketDemo, "窗口3");
        sell1.start();
        sell2.start();
        sell3.start();
    }
}

10、线程间的通信

方法介绍

在Java的Object类中提供了wait,notify,notifyall等方法,这三个方法是用来做线程间通信的

  • wait():调用一个对象的wait()方法,会导致当前持有该对象锁的线程等待,直到该对象锁的另一个线程调用notify,notifyAll方法来唤醒
  • notify():调用一个对象的notify方法,会导致当前持有该对象锁的所有线程中的随机一个线程被唤醒
  • notifyAll():调用一个对象的notifyAll方法,会使持有该对象锁的所有线程都被唤醒

使用示例

  • wait方法的demo
public class WaitDemo extends Thread {
    
    
    private Object o;
    private String name;
    public  WaitDemo(Object o,String name) {
    
    
        this.o = o;
        this.name = name;
    }

    @Override
    public void run() {
    
    
        synchronized (o) {
    
    
            System.out.println(name+" 线程已获取O对象锁");
            try {
    
    
                //当前线程阻塞
                o.wait();
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            System.out.println(name+" 线程即将释放O对象锁");
        }
    }
}
  • notify方法的demo
public class NotifyDemo extends Thread {
    
    
    private Object o;
    private String name;
    public NotifyDemo(Object o,String name) {
    
    
        this.o = o;
        this.name = name;
    }

    @Override
    public void run() {
    
    
        synchronized (o) {
    
    
            System.out.println(name+"线程已获取O对象锁");
            try {
    
    
                Thread.sleep(10000);
            } catch (InterruptedException e) {
    
    
                e.printStackTrace();
            }
            o.notify();
            System.out.println(name+"线程释放O对象锁");

        }
    }
}
  • 使用
public static void main(String[] args) {
    
    
        //共享对象
        Object o = new Object();
        new WaitDemo(o,"wait").start();
        new NotifyDemo(o,"notify").start();

    }

这里使用需要注意的点:

  1. wait、notify和notifyAll方法必须是同一个对象调用
  2. 对于wait、notify和notifyAll的调用,必须在该对象的同步方法或者同步代码块中,
    锁作用的对象和wait方法必须作用于同一个对象
  3. wait方法的在调用进入阻塞之前会释放锁
  4. 线程的状态转化问题:当wait被唤醒或超时时,并不是直接进入到运行或者就绪状态,而是先进入到block状态,抢锁成功后,才能进入到就绪状态

注意点演示实例:

1、如果调用wait和notify的不是同一个对象,是否可以?
在这里插入图片描述
通过demo演示可知,如果调用wait方法的线程和调用notify方法的线程不是同一个对象实例,则无法进行线程间通信,即wait方法的线程没有调用notify的线程唤醒

2、假如加锁对象和调用wait方法的对象不是同一个对象,是否可以?
在这里插入图片描述
通过demo可知,当加锁的对象和调用wait方法(notify、notifyAll)的对象
必须是同一个对象,否则会抛出IllegalMonitorStateException异常

锁池和等待池

  • 锁池:假设线程A已经拥有了某个对象的对象锁,而其他的线程想要调用该兑现个的某个Synchronized方法,而这些线程在进入该对象之前先要竞争获取该对象的锁(monitor锁),因为该对象的锁已经被线程A所持有,所以未获取锁这些线程就进入到对象的锁池中
  • 等待池:假如线程A调用了某个对象的wait方法,线程A就会释放掉锁后,进入的就是对象的等待池
    在这里插入图片描述

在说notify和notifyAll的区别:

  • notify唤醒的是对象等待池中的一个线程,这个线程进入到blocked状态,即进入到该对象的锁池中,当开始抢锁时,该线程才有资格竞争该对象的锁,其他的处于等待池的线程需要等待notify或者notifyAll的通知才能进入到锁池,只有进入到锁池的线程才有资格来竞争锁

  • notifyAll方法的调用是唤醒该对象的所有处于等待池中的线程,此时等待池中的线程进入到对象的锁池中,全部线程才能够竞争对象的锁。

  • notify和wait方法的调用必须加上循环进行保证。如果是多个wait线程在等待唤醒,没有循环条件的保证,当notifyAll通知是,有两个以上的线程被唤醒,此时只能有一个来正在执行,另一个wait线程还需要等待。因为是非锁资源,所以会继续执行,容易造成问题

作业:打印10遍ABCABC

有ABC三个线程,每一个线程打印自己名字,需要打印结果为ABCABC…,打印10遍

循环重复执行的问题

ABC三个线程需要通信,三个线程共用一个对象,自定义一个对象,知道当前是哪个线程执行,下一个通知的执行线程

/**
 * 自定义对象
 */
public class DIYObject {
    
    
    private int index;//当前执行的线程编号  0  1  2

    public int getIndex() {
    
    
        return index;
    }

    public void setIndex(int i) {
    
    
        index = i;
    }
}

public class ABCThread extends Thread {
    
    
    private DIYObject object;//线程共享对象,线程间通知也是该对象
    private int threadId;//当前线程编号
    /**
     * 三个线程命名为ABC
     * 传递的线程ID为int类型的数据
     * name数组下标  0 1 2 分别 A B C
     * name[0] = 'A'
     * 线程循环使用(threadId+1)% 3
     * threadId=0当前(0+1)%3=1    即下一个执行threadID=1  name[1] = b
     * threadId=1d (1+1)%3 =2     即threadID = 2        name[2] = c
     * threadId =2  (2+1)%3 = 0   即threadID= 0        name[0]=A
     *
     */
    private String name[] = {
    
    "A","B","C"};

    public ABCThread(DIYObject o,int id) {
    
    
        this.object = o;
        this.threadId = id;
    }

    @Override
    public void run() {
    
    
         int num = 10;
         while (num-- > 0) {
    
    
             synchronized (object) {
    
    
                 //先判断通知的是否是当前线程
                 while (!(threadId == object.getIndex())) {
    
    
                     //循环判断是否当前的线程执行,执行条件:
                     // 当先线程threadID和即将执行的线程DIYObject.getIndex()相同
                     try {
    
    
                         object.wait();
                     } catch (InterruptedException e) {
    
    
                         e.printStackTrace();
                     }
                 }
                 //执行
                 System.out.print(name[threadId]+" ");

                 //执行下一个执行的线程ID
                 object.setIndex((threadId+1)%3);

                 //通过之其他的两个线程
                 object.notifyAll();
             }
         }
    }

    public static void main(String[] args) {
    
    
        DIYObject diyObject = new DIYObject();
        new ABCThread(diyObject,0).start();
        new ABCThread(diyObject,1).start();
        new ABCThread(diyObject,2).start();
        //启动三个线程

    }
}

11、生产者/消费者模型

生产者、消费者问题也叫做有界缓冲区问题,两个线程共享一个公共的固定的缓冲区
其中一个是生产者,用于将生产的消息放入缓冲区,另一个称之为消费者,从缓冲区例来取出消息
问题出现在当缓冲区满了,此时生产者是无法继续往缓冲区中放数据,其解决方案是让生产者进入休眠,等待消费者从缓冲区取出一个或者多个数据后在唤醒他
同样的,当缓冲区空了,而消费者是无法继续从缓冲区取出数据,此时让消费者进行休眠,等待生产者生产一个或者多个数据之后在唤醒消费者

在这里插入图片描述
具体说明:

1、生产者生产数据放到缓冲区,消费者从缓冲区获取数据
2、如果缓冲区满了,则生产者线程阻塞
3、如果缓冲区为空,则消费者线程阻塞

代码实现上,考虑的是线程间通信的问题

生产者、消费者都是单独的线程,缓冲区是线程的共享空间,且该空间的访问是需要同步操作的,当生产者线程生产了数据放入缓冲区需要通知消费者线程可以消费了,当消费者线程从缓冲区取出数据后需要通知生产者继续生产

/**
 * 生产者
 */
public class ProducerDemo extends Thread {
    
    
    private LinkedList<Integer>  C;
    private Random random = new Random();
    public ProducerDemo(LinkedList<Integer> c) {
    
    
        this.C = c;
    }

    @Override
    public void run() {
    
    

        while (true) {
    
    
            synchronized (C) {
    
    
                //判断仓库是否满了,满了则阻塞
                while (C.size() == 2) {
    
    
                    try {
    
    
                        System.out.println("仓库已满,生产者线程阻塞");
                        C.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

                int count = random.nextInt(1000);
                System.out.println("生产者线程往仓库放数据:"+count);
                try {
    
    
                    Thread.sleep(count);
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }

                //将生产数据放入仓库
                C.addLast(count);

                //通知消费者
                C.notifyAll();
            }
        }
    }
}

/**
 * 消费者
 */
public class ConsumerDemo extends Thread{
    
    
    private LinkedList<Integer>  C;
    private Random random = new Random();

    public ConsumerDemo(LinkedList<Integer> c){
    
    
        this.C = c;
    }

    @Override
    public void run() {
    
    
        while (true) {
    
    
            synchronized (C) {
    
    
                //判断仓库是否为空,空时则阻塞
                while (C.size() == 0) {
    
    
                    System.out.println("仓库已空,消费者线程则塞");
                    try {
    
    
                        C.wait();
                    } catch (InterruptedException e) {
    
    
                        e.printStackTrace();
                    }
                }

                //从仓库中获取数据
                Integer count = C.removeFirst();
                System.out.println("消费者线程从仓库消费:"+count);

                //通知生产者线程生产数据
                C.notifyAll();

                try {
    
    
                    Thread.sleep(random.nextInt(1000));
                } catch (InterruptedException e) {
    
    
                    e.printStackTrace();
                }
            }
        }
    }
}

 
参考文献:https://www.yuque.com/docs/share/11d308f6-7bec-45dc-9564-677b23387e69?#

猜你喜欢

转载自blog.csdn.net/Super_Powerbank/article/details/109465550