02_Java语音进阶||day07_等待与唤醒案例、线程池、Lambda表达式

走远了吗. 提交于 2020-03-05 21:55:28

第一章 等待唤醒机制

1.1 线程间通信

  1. 概念:
    • 多个线程在处理同一个资源(包子),但是处理的动作(线程的任务)却不相同。
  2. 比如:
    • 线程A用来生产包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题

1.2 等待唤醒机制

  1. 等待与唤醒机制:线程之间的通信
    • 【重点】:有效的利用资源[包子](生产一个包子,吃一个包子,再生产一个包子,再吃一个包子)
  2. 通信:对包子的状态进行判断
        * ==没有==包子-->吃货线程唤醒包子铺线程-->吃货线程等待-->包子铺线程做包子-->做好包子-->修改包子的状态==有==
        * ==有==包子-->包子铺线程唤醒吃货线程-->包子铺线程等待-->吃货线程吃包子-->修改包子的状态==没有==
        * ==没有==包子-->吃货线程唤醒包子铺线程-->吃货线程等待-->包子铺线程做包子-->做好包子-->修改包子的状态==有==
        * ...
    
  3. 等待唤醒中的方法
    • void wait()
      • 在其他线程调用此对象的 notify() 方法或 notifyAll() 方法前,导致当前线程等待。
    • void notify()
      • 唤醒在此对象监视器上等待的单个线程
      • 注:会继续执行wait方法之后的代码
    • void notifyAll()
      • 唤醒在此对象监视器上等待的所有线程
  4. 注意事项:
    • 哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行因为它当初中断的地方是在同步代码块内,而此刻它已经不持有锁,所以它需要再次去获取锁(++很有可能面临其他线程的竞争++),成功后才能在当初调用wait方法之后的地方恢复执行
  5. 总结:
    • 如果能获取锁,线程就从WAITING状态变成RUNNABLE状态
    • 否则,从wait set出来,又进入entry set,线程又从WAITING状态变成BOLCKED状态
  6. 调用wait和notify方法需要注意的细节
    1. wait和notify必须要由同一个锁对象调用
      • 对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
    2. wait与notify是属于Object方法
      • 锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
    3. wait和notify必须要在同步代码块或者是同步方法(同步函数)中使用
      • 必须要通过锁对象调用这2个方法

1.3 等待唤醒机制案例分析

  1. 等待唤醒机制其实就是经典的“生产者与消费者”问题
  2. 1.1中包子案例分析:

