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

 找回密码
 加入黑马

QQ登录

只需一步,快速开始

本帖最后由 Java-yao 于 2018-11-26 15:23 编辑

[就业班 2018.11.21]
线程间通信 线程池 Lambda表达式

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

等待唤醒机制介绍
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个方法

吃包子案例
BaoziPu.java 代码:
[Java] 纯文本查看 复制代码
public class BaoziPu extends Thread { // 继承Thread类
    // 定义包子成员变量, 作为共享的数据
    Baozi baozi;
    // 定义有参构造方法, 便于将共享的同一个包子对象传入
    public BaoziPu(Baozi baozi) {
        this.baozi = baozi;
    }
    // 定义包子铺的任务
    @Override
    public void run() {  // 重写run方法
        // 定义一个计数器变量, 用于判断生产什么包子
        int count = 0;
        // 循环生产包子
        while (true) {
            // 使用同步代码块
            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(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                // 包子做好了, 要修改包子的状态
                baozi.you = true;
                // 通知吃货来吃
                System.out.println("[包子铺] 做好了" + baozi.pi + baozi.xian + "的包子, 快来取餐!");
                baozi.notify();
            }
        }
    }
}


Baozi.java 代码:
[Java] 纯文本查看 复制代码
public class Baozi {
    String pi;
    String xian;
    boolean you = false;  // 一开始没有包子
}


ChiHuo.java 代码:
[Java] 纯文本查看 复制代码
public class ChiHuo extends Thread {
    Baozi baozi;
    public ChiHuo(Baozi baozi) {
        this.baozi = baozi;
    }

    @Override
    public void run() {
        // 循环吃包子
        while (true) {
            // 使用同步代码块
            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("---------------------------");
            }
        }
    }
}


Test.java 代码:
[Java] 纯文本查看 复制代码
public class Test {
    public static void main(String[] args) {
        // 创建一个包子对象, 同时传入包子铺和吃货, 作为共享的锁对象
        Baozi baozi = new Baozi();

        // 创建包子铺, 并开始
        new BaoziPu(baozi).start();
        // 创建吃货, 并开始
        new ChiHuo(baozi).start();
    }
}


2. 线程池
普通创建线程方式的缺点:
"创建"线程和"销毁"线程都是比较占用内存CPU的操作.
对于一些数量多, 执行时间短的任务, 频繁创建销毁线程来执行, 会降低程序运行效率.

一个容纳多个线程的容器(集合)
其中的线程可以反复使用, 省去了频繁创建线程对象的操作, 无需反复创建线程而消耗过多资源

工作原理:
        提前创建好多个线程对象, 放在集合中. 多个任务来了反复使用这些线程对象来执行

线程池的代码实现
java.util.concurrent.Executors类: 线程池工厂类, 用于创建和管理线程池
        // 静态方法
        static ExecutorService newFixedThreadPool(int nThreads)
创建固定数量线程的线程池(常用)

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() 方法, 销毁线程池 (不建议执行)

案例:使用线程池模拟银行服务员
需求: 模拟银行柜台办理业务
银行有2个柜台, 现在来了10个客户. 同一时间只能有2个客户办理业务, 其余客户要等待

定义RunnableImpl类, 实现Runnable接口, 模拟办理业务:
        重写run()方法:
                打印: 线程名称+"号柜台为您服务"
        打印: "顾客正在办理业务..."
                sleep()2秒模拟客户办理业务耗时
                打印: 线程名称+"号柜台期待您再次光临"
                打印: 分割线--------
定义测试类:
        Executors.newFixedThreadPool(2) 创建有2个线程的线程池ExecutorService
        for循环10次, 循环内使用ExecutorService对象的 submit() 提交任务对象, 用来模拟10个用户
        循环结束后使用ExecutorService对象的 shutdown() 方法结束线程池

RunnableImpl.java 代码:
[Java] 纯文本查看 复制代码
public class RunnableImpl implements Runnable { //定义RunnableImpl类, 实现Runnable接口
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "号柜台为您服务");
        System.out.println("顾客正在办理业务...");
        try {
            Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName() + "号柜台期待您再次光临");
        System.out.println("--------------------------------------------");
    }
}


Test.java 代码:
[Java] 纯文本查看 复制代码
public class Test {
    public static void main(String[] args) {
        // 创建线程池
        ExecutorService executorService = Executors.newFixedThreadPool(2);
        // 提交10个任务
        for (int i = 0; i < 10; i++) {
            executorService.submit(new RunnableImpl());
        }
        // 可以关闭也可以不关闭
        executorService.shutdown();
    }
}


3. Lambda表达式
JDK 8 中, 加入的Lambda表达式, 是函数式编程思想中的重点

函数式编程相对于面向对象的优点
面向对象: 强调"用哪个对象的哪个方法"来做事 (注重语法形式: 继承 方法重写)
函数式: 强调"传入的参数 和 要执行的代码"
函数式编程的好处:         简化代码编写 (使用 λ Lambda表达式, 简化匿名内部类的代码)

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

        // 如果代码只有一行, 也可以合并写成一行
        (参数列表) -> {一些重写方法的代码}
格式对比:
[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标准格式(重写"有参有返回值"的方法)
对数组排序 代码:
[Java] 纯文本查看 复制代码
        // 匿名内部类方式
        /*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);
        }


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

Lambda表达式的使用前提:
    1. Lambda只能用于接口, 且"接口中有且仅有一个抽象方法"(也称为"函数式接口")
        普通类, 抽象类不能用
    2. 使用Lambda必须具有上下文推断
        接口中只能有一个抽象方法, 才能推断出来重写的是这个抽象方法
            简而言之: 参数类型必须是函数式接口

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

1 个回复

正序浏览
回复 使用道具 举报
您需要登录后才可以回帖 登录 | 加入黑马