A股上市公司传智教育(股票代码 003032)旗下技术交流社区北京昌平校区

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

day07 线程池 Lambda表达式
今天我们来学习线程间的通信,线程池,以及函数式编程。以下是今天的学习目标:
  • 能够理解线程通信概念
  • 能够理解等待唤醒机制
  • 能够描述Java中线程池运行原理
  • 能够理解函数式编程相对于面向对象的优点
  • 能够掌握Lambda表达式的标准格式
  • 能够使用Lambda标准格式使用Runnable与Comparator接口
  • 能够掌握Lambda表达式的省略格式与规则
  • 能够使用Lambda省略格式使用Runnable与Comparator接口
  • 能够通过Lambda的标准格式使用自定义的接口(有且仅有一个抽象方法)
  • 能够通过Lambda的省略格式使用自定义的接口(有且仅有一个抽象方法)
  • 能够明确Lambda的两项使用前提


以下是今天的详细笔记:


线程间通信线程间通信介绍
线程间通信: 多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同(需要协作)

为什么要进行线程间通信:
        通常是竞争关系: 多个线程并发执行时, 在默认情况下CPU是随机切换线程的.
        有时也需要合作: 当我们需要多个线程来共同完成一件任务, 并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信, 以此来帮我们达到多线程共同操作一份数据

如何保证线程间通信有效利用资源:
        等待唤醒机制
等待唤醒机制介绍
等待唤醒机制:
        一个线程进行了规定操作后, 就进入等待状态( wait() ), 等待其他线程执行完他们的指定代码过后, 再将其唤醒( notify() )
    在有多个线程进行等待时, 如果需要, 可以使用 notifyAll() 来唤醒所有的等待线程
        wait/notify 就是线程间的一种协作机制, 用于解决线程间通信的问题


等待唤醒中的方法
java.lang.Object类:
        // 成员方法 (只能通过"锁对象"调用)
        void notify(): 随机唤醒在同一个锁对象上的某一个处于等待状态的线程
        void notifyAll(): 唤醒所有在同一个锁对象上处于等待状态的线程
        void wait(): 让当前线程处于无限等待状态, 同时释放锁


wait和notify/notifyAll的执行原理:
        wait:
                线程不再活动, 不再参与调度, 进入 wait set 中, 因此不会浪费 CPU 资源, 也不会去竞争锁, 这时的线程状态即是"WAITING". 它还要等着别的线程执行"通知(notify)", 让在锁对象上等待的线程从 wait set 中释放出来, 重新进入到调度队列(ready queue)中
        notify/notifyAll:
                哪怕只通知了一个等待的线程, 被通知线程也不能立即恢复执行, 因为它当初中断的地方是在同步块内, 而
此刻它已经不持有锁, 所以它需要再次尝试去获取锁(很可能面临其它线程的竞争), 成功后才能在当初调用 wait() 之后的地方恢复执行
                总结如下:
                        如果能获取锁, 线程就从"WAITING"状态变成"RUNNABLE"状态
                        否则, 从 wait set 出来, 又进入 entry set, 线程就从"WAITING"状态又变成"BLOCKED"状态


调用 wait() 和 notify() 需要注意的细节:
        1. wait() 与 notify() 必须要由"同一个锁对象"调用
                因为对应的锁对象可以通过 notify() 唤醒使用同一个锁对象调用的 wait() 后的线程
        2. wait() 与 notify() 是属于Object类的方法
                因为锁对象可以是任意对象, 而任意对象的所属类都是继承了Object类的
        3. wait() 与 notify() 必须要在"同步代码块"或者是"同步方法"中使用
                因为必须要通过锁对象调用这2个方法
等待唤醒机制: 吃包子(需求分析)
资源: 包子类
        属性:
                皮
                馅
                是否有包子: true有, false没有
生产者: 包子铺类, 继承Thread
        任务:
                对包子状态进行判断:
                true: 有包子, 则wait()
                false: 没有包子, 则交替生产包子, 修改包子状态为true, 并notify()吃货来吃
消费者: 吃货类, 继承Thread
        任务:
                对包子状态进行判断:
                true: 有包子, 则吃, 修改包子状态为false, 并notify()包子铺生产
                false: 没有包子, 则wait()
测试类:
        创建包子对象
        创建包子铺对象, 启动线程
        创建吃货对象, 启动线程
等待唤醒机制: 吃包子代码实现(包子类, 包子铺类)5分钟练习: 定义包子类和包子铺类
需求:
定义包子类: Baozi
        属性: 为了写代码方便不用加private
        皮儿: String pi;
        馅儿: String xian;
        是否有包子: boolean you = false; 默认没有
