当前位置: 首页 > news >正文

再探Java多线程Ⅱ --- (创建方式+等待唤醒+Lock锁)

       接上篇我们详细解析了: 多线程的两证创建方式(继承Thread+实现Runnable接口),  synchronized(同步代码块+同步方法), 多线程常用方法 以及 死锁

        本篇我要继续在以上基础扩展两种多线程的创建方式, 等待唤醒机制以及代码实现 , Lock锁的实现.

  • 1. 等待唤醒机制(wait-notify)

    • 1.1 核心三方法

      • a. wait(): 线程等待, 等待的过程中会释放锁, 需要其他线程调用notify去唤醒

      • b. notify(): 随机唤醒一条等待线程

      • c. notifyAll(): 唤醒当前所有在等待的线程

    • 1.2 生产消费模型展示等待唤醒机制

      • 1.2.1 设计模型

        • 事件: 有一个包子铺卖包子和一群大妈买包子(两个线程对象和两个线程任务对象), 我们想在代码中实现卖一个包子买一个包子(轮流交替执行), 没有包子就生产,有了包子就买.
        •  类的设计:
          • a. 一个生产者类,一个消费者类: 通过实现Runnable接口,重写run方法调用各自的生产消费方法.
          • 代码示例:
          • public class Consumer implements Runnable {BaoZiPu baoZiPu;public Consumer(BaoZiPu baoZiPu) {this.baoZiPu = baoZiPu;}@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}baoZiPu.SubCount();}}
            }
            public class Producer implements Runnable {BaoZiPu baoZiPu;public Producer(BaoZiPu baoZiPu) {this.baoZiPu = baoZiPu;}@Overridepublic void run() {while (true) {try {Thread.sleep(100);} catch (InterruptedException e) {throw new RuntimeException(e);}baoZiPu.AddCount();}}
            }
          • b. 一个包子铺类: 设置基本属性包括包子数,有无包子标识, 生产包子和消费包子方法.
          • 代码示例:
          • public class BaoZiPu {private int count;private boolean flag;public synchronized void SubCount() {//有包子则消费//判断 flag是否为trueif (!this.flag) {//1.判断 flag是否为false ,无则等待,有则消费try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println("消费了第" + count + "个包子-----");//2.有包子则输出this.flag = false;//3. 标记为无包子(已消费)this.notify();//4.消费后唤醒生产者线程}public synchronized void AddCount() {if (this.flag) {//1.判断 flag是否为true ,有则等待,无则生产try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}count++;//2.无包子则生产System.out.println("生产了第" + count + "个包子+++++");this.flag = true;//3. 标记为有包子this.notify();//4. 生产后唤醒消费者}}
          • c. 一个测试类: 创建线程对象和创建线程任务对象 , 启动线程.
          • 代码示例: 
          • public class Test {//通过同步方法实现生产消费public static void main(String[] args) {BaoZiPu baoZiPu = new BaoZiPu();//new一个包子铺,将其作为参数传入两个线程类的构造器里Producer producer = new Producer(baoZiPu);Consumer consumer = new Consumer(baoZiPu);Thread t1 = new Thread(producer, "生产者");Thread t2 = new Thread(consumer, "消费者");t1.start();t2.start();}
            }
        • 关键点分析:
          •     扔掉上面的代码, 你会不会在生产者和消费者类中各创建一个包子铺对象, 并在下面的run方法中 用刚才创建的包子铺对象调用写在包子铺类里的生产/消费方法?  显然 这样执行出来的结果不是我们预期的轮流交替执行生产消费操作那么问题在哪呢?
          •     答: 因为我们没有保证在整个买卖的任务中只有一把锁 , 创建了两个包子铺对象就是创建了两把锁, 自然两个线程就不需要抢锁了
          •     解决方案: 我们在核心测试类中创建一个包子铺对象并将其作为参数传入两个任务类的有参构造中即: 
          • BaoZiPu baoZiPu = new BaoZiPu();//new一个包子铺,将其作为参数传入两个线程类的构造器里Producer producer = new Producer(baoZiPu);
            Consumer consumer = new Consumer(baoZiPu);
          • 这样保证了两个线程同时用包子铺这把锁,保证了锁同一时刻只能由一人所有
          • 代码结果:
      • 1.2.2 如果一个包子铺有三个师傅和三个大妈咋办?(多唤醒,多等待)

        • 为了保证交易的轮流交替性,我们沿用上面的代码框架进行扩展:
        • 我们增加交易中买卖人数:
        • public class Test {//多等待,多唤醒,多生产,多消费public static void main(String[] args) {BaoZiPu baoZiPu = new BaoZiPu();//new一个包子铺,将其作为参数传入两个线程类的构造器里Producer producer = new Producer(baoZiPu);Consumer consumer = new Consumer(baoZiPu);new Thread(producer, "生产者1").start();new Thread(producer, "生产者2").start();new Thread(producer, "生产者3").start();new Thread(consumer, "消费者1").start();new Thread(consumer, "消费者2").start();new Thread(consumer, "消费者3").start();}
          }
        • 运行的结果如下: 没有达到我们轮流执行的预期 -> 卖一买一
        • 我们用具体的推理分析一下为什么会产生这样的问题:
        •     当创建了多个线程对象时, 运行程序, 假设厨师1抢到了锁 ,由于没有包子, 所以生产包子 ,同时包子数增加, 标志变为true, 空唤醒一次, 交出锁 ; 之后六个人继续抢锁, 假设厨师2抢到了锁子, 由于有包子了于是厨师2进入待命状态同时交出锁, 此时假设厨师3抢到了锁, 由于标记为true于是待命并交锁, 假设厨师1又抢到了锁, 进入待命并交锁, 此时仅剩三个醒着的大妈, 大妈1抢到锁 输出, 变标志 ,交锁, 并随机唤醒一个厨师1 , 此时我们假设三个大妈连续三次抢到锁并进入等待 ,此时仅剩一个厨师1是醒的,于是拿到锁后继续生产,输出, 变标志 ,关键来了!!!!! 如果这个时候唤醒的还是厨师, 那么他不会进入if判断而直接生产包子, 变标志, 交锁, 并随机唤醒.  于是就出现了输出结果里有连续多次的生产或者消费的结果
        •     改进: notifyAll(), while循环
        •     此时如果我们能始终保证某人在唤醒别人时, 能够把当前所有待命中的人唤醒, 就能产生一个始终平等的抢锁环境 , 同时 ,在提供平等的抢锁环节后如果能 再一次进行有无包子的判断 ,就始终能保证买卖过程的交叠进行.
        •  代码改进: BaoZiPu类的两个买卖方法
        • public synchronized void SubCount() {//有包子则消费//判断 flag是否为truewhile (!this.flag) {//1. 保证了解除了等待状态并抢到锁的的线程再次经过判断flag的过程 以保证生产消费的正常交替try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}System.out.println(Thread.currentThread().getName() + "消费了第" + count + "个包子-----");//2.有包子则输出this.flag = false;//3. 标记为无包子(已消费)this.notifyAll();//4.消费后唤醒所有线程}
          public synchronized void AddCount() {while (this.flag) {//1.  保证解除了等待状态的线程再次判断flag的值以保证生产消费的正常交替try {this.wait();} catch (InterruptedException e) {throw new RuntimeException(e);}}count++;//2.无包子则生产System.out.println(Thread.currentThread().getName() + "生产了第" + count + "个包子+++++");this.flag = true;//3. 标记为有包子this.notifyAll();//4. 生产后唤醒所有线程}
        • 运行结果:
    • 2. Lock 锁  (代替synchronized的解决方案)

      • ​​​​​a. 概述: Lock是一个接口

      • b. 实现类: ReentrantLock 
      • c. 核心方法: lock对象.lock();//获取锁    lock对象.unlock();//释放锁
      • 代码展示:
      • public class Test01 {public static void main(String[] args) {MyTicket myTicket=new MyTicket();Thread t1=new Thread(myTicket,"马牛逼");Thread t2=new Thread(myTicket,"蔡徐坤");Thread t3=new Thread(myTicket,"鸽哥");t1.start();t2.start();t3.start();}
        }
      • 
        public class MyTicket implements Runnable {//通过Lock对象实现获取及释放锁int ticket = 10;Lock lock1 = new ReentrantLock();@Overridepublic void run() {while (true) {try {//一定延时Thread.sleep(100);//获取锁lock1.lock();if (ticket > 0) {System.out.println(Thread.currentThread().getName() + "买了第" + ticket + "张票");ticket--;}} catch (InterruptedException e) {throw new RuntimeException(e);}finally {lock1.unlock();//释放锁}}}
        }
      • 代码结果:
    • 3. 实现Callable接口 (实现多线程法三)

      • a. 概述: Callable<V>是一个借口 ,类似于法二的Runnable
      • b. 方法: V call() -> 设置线程任务 类似于run()方法
      • c. call() 与 run() 的区别: 
        • a. 相同: 都用来设置线程任务
        • b. 不同: call()方法有返回值, 有异常可以throw
        •              run()方法无返回值, 有异常能直接throw
      • 源码:
      • d. 获取call()方法的返回值:  FutureTask<V>
        •  a. FutureTask<V>实现了一个接口: Future<V>
        •  b.  FutureTask<V>中有一个方法: V get() -> 获取call()方法的返回值.
      • 代码展示:

      • public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {MyCallable myCallable=new MyCallable();FutureTask<String> futureTask=new FutureTask<>(myCallable);//FutureTask<>获取call方法的返回值// 创建Thread对象 -> Thread(Runnable target)Thread t1=new Thread(futureTask);t1.start();//调用get方法获取call方法的返回值System.out.println(futureTask.get());}
        }
        public class MyCallable implements Callable<String> {//实现多线程的方式3 ->实现Callable接口@Overridepublic String call() throws Exception {return "要塞军阀-鸽哥的故事";//线程任务}}
      • 运行结果:

    • 4. 线程池(ThreadPool) (实现多线程法四)

      • 4.1 问题:

        • 之前来一个线程任务,就需要创建一个线程对象去start, 用完还需要销毁线程对象, 如果线程任务多了, 就需要频繁创建线程对象和销毁线程对象,这样会消耗内存资源, 所以我们就想循环利用线程资源, 用的时候直接拿线程对象, 用完再还回去
      • 4.2 创建线程池对象

        • 用具类 -> Executors
        • 获取线程池对象: Executors中的静态方法:
          • static ExecutorServices newFixedThreadPool (int nThreads)
          • a.  参数:指定线程池中最多创建的线程对象条数
          • b. 返回值ExecutorServices 是线程池, 用来管理线程对象
      • 4.3 执行线程任务

        • ExecutorService中的方法
          • Future<?> submit (Runnable task) 提交一个Runnable 任务用于执行
          • Future<T> submit (Callable<T> task)提交一个Callable任务用于执行
        • submit方法的返回值:Future接口
          • 用于接受run 方法或者call 方法返回值的, 但是run 方法没有返回值, 所以可能不用Future接收,执行call方法需要用Future接受
          • Future 中有一个方法 : V get () 用于接受call() 方法的返回值
        • ExecutorService中的方法:
          • void shutdown() 启动有序关闭 , 其中先提交的任务将被执行, 但不会接受任何新任务
      • 4.4 实现Runnable接口 创建线程池

        • 代码示例:
        • public class Test01 {public static void main(String[] args) {ExecutorService pool=Executors.newFixedThreadPool(2); //创建线程池对象pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.submit(new MyRunnable());pool.shutdown();//关闭线程池对象,若不关闭则线程池等待新任务来}
          }
          
          public class MyRunnable implements Runnable{@Overridepublic void run() {for(int i=0;i<2;i++) {System.out.println(Thread.currentThread().getName() + "在执行"+i);}}
          }
        • 运行结果:
      • 4.5 线程池实现多任务处理:

        • 代码示例:(输出鸽哥并 输出 1-100的和)
        • public class Test {public static void main(String[] args) throws ExecutionException, InterruptedException {ExecutorService es= Executors.newFixedThreadPool(2);Future<String> future=es.submit(new MyString());Future<Integer> future1=es.submit(new MySum());System.out.println(Thread.currentThread().getName()+future1.get());System.out.println(Thread.currentThread().getName()+future.get());}
          }
          public class MySum implements Callable<Integer> {@Overridepublic Integer call() throws Exception {int sum=0;for(int i=1;i<=100;i++){sum+=i;}return sum;}
          }
          public class MyString implements Callable<String> {@Overridepublic String call() throws Exception {return "鸽哥";}
          }
        • 运行结果:
http://www.lryc.cn/news/592367.html

相关文章:

  • 【论文蒸馏】Recent Advances in Speech Language Models: A Survey
  • 《设计模式之禅》笔记摘录 - 8.命令模式
  • 企业如何让内部视频仅限公司官网或指定域名播放?
  • 2025年SEVC SCI2区,利用增强粒子群算法(MR-MPSO)优化MapReduce效率和降低复杂性,深度解析+性能实测
  • 某邮生活旋转验证码逆向
  • 5W8-3D牢游戏超级大集合[2012年6月] 地址 + 解压密码
  • Python绘制数据(二)
  • C语言实战:超级玛丽游戏
  • 工业数据集成中间件工具OPC Router详细介绍
  • 大模型格式
  • sky-take-out项目Mybatis的使用
  • AE电源MDX 5K 10K 15K 20K 25K 30K手侧操作使用说明
  • 【Linux】环境基础与开发工具的使用
  • 数据结构--JDK17新增语法和顺序表
  • blender如何队列渲染多个工程文件的动画?
  • 集训Demo4
  • 本地部署 Kimi K2 全指南(llama.cpp、vLLM、Docker 三法)
  • 【playwright篇】教程(十六)[macOS+playwright相关问题]
  • ClickHouse物化视图避坑指南:原理、数据迁移与优化
  • IntelliJ IDEA大括号格式设置:换行改行尾
  • C#测试调用ServiceController类查询及操作服务的基本用法
  • vscode编辑Markdown文件
  • 【51】MFC入门到精通——MFC串口助手(一)---初级版(串口设置、初始化、打开/关闭、状态显示),附源码
  • el-date-picker 如何给出 所选月份的最后一天
  • 几款开源的安全监控与防御工具分享
  • 电脑装机软件一键安装管理器
  • 开源的大语言模型(LLM)应用开发平台Dify
  • 飞凌嵌入式亮相第九届瑞芯微开发者大会:AIoT模型创新重做产品
  • 【48】MFC入门到精通——MFC 文件读写总结 CFile、CStdioFile、CFileDialog
  • 源鉴SCA4.9︱多模态SCA引擎重磅升级,开源风险深度治理能力再次进阶