1.4 等待唤醒机制代码实现

  • 包子铺类的注意事项:
    • 包子铺线程与吃货线程关系–>通信(互斥)
    • 必须使用同步技术两个线程只能有一个在执行
    • 锁对象必须保证唯一,可以使用包子对象作为锁对象
    • 包子铺这个类和吃货这个类把包子对象作为参数传入进来
      1. 需要在成员位置创建一个变量
      2. 使用带参构造方法,为这个包子变量赋值
    //定义一个包子类:BaoZi.java
    /*
    包子类
        属性:
            皮
            馅
            包子的有无状态:true,false
     */
    public class BaoZi {
        //皮
        String pi;
        //馅
        String xian;
        //包子的有无状态:true,false,初始为false
        boolean flag = false;
    
    }
    
    //定义一个包子铺类来继承Thread:BaoZiPu.java
    public class BaoZiPu extends Thread{
        //1. 需要在成员位置创建一个变量
        private BaoZi bz;
    
        //2. 使用带参构造方法,为这个包子变量赋值
        public BaoZiPu(BaoZi bz) {
            this.bz = bz;
        }
    
        //重写run方法:生产包子
        @Override
        public void run() {
            //定义一个变量(用来判断生产那种包子)
            int count = 0;
    
            //while让包子铺一直生产包子
            while (true){
    
                //同步技术,保证两个线程只有一个在执行
                synchronized(bz){ //同步代码块的锁对象就用包子对象
                    //对包子状态进行判断
                    if(bz.flag == true){
                        //调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //被唤醒之后执行,包子铺生产包子
                    //交替生产两种包子
                    if(count % 2 == 0){
                        //生产 薄皮 三鲜馅包子
                        bz.pi = "薄皮";
                        bz.xian = "三鲜馅";
                    }else {
                        //生产 冰皮 牛肉大葱馅包子
                        bz.pi = "冰皮";
                        bz.xian = "牛肉大葱馅";
                    }
                    count++;
                    System.out.println("包子铺正在生产" + bz.pi + bz.xian + "的包子");
    
                    //生产包子需要3秒钟
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //包子铺生产好包子后,将包子状态修改为有
                    bz.flag = true;
                    //唤醒吃货线程,让吃货线程吃包子
                    bz.notify();
                    System.out.println("包子铺已经生产好了:" + bz.pi + bz.xian + "的包子,吃货可以开始吃了");
                }
            }
        }
    }
    
    //定义一个吃货类来继承Thread:ChiHuo.java
    public class ChiHuo extends Thread{
        //1. 需要在成员位置创建一个变量
        private BaoZi bz;
    
        //2. 使用带参构造方法,为这个包子变量赋值
        public ChiHuo(BaoZi bz) {
            this.bz = bz;
        }
    
        //重写线程任务run:吃包子
        @Override
        public void run() {
    
            //while让吃货一直吃包子
            while (true){
    
                //同步技术,保证两个线程只有一个在执行
                synchronized(bz) { //同步代码块的锁对象就用包子对象
                    //对包子的状态进行判断
                    if(bz.flag == false){
                        //吃货调用wait方法进入等待状态
                        try {
                            bz.wait();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                    }
    
                    //被唤醒之后执行的代码,吃包子
                    System.out.println("吃货正在吃" + bz.pi + bz.xian + "的包子");
                    //吃货吃完包子后,将包子状态修改为无
                    bz.flag = false;
                    //唤醒包子铺线程,让包子铺生产包子
                    bz.notify();
                    System.out.println("吃货已经把:" + bz.pi + bz.xian + "的包子吃完了,包子铺开始生产包子");
                    System.out.println("=======================");
                }
            }
        }
    }
    
    //主方法(测试类):Demo.java
    public static void main(String[] args) {
        //创建包子对象
        BaoZi bz = new BaoZi();
        //创建包子铺线程,开启,生产包子
        new BaoZiPu(bz).start();
        //创建吃货线程,开启,吃包子
        new ChiHuo(bz).start();
    }
    
    //结果:
    包子铺正在生产薄皮三鲜馅的包子
    包子铺已经生产好了:薄皮三鲜馅的包子,吃货可以开始吃了
    吃货正在吃薄皮三鲜馅的包子
    吃货已经把:薄皮三鲜馅的包子吃完了,包子铺开始生产包子
    =======================
    包子铺正在生产冰皮牛肉大葱馅的包子
    包子铺已经生产好了:冰皮牛肉大葱馅的包子,吃货可以开始吃了
    吃货正在吃冰皮牛肉大葱馅的包子
    吃货已经把:冰皮牛肉大葱馅的包子吃完了,包子铺开始生产包子
    =======================
    包子铺正在生产薄皮三鲜馅的包子
    ...

第二章 线程池

2.1 线程池的概念和原理

  1. 线程池的思想概念

2.2 线程池的概念和原理

  1. 概念:
    • 线程池:其实就是一个容纳多个线程的容器【!】,其中的线程可以反复使用省去了频繁创建线程对象的操作无需反复创建线程而消耗过多的资源
  2. 线程池的底层原理:
    • 当程序第一次启动的时候,创建多个线程,保存到一个集合中
    • 当我们想要使用线程的时候,就可以从集合中取出来线程使用
      • Thread t = list.remove(0); //返回的是被移除的元素,(线程只能被一个任务使用
      • Thread t = linked.removeFirst(0);
    • 当我们使用完线程,需要把线程归还给线程池
      • list.add(t);
      • linked.addLast(t);
    • 常用LinkedList集合
    • 注:JDK1.5之后,内置了线程池,可以直接使用,无需自己写2.3会讲【重点】
  3. 线程池工作原理:
    • 注:如果有5个任务,3个线程,如果线程池中无空闲线程时
      • 任务等待执行
      • 等待其他某个任务执行完毕后,归还线程到线程池
      • 再从线程池中获取线程,执行任务
  4. 线程池的优点:
    1. 降低资源消耗
    2. 提高响应速度
    3. 提高线程的可管理性

2.3 线程池的使用

  1. 线程池:JDK1.5之后提供的
  2. java.util.concurrent.Executors:线程池的工厂类,用来生成线程池
  3. Executors类中的静态方法😦静态方法之间类名调用【1】)
    • static ++ExecutorService++ newFixedThreadPool(int nThreads)
      • 创建一个可重用固定线程数的线程池
    • 参数:
      • int nThreads:创建线程池中包含的线程数量
    • 返回值:
      • ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)
  4. java.util.concurrent.ExecutorService:线程池接口
    • 用来从线程池中获取线程,调用start方法,执行线程任务【重点】
      • submit(Runnable task)
        • 提交一个Runnable 任务用于执行
      • 关闭/销毁线程池的方法(不常用)
        • void shutdown()
  5. 线程池的使用步骤:
    1. 使用线程池的工厂类Executors里边提供的静态方法newFixedThreadPool生产一个指定线程数量的线程池
    2. 创建一个,实现Runnable接口,重写run方法,设置线程任务
    3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法(创建实现类的对象)
    4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
    import java.util.concurrent.ExecutorService;
    import java.util.concurrent.Executors;
    
    public class Demo01ThreadPool {
        public static void main(String[] args) {
            //1. 使用线程池的==工厂类Executors里边==提供的==静态方法newFixedThreadPool==生产一个==指定线程数量==的线程池
            //指定线程池中有两个线程
            ExecutorService es = Executors.newFixedThreadPool(2);
    
            //3. 调用==ExecutorService中==的方法==submit==,==传递线程任务==(实现类),==开启线程,执行run方法==
            es.submit(new RunnableImpl());  //pool-1-thread-1创建了一个新的线程执行
            //线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
            es.submit(new RunnableImpl());  //pool-1-thread-2创建了一个新的线程执行
            es.submit(new RunnableImpl());  //pool-1-thread-2创建了一个新的线程执行
    
            //4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
            es.shutdown();
    
            es.submit(new RunnableImpl());  //抛出异常,线程池都没了,就不能获取线程了
        }
    }
    
    //定义一个实现类,实现Runnable接口:RunnableImpl.java
    //2. ==创建==一个==类==,==实现Runnable==接口,==重写run==方法,设置线程任务
    public class RunnableImpl  implements Runnable{
    
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "创建了一个新的线程执行");   //Thread.currentThread().getName()-->获取线程名称
        }
    }
    
    //结果:
    pool-1-thread-2创建了一个新的线程执行
    pool-1-thread-2创建了一个新的线程执行
    pool-1-thread-1创建了一个新的线程执行
  • 注: 线程池会一直开启,使用完了线程,会自动把线程归还给线程池,线程可以继续使用
  • 注:如果调用ExecutorService中的方法shutdown销毁线程池后再次创建新的线程执行。
    • 抛出异常,线程池都没了,就不能获取线程了