定义包子铺类: BaoziPu, 继承Thread类
        属性: 为了写代码方便不用加private
                Baozi baozi;
        有参构造: public BaoziPu(Baozi baozi)
        重写run()方法:
                定义变量int count = 0; 用于每次做不同的包子
                死循环while(true)做包子
                循环中定义同步代码块, 锁对象是baozi对象
                同步代码块中, 先if判断是否有包子, 有则wait()
                if代码块外, 判断count是奇数还是偶数, 奇数将包子属性修改为冰皮牛肉馅, 偶数将包子修改为薄皮三鲜馅
                count++增加
                打印输出包子铺生产的包子
                sleep 3秒模拟生产包子的耗时
                修改baozi属性为有包子
                notify()通知吃货来吃
代码:
[Java] 纯文本查看 复制代码
/*
包子
 */
public class Baozi {
    String pi;               // 皮儿
    String xian;             // 馅儿
    boolean you = false;     // 有没有包子, 默认没有
}

/*
包子铺: 生产包子的线程(生产者)
 */
public class BaoziPu extends Thread {
    // 包子
    Baozi baozi;

    // 有参构造
    public BaoziPu(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        // 定义变量用来改变包子的皮儿和馅儿
        int count = 0;
        // 循环生成包子
        while (true) {
            // 使用同步代码块, 将操作共享变量包子的地方包裹, 同时也是为了使用wait/notify
            synchronized (baozi) {
                // 先判断包子是不是已经有了
                if (baozi.you) {
                    // 有包子, 则等待, 直到被吃货唤醒
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 如果被吃货唤醒, 说明没有包子了, 开始生产包子
                if (count % 2 == 0) {
                    baozi.pi = "薄皮";
                    baozi.xian = "三鲜馅";
                } else {
                    baozi.pi = "冰皮";
                    baozi.xian = "牛肉馅";
                }
                count++;
                System.out.println("[包子铺]我在生产" + baozi.pi + baozi.xian + "的包子");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 生产好了, 修改包子状态为有
                baozi.you = true;
                // 通知吃货来吃
                baozi.notify();
            }
        }
    }
}

等待唤醒机制: 吃包子代码实现 (吃货类, 测试类)5分钟练习: 定义吃货类和测试类
需求:
定义吃货类: Chihuo, 继承Thread类
        属性: 为了写代码方便不用加private
                Baozi baozi;
        有参构造: public BaoziPu(Baozi baozi)
        重写run()方法:
                while(true)循环吃包子
                循环内定义同步代码块, 锁对象为baozi对象
                if判断包子状态, 如果没有包子, 则wait()等待
                if代码块外, 打印吃货吃包子
                修改包子状态为没有包子
                notify()通知包子铺做包子
                打印分割线
定义测试类:
        创建baozi对象
        创建包子铺对象, 传入包子, 调用start()启动线程
        创建吃货对象, 传入包子, 调用start()启动线程
代码:
[Java] 纯文本查看 复制代码
/*
吃货类: 消费包子(消费者)
 */
public class Chihuo extends Thread {
    // 包子成员变量
    Baozi baozi;

    // 有参构造
    public Chihuo(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        // 循环吃包子
        while (true) {
            // 使用同步代码块保证操作共享的包子, 同时也为了使用wait/notify
            synchronized (baozi) {
                // 判断是否没有包子
                if (!baozi.you) {
                    // 如果没有包子, 则等待
                    try {
                        baozi.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }

                // 如果能执行到这里, 说明已经被包子铺唤醒, 且有包子, 那么开始吃
                System.out.println("[吃货]我开始吃" + baozi.pi + baozi.xian + "的包子");
                // 吃完后将包子状态改为没包子
                baozi.you = false;
                // 通知老板再做包子
                System.out.println("[吃货]老板我吃完了, 继续做包子");
                baozi.notify();
                System.out.println("-----------------------------");
            }
        }
    }
}

/*
生产者消费者案例: 吃包子
 */
public class Test {
    public static void main(String[] args) {
        // 创建包子对象
        Baozi baozi = new Baozi();

        // 创建包子铺
        BaoziPu baoziPu = new BaoziPu(baozi);
        baoziPu.start();
        // 创建吃货
        Chihuo chihuo = new Chihuo(baozi);
        chihuo.start();
    }
}

线程池线程池概念和原理
普通创建线程方式的缺点:
        "创建"线程和"销毁"线程都是比较占用内存和CPU的操作.
        对于一些数量多, 执行时间短的任务, 频繁的创建和销毁线程来执行, 会降低程序运行效率.
       
线程池:
        一个容纳多个线程的容器
       
线程池可以解决的问题:
        其中的线程可以反复使用, 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源
       
线程池的工作原理:
        提前创建好多个线程对象, 放在集合中. 多个任务来了反复使用这些线程对象来执行
线程池的代码实现
                     一哥载Q特儿斯
java.util.concurrent.Executors类: 线程池工厂类, 用于管理线程池
        // 静态方法:
        static ExecutorService newFixedThreadPool(int nThreads): 创建固定数量线程的线程池(常用)
       
