Java进阶 —— Java多线程编程笔记

news2024/11/25 2:39:16

❤ 作者主页:欢迎来到我的技术博客😎
❀ 个人介绍:大家好,本人热衷于Java后端开发,欢迎来交流学习哦!( ̄▽ ̄)~*
🍊 如果文章对您有帮助,记得关注点赞收藏评论⭐️⭐️⭐️
📣 您的支持将是我创作的动力,让我们一起加油进步吧!!!🎉🎉

1、什么是多线程?

1.1 简单了解多线程【理解】

是指从软件或者硬件上实现多个线程并发执行的技术。
具有多线程能力的计算机因有硬件支持而能够在同一时间执行多个线程,提升性能。


1.2 并发和并行【理解】

  • 并行:在同一时刻,有多个指令在多个CPU上 同时 执行。

  • 并发:在同一时刻,有多个指令在单个CPU上交替执行。


1.3 进程和线程【理解】

  • 进程:是正在运行的程序

    独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
    动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的
    并发性:任何进程都可以同其他进程一起并发执行

  • 线程:是进程中的单个顺序控制流,是一条执行路径

    ​ 单线程:一个进程如果只有一条执行路径,则称为单线程程序

    ​ 多线程:一个进程如果有多条执行路径,则称为多线程程序


2、多线程的实现方式

2.1 实现多线程方式一:继承Thread类【应用】

  • 方法介绍

    方法名说明
    void run()在线程开启后,此方法将被调用执行
    void start()使此线程开始执行,Java虚拟机会调用run方法()
  • 实现步骤

    • 定义一个类MyThread继承Thread类
    • 在MyThread类中重写run()方法
    • 创建MyThread类的对象
    • 启动线程
  • 代码演示

    public class MyThread extends Thread {
        @Override
        public void run() {
            for(int i = 0; i < 100; i ++) {
                System.out.println(getName() + i);
            }
        }
    }
    public class MyThreadDemo {
        public static void main(String[] args) {
            MyThread my1 = new MyThread();
            MyThread my2 = new MyThread();
    
    //        my1.run();
    //        my2.run();
            
            my1.setName("线程1");
            my2.setName("线程2");
    
            //void start() 导致此线程开始执行; Java虚拟机调用此线程的run方法
            my1.start();
            my2.start();
        }
    }
    
  • 两个小问题

    • 为什么要重写run()方法?

      因为run()是用来封装被线程执行的代码

    • run()方法和start()方法的区别?

      run():封装线程执行的代码,直接调用,相当于普通方法的调用

      start():启动线程,然后由JVM调用此线程的run()方法


2.2 实现多线程方式二:实现Runnable接口【应用】

  • Thread构造方法

    方法名说明
    Thread(Runnable target)分配一个新的Thread对象
    Thread(Runnable target, String name)分配一个新的Thread对象
  • 实现步骤

    • 定义一个类MyRunnable实现Runnable接口
    • 在MyRunnable类中重写run()方法
    • 创建MyRunnable类的对象
    • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数
    • 启动线程
  • 代码演示

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for(int i=0; i<100; i++) {
                System.out.println(Thread.currentThread().getName()+":"+i);
            }
        }
    }
    
    public class MyRunnableDemo {
        public static void main(String[] args) {
            //创建MyRunnable类的对象
          MyRunnable my = new MyRunnable();
    
            //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
            //Thread(Runnable target)
    //        Thread t1 = new Thread(my);
    //        Thread t2 = new Thread(my);
            //Thread(Runnable target, String name)
            Thread t1 = new Thread(my,"坦克");
          Thread t2 = new Thread(my,"飞机");
    
            //启动线程
            t1.start();
            t2.start();
        }
    }
    

2.3 实现多线程方式三: 实现Callable接口【应用】

  • 方法介绍

    方法名说明
    V call()计算结果,如果无法计算结果,则抛出一个异常
    FutureTask(Callable callable)创建一个 FutureTask,一旦运行就执行给定的 Callable
    V get()如有必要,等待计算完成,然后获取其结果
  • 实现步骤

    • 定义一个类MyCallable实现Callable接口
    • 在MyCallable类中重写call()方法
    • 创建MyCallable类的对象
    • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数
    • 创建Thread类的对象,把FutureTask对象作为构造方法的参数
    • 启动线程
    • 再调用get方法,就可以获取线程结束之后的结果。
  • 代码演示

    public class MyCallable implements Callable<Integer> {
        @Override
        public Integer call() throws Exception {
            int sum = 0;
            for (int i = 1; i <= 100; i++) {
                sum += i;
            }
            return sum;
        }
    }
    
    public class MyCallableDemo {
    
        public static void main(String[] args) throws  Exception{
            //创建MyCallable的对象(表示多线程要执行的任务)
            MyCallable mc = new MyCallable();
    
            //创建FutureTask的对象(作用管理多线程运行的结果)
            FutureTask<Integer> ft = new FutureTask<>(mc);
    
            //创建线程的对象
            Thread t1 = new Thread(ft);
    
            //启动线程
            t1.start();
    
            //获取多线程运行的结果
            Integer result = ft.get();
            System.out.println(result);
        }
    
    }
    
  • 三种实现方式的对比

    • 实现Runnable、Callable接口
      • 好处: 扩展性强,实现该接口的同时还可以继承其他的类
      • 缺点: 编程相对复杂,不能直接使用Thread类中的方法
    • 继承Thread类
      • 好处: 编程比较简单,可以直接使用Thread类中的方法
      • 缺点: 可以扩展性较差,不能再继承其他的类

3、常见的成员方法

3.1 设置和获取线程名称【应用】

  • 方法介绍

    方法名说明
    void setName(String name)将此线程的名称更改为等于参数name
    String getName()返回此线程的名称
    Thread currentThread()返回对当前正在执行的线程对象的引用
  • 代码演示

    public class MyThread extends Thread {
    
        public MyThread() {
        }
    
        public MyThread(String name) {
            super(name);
        }
    
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + ":" + i);
            }
        }
    }
    
    public class ThreadDemo {
        public static void main(String[] args) throws InterruptedException {
           /*
                String getName()                    返回此线程的名称
                void setName(String name)           设置线程的名字(构造方法也可以设置名字)
                细节:
                    1、如果我们没有给线程设置名字,线程也是有默认的名字的
                            格式:Thread-X(X序号,从0开始的)
                    2、如果我们要给线程设置名字,可以用set方法进行设置,也可以构造方法设置
    
                static Thread currentThread()       获取当前线程的对象
                细节:
                    当JVM虚拟机启动之后,会自动的启动多条线程
                    其中有一条线程就叫做main线程
                    他的作用就是去调用main方法,并执行里面的代码
                    在以前,我们写的所有的代码,其实都是运行在main线程当中
           */
    
    
            //1.创建线程的对象
            MyThread t1 = new MyThread("飞机");
            MyThread t2 = new MyThread("坦克");
    
    
            //2.开启线程
            t1.start();
            t2.start();
    
            //返回当前正在执行的对象的引用
            System.out.println(Thread.currentThread().getName());
        }
    }
    