第三章 Lambda表达式

3.1 函数式编程思想概述

  • 面向对象的思想:
    • 做一件事情,一个能解决这个事情的对象调用对象的方法,完成事情。
  • 函数式编程思想:
    • 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程

3.2 冗余的Runnable代码

    //实现接口Runnable的实现类:RunnableImpl.java
    public class RunnableImpl implements Runnable{

        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "新线程创建了");
        }
    }
    
    //主方法:Demo01Runnable.java
    public static void main(String[] args) {
        /*RunnableImpl run = new RunnableImpl();
        Thread t = new Thread(run);
        t.start();*/

        //简化-使用匿名内部类
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建了...");
            }
        }).start();
    }


* 只有方法体才是关键所在

3.3 编程思想转换

  1. JDK1.8加入了Lambda表达式重量级特性
    • 传递一段代码–才是我们真正的目的
    //主方法:Demo02Lambda.java
    public static void main(String[] args) {

        //简化-使用匿名内部类,实现多线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "新线程创建了...");
            }
        }).start();

        //使用Lambda表达式,实现多线程
        new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "新线程创建了...");
            }).start();
    }
    
    //结果:
    Thread-0新线程创建了...
    Thread-1新线程创建了...

3.4 Lambda标准格式

  1. 匿名内部类的好处与弊端
    • 好处:帮我们省去实现类的定义
    • 弊端:匿名内部类的语法—>确实太复杂了
  2. 语义分析
  3. 同样的语义体现在Lambda语法中,更加简单
        () -> {System.out.println("多线程任务执行!")};
    
  4. Lambda表达式的标准格式:
    • 由三部分组成:
      a. 一些参数
      b. 一个箭头
      c. 一段代码
  5. 格式:
        (参数列表) -> {一些重写方法的代码};
    
  6. 解释说明格式:
    • ():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数使用逗号分隔
    • ->:传递的意思,把参数传递给方法体{}
    • {}:重写接口抽象方法的方法体【重点】