                     一哥载Q特儿 色儿微斯
java.util.concurrent.ExecutorService接口: 真正执行任务的线程池服务
        // 成员方法:
        Future submit(Runnable task): 提交一个Runnable任务
        void shutdown(): 通知线程执行完任务后关闭. 如不调此方法, 则线程执行完任务后仍在运行以便重复使用
       
线程池的创建和使用步骤:
        1. 使用Executors的静态方法 newFixedThreadPool(int nThreads) 创建线程池ExecutorService
        2. 创建一个任务类, 实现Runnable接口, 重写run()方法
        3. 调用ExecutorService对象的 submit(Runnable task) 方法, 传递任务给线程池, 执行任务
        4. 调用ExecutorService对象的 shutdown() 方法, 销毁线程池 (不建议执行)
5分钟练习: 使用线程池模拟饭店服务员
需求: 模拟饭店服务员
饭店有2个服务员, 有2个桌子, 来了10个顾客, 同一时间只能有2个顾客入座用餐, 其余顾客要等待
定义RunnableImpl类, 实现Runnable接口:
        重写run()方法:
                打印: 线程名称+"服务员开始为顾客点菜"
                打印: "顾客开始用餐"
                sleep()2秒模拟顾客吃饭耗时
                打印: "顾客用餐完毕"
                打印: 线程名称+"服务员收拾桌子"
                打印: 分割线--------
定义测试类:
        Executors.newFixedThreadPool(2)创建有2个线程的线程池ExecutorService
        for循环10次, 使用ExecutorService对象的submit提交任务对象
        循环结束后使用ExecutorService对象的shutdown()方法结束线程池
代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName()+"服务员开始为顾客点菜");
        System.out.println("顾客"+this.hashCode()+"开始用餐");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("顾客"+this.hashCode()+"用餐完毕");
        System.out.println(Thread.currentThread().getName()+"服务员收拾桌子");
        System.out.println("-------------------------------");
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建线程池, 有2个线程, 模拟2个桌子
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        // 循环10次模拟10个顾客来用餐
        for (int i = 0; i < 10; i++) {
            executorService.submit(new RunnableImpl());
        }
        // 都执行完后关闭线程池
        executorService.shutdown();
    }
}

函数式编程思想: Lambda表达式函数式编程思想概述
函数式:
        在数学中, 函数就是有输入量, 输出量的一套计算方案, 也就是"拿什么东西做什么事情"

面向对象: 强调"用哪个对象的哪个方法"来做事 (注重语法形式)
函数式: 强调做事 (不关心用什么对象, 重写什么方法)

函数式编程的好处: 简化代码编写
冗余的Runnable代码
[Java] 纯文本查看 复制代码
// 我们要通过Lambda表达式简化以下的代码
new Thread(new Runnable() {
    @Override
    public void run() {
        // 要执行的代码才是重要的
    }
}).start();

关键代码是: run()方法中要执行的任务
而其他代码都只是形式
编程思想的转换, 体验Lambda更优写法
JDK 8 中加入的Lambda表达式, 是函数式编程思想中的重点

对比:
[Java] 纯文本查看 复制代码
// 面向对象方式的代码
    new Thread(new Runnable() {
        @Override
        public void run() {
            System.out.println(Thread.currentThread().getName() + "新线程创建了");
        }
    }).start();


    // 函数式编程的代码
    new Thread(()-> {
			System.out.println(Thread.currentThread().getName() + "新线程创建了");
    	}
    ).start();

Lambda标准格式
Lambda表达式的3个部分:
    1. 一些参数 ()
                接口中抽象方法的参数列表. 没参数就空着; 有参数就写, 多个参数用逗号分隔
    2. 一个箭头 ->
                将参数传递给方法体
    3. 一段代码 {}
                重写接口抽象方法的方法体

格式:
        // 写成一行
        (参数列表) -> {一些重写方法的代码}
        // 写成多行
        (参数列表) -> {
        一些重写方法的代码
    }