3.2 线程休眠【应用】

  • 相关方法

    方法名说明
    static void sleep(long millis)使当前正在执行的线程停留(暂停执行)指定的毫秒数
  • 代码演示

    public class MyRunnable implements Runnable {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
    
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
    
    public class Demo {
        public static void main(String[] args) throws InterruptedException {
            /*System.out.println("睡觉前");
            Thread.sleep(3000);
            System.out.println("睡醒了");*/
    
            MyRunnable mr = new MyRunnable();
    
            Thread t1 = new Thread(mr);
            Thread t2 = new Thread(mr);
    
            t1.start();
            t2.start();
        }
    }
    

3.3 线程优先级【应用】

  • 线程调度

    • 两种调度方式

      • 分时调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片
      • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,优先级高的线程获取的 CPU 时间片相对多一些
    • Java使用的是抢占式调度模型

    • 随机性

      假如计算机只有一个 CPU,那么 CPU 在某一个时刻只能执行一条指令,线程只有得到CPU时间片,也就是使用权,才可以执行指令。所以说多线程程序的执行是有随机性,因为谁抢到CPU的使用权是不一定的。

  • 优先级相关方法

    方法名说明
    final int getPriority()返回此线程的优先级
    final void setPriority(int newPriority)更改此线程的优先级线程默认优先级是5;线程优先级的范围是:1-10
  • 代码演示

    public class MyRunnable implements Runnable{
    
        @Override
        public void run() {
            for (int i = 1; i <= 100; i++) {
                System.out.println(Thread.currentThread().getName() + "---" + i);
            }
        }
    }
    
        public static void main(String[] args){
           /*
                setPriority(int newPriority)        设置线程的优先级
                final int getPriority()             获取线程的优先级
           */
    
            //创建线程要执行的参数对象
            MyRunnable mr = new MyRunnable();
            //创建线程对象
            Thread t1 = new Thread(mr,"飞机");
            Thread t2 = new Thread(mr,"坦克");
    
            //System.out.println(t1.getPriority()); //5,优先级默认为5
           //System.out.println(t1.getPriority());  //5,优先级默认为5
    
            t1.setPriority(1);
            t2.setPriority(10);
    
            t1.start();
            t2.start();
    
        }
    }
    

3.4 守护线程【应用】

  • 相关方法

    方法名说明
    void setDaemon(boolean on)将此线程标记为守护线程,当运行的线程都是守护线程时,Java虚拟机将退出
  • 代码演示

    public class MyThread1 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 10; i++) {
                System.out.println(getName() + "---" + i);
            }
        }
    }
    
    public class MyThread2 extends Thread {
        @Override
        public void run() {
            for (int i = 0; i < 100; i++) {
                System.out.println(getName() + "---" + i);
            }
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            MyThread1 t1 = new MyThread1();
            MyThread2 t2 = new MyThread2();
    
            t1.setName("女神");
            t2.setName("备胎");
    
            //把第二个线程设置为守护线程
            //当普通线程执行完之后,那么守护线程也没有继续运行下去的必要了.
            t2.setDaemon(true);
    
            t1.start();
            t2.start();
        }
    }
    

3.5 出让线程/礼让线程【应用】

  • 相关方法

    方法名说明
    public static void yield()出让线程/礼让线程
  • 代码演示

    public class MyThread extends Thread{
    
        @Override
        public void run() {//"飞机"  "坦克"
            for (int i = 1; i <= 100; i++) {
    
                System.out.println(getName() + "@" + i);
                //表示出让当前CPU的执行权
                Thread.yield();
            }
        }
    }
    
    public class ThreadDemo {
        public static void main(String[] args) {
           /*
                public static void yield()      出让线程/礼让线程
    
           */
    
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
    
            t1.setName("飞机");
            t2.setName("坦克");
    
            t1.start();
            t2.start();
    
    
        }
    }
    

3.6 插入线程/插队线程

  • 相关方法

    方法名说明
    public finalvoid join()插入线程/插队线程
  • 代码演示

public class MyThread extends Thread{

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

        }
    }
}

public class ThreadDemo {
    public static void main(String[] args) throws InterruptedException {
       /*
            public final void join()  插入线程/插队线程
       */



        MyThread t = new MyThread();
        t.setName("土豆");
        t.start();

        //表示把t这个线程,插入到当前线程之前。
        //t:土豆
        //当前线程: main线程
        t.join();

        //执行在main线程当中的
        for (int i = 0; i < 10; i++) {
            System.out.println("main线程" + i);
        }


    }
}

4、 线程的生命周期

在这里插入图片描述


5、线程安全的问题

