Kaze
Kaze
Published on 2022-04-15 / 77 Visits
0
0

java并发编程

Lambda 表达式

无类型参数

  • f -> { }
  • f的类型是系统根据上下文 自动 识别的。
  • 箭头(->)前表示参数变量,有 多个参数和没有参数 的时候,必须使用小括号包裹:()
  • 箭头(->)后的执行语句 只有一条 时,可以不加大括号 {} 包裹

有类型参数

  • 如果代码比较复杂,而为了易阅读、易维护,也可以为参数变量指定类型。此时即使只有一个参数,也必须使用小括号 () 包裹,否则会出错。

引用外部变量

  • Lambda 表达式 {} 内的执行语句,除了能引用参数变量以外,还可以引用外部的变量。
  • 规范:
    • 从lambda表达式引用的本地变量必须是最终变量或实际上的最终变量,即引用的 局部变量 不允许被修改,即使写在表达式里面或后面也不行。Lambda 表达式引用的 局部变量 即使不声明为 final,也要具备 final 的特性:变量值初始化后不允许被修改。
    • 参数变量不能与 局部变量 同名。

双冒号(::)操作符

  • System.out::println 等同于 n -> {System.out.println(n);}

  • 元素(n)会 自动 作为参数传递给 System.out.println() 方法。:: 语法省略了参数变量

    image-20220410183037214

Stream API

简介

  • Stream 的主要作用是对集合(Collection)中的数据进行各种操作,增强了集合对象的功能。

流迭代

  • 创建流

    • 由数组转化

      String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"};
      Stream<String> stream = Stream.of(fruitArray);
      
    • 由集合转化

      List<String> fruits = new ArrayList<>();
      fruits.add("苹果");
      fruits.add("哈密瓜");
      fruits.add("香蕉");
      fruits.add("西瓜");
      fruits.add("火龙果");
      Stream<String> stream = fruits.stream();
      
  • 迭代流

    • Stream 提供的迭代方法叫做forEach()

      Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
      stream.forEach(System.out::println);
      
    • 集合类的 forEach() 方法与流的 forEach() 方法无关,仅仅是方法名相同而已。

流数据过滤

  • filter() 方法

    • 方法参数是一个 Lambda 表达式,箭头后面是条件语句,判断数据需要 符合 的条件。

      image-20220410185032616
    • 箭头后的过滤条件语句(非可执行的语句)。传统代码中,条件语句写在 () 中的,所以条件语句可以用 () 而不能用 {} 。

流数据映射

  • map() 方法通常称作映射,其作用就是用新的元素替换掉流中原来相同位置的元素。

  • map() 方法的参数是一个 Lambda 表达式,在语句块中对流中的每个数据对象进行计算、处理,最后用 return 语句返回的对象,就是转换后的对象。

    image-20220410185614870
  • 映射后的对象类型,可以与流中原始的对象类型不一致。

流数据排序

  • sorted() 是完成排序的方法。把排序规则写成一个 Lambda 表达式传给此方法即可。

    students.stream()
        // 实现升序排序
        .sorted((student1, student2) -> {
            return student1.getRollNo() - student2.getRollNo();
        })
        .forEach(System.out::println);
    
  • student1 指代后一个元素,student2 指代前一个元素。

  • 集合排序 Collections.sort() 和流排序 sorted()都需要返回一个数值,返回 非正数 表示两个相比较的元素需要 交换位置 ,返回正数则不需要。

  • 使用 student2.getRollNo() - student1.getRollNo()可以实现降序排序

流数据摘取

  • limit(n) 方法的作用是返回流的 n 个元素, n 不能为负数。

    不是摘取任意位置,只能是流开头的

流的设计思想

  • 数据流的操作过程,可以看做一个管道,管道由多个节点组成,每个节点完成一个操作。数据流输入这个管道,按照 顺序 经过各个节点。

    image-20220410190921383
  • Stream 的显著特点是:编程的重点,不再是对象的运用,而是数据的计算。

  • Stream 的这种变化,特征是:函数式风格。即弱化了面向对象的严格、完整的语法,重心变为通过函数完成数据计算。