5分钟练习: 使用Lambda标准格式(简化线程创建)
需求:
定义测试类, 使用实现Runnable接口方式创建线程
        1. 创建Thread对象, 在构造方法参数中, 使用匿名内部类方式传入Runnable实现类对象, 重写run()方法, 打印线程名字+"新线程创建了", 并启动线程
        2. 创建Thread对象, 在构造方法参数中, 使用Lambda表达式方式传入Runnable实现类对象, 重写run()方法, 打印线程名字+"新线程创建了", 并启动线程
代码:
[Java] 纯文本查看 复制代码
public class Test {
    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();
    }
}

练习: 使用Lambda标准格式(重写无参无返回方法)5分钟练习: 厨师喊吃饭
需求:
给定一个厨子 Cook 接口,内含唯一的抽象方法 makeFood ,且无参数、无返回值。如下:
public interface Cook {
        void makeFood();
}

在下面的代码中,请分别使用匿名内部类方式, 和Lambda的标准格式, 调用 invokeCook 方法,打印输出“吃饭啦!”字样:
[Java] 纯文本查看 复制代码
public class Demo05InvokeCook {
	public static void main(String[] args) {
		// TODO 请在此使用匿名内部类方式调用invokeCook方法
		
		// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
		
	}
	
	private static void invokeCook(Cook cook) {
		cook.makeFood();
	}
}

代码:
[Java] 纯文本查看 复制代码
/*
厨师接口
 */
public interface Cook {
    void makeFood();
}

/*
Lambda标准格式: 厨师做饭(无参无返回值)
 */
public class Test {
    public static void main(String[] args) {
        // 匿名内部类方式
        invodeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭啦");
            }
        });

        // Lambda表达式方式
        invodeCook(()->{
            System.out.println("吃饭啦");
        });
    }

    // 调用厨师做饭
    public static void invodeCook(Cook cook) {
        cook.makeFood();
    }
}

练习: 使用Lambda标准格式(重写有参有返回值方法)5分钟练习: 数组排序
需求:
定义Person类, 属性private String name, private int age
使用数组存储3个Person对象:
        柳岩, 38
        迪丽热巴, 18
        古力娜扎, 19
对数组中的Person对象使用Arrays的静态方法sort(T[] a, Comparator<? super T> c)方法通过年龄进行升序排序
分别使用匿名内部类比较器和Lambda表达式比较器来实现
代码:
[Java] 纯文本查看 复制代码
public class Person {
    private String name;
    private int age;

    public Person() {
    }

    public Person(String name, int age) {
        this.name = name;
        this.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;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}

public class Test {
    public static void main(String[] args) {
        // 创建Person数组, 静态初始化
        Person[] persons = {
                new Person("柳岩", 38),
                new Person("迪丽热巴", 18),
                new Person("古力娜扎", 19)
        };

        // 匿名内部类
//        Arrays.sort(persons, new Comparator<Person>() {
//            @Override
//            public int compare(Person o1, Person o2) {
//                return o1.getAge() - o2.getAge();
//            }
//        });

        // Lambda表达式
        Arrays.sort(persons, (Person o1, Person o2) -> {
            return o1.getAge() - o2.getAge();
        });

        for (Person person : persons) {
            System.out.println(person);
        }
    }
}

练习: 使用Lambda标准格式(自定义接口, 抽象方法有参有返回值)5分钟练习: 计算器
需求:
给定一个计算器 Calculator 接口,内含抽象方法 calc 可以将两个int数字相加得到和值:
[Java] 纯文本查看 复制代码
public interface Calculator {  // 计算器
    int calc(int a, int b);  // 计算a和b的结果返回
}

在下面的代码中,请使用匿名内部类和Lambda的标准格式调用 invokeCalc 方法,完成120和130的相加计算:
[Java] 纯文本查看 复制代码
public class Demo08InvokeCalc {
    public static void main(String[] args) {
    	// TODO 请在此使用匿名内部类方式调用invokeCalc方法来计算120+130的结果
    	
        // TODO 请在此使用Lambda【标准格式】调用invokeCalc方法来计算120+130的结果
        
    }
    
    private static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println("结果是:" + result);
    }
}

代码:
[Java] 纯文本查看 复制代码
/*
计算器
 */
public interface Calculator {
    // 计算两个数的结果
    int calc(int a, int b);
}

public class Test {
    public static void main(String[] args) {
        // 匿名内部类方式
        invokeCalc(120, 130, new Calculator() {
            @Override
            public int calc(int a, int b) {
                return a + b;
            }
        });

        // Lambda表达式方式
        invokeCalc(120, 130, (int a, int b) -> {
            return a + b;
        });
    }