5.1 卖票【应用】

  • 案例需求

    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

  • 代码实现

    public class MyThread extends Thread {
    
        //表示这个类的所有对象,都共享ticket数据
        static int ticket = 0;
    
    
        @Override
        public void run() {
                while (true) {
                    if (ticket < 100) {
                        try {
                            Thread.sleep(10);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        ticket ++;
                        System.out.println(getName() + "在在卖第" + ticket + "张票!");
                    }
                    else {
                        break;
                    }
            }
        }
    }
    
    public class ThreadDemo {
    
        public static void main(String[] args) {
           /*
               需求:
                    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
           */
    
    
            //创建线程对象
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
    
            //起名字
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
    
            //开启线程
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    

5.2 卖票案例的问题

  • 卖票出现了问题

    • 相同的票出现了多次

    • 出现了负数的票

  • 问题产生原因

    线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题


5.3 同步代码块解决数据安全问题

  • 安全问题出现的条件

    • 是多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

  • 如何解决多线程安全问题呢?

    • 基本思想:让程序没有安全问题的环境
  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

    synchronized(任意对象) { 
    	多条语句操作共享数据的代码 
    }
    

    synchronized(任意对象):就相当于给代码加锁了,任意对象就可以看成是一把锁

  • 同步的好处和弊端

    • 好处:解决了多线程的数据安全问题

    • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

  • 代码演示

    public class MyThread extends Thread {
    
        //表示这个类的所有对象,都共享ticket数据
        static int ticket = 0;
    
    
        @Override
        public void run() {
                while (true) {
                    //同步吧代码块
                   synchronized (MyThread.class) {
                       if (ticket < 100) {
                           try {
                               Thread.sleep(10);
                           } catch (InterruptedException e) {
                               e.printStackTrace();
                           }
                           ticket ++;
                           System.out.println(getName() + "在在卖第" + ticket + "张票!");
                       }
                       else {
                           break;
                       }
                   }
                }
        }
    }
    
    public class ThreadDemo {
    
        public static void main(String[] args) {
           /*
               需求:
                    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
           */
    
    
            //创建线程对象
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
    
            //起名字
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
    
            //开启线程
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    

5.4 同步方法解决数据安全问题

  • 同步方法的格式

    同步方法:就是把 synchronized 关键字加到方法上

    修饰符 synchronized 返回值类型 方法名(方法参数) { 
    	方法体;
    }
    

    同步方法的锁对象是什么呢?

    this

  • 静态同步方法

    同步静态方法:就是把 synchronized 关键字加到静态方法上

    修饰符 static synchronized 返回值类型 方法名(方法参数) { 
    	方法体;
    }
    

    同步静态方法的锁对象是什么呢?

    类名.class

  • 代码演示

    public class MyRunnable implements Runnable {
    
        int ticket = 0;
    
        @Override
        public void run() {
            //1.循环
            while (true) {
                //2.同步代码块(同步方法)
                if (method()) break;
            }
        }
    
        //同步方法
        //this
        private synchronized boolean method() {
            //3.判断共享数据是否到了末尾,如果到了末尾
            if (ticket == 100) {
                return true;
            } else {
                //4.判断共享数据是否到了末尾,如果没有到末尾
                try {
                    Thread.sleep(10);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                ticket++;
                System.out.println(Thread.currentThread().getName() + "在卖第" + ticket + "张票!!!");
            }
            return false;
        }
    }
    
    public class ThreadDemo {
    
        public static void main(String[] args) {
           /*
               需求:
                    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
                    利用同步方法完成
                    技巧:同步代码块
           */
    
            MyRunnable mr = new MyRunnable();
    
            Thread t1 = new Thread(mr, "窗口1");
            Thread t2 = new Thread(mr, "窗口2");
            Thread t3 = new Thread(mr, "窗口3");
    
            t1.start();
            t2.start();
            t3.start();
    
    
        }
    }
    

5.5 Lock锁

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

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

  • ReentrantLock构造方法

    方法名说明
    ReentrantLock()创建一个ReentrantLock的实例
  • 加锁解锁方法

    方法名说明
    void lock()获得锁
    void unlock()释放锁
  • 代码演示

    public class MyThread extends Thread{
    
        static int ticket = 0;
    
        static Lock lock = new ReentrantLock();
    
    
        @Override
        public void run() {
            //1.循环
            while(true){
                //2.同步代码块
                //synchronized (MyThread.class){
                lock.lock();
                try {
                    //3.判断
                    if(ticket == 100){
                        break;
                        //4.判断
                    }else{
                        Thread.sleep(10);
                        ticket++;
                        System.out.println(getName() + "在卖第" + ticket + "张票!!!");
                    }
                    //  }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    lock.unlock();
                }
            }
        }
    }
    
    public class ThreadDemo {
        public static void main(String[] args) {
           /*
               需求:
                    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票
                    用JDK5的lock实现
           */
    
    
            MyThread t1 = new MyThread();
            MyThread t2 = new MyThread();
            MyThread t3 = new MyThread();
    
            t1.setName("窗口1");
            t2.setName("窗口2");
            t3.setName("窗口3");
    
            t1.start();
            t2.start();
            t3.start();
    
        }
    }
    

5.6 死锁

  • 概述

    线程死锁是指由于两个或者多个线程互相持有对方所需要的资源,导致这些线程处于等待状态,无法前往执行。

  • 什么情况下会产生死锁

    1. 资源有限
    2. 同步嵌套
  • 代码演示

    public class Demo {
        public static void main(String[] args) {
            Object objA = new Object();
            Object objB = new Object();
    
            new Thread(()->{
                while(true){
                    synchronized (objA){
                        //线程一
                        synchronized (objB){
                            System.out.println("小康同学正在走路");
                        }
                    }
                }
            }).start();
    
            new Thread(()->{
                while(true){
                    synchronized (objB){
                        //线程二
                        synchronized (objA){
                            System.out.println("小薇同学正在走路");
                        }
                    }
                }
            }).start();
        }
    }
    

6、生产者和消费者(等待唤醒机制)

6.1 生产者和消费者模式概述

  • 概述

    生产者消费者模式是一个十分经典的多线程协作的模式,弄懂生产者消费者问题能够让我们对多线程编程的理解更加深刻。

    所谓生产者消费者问题,实际上主要是包含了两类线程:

    ​ 一类是生产者线程用于生产数据

    ​ 一类是消费者线程用于消费数据

    为了解耦生产者和消费者的关系,通常会采用共享的数据区域,就像是一个仓库。

    生产者生产数据之后直接放置在共享数据区中,并不需要关心消费者的行为。

    消费者只需要从共享数据区中去获取数据,并不需要关心生产者的行为。

  • Object类的等待和唤醒方法

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

6.2 生产者和消费者案例

  • 案例需求

    • 桌子类(Desk):定义表示包子数量的变量,定义锁对象变量,定义标记桌子上有无包子的变量

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果有包子,就进入等待状态,如果没有包子,继续执行,生产包子

      3.生产包子之后,更新桌子上包子状态,唤醒消费者消费包子

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.判断是否有包子,决定当前线程是否执行

      2.如果没有包子,就进入等待状态,如果有包子,就消费包子

      3.消费包子后,更新桌子上包子状态,唤醒生产者生产包子

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

      创建生产者线程和消费者线程对象

      分别开启两个线程

  • 代码实现

    public class Desk {
    
        //定义一个标记
        //true 就表示桌子上有汉堡包的,此时允许吃货执行
        //false 就表示桌子上没有汉堡包的,此时允许厨师执行
        public static boolean flag = false;
    
        //汉堡包的总数量
        public static int count = 10;
    
        //锁对象
        public static final Object lock = new Object();
    }
    
    public class Cooker extends Thread {
    //    生产者步骤:
    //            1,判断桌子上是否有汉堡包
    //    如果有就等待,如果没有才生产。
    //            2,把汉堡包放在桌子上。
    //            3,叫醒等待的消费者开吃。
        @Override
        public void run() {
            while(true){
                synchronized (Desk.lock){
                    if(Desk.count == 0){
                        break;
                    }else{
                        if(!Desk.flag){
                            //生产
                            System.out.println("厨师正在生产汉堡包");
                            Desk.flag = true;
                            Desk.lock.notifyAll();
                        }else{
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        @Override
        public void run() {
    //        1,判断桌子上是否有汉堡包。
    //        2,如果没有就等待。
    //        3,如果有就开吃
    //        4,吃完之后,桌子上的汉堡包就没有了
    //                叫醒等待的生产者继续生产
    //        汉堡包的总数量减一
    
            //套路:
                //1. while(true)死循环
                //2. synchronized 锁,锁对象要唯一
                //3. 判断,共享数据是否结束. 结束
                //4. 判断,共享数据是否结束. 没有结束
            while(true){
                synchronized (Desk.lock){
                    if(Desk.count == 0){
                        break;
                    }else{
                        if(Desk.flag){
                            //有
                            System.out.println("吃货在吃汉堡包");
                            Desk.flag = false;
                            Desk.lock.notifyAll();
                            Desk.count--;
                        }else{
                            //没有就等待
                            //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                            try {
                                Desk.lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            /*消费者步骤:
            1,判断桌子上是否有汉堡包。
            2,如果没有就等待。
            3,如果有就开吃
            4,吃完之后,桌子上的汉堡包就没有了
                    叫醒等待的生产者继续生产
            汉堡包的总数量减一*/
    
            /*生产者步骤:
            1,判断桌子上是否有汉堡包
            如果有就等待,如果没有才生产。
            2,把汉堡包放在桌子上。
            3,叫醒等待的消费者开吃。*/
    
            Foodie f = new Foodie();
            Cooker c = new Cooker();
    
            f.start();
            c.start();
    
        }
    }
    

6.3 生产者和消费者案例优化

  • 需求

    • 将Desk类中的变量,采用面向对象的方式封装起来
    • 生产者和消费者类中构造方法接收Desk类对象,之后在run方法中进行使用
    • 创建生产者和消费者线程对象,构造方法中传入Desk类对象
    • 开启两个线程
  • 代码实现

    public class Desk {
    
        //定义一个标记
        //true 就表示桌子上有汉堡包的,此时允许吃货执行
        //false 就表示桌子上没有汉堡包的,此时允许厨师执行
        //public static boolean flag = false;
        private boolean flag;
    
        //汉堡包的总数量
        //public static int count = 10;
        //以后我们在使用这种必须有默认值的变量
       // private int count = 10;
        private int count;
    
        //锁对象
        //public static final Object lock = new Object();
        private final Object lock = new Object();
    
        public Desk() {
            this(false,10); // 在空参内部调用带参,对成员变量进行赋值,之后就可以直接使用成员变量了
        }
    
        public Desk(boolean flag, int count) {
            this.flag = flag;
            this.count = count;
        }
    
        public boolean isFlag() {
            return flag;
        }
    
        public void setFlag(boolean flag) {
            this.flag = flag;
        }
    
        public int getCount() {
            return count;
        }
    
        public void setCount(int count) {
            this.count = count;
        }
    
        public Object getLock() {
            return lock;
        }
    
        @Override
        public String toString() {
            return "Desk{" +
                    "flag=" + flag +
                    ", count=" + count +
                    ", lock=" + lock +
                    '}';
        }
    }
    
    public class Cooker extends Thread {
    
        private Desk desk;
    
        public Cooker(Desk desk) {
            this.desk = desk;
        }
    //    生产者步骤:
    //            1,判断桌子上是否有汉堡包
    //    如果有就等待,如果没有才生产。
    //            2,把汉堡包放在桌子上。
    //            3,叫醒等待的消费者开吃。
    
        @Override
        public void run() {
            while(true){
                synchronized (desk.getLock()){
                    if(desk.getCount() == 0){
                        break;
                    }else{
                        //System.out.println("验证一下是否执行了");
                        if(!desk.isFlag()){
                            //生产
                            System.out.println("厨师正在生产汉堡包");
                            desk.setFlag(true);
                            desk.getLock().notifyAll();
                        }else{
                            try {
                                desk.getLock().wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        private Desk desk;
    
        public Foodie(Desk desk) {
            this.desk = desk;
        }
    
        @Override
        public void run() {
    //        1,判断桌子上是否有汉堡包。
    //        2,如果没有就等待。
    //        3,如果有就开吃
    //        4,吃完之后,桌子上的汉堡包就没有了
    //                叫醒等待的生产者继续生产
    //        汉堡包的总数量减一
    
            //套路:
                //1. while(true)死循环
                //2. synchronized 锁,锁对象要唯一
                //3. 判断,共享数据是否结束. 结束
                //4. 判断,共享数据是否结束. 没有结束
            while(true){
                synchronized (desk.getLock()){
                    if(desk.getCount() == 0){
                        break;
                    }else{
                        //System.out.println("验证一下是否执行了");
                        if(desk.isFlag()){
                            //有
                            System.out.println("吃货在吃汉堡包");
                            desk.setFlag(false);
                            desk.getLock().notifyAll();
                            desk.setCount(desk.getCount() - 1);
                        }else{
                            //没有就等待
                            //使用什么对象当做锁,那么就必须用这个对象去调用等待和唤醒的方法.
                            try {
                                desk.getLock().wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                    }
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            /*消费者步骤:
            1,判断桌子上是否有汉堡包。
            2,如果没有就等待。
            3,如果有就开吃
            4,吃完之后,桌子上的汉堡包就没有了
                    叫醒等待的生产者继续生产
            汉堡包的总数量减一*/
    
            /*生产者步骤:
            1,判断桌子上是否有汉堡包
            如果有就等待,如果没有才生产。
            2,把汉堡包放在桌子上。
            3,叫醒等待的消费者开吃。*/
    
            Desk desk = new Desk();
    
            Foodie f = new Foodie(desk);
            Cooker c = new Cooker(desk);
    
            f.start();
            c.start();
    
        }
    }
    

6.4 阻塞队列的基本使用

  • 阻塞队列继承结构

    [外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MDfvef2S-1686383246650)(.\img\06_阻塞队列继承结构.png)]

  • 常见BlockingQueue:

    ArrayBlockingQueue: 底层是数组,有界

    LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

  • BlockingQueue的核心方法:

    put(anObject): 将参数放入队列,如果放不进去会阻塞

    take(): 取出第一个数据,取不到会阻塞

  • 代码示例

    public class Demo02 {
        public static void main(String[] args) throws Exception {
            // 创建阻塞队列的对象,容量为 1
            ArrayBlockingQueue<String> arrayBlockingQueue = new ArrayBlockingQueue<>(1);
    
            // 存储元素
            arrayBlockingQueue.put("汉堡包");
    
            // 取元素
            System.out.println(arrayBlockingQueue.take());
            System.out.println(arrayBlockingQueue.take()); // 取不到会阻塞
    
            System.out.println("程序结束了");
        }
    }
    

6.5 阻塞队列实现等待唤醒机制

  • 案例需求

    • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环向阻塞队列中添加包子

      3.打印添加结果

    • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

      1.构造方法中接收一个阻塞队列对象

      2.在run方法中循环获取阻塞队列中的包子

      3.打印获取结果

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

      创建阻塞队列对象

      创建生产者线程和消费者线程对象,构造方法中传入阻塞队列对象

      分别开启两个线程

  • 代码实现

    public class Cooker extends Thread {
    
        private ArrayBlockingQueue<String> bd;
    
        public Cooker(ArrayBlockingQueue<String> bd) {
            this.bd = bd;
        }
    //    生产者步骤:
    //            1,判断桌子上是否有汉堡包
    //    如果有就等待,如果没有才生产。
    //            2,把汉堡包放在桌子上。
    //            3,叫醒等待的消费者开吃。
    
        @Override
        public void run() {
            while (true) {
                try {
                    bd.put("汉堡包");
                    System.out.println("厨师放入一个汉堡包");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    }
    
    public class Foodie extends Thread {
        private ArrayBlockingQueue<String> bd;
    
        public Foodie(ArrayBlockingQueue<String> bd) {
            this.bd = bd;
        }
    
        @Override
        public void run() {
    //        1,判断桌子上是否有汉堡包。
    //        2,如果没有就等待。
    //        3,如果有就开吃
    //        4,吃完之后,桌子上的汉堡包就没有了
    //                叫醒等待的生产者继续生产
    //        汉堡包的总数量减一
    
            //套路:
            //1. while(true)死循环
            //2. synchronized 锁,锁对象要唯一
            //3. 判断,共享数据是否结束. 结束
            //4. 判断,共享数据是否结束. 没有结束
            while (true) {
                try {
                    String take = bd.take();
                    System.out.println("吃货将" + take + "拿出来吃了");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
    
        }
    }
    
    public class Demo {
        public static void main(String[] args) {
            ArrayBlockingQueue<String> bd = new ArrayBlockingQueue<>(1);
    
            Foodie f = new Foodie(bd);
            Cooker c = new Cooker(bd);
    
            f.start();
            c.start();
        }
    }
    

7、综合练习

7.1 多线程联系1(卖电影票)

一共有1000张电影票,可以在两个窗口领取,假设每次领取的时间为3000毫秒。
要求:请用多线程模拟卖票过程并打印剩余电影票的数量。

代码实现:

public class MyThread extends Thread {

    //第一种方式多线程,测试类中MyThread会创建多次,所以需要加static
    static int ticket = 1000;

    @Override
    public void run() {
        //1.循环
        while (true) {
            //2.同步代码块
            synchronized (MyThread.class) {
                //3.判断共享数据(已经到末尾)
                if (ticket == 0) {
                    break;
                } else {
                    //4.判断共享数据(没有到末尾)
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    ticket --;
                    System.out.println(getName() + "在卖票,还剩下" + ticket + "张票!");
                }
            }
        }
    }
}

public class Test {

    /*
          一共有1000张电影票,可以在两个窗口领取,假设每次领取的时间为3000毫秒,
          要求:请用多线程模拟卖票过程并打印剩余电影票的数量
      */
    public static void main(String[] args) {

        //创建线程对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();

        //给线程设置名字
        t1.setName("窗口1");
        t2.setName("窗口2");

        //开启线程
        t1.start();
        t2.start();

    }

}

7.2 多线程练习2(送礼品)

需求分析:

1000份礼品。两人同时发送,当剩下的礼品小于10份的时候则不再送出。
要求:利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来。

代码实现:

public class MyRunable implements Runnable {
 
    //第二种方式实现多线程,测试类中MyRunable只创建一次,所以不需要加static
    int count = 100;
 
    @Override
    public void run() {
        //1.循环
        while (true) {
            //2.同步代码块
            synchronized (MyRunable.class) {
                //3.判断共享数据(已经到末尾)
                if (count < 10) {
                    System.out.println("礼物还剩下" + count + ",不再赠送");
                    break;
                } else {
                    //4.判断共享数据(没有到末尾)
                    count --;
                    System.out.println(Thread.currentThread().getName() + "在赠送礼物,还剩下" + count + "个礼物!!!");
                }
            }
        }
    }
}

public class Test {

     /*
            有100份礼品,两人同时发送,当剩下的礼品小于10份的时候则不再送出,
            利用多线程模拟该过程并将线程的名字和礼物的剩余数量打印出来.
        */
     public static void main(String[] args) {
         //创建参数对象
         MyRunable mr = new MyRunable();

         //创建线程对象

         Thread t1 = new Thread(mr, "窗口1");
         Thread t2 = new Thread(mr, "窗口2");

         //启动线程
         t1.start();
         t2.start();
     }
}

7.3 多线程练习3(打印奇数数字)

需求分析:

同时开启两个线程,共同获取1~100之间的所有数字。
要求:将输出所有的奇数。

代码实现:

public class MyRunnable implements Runnable {

    //第二种方式实现多线程,测试类中MyRunnable只创建一次,所以不需要加static
    int number = 1;

    @Override
    public void run() {
        //1.循环
        while (true) {
            //2.同步代码块
            synchronized (MyRunnable.class) {
                //3.判断共享数据(已经到末尾)
                if (number > 100) {
                    break;
                } else {
                    //4.判断共享数据(没有到末尾)
                    if (number % 2 == 1) {
                        System.out.println(Thread.currentThread().getName() + "打印数字:" + number);
                    }
                    number ++;
                }
            }
        }
    }
}

public class Test {

    public static void main(String[] args) {
        /*
           同时开启两个线程,共同获取1-100之间的所有数字。
           要求:将输出所有的奇数。
        */


        //创建参数对象
        MyRunnable mr = new MyRunnable();

        //创建线程对象
        Thread t1 = new Thread(mr,"线程A");
        Thread t2 = new Thread(mr,"线程B");

        //启动线程
        t1.start();
        t2.start();
    }
}

7.4 多线程练习4(抢红包)

需求分析:

微信中的抢红包也用到了多线程。
            假设:100块,分成了3个包,现在有5个人去抢。
            其中,红包是共享数据。
            5个人是5条线程。
            打印结果如下:
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX没抢到
                XXX没抢到

代码实现1:

public class MyThread extends Thread{
 
    //共享数据
    //100块,分成了3个包
    static double money = 100;
    static int count = 3;
 
    //最小的中奖金额
    static final double MIN = 0.01;
 
    @Override
    public void run() {
        //同步代码块
        synchronized (MyThread.class){
            if(count == 0) {
                //判断,共享数据是否到了末尾(已经到末尾)
                System.out.println(getName() + "没有抢到红包!");
            } else {
                //判断,共享数据是否到了末尾(没有到末尾)
                //定义一个变量,表示中奖的金额
                double prize = 0;
                if(count == 1){
                    //表示此时是最后一个红包
                    //就无需随机,剩余所有的钱都是中奖金额
                    prize = money;
                } else {
                    //表示第一次,第二次(随机)
                    Random r = new Random();
                    //100 元   3个包
                    //第一个红包:99.98
                    //100 - (3-1) * 0.01
                    double bounds = money - (count - 1) * MIN;
                    prize = r.nextDouble(bounds);
                    if(prize < MIN){
                        prize = MIN;
                    }
                }
                //从money当中,去掉当前中奖的金额
                money = money - prize;
                //红包的个数-1
                count --;
                //本次红包的信息进行打印
                System.out.println(getName() + "抢到了" + prize + "元");
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            微信中的抢红包也用到了多线程。
            假设:100块,分成了3个包,现在有5个人去抢。
            其中,红包是共享数据。
            5个人是5条线程。
            打印结果如下:
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX没抢到
                XXX没抢到
        */
 
        //创建线程的对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t4 = new MyThread();
        MyThread t5 = new MyThread();
 
        //给线程设置名字
        t1.setName("小A");
        t2.setName("小B");
        t3.setName("小C");
        t4.setName("小D");
        t5.setName("小E");
 
        //启动线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
 
 
 
 
    }
}

代码实现2:

public class MyThread extends Thread{
 
    //总金额
    static BigDecimal money = BigDecimal.valueOf(100.0);
    //个数
    static int count = 3;
    //最小抽奖金额
    static final BigDecimal MIN = BigDecimal.valueOf(0.01);
 
    @Override
    public void run() {
        synchronized (MyThread.class){
            if(count == 0){
                System.out.println(getName() + "没有抢到红包!");
            }else{
                //中奖金额
                BigDecimal prize;
                if(count == 1){
                    prize = money;
                }else{
                    //获取抽奖范围
                    double bounds = money.subtract(BigDecimal.valueOf(count-1).multiply(MIN)).doubleValue();
                    Random r = new Random();
                    //抽奖金额
                    prize = BigDecimal.valueOf(r.nextDouble(bounds));
                }
                //设置抽中红包,小数点保留两位,四舍五入
                prize = prize.setScale(2,RoundingMode.HALF_UP);
                //在总金额中去掉对应的钱
                money = money.subtract(prize);
                //红包少了一个
                count--;
                //输出红包信息
                System.out.println(getName() + "抽中了" + prize + "元");
            }
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            微信中的抢红包也用到了多线程。
            假设:100块,分成了3个包,现在有5个人去抢。
            其中,红包是共享数据。
            5个人是5条线程。
            打印结果如下:
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX抢到了XXX元
                XXX没抢到
                XXX没抢到
        */
 
 
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        MyThread t3 = new MyThread();
        MyThread t4 = new MyThread();
        MyThread t5 = new MyThread();
 
        t1.setName("小A");
        t2.setName("小B");
        t3.setName("小C");
        t4.setName("小D");
        t5.setName("小E");
 
        t1.start();
        t2.start();
        t3.start();
        t4.start();
        t5.start();
 
 
    }
}

7.5 多线程练习5(抽奖箱抽奖)

需求分析:

 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
                每次抽出一个奖项就打印一个(随机)
                抽奖箱1 又产生了一个 10 元大奖
                抽奖箱1 又产生了一个 100 元大奖
                抽奖箱1 又产生了一个 200 元大奖
                抽奖箱1 又产生了一个 800 元大奖
                抽奖箱2 又产生了一个 700 元大奖

代码实现:

public class MyThread extends Thread {
 
    ArrayList<Integer> list;
 
    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }
 
    @Override
    public void run() {
        //1.循环
        //2.同步代码块
        //3.判断
        //4.判断
 
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    break;
                } else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    System.out.println(getName() + "又产生了一个" + prize + "元大奖");
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
                             每次抽出一个奖项就打印一个(随机)
                抽奖箱1 又产生了一个 10 元大奖
                抽奖箱1 又产生了一个 100 元大奖
                抽奖箱1 又产生了一个 200 元大奖
                抽奖箱1 又产生了一个 800 元大奖
                抽奖箱2 又产生了一个 700 元大奖
                .....
        */
 
 
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
 
        //创建线程
        MyThread t1 = new MyThread(list);
        MyThread t2 = new MyThread(list);
 
        //设置名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");
 
        //启动线程
        t1.start();
        t2.start();
    }
}

7.6 综合练习6(多线程统计并求最大值)

需求分析:

有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
            每次抽的过程中,不打印,抽完时一次性打印(随机)    在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
                分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
                分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835

代码实现1:

public class MyThread extends Thread {
 
    ArrayList<Integer> list;
 
    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }
 
    //线程一
    static ArrayList<Integer> list1 = new ArrayList<>();
    //线程二
    static ArrayList<Integer> list2 = new ArrayList<>();
 
    @Override
    public void run() {
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    if("抽奖箱1".equals(getName())){
                        System.out.println("抽奖箱1" + list1);
                    }else {
                        System.out.println("抽奖箱2" + list2);
                    }
                    break;
                } else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    if("抽奖箱1".equals(getName())){
                        list1.add(prize);
                    }else {
                        list2.add(prize);
                    }
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
            每次抽的过程中,不打印,抽完时一次性打印(随机)    在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
                分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
                分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
        */
 
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
 
        //创建线程
        MyThread t1 = new MyThread(list);
        MyThread t2 = new MyThread(list);
 
        //设置名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");
 
        //启动线程
        t1.start();
        t2.start();
    }
}

代码实现2:

public class MyThread extends Thread {
 
    ArrayList<Integer> list;
 
    public MyThread(ArrayList<Integer> list) {
        this.list = list;
    }
 
    @Override
    public void run() {
        ArrayList<Integer> boxList = new ArrayList<>();//1 //2
        while (true) {
            synchronized (MyThread.class) {
                if (list.size() == 0) {
                    System.out.println(getName() + boxList);
                    break;
                } else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    boxList.add(prize);
                }
            }
            try {
                Thread.sleep(10);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
 
        }
    }
}

public class Test {
    public static void main(String[] args) {
        /*
            有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为“抽奖箱1”,“抽奖箱2”
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
            每次抽的过程中,不打印,抽完时一次性打印(随机)    在此次抽奖过程中,抽奖箱1总共产生了6个奖项。
                分别为:10,20,100,500,2,300最高奖项为300元,总计额为932元
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项。
                分别为:5,50,200,800,80,700最高奖项为800元,总计额为1835元
        */
 
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
 
        //创建线程
        MyThread t1 = new MyThread(list);
        MyThread t2 = new MyThread(list);
 
 
        //设置名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");
 
 
        //启动线程
        t1.start();
        t2.start();
 
    }
}

7.7 综合练习7(多线程之间的比较)

需求分析:

 有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为    "抽奖箱1", "抽奖箱2"
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
 
            在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,20,100,500,2,300
                最高奖项为300元,总计额为932元
 
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:5,50,200,800,80,700
                最高奖项为800元,总计额为1835元
 
            在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
            核心逻辑:获取线程抽奖的最大值(看成是线程运行的结果)
 
 
            以上打印效果只是数据模拟,实际代码运行的效果会有差异

代码实现:

public class MyCallable implements Callable<Integer> {
 
    ArrayList<Integer> list;
 
    public MyCallable(ArrayList<Integer> list) {
        this.list = list;
    }
 
    @Override
    public Integer call() throws Exception {
        ArrayList<Integer> boxList = new ArrayList<>();//1 //2
        while (true) {
            synchronized (MyCallable.class) {
                if (list.size() == 0) {
                    System.out.println(Thread.currentThread().getName() + boxList);
                    break;
                } else {
                    //继续抽奖
                    Collections.shuffle(list);
                    int prize = list.remove(0);
                    boxList.add(prize);
                }
            }
            Thread.sleep(10);
        }
        //把集合中的最大值返回
        if(boxList.size() == 0){
            return null;
        }else{
            return Collections.max(boxList);
        }
   

public class Test {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        /*
            有一个抽奖池,该抽奖池中存放了奖励的金额,该抽奖池中的奖项为 {10,5,20,50,100,200,500,800,2,80,300,700};
            创建两个抽奖箱(线程)设置线程名称分别为    "抽奖箱1", "抽奖箱2"
            随机从抽奖池中获取奖项元素并打印在控制台上,格式如下:
 
            在此次抽奖过程中,抽奖箱1总共产生了6个奖项,分别为:10,20,100,500,2,300
                最高奖项为300元,总计额为932元
 
            在此次抽奖过程中,抽奖箱2总共产生了6个奖项,分别为:5,50,200,800,80,700
                最高奖项为800元,总计额为1835元
 
            在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
            核心逻辑:获取线程抽奖的最大值(看成是线程运行的结果)
 
 
            以上打印效果只是数据模拟,实际代码运行的效果会有差异
        */
 
        //创建奖池
        ArrayList<Integer> list = new ArrayList<>();
        Collections.addAll(list,10,5,20,50,100,200,500,800,2,80,300,700);
 
        //创建多线程要运行的参数对象
        MyCallable mc = new MyCallable(list);
 
        //创建多线程运行结果的管理者对象
        //线程一
        FutureTask<Integer> ft1 = new FutureTask<>(mc);
        //线程二
        FutureTask<Integer> ft2 = new FutureTask<>(mc);
 
        //创建线程对象
        Thread t1 = new Thread(ft1);
        Thread t2 = new Thread(ft2);
 
        //设置名字
        t1.setName("抽奖箱1");
        t2.setName("抽奖箱2");
 
        //开启线程
        t1.start();
        t2.start();
 
 
        Integer max1 = ft1.get();
        Integer max2 = ft2.get();
 
        System.out.println(max1);
        System.out.println(max2);
 
        //在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为800元
        if(max1 == null){
            System.out.println("在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为"+max2+"元");
        }else if(max2 == null){
            System.out.println("在此次抽奖过程中,抽奖箱1中产生了最大奖项,该奖项金额为"+max1+"元");
        }else if(max1 > max2){
            System.out.println("在此次抽奖过程中,抽奖箱1中产生了最大奖项,该奖项金额为"+max1+"元");
        }else if(max1 < max2){
            System.out.println("在此次抽奖过程中,抽奖箱2中产生了最大奖项,该奖项金额为"+max2+"元");
        }else{
            System.out.println("两者的最大奖项是一样的");
        }
 
 
    }
}

8、线程池

8.1 使用线程池的优点

  1. 减少资源的消耗。重复利用已经创建的线程,避免频繁的创造和销毁线程,减少消耗。

  2. 提高响应速度。当执行任务时,不需要去创建线程再来执行,只要调动现有的线程来执行即可。

  3. 提高了线程的管理性。线程是稀缺资源,使用线程池可以进行统一的分配、调优和监控。


8.2 创建线程池的两种方案

  1. 通过Executors创建,它提供了四种线程池:

    newCachedThreadPool创建一个可缓存线程池,如果线程池长度超过处理需要,可灵活回收空闲线程,若无可回收,则新建线程。

    newFixedThreadPool 创建一个定长线程池,可控制线程最大并发数,超出的线程会在队列中等待。

    newScheduledThreadPool 创建一个定长线程池,支持定时及周期性任务执行。

    newSingleThreadExecutor 创建一个单线程化的线程池,它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO, LIFO, 优先级)执行。

    注意:不建议使用这种方法创建线程池。因为newFixedThreadPool 和newSingleThreadExecutor允许的最大请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM。newCachedThreadPoo和newScheduledThreadPool允许的创建线程的最大数量为Integer.MAX_VALUE,,从而导致OOM。


  1. 通过ThreadPoolExecutor创建,new ThreadPoolExecutor有7个核心参数,分别如下:
    ThreadPoolExecutor(int corePoolSize, //核心线程池大小,始终存在
                   int maximumPoolSize, //最大线程数
                    long keepAliveTime, //空闲线程等待时间,超时则销毁
                         TimeUnit unit, //时间单位
     BlockingQueue<Runnable> workQueue, //等待阻塞队列
           ThreadFactory threadFactory, //线程工厂
       RejectedExecutionHandler handler) //线程拒绝策略
    
    
    其最后一个参数拒绝策略有四种:
    new ThreadPoolExecutor.AbortPolicy():达到最大承载量,不再处理,并且抛出异常
    new ThreadPoolExecutor.CallerRunsPolicy():达到最大承载量,从哪来的去哪里
    new ThreadPoolExecutor.DiscardPolicy():达到最大承载量,丢掉任务,但不抛出异常
    new ThreadPoolExecutor.DiscardOldestPolicy():达到最大承载量,尝试与最早执行的线程去竞争,不抛出异常
    
    public class Demo02 {
        public static void main(String[] args) {
            //new ThreadPoolExecutor.AbortPolicy():达到最大承载量,不再处理,并且抛出异常
            //new ThreadPoolExecutor.CallerRunsPolicy():达到最大承载量,从哪来的去哪里
            //new ThreadPoolExecutor.DiscardPolicy():达到最大承载量,丢掉任务,但不抛出异常
            //new ThreadPoolExecutor.DiscardOldestPolicy():达到最大承载量,尝试与最早执行的线程去竞争,不抛出异常
    
            //最大线程池大小该如何定义
            //1.cpu密集型,逻辑处理器个数
            //2.io密集型 > 判断程序十分耗IO的线程,最大线程池大小应该比这个大
            int maxPools= Runtime.getRuntime().availableProcessors();
            System.out.println(maxPools);
    
            ThreadPoolExecutor threadPool = new ThreadPoolExecutor(
                    2,
                    maxPools,
                    3,
                    TimeUnit.SECONDS,
                    new ArrayBlockingQueue<>(3),
                    Executors.defaultThreadFactory(),
                    new ThreadPoolExecutor.DiscardOldestPolicy()
            );
    
            try {
                for (int i = 0; i < 10; i++) {
                    //使用线程池来创建线程
                    //最大承载:maximumPoolSize+workQueue,超过执行拒绝策略
                    threadPool.execute(()->{
                        System.out.println(Thread.currentThread().getName()+" ok");
                    });
                }
            } catch (Exception e) {
                e.printStackTrace();
            } finally {
                threadPool.shutdown(); //线程池使用完毕后需要关闭
            }
    
        }
    }
    
    

 
非常感谢您阅读到这里,如果这篇文章对您有帮助,希望能留下您的点赞👍 关注💖 分享👥 留言💬thanks!!!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/644386.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

【头歌-Python】9.3 中英文词云绘制(project) 第1~3关

第1关&#xff1a;词云练习1 任务描述 本关任务&#xff1a;编写一个能制作词云的小程序。 相关知识 词云 词云&#xff0c;也叫文字云&#xff0c;是一种应用广泛的数据可视化方法。是过滤掉文本中大量的低频信息&#xff0c;形成“关键词云层”或“关键词渲染”&#xf…

基于VMWare组件安装Centos7.9

1.前提条件 使用VMware进行安装&#xff0c;VMware可以自行下载&#xff0c;需要介质(VMware和CentOS7.9)的同仁&#xff0c;请留言&#xff0c;我给你下载链接。 2.CentOS7.9安装 1.打开VMware&#xff0c;点击“新建虚拟机(N)...” 2.选择“典型” &#xff0c;点击“下一步…

基础知识学习---牛客网C++面试宝典(六)操作系统--第一节

1、本栏用来记录社招找工作过程中的内容&#xff0c;包括基础知识学习以及面试问题的记录等&#xff0c;以便于后续个人回顾学习&#xff1b; 暂时只有2023年3月份&#xff0c;第一次社招找工作的过程&#xff1b; 2、个人经历&#xff1a; 研究生期间课题是SLAM在无人机上的应…

A100 GPU服务器安装GPU驱动教程

大家好,我是爱编程的喵喵。双985硕士毕业,现担任全栈工程师一职,热衷于将数据思维应用到工作与生活中。从事机器学习以及相关的前后端开发工作。曾在阿里云、科大讯飞、CCF等比赛获得多次Top名次。现为CSDN博客专家、人工智能领域优质创作者。喜欢通过博客创作的方式对所学的…

【OpenCV DNN】Flask 视频监控目标检测教程 06

欢迎关注『OpenCV DNN Youcans』系列&#xff0c;持续更新中 【OpenCV DNN】Flask 视频监控目标检测教程 06 3.6 OpenCVFlask实时监控和视频播放cvFlask06 项目的文件树cvFlask06 项目的程序文件cvFlask06 项目的网页模板cvFlask06 项目的运行 本系列从零开始&#xff0c;详细…

chatgpt赋能python:Python排序算法实现及其应用

Python排序算法实现及其应用 排序是计算机科学中最基础也是最常用的算法之一。在数据分析、数据挖掘和机器学习等领域&#xff0c;排序算法有着广泛的应用。Python作为一种流行的编程语言&#xff0c;在排序方面具有一定的优势。本文将介绍一些常见的Python排序算法实现以及应…

有趣的图(三)(57)

小朋友们好&#xff0c;大朋友们好&#xff01; 我是猫妹&#xff0c;一名爱上Python编程的小学生。 和猫妹学Python&#xff0c;一起趣味学编程。 今日主题 咱们之前分别学习了图的基本概念&#xff0c;和图的深度优先遍历算法dfs。 你学会了吗&#xff1f; 咱们今天要学…

Linux系统的tty架构及UART驱动详解

​一、模块硬件学习 1.1. Uart介绍 通用异步收发传输器&#xff08;Universal Asynchronous Receiver/Transmitter)&#xff0c;通常称为UART&#xff0c;是一种异步收发传输器&#xff0c;是电脑硬件的一部分。它将要传输的资料在串行通信与并行通信之间加以转换。 作为把并…

面试问题总结----C/C++部分

1、本栏用来记录社招找工作过程中的内容,包括基础知识学习以及面试问题的记录等,以便于后续个人回顾学习; 暂时只有2023年3月份,第一次社招找工作的过程; 2、个人经历: 研究生期间课题是SLAM在无人机上的应用,有接触SLAM、Linux、ROS、C/C++、DJI OSDK等; 3、参加工作后…

C++程序流程结构

目录 程序流程结构 一、选择结构 1.1 If语句 1.2 三目运算符 1.3 switch语句 二、循环结构 2.1 while 循环语句 2.2 do…while循环 2.3 for循环 2.4 嵌套循环 三、跳转语句 3.1 break语句 3.2 continue 语句 3.3 goto语句 程序流程结构 C/C支持最基本的三种程…

20230623在WIN10安装PROTEL DXP2004(STEP-BY-STEP)

20230623在WIN10安装PROTEL DXP2004&#xff08;STEP-BY-STEP&#xff09; https://xiazai.zol.com.cn/detail/43/428470.shtml Protel DXP 2004 https://www.onlinedown.net/soft/580490.htm Protel DXP 2004 DXP2004 安装步骤 Failed To load Parallel Port Driver Welcom…

vue或react中修改组件样式的方法

vue或react中修改组件样式的方法 从组件库中引入的组件深度选择器&#xff1a;deep和&#xff1a;global深度选择器在scss中的使用关键点 常规的组件样式修改vue中的样式修改react中的样式修改 从组件库中引入的组件 深度选择器&#xff1a;deep和&#xff1a;global 在 Vue …

Python 算法交易实验63 关于回测

说明 项目结束了&#xff0c;这几天把量化第一版搭起来&#xff0c;量化很重要&#xff0c;现在可以迈出第一步了。首先要关注的是回测&#xff0c;和前不久写的这篇文章呼应&#xff0c;测试的确是一个相对独立&#xff0c;又非常重要的部件。过去比较少关注在方面上&#xf…

数据分析案例-航空公司满意度数据可视化

&#x1f935;‍♂️ 个人主页&#xff1a;艾派森的个人主页 ✍&#x1f3fb;作者简介&#xff1a;Python学习者 &#x1f40b; 希望大家多多支持&#xff0c;我们一起进步&#xff01;&#x1f604; 如果文章对你有帮助的话&#xff0c; 欢迎评论 &#x1f4ac;点赞&#x1f4…

【瑞萨RA_FSP】ADC——电压采集

文章目录 一、ADC简介二、ADC的结构框图1. 电压输入范围2. 工作模式3. 转换过程顺序4. 触发源5. ADC转换时间6. 数据寄存器7. 电压转换 一、ADC简介 ADC即模拟数字转换器&#xff0c;ADC英文全称&#xff08;Analog-to-digital converter&#xff09;&#xff0c; 是一种用于将…

Delphi XE10 dxLayoutControl 控件应用指南

Delphi XE10 dxLayoutControl 控件应用指南 DevExpress VCL套件是一套非常强大的界面控件&#xff0c;可惜关于Delphi开发方面的说明太少&#xff0c;有些控件使用起来一头雾水&#xff0c;不知从何下手。本节详细介绍在Delphi Xe10 Seattle中如何利用dxLayoutControl 控件来做…

MMENGINE.LOGGING

MMENGINE.LOGGING 文章目录 MMENGINE.LOGGING[Print_log](https://mmengine.readthedocs.io/zh_CN/latest/api/generated/mmengine.logging.print_log.html "Print_log")[MMENGINE.LOGGING.LOGGER 源代码](https://mmengine.readthedocs.io/zh_CN/latest/_modules/mm…

Rust每日一练(Leetday0030) 合并有序数组、格雷编码、子集II

目录 88. 合并两个有序数组 Merge Sorted Array &#x1f31f;  89. 格雷编码 Gray Code &#x1f31f;&#x1f31f; 90. 子集 II Subsets II &#x1f31f;&#x1f31f; &#x1f31f; 每日一练刷题专栏 &#x1f31f; Rust每日一练 专栏 Golang每日一练 专栏 Py…

计算机网络-网络体系结构

目录 计算机网络的基本概念计算机网络的定义组成与功能计算机网络的分类按照网络的作用范围进行分类按照网络的使用者进行分类 计算机网络主要性能指标 计算机网络体系结构计算机网络协议、接口、服务等概念ISO/OSI 参考模型和 TCP/IP 模型OSI七层模型TCP/IP 模型封装与分用 计…

[Hadoop之Hive安装配置 第二篇 ]

前言: 记录一下Hive笔记 目录 前言: Hive的基本简介,使用场景介绍 安装地址: 官网的Hive文档地址: 官网 Hive 的教程地址 Hive安装涉及到的应用介绍 Hive安装步骤: 1.解压hive压缩包, tar -zxvf 压缩包,然后进入module目录 查看压缩的文件 ,并改名为hive 2. pwd查看当…