3.5 Lambda标准格式练习(无参无返回)

    //定义一个接口:Cook.java
    public interface Cook {
        public abstract void makeFood();
    }
    
    //主方法:Demo01Cook.java
    public class Demo01Cook {
        public static void main(String[] args) {
            invokeCook(new Cook() {
                @Override
                public void makeFood() {
                    System.out.println("吃饭了");
                }
            });
    
            //使用Lambda表达式,简化匿名内部类
            invokeCook(()->{
                System.out.println("吃饭了");
            });
        }
    
        public static void invokeCook(Cook cook){
            cook.makeFood();
        }
    }
    
    //结果:
    吃饭了
    吃饭了

3.6 Lambda标准格式练习(有参有返回)

    //定义一个Person类:Person.java
    public class Person {
        private String name;
        private int age;
    
        public Person() {
        }
    
        public Person(String name, int age) {
            this.name = name;
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Person{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public int getAge() {
            return age;
        }
    
        public void setAge(int age) {
            this.age = age;
        }
    }
    
    //主方法:Demo01Arrays
    public static void main(String[] args) {
        //使用数组来存储多个对象
        Person[] arr = {
                new Person("柳岩", 21),
                new Person("程超", 18),
                new Person("迪丽热巴", 33)
        };

        //使用Arrays中的sort方法对年龄进行升序(前面-后面)排列
        /*Arrays.sort(arr, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();   //前面年龄-后面年龄
            }
        });*/

        //使用Lambda表达式,简化匿名内部类
        Arrays.sort(arr, (Person o1, Person o2) -> {
            return o1.getAge() - o2.getAge();
        });

        //遍历数组
        for (Person person : arr) {
            System.out.println(person);
        }
    }
    
    //结果:
    Person{name='程超', age=18}
    Person{name='柳岩', age=21}
    Person{name='迪丽热巴', age=33}

3.7 Lambda标准格式练习(有参有返回)-自定义接口

    //定义一个自定义接口:Calculator.java
    public interface Calculator {

        //自定义接口:计算两个整数的和的方法并返回结果
        public abstract int calc(int a, int b);
    }
    
    //主方法
    public class Demo01Calculator {
        public static void main(String[] args) {
            //调用invokeCalc方法,方法参数是一个接口,使用匿名内部类
            /*invokeCalc(10, 20, new Calculator() {
                @Override
                public int calc(int a, int b) {
                    return a + b;
                }
            });*/
    
            //使用Lambda表达式,简化匿名内部类
            invokeCalc(10, 20, (int a, int b) -> {
                return a + b;
            });
        }
    
        /*
        定义一个方法
        参数传递两个int类型的整数
        参数传递calculator接口
        方法内部调用calculator中的方法计算两个数的和
         */
        public static void invokeCalc(int a, int b, Calculator c){
            int sum = c.calc(a, b);
            System.out.println(a + "+" + b + "和为" + sum);
        }
    }

3.8 Lambda省略格式

  1. Lambda表达式:是可推导,可以省略
    • 凡是根据上下文可以推导出来的内容,都可以省略书写
  2. 可以省略的内容:
    1. (参数列表):括号中参数列表的数据类型,可以省略不写
    2. (参数列表):括号中的参数如果只有一个,那么类型和()都可以省略
    3. {一些代码}:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号)
      • 注意:要省略{},return,分号必须一起省略
    import cn.javaadvance.day07.demo04.Cook;
    import cn.javaadvance.day07.demo05.Person;
    import cn.javaadvance.day07.demo06.Calculator;
    
    import java.util.ArrayList;
    import java.util.Arrays;

    //主方法:
    public class Demo01ArrayList {
        public static void main(String[] args) {
            //JDK1.7之前,创建集合对象必须把前后泛型都写上
            //ArrayList<String> listA = new ArrayList<String>();
    
            //JDK1.7之后,=号后面的泛型可以省略,后面的泛型可以根据前面的泛型“推导出来”
            //ArrayList<String> listA2 = new ArrayList<>();
    
            //优化前面案例:
            //demo03
            /*
            //只有一行方法体:所以{},;,return可以省略
            new Thread(()->{
                System.out.println(Thread.currentThread().getName() + "新线程创建了...");
            }).start();
            //省略后
            new Thread(()-> System.out.println(Thread.currentThread().getName() + "新线程创建了...")).start();
            */
            
            //demo04
            /*
            invokeCook(()->{
                System.out.println("吃饭了");
            });
            //省略后
            //只有一行方法体:所以{},;,return可以省略
            invokeCook(()-> System.out.println("吃饭了"));
            */
    
             
            //demo05
            /*
            Person[] arr = {
                    new Person("柳岩", 21),
                    new Person("程超", 18),
                    new Person("迪丽热巴", 33)
            };
            //使用Lambda表达式,简化匿名内部类
            Arrays.sort(arr, (Person o1, Person o2) -> {
                return o1.getAge() - o2.getAge();
            });
            //省略后
            //只有一行方法体:所以{},;,return可以省略
            //括号中==参数列表的数据类型==,可以省略不写
            Arrays.sort(arr, (o1, o2) -> o1.getAge()-o2.getAge());
            //遍历数组
            for (Person person : arr) {
                System.out.println(person);
            }
            */
    
            //demo06
            /*
            //使用Lambda表达式,简化匿名内部类
            invokeCalc(10, 20, (int a, int b) -> {
                return a + b;
            });
            //简化后
            //
            invokeCalc(10, 20, (a, b) -> a + b);
            */
        }
    
        //demo04
        public static void invokeCook(Cook cook){
            cook.makeFood();
        }
    
        //demo06
        public static void invokeCalc(int a, int b, Calculator c){
            int sum = c.calc(a, b);
            System.out.println(a + "+" + b + "和为" + sum);
        }
    }

3.9 Lambda的使用前提

  1. 使用Lambda必须具有接口,且要求“接口中有且只有一个抽象方法【重点】
  2. 使用Lambda必须具有上下文推断【重点】
    • 就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
  • 注:有且仅有一个抽象方法的接口,称为“函数式接口【!】
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!