    // 调用计算器
    public static void invokeCalc(int a, int b, Calculator calculator) {
        int result = calculator.calc(a, b);
        System.out.println(result);
    }
}

Lambda省略格式和使用前提
省略原则:
        可推导的都可省略 (凡是能根据前后代码能猜测出来的代码, 都可以省略不写)

    可以省略的部分:
        1. (参数列表): 参数"类型"可以省略
        2. (参数列表): 如果参数只有1个, 则"类型"和"小括号"都可以省略  a -> sout(a)
        3. {一些代码}: 如果只有一条代码, 则"大括号", "return", "分号"都可以"一起省略"


*函数式接口:
        函数式接口"有且仅有一个抽象方法"
        但函数式接口对于哪些方法算作抽象方法有特殊规定:
                1. 有方法体的方法"不算作"抽象方法, 如默认方法, 静态方法, 私有方法
                2. 抽象方法与java.lang.Object类中的方法定义相同的, 也"不算作"抽象方法
                        因为任何实现本接口的实现类, 都会直接或间接继承java.lang.Object类的public的方法, 所以在创建实现类时其实不用重写该抽象方法, 也就不算作抽象方法


Lambda表达式的使用前提:
    1. Lambda只能用于接口, 且"接口中有且仅有一个抽象方法"(也称为"函数式接口")
        普通类, 抽象类不能用
    2. 使用Lambda必须具有上下文推断
        方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例
        (简而言之: 作为参数类型的接口, 必须是函数式接口)


        比如以下构造方法
        Thread(Runnable r): 该构造方法的参数类型是Runnable
[Java] 纯文本查看 复制代码
// Runnable接口中, 有且仅有一个抽象方法, 该接口就是一个函数式接口, 可以使用Lambda表达式
        public interface Runnable {
            public abstract void run();
        }
        
		// Comparator接口中, 有且仅有一个抽象方法
        public interface Comparator<T> {
            // 这是该接口中有且仅有的一个抽象方法
            int compare(T o1, T o2);

            // 该方法与Object类中equals定义相同, 所以不算抽象方法
            boolean equals(Object obj);

            // 一些default方法, 有方法体, 不算抽象方法
            // 一些静态方法, 有方法体, 不算抽象方法
        }

		// 我们今天自己定义的接口, 也满足函数式接口的要求
		public interface Cook {
			void makeFood();
		}

10分钟练习: 使用Lambda的省略格式
需求:
将今日使用Lambda标准格式写的4个练习(创建线程, 厨师喊吃饭, 数组排序, 计算器)的代码, 使用Lambda省略格式再次实现
代码:
[Java] 纯文本查看 复制代码
// Lambda省略格式
new Thread(() -> System.out.println(Thread.currentThread().getName() + "新线程创建了")).start();
        
// Lambda省略格式
invodeCook(()->System.out.println("吃饭啦"));  

// Lambda省略格式
Arrays.sort(persons, (o1, o2) -> o1.getAge() - o2.getAge());

// Lambda省略格式
invokeCalc(120, 130, (a, b) -> a + b);

今日API
java.lang.Object类:
        // 成员方法
        void wait(): 使用锁对象调用, 当前线程进入WAITING无限等待状态, 直到被其他线程唤醒
        void notify(): 使用锁对象调用, 随机唤醒一个处于等待状态的线程
        void notifyAll(): 使用锁对象调用, 唤醒所有处于等待状态的线程

java.util.concurrent.Executors类: 线程池工厂类, 用于管理线程池
        // 静态方法:
        static ExecutorService newFixedThreadPool(int nThreads): 创建固定数量线程的线程池(常用)
       
java.util.concurrent.ExecutorService接口: 真正执行任务的线程池服务
        // 成员方法:
        Future submit(Runnable task): 提交一个Runnable任务
        void shutdown(): 通知线程执行完任务后关闭. 如不调此方法, 则线程执行完任务后仍在运行以便重复使用
























3 个回复

正序浏览
cuipu 发表于 2018-9-22 08:22
怎么把一个集合复制到另一个集合,除了遍历,
就是List.of添加元素返回的集合长度不可变,怎么变成可变的往里 ...

复制集合可以使用Collections的静态方法void copy(List<? super T> dest, List<? extends T> src)。
往集合中添加元素可以使用Collections的静态方法boolean addAll(Collection<? super T> c,T... elements)。
回复 使用道具 举报
怎么把一个集合复制到另一个集合,除了遍历,
就是List.of添加元素返回的集合长度不可变,怎么变成可变的往里添加元素
回复 使用道具 举报
沙发~
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马