并行数据

流合并

  • reduce() 方法的作用是合并所有的元素,终止 计算出一个结果。这里终止的意思,就是流已经到达终点结束了,不能再继续流动了。

    forEach() 也是流的终点

  • reduce() 方法的返回值是一个比较复杂的对象,需要调用 get() 方法返回最终的值。

    int sum = numbers.stream()
        .reduce((a, b) -> a + b)
        .get();
    
  • reduce() 方法的参数

    • a 在第一次执行计算语句 a + b 时,指代流的第一个元素;然后充当缓存作用以存放本次计算结果。此后执行计算语句时,a 的值就是上一次的计算结果并继续充当缓存存放本次计算结果。

    • b 参数第一次执行计算语句时指代流的第二个元素。此后依次指代流的每个元素。

      a、b 两个参数的作用是由***位置***决定的,变量名是任意的

  • reduce() 方法也可以操作对象

    • reduce() 提供了另一种参数形式,可以自己 new 一个对象充当缓存角色,而不是使用流中的原始对象。

      Student result = students.stream()
          .reduce(new Student("", 0),
              (a, b) -> {
                  a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());
                  return a;
              }
          );
      
    • 参数:

      • 第一个参数,是作为缓存角色的对象
      • 第二个参数,是Lambda表达式,完成计算,格式是一样的。
        • a 变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。
        • b 变量依次指代流的每个元素,包括第一个元素。
    • reduce() 方法的返回值同样发送了变化,返回作为缓存角色的对象,即第一个参数,不用再调用一次 get() 方法了

流收集

  • 收集也是一种属于终点的流操作。
  • 流收集可以把结果元素放在一个新的集合中,待进一步使用。
  • collect() 方法的作用是收集元素,Collectors.toList() 是一个静态方法,作为参数告诉 collect() 方法存入一个 List 集合。
  • collect(Collectors.toList()) 是一个标准用法。

并行流

  • 使用并行流的代码很简单,不再调用 stream() 方法,改为调用 parallelStream() 方法即可。其它的计算方法是一样的。
  • parallelStream() 以并行的方式执行任务,同时也支持流的收集、合并等计算。
  • 不适合使用并行计算的场景:
    • 流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算。
  • 并行 计算模式的性能 不是 任何情况下都优于 串行 计算模式。如:硬件太差、任务简单。一般来说,任务执行超过一小时的情况下,可以考虑使用 并行 模式优化性能。

常用设计模式

单例模式

  • 单例模式可以保证一个类仅有一个实例。如:

    image-20220412174240068

简单工厂模式

  • 实现简单工厂,需要两个步骤:

    • 从具体的产品类抽象出接口。工厂应该生产 一种 产品,不应该生产某 一个 产品。
    • 把生产实例对象的过程,收拢到工厂类中实现。
  • 使用简单工厂完成功能开发时,重点就是要明确 什么条件 下创建 什么实例对象 的需求逻辑。

    image-20220412183012350
  • public class FruitFactory {
        public static Fruit getFruit(Customer customer) {
            Fruit fruit = null;
            if ("sweet".equals(customer.getFlavor())) {
                fruit = new Watermelon();
            } else if ("acid".equals(customer.getFlavor())) {
                fruit = new Lemon();
            } else if ("smelly".equals(customer.getFlavor())) {
                fruit = new Durian();
            }
    
            return fruit;
        }
    }
    
  • image-20220412203144803
  • 在接口 Car 与实现类之间,加一个 AbstractCar,轿车类不再直接实现接口,而是继承 AbstractCar,这样可以减少重复代码

    • 抽象类存放公共属性和方法
    • 每个实现类存放各种特有的属性和方法

抽象工厂模式

  • 简单工厂的主要把多个产品抽象,使用一个工厂统一创建;那么抽象工厂的主要作用是把 多个工厂 也进一步 抽象

    image-20220412210540125
  • public class SnacksFactoryBuilder {
        public SnacksFactory buildFactory(String choice) {
            if (choice.equalsIgnoreCase("fruit")) {
                return new FruitFactory();
            } else if (choice.equalsIgnoreCase("drink")) {
                return new DrinkFactory();
            }
            return null;
        }
    }
    

观察者模式

  • 订阅/通知 的场景,很适合用 观察者模式 来实现。

    image-20220413201211839

并发编程

继承 Thread 类

  • 可以继承 Java 的 Thread 类实现线程类。继承 Thread 类后,需要重写父类的 run() 方法,注意必须是修饰为 public void,方法是没有参数的。

  • 线程类的作用就是完成一段相对独立的任务。

  • 线程需要调用 start() 方法才能启动。

    image-20220413205940132
  • Thread 父类中有 name 属性,但是 private 的,所以可以调用 setName() 方法为线程设置名字,通过 getName() 就知道是哪个线程在运行。

  • 线程类的 run() 方法是系统调用 start() 后自动执行的,编程时不需要调用 run() 方法。但永远无法知道系统在什么时刻调用,是立即调用,还是延迟一小段时间调用,都由系统自动决定,无法干预。不仅不能确定系统什么时刻调用 run() 方法,也不能确定调用顺序一定是代码中 start() 方法的书写顺序。

实现 Runnable 接口

  • 因为 Java 是单继承的,只允许继承一个类,继承 Thread 类定义多线程程序后无法再继承其它类,可扩展性大大降低,所以定义多线程程序,优先采用另一种方式:实现 java.lang.Runnable 接口。

  • Runnable 接口中只有一个待实现的 run() 方法,要自己补充属性。

  • 无论是 Thread 类还是 Runnable 接口,run() 方法都是系统 适时 自动 执行的

    Thread.sleep()这个静态方法仍然可以用

  • 实现了 Runnable 接口的线程类,还需要包装在 Thread 类的实例中运行

    Person person1 = new Person();
    person1.setName("张三");
    Thread thread1 = new Thread(person1);
    
  • Thread 实例(new Thread(person1))相当于调度器,触发线程任务执行,线程类的实例(new Person())就相当于任务。任务是不能自己启动的,需要被调度。

线程安全

  • Thread.currentThread() 返回当前正在运行的线程的实例对象

  • 多个线程 操作 同一个资源 的时候,发生了冲突的现象,就叫做 线程不安全

  • 在 Java 中,可以用 synchronized 关键字来解决余量错乱的问题。

    synchronized 不能作用于变量

  • synchronized 也叫线程 同步锁 ,表示此方法是锁定的,同一时刻只能由一个线程执行此方法。

  • 因为方法加锁,同时只有一个线程竞争成功能继续执行,其它很多线程是持续等待、响应慢的,所以 synchronized 不能滥用

  • synchronized使用场景:

    • 写操作的场景。例如用户修改个人信息、点赞、收藏、下单等。
    • 尽量精确锁住最小的代码块,把最关键的写操作抽象成独立的方法加锁。不建议给大段的方法加锁。

悲观锁和乐观锁

  • 乐观锁:

    • 乐观锁其实是不上锁,总是保证基于最新的数据进行更新。由于没有上锁,就提高了性能。不上锁的思想是乐观的,所以称之为乐观锁。AtomicInteger 类的 incrementAndGet()decrementAndGet() 方法就是典型的乐观锁实现。

      decrementAndGet() 是三个操作的组合,多线程情况下也不会出现数值重复的错误,但是可能会出现负值的情况。

      img
    • 不适用于多条数据需要修改、以及多个操作的整体顺序要求很严格的场景,乐观锁适用于读数据比重更大的应用场景

  • 悲观锁:

    • synchronized 关键字是把整个方法执行前就上锁,假设 其他线程 一定会修改 数据,所以提前防范。上锁的思想是悲观的,所以称之为悲观锁。
    • 适合写数据比重更大的应用场景。一般来说写数据的整体消耗时间更长些,是可以接受的。
  • 1

并发容器

  • CompletableFuture 是一个异步任务编排、调度框架,以更优雅的方式实现组合式异步编程。

    studentList.forEach(s -> {
          CompletableFuture.supplyAsync(
              // 每个学生都注册学号
              () -> reg.regId(s)
            )
            // 学号注册完毕后,打印欢迎消息
            .thenAccept(student -> {
              System.out.println("你好 " + student.getName() + ", 欢迎来到春蕾中学大家庭");
            });
        });
    
  • 系统会自动优化,把作为 supplyAsync() 方法参数的整个 () -> reg.regId(s) 表达式语句包装在另一个对象中;这个对象是 JDK 内置的,它实现了 Runnable 接口,在这个对象中执行表达式语句。

  • supplyAsync() 方法的作用是:在一个单独的线程中执行 reg.regId(s) 语句,本质上就是多线程编程。

  • 使用 thenAccept() 方法完成后继的任务步骤,系统会在前一个任务完成后,自动执行 student -> {} 后继任务。所以本质上,后继任务也是多线程方式执行的。

    image-20220414205115419
  • supplyAsync() 用于开头thenAccept() 用于末尾,各自调用一次即可。中间有多个步骤,可以调用多次 thenApply()

  • CompletableFuture.allOf() 是静态方法,作用是收集所有的任务实例对象,再调用类方法 get(),其作用就是等待所有的任务线程(allOf() 收集的)都执行完毕。

    // 等待所有的线程执行完毕
      CompletableFuture.allOf(cfs.toArray(new CompletableFuture[] {})).get();
    

线程池

  • 所谓 线程池 ,顾名思义,就像一个池子,里面装满了线程,随用随取。线程可以被 复用,一个线程,可以执行 A 任务,也可以执行 B 任务,于是线程不再频繁创建和销毁。

    new Thread(register) 意味着一个线程对象只能执行一个任务,而线程池让线程与任务分离,不再紧密绑定。

  • 创建线程池

    // 线程工厂
    private static final ThreadFactory namedThreadFactory = new BasicThreadFactory.Builder()
        .namingPattern("xxx-pool-%d")//线程名称模板
        .daemon(true)
        .build();
    
      // 等待队列
      private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(1024);//1024表示能排队的任务个数
    
      // 线程池服务
      private static final ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(
    20,
    200,
    30,
    TimeUnit.SECONDS,
    workQueue,
    namedThreadFactory,
    new ThreadPoolExecutor.AbortPolicy()
    );
    
  • BasicThreadFactory 需要依赖一个库

    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.10</version>
    </dependency>
    
  • ThreadPoolExecutor 构造函数参数

    参数序号解释
    1线程池初始化核心线程数量,一般是两位数,通常不大
    2线程池最大线程数,计算机性能强就大一些,否则小一些,通常不超过 200
    3线程池中的线程数超过核心线程数时,如果一段时间后还没有任务指派,就回收了。想立即回收就填 0,一般 30
    4第三个参数的时间单位。30 + TimeUnit.SECONDS 表示 30 秒
    5等待队列实例,已经创建过了
    6线程工厂实例,已经创建过了
    7任务太多,超过队列的容量时,用什么样的策略处理。一般用 AbortPolicy 表示拒绝,让主程序自己处理
  • 使用线程池运行任务:只要执行线程池对象的 execute() 方法,把实现了 Runnable 接口的实例对象传入即可。

  • 等待线程池执行:使用 main() 函数运行多线程程序,往往会遇到一个问题:多线程程序还没有运行完,main() 函数就退出了。可以使用Thread.sleep();进行等待

线程池与并发容器

  • 实际上,CompletableFuture 内部也用到了线程池,它是把任务放入内部的默认线程池里执行的。CompletableFuture 也可以指定线程池来运行任务:supplyAsync() 方法可以有第二个参数,传入我们构建好的线程池对象。那么任务就是用指定的线程池而不是默认的。
  • 当发现任务执行慢、任务堆积的问题时,就要考虑指定线程池,并调整线程池参数。

Comment