并发编程

171 阅读23分钟

Lambda表达式

lambda表达式为Java8的新特性可以让代码更加简洁,易于阅读和维护

基本结构:f->{ }

在这里插入图片描述

lambda表达式在功能上等同一个匿名方法,f变量的类型是系统自识别的,所以Lambda表达式要配合上下文,和其他方法配合使用,而不是一个独立的语句,等同于:

public void print(f) {
  System.out.println(f.getName());
}

举个栗子

List<Animal> animals = Arrays.asList(
            new Animal("Wolf"),
            new Animal("Dog"),
            new Animal("Cat"),
            new Animal("Cattle"),
            new Animal("Chicken")
    );

打印动物的名字:

for (int i = 0; i < animals.size(); i++) {
  Animal a = animals.get(i);
  System.out.println(a.getName());
}

使用Lambda:

animals.forEach(f->{
      System.out.println(f.getName());
    });

Collections中sort()方法的排序功能,使用Lambda表达式优化

List<Student> students = new ArrayList<Student>();
students.add(new Student(111, "bbbb", "london"));
students.add(new Student(131, "aaaa", "nyc"));
students.add(new Student(121, "cccc", "jaipur"));

// 实现升序排序
Collections.sort(students, (student1, student2) -> {
  // 第一个参数的学号 vs 第二个参数的学号
  return student1.getRollNo() - student2.getRollNo();
});

students.forEach(s -> System.out.println(s));

多参数

箭头(->)前表示参数变量,有多个参数的时候,必须使用小括号(student1, student2) -> {}

无参数

箭头(->)前表示参数变量,没有参数的时候,必须使用小括号() -> {}

单条执行语句

箭头后执行的语句只有一条时,可以不加{}s -> System.out.println(s);

有类型参数使用

animals.forEach((Animal a) -> {
  System.out.println(a.getName());
});

Lambda表达式{ }内执行的代码还可以引用外部的变量

  1. 规范一:引用的局部变量不允许被修改,即使写在表达式后面也不行,因为Lambda表达式引用的局部变量具备final的特性,初始化后不允许被修改
  2. 规范二:参数不能与局部变量同名

双冒号操作符

双冒号::这是Lambda写法

List<String> names = Arrays.asList("zhangSan", "LiSi", "WangWu");
names.forEach(n -> {
  System.out.println(n);
});

简化后:

names.forEach(System.out::println);

使用::时,系统每次遍历取得的元素,会自动作为参数传递给System.out.println()方法打印输出;System.out::println等同于n -> {System.out.println(n);}

在这里插入图片描述

不同用法

静态方法调用

则使用LambdaTest::print代替f -> LambdaTest.print(f)

非静态方法调用

print()方法不再标识为static,于是需要实例对象来调用

animals.forEach(new LambdaTest()::print);

多参数

Collections.sort(students, (student1, student2) -> {
  // 第一个参数的学号 vs 第二个参数的学号
  return student1.getRollNo() - student2.getRollNo();
});

如果遇到了多参数的情况,把比较过程定义成一个方法

private static int compute(Student student1, Student student2) {
	return student1.getRollNo() - student2.getRollNo();
}

排序过程则可以简写为:

Collections.sort(students, SortTest::compute);

系统会自动获取上下文的参数,按照定义的顺序传递指定的方法

案例如下:

public class Student{
    private int no;
    private String name;

    public int getNo() {
        return no;
    }

    public void setNo(int no) {
        this.no = no;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Student(int no, String name) {
        this.no = no;
        this.name = name;
    }

    @Override
    public String toString() {
        return "Student{" +
                "no=" + no +
                ", name='" + name + '\'' +
                '}';
    }
}
import java.util.*;

/**
 * @author peiqi
 * @date 2022/4/917:13
 */
public class LambdaDemo {

    public static void main(String[] args) {
        List<Student> students = new ArrayList<>();
        students.add(new Student(111, "bbbb"));
        students.add(new Student(131, "aaaa"));
        students.add(new Student(121, "cccc"));

        Collections.sort(students, LambdaDemo::compute);
        students.forEach(System.out::println);
    }
    private static int compute(Student student1, Student student2) {
        return student1.getNo() - student2.getNo();
    }

}

执行结果:

在这里插入图片描述

父类方法

::语法也可以用super关键字调用父类的非静态方法

import java.util.Arrays;
import java.util.List;

public class LambdaTest extends LambdaExample {
    public static void main(String[] args) {
        List<Student> students = Arrays.asList(
                new Student(1,"李华"),
                new Student(2,"李雷"),
                new Student(3,"小明")
        );

        LambdaTest at = new LambdaTest();
        at.print(students);
    }

    public void print(List<Student> students){
        students.forEach(super::print);
    }
}

class LambdaExample {
    public void print(Student s){
        System.out.println(s.getName());
    }
}

Stream API

Stream(流)主要优点是提升开发效率,使代码更加简洁干净,主要作用是对集合中的数据进行各种操作增强集合对象的功能

流迭代

创建流

  1. 直接创建
import java.util.stream.Stream;

Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
  1. 由数组转化
String[] fruitArray = new String[] {"苹果", "哈密瓜", "香蕉", "西瓜", "火龙果"};
Stream<String> stream = Stream.of(fruitArray);
  1. 由集合创建
List<String> fruits = new ArrayList<>();
fruits.add("苹果");
fruits.add("哈密瓜");
fruits.add("香蕉");
fruits.add("西瓜");
fruits.add("火龙果");
Stream<String> stream = fruits.stream();

流的迭代也是由foreach()方法但和集合中的foreach不一样(遍历数据是一样的)

流数据的过滤

filter() 方法

功能为对流中的数据进行过滤,方法参数是一个Lambda表达式,箭头后是条件语句,判断数据需要符合的条件也就是使用Lambda表达式告诉过滤器,需要那些符合条件的数据。

在这里插入图片描述

比如过滤分数低于80分且无违规记录的学生

public class Pupil {
    private String name;
    // 平均分
    private int averageScore;
    // 违规次数
    private int violationCount;
}
List<Pupil> pupils = new ArrayList<>();
// 这里假设小学生数据对象已经存入了
pupils.stream()
    .filter(pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1)
    .forEach(pupil -> {System.out.println(pupil.getName());});

这里的Lambda表达式略有不同,因为是条件语句所以表达式放在()而不是{}中

流数据映射

对于一组数字计算每个数字的平方数并输出

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(1,2,3,4);
        numbers.stream()
                .map(num -> num * num)
                .forEach(System.out::println);
    }

在这里插入图片描述

流中的map()方法就是映射,作用是用新的元素替换掉原来相同位置的元素,相当于每个对象都经历过依次转换

在这里插入图片描述

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

在这里插入图片描述

优点:映射后的对象类型可以与原始的不一致提供了灵活性和扩展性

流数据排序

使用Stream API实现学生学号排序:

List<Student> students = new ArrayList<Student>();
students.add(new Student(111, "bbbb"));
students.add(new Student(131, "aaaa"));
students.add(new Student(121, "cccc"));

students.stream()
    // 实现升序排序
    .sorted((student1, student2) -> {
        return student1.getNo() - student2.getNo();
    })
    .forEach(System.out::println);

这里的student1指的是后一个元素,student2是前一个元素

流数据的摘取

对于一组数字,找出最大的前3个数字

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);
        numbers.stream()
                .sorted((n1, n2) -> n2 - n1)
                .limit(3)
                .forEach(System.out::println);
    }

img

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

流的设计思想

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

在这里插入图片描述Stream的显著特点是:编程的重点不再是对象的运用而是数据的计算,Stream的特征为:函数式风格,弱化了面向对象的严格,完整的语法重心变为通过函数完成数据计算

在这里插入图片描述

并行数据

流合并

对1-10的十个正整数求和:

List<Integer> numbers = Arrays.asList(1, 2, 3, 4, 5, 6, 7, 8, 9, 10);
int sum = numbers.stream()
    .reduce((a, b) -> a + b)
    .get();

System.out.println("1-10求和 : " + sum);

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

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

reduce()方法的参数:

  • 方法参数a在第一次执行计算语句时,指代流的第一个元素,然后充当缓存作用存放本次计算结果,此后执行计算语句时,a就是上一次的计算结果并继续充当缓存存放本次计算结果
  • b第一次执行是指代第二个元素,此后依次指代流的每个元素

在这里插入图片描述

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

reduce()方法还可以操作对象,但第一个对象会充当缓存角色,正确性被破坏了,所以可以自己new一个对象充当缓存角色而不是使用流中的原始对象:

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

System.out.println(result.getName() + " - " + result.getMidtermScore());

这样reduce()方法参数变成了2个:

  • 第一个参数,是作为缓存角色的对象
  • 第二个参数,是Lambda表达式完成计算
    • 此时a变量不在是指代流中第一个元素了,专门指代缓存角色的对象(即第一个new的对象)
    • b依次指代流的每个元素(包括第一个)

在这里插入图片描述

reduce()返回值变为了缓存角色的对象即第一个参数,也不再需要使用get()

流收集

对一组数字找出前三个数字放入一个新的集合中,用-组合成字符串打印

public static void main(String[] args) {
        List<Integer> numbers = Arrays.asList(3, 2, 2, 7, 63, 2, 3, 5);
        List<String> numResult = numbers.stream()
                .sorted((n1, n2) -> n2 - n1)
                .limit(3)
                .map(a -> "" + a)
                .collect(Collectors.toList());

        String string = String.join("-", numResult);
        System.out.println("字符串是: " + string);
    }

collect()方法作用就是收集元素,Collectors.toList()将元素存入List集合,所以返回值就是List类型,这里map()将整数映射成字符串,所以collect()方法返回类型就是List<String>

java.util.stream.Collectors是流工具包中提供的收集器

并行流

Stream API的设计类似于管道,管道的显著特点就是每个节点依次执行,下一个节点必须等待上一个节点执行完毕,这种执行方式一般叫做串行

如果计算过程越来越复杂,数据量越来越大,串行的效率就会越来越低,这种模式无法发挥多核CPU的优势

所以可以把串行计算模式改为并行计算模式,并行就是利用多线程变成同时执行,多线程可以充分发掘多核CPU的优势。

使用并行流的方法就是调用parallelStream()即可

在这里插入图片描述

并行流的性能意外

由于并行计算使用了多线程,线程输出的时机有CPU动态决定的,无法确定,所以当流中的每个数据元素之间有逻辑依赖关系时,不适合使用并行计算。

并行计算模式的性能不是任何情况都优于串行模式

  • 硬件太差:CPU核数很低时并行计算模式不一定更好
  • 任务简单:数据量小任务简单,并行模式的多线程管理会消耗CPU内存等资源

常用设计模式

学习设计模式有助于提升代码质量(重用代码,易阅读,易维护,可靠性,扩展性等),也有助于提升设计能力

单例模式

保证一个类仅有一个实例,比如现实生活中地球只有他一个,核心办法就是把构造方法设置为私有的,只有自己才能实例化自己

/**
 * @author peiqi
 * @date 2022/4/118:43
 */
public class Earth {
    private int area;
    private String radius;

    //唯一实例
    private static Earth instance = new Earth();
    private Earth() {
    }

    // 外部类可以通过这个方法访问唯一的实例
    public static Earth getInstance(){
        return instance;
    }
}

必须使用static修饰符,否则会造成死递归的错误

在这里插入图片描述

饿汉与懒汉模式

  1. 饿汉式单例:在定义开始便实例自己
public class Singer{
   private static Singler singler = new Singler();

   private Singler(){}

  public static Singler instance(){
     return singler;
  }
}

  1. 懒汉单例:在第一次调用时实例化自己
public class Singer{
   private static Singler singler = null;

   private Singler(){}

  public static Singler instance(){
     if(singler==null){
        singler = new Singler()
     }
     return singler;
  }
}

区别:

  • 线程安全:
    • 饿汉式天生线程安全,可以直接用于多线程而不会出现问题。
    • 懒汉式本身非线程安全,需要人为实现线程安全。
  • 资源加载和性能:
    • 饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,造成内存泄漏,但相应的,在第一次调用时速度也会更快。
    • 而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。

有时候从技术角度出发为了节省系统资源时,也会使用单例模式,比如在spring任何自动注入实例对象,默认只有一个实例对象

简单工厂模式

程序中的工厂是生产实例对象的地方

为了实现不同条件创建不同对象的需求,核心问题是:

  1. 减少代码重复,减少相同逻辑代码
  2. 降低耦合紧密,减少代码之间的相互影响

实现简单工厂:

  1. 从具体的产品类抽象出接口,java面向接口编程,所以工厂应该生产一种产品而不是一个产品
  2. 把生产实例对象的过程收敛到工厂类中实现

UML图

img

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;
    }
}

工厂主要的优点在于职责明确,餐馆和甜品店只需要告诉工厂口味,就可以获得工厂的水果,但又不需要知道具体是什么水果,一般工厂命名为XXXFactory,这样的辨识度较高易于理解,重点在于明确什么条件下创建什么实例对象。

抽象工厂模式

对于一批,多种类型的对象需要创建时,使用抽象工厂模式,简单工厂主要是把多个产品抽象,使用一个工厂统一创建,抽象工厂则是把多个工厂进一步抽象

UML图

在这里插入图片描述

就是进一步抽象工厂接口(SnackFactory)多出一个SnacksFactoryBuilder

  1. 工厂的接口

规定工厂应该提供什么样的产品,包含了所有工厂的方法

public interface SnacksFactory {
    // 取得水果
    public Fruit getFruit(Customer customer);
    // 取得饮料
    public Drink getDrink(Customer customer);
}
public class FruitFactory implements SnacksFactory {
    public 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;
    }
//水果工厂不提供饮料
    public Drink getDrink(Customer customer) {
        return null;
    }
}
  1. 工厂的工厂

工厂用来生成产品实例,生产工厂的工厂用于生成工厂实例

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;
    }
}

SnacksFactoryBuilderbuildFactory()方法并不是static的,在复杂场景下尽量不要使用static方法

工厂模式结合spring工程

在工厂中减少定义static方法是为了在使用Spring框架时,可以为FactoryBuilder加上@Component注解让框架管理实例,同样任何需要使用工厂的地方只需要使用@Autowired注解让框架自动注入实例即可,这样可以让工厂模式的代码与Spring互为一体,扩展性更好,易于维护。

观察者模式

订阅和通知这种场景比较适合观察者模式

把需要观察的对象类也就是会改变的类继承Observable类,继承了就表示是核心的,需要观察的类和以往的模型设计不一样,需要观察的类要去掉全部属性的setter方法。

Observable提供的setChanged()方法就是标记被观察者对象发送了变化,notifyObservers()就是发出通知,接受通知的类就叫做观察者,观察者需要实现Observer接口,表示为观察者

 @Override
 public void update(Observable o, Object arg) {

}
  1. 第一个参数就是被观察者对象
  2. 第二个参数就是额外的信息,具体就是调用super.notifyObservers()是传入的参数对象,传入什么对象,arg的值就是什么对象

update()方法的作用就是接受通知,系统在super.notifyObservers()发出通知后,及调用了所有的观察者的update()方法

在这里插入图片描述

并发编程

学习多线程是为了更好的优化程序,提升程序整体的运行效率

继承Thread类

继承Java的Thread类实现线程类,需要重写父类的run()方法

public class Person extends Thread {
    @Override
    public void run() {
        try {
            System.out.println(getName() + " 开始取钱");
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(getName() + " 取钱完毕");
    }
}

线程类的作用就是完成一段相对独立的任务,这里使用Thread.sleep(200)模拟取钱的过程。sleep()方法就是要线程睡眠,暂时不再继续执行交出CPU,让CPU去执行其他的任务,sleep方法参数是毫秒数

运行线程

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

public class Bank {
    public static void main(String[] args) {
        Person thread1 = new Person();
        thread1.setName("张三");

        Person thread2 = new Person();
        thread2.setName("李四");

        thread1.start();
        thread2.start();
    }
}

img

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

线程类的run()方法是系统调用start()后执行的,编程不需要调用run方法但无法知道系统啥时候调用

实现Runnable接口

由于Java是单继承,继承了Thread类后程序可扩展性大大降低所以优先考虑实现java.lang.Runnable

张三李四去取钱

public class Person implements Runnable {
    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    @Override
    public void run() {
        try {
            System.out.println(name + " 开始取钱");
            Thread.sleep(200);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(name + " 取钱完毕");
    }
}

无论是Runnable接口还是Thread类,run()方法都是系统适时自动执行的,实现了Runnable接口的线程类,还需要包装到Thread类的实例中

public class Bank {
    public static void main(String[] args) {
        Person person1 = new Person();
        person1.setName("张三");
        Thread thread1 = new Thread(person1);

        Person person2 = new Person();
        person2.setName("李四");
        Thread thread2 = new Thread(person2);

        thread1.start();
        thread2.start();
    }
}

在这里插入图片描述

Thread实例(new Thread(person1))相当于调度器,触发线程任务执行,任务是不能自己启动的,需要调度

在这里插入图片描述

线程安全

车票类

车票类的主要作用是控制车票的总数,每卖一次,票数减一

public class Ticket {
    private int count = 30;

    public void sell() {
        if (count > 0) {
            count--;
        }
        System.out.println(Thread.currentThread().getName() + ":卖出一张,还剩下 " + count + " 张票");
    }

    public int getCount() {
        return count;
    }
}

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

售票窗口类

售票窗口就是线程类,以多线程的方式售票。售票简单来说就是循环减一即可。当然了,票数为·的时候不能再售卖了。这里假定打印票据需要点时间,售卖一张票休息100毫秒。

public class TicketWindow implements Runnable {
    private Ticket ticket;

    public TicketWindow(Ticket ticket) {
        this.ticket = ticket;
    }

    @Override
    public void run() {
        while (ticket.getCount() > 0) {
            try {
                Thread.sleep(100);
                ticket.sell();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

火车站类

火车站有4个窗口所以启动4个线程

public class TrainStation {
    public static void main(String[] args) {
        Ticket ticket = new Ticket();

        for (int i = 1; i <= 4; i++) {
            TicketWindow office = new TicketWindow(ticket);
            Thread thread = new Thread(office);
            thread.setName("售票窗口" + i);
            thread.start();
        }
    }
}

此时运行代码会发现问题,余量错乱甚至相同

在这里插入图片描述

问题分析:这种多个线程运行同一个实例对象(ticket)的情况下,修改了同一个变量(调用sel1()方法同时执行count--语句),后果是不可预料的。所以会出现余量打印错乱甚至相同的情况。

j5-5-4-2.svg

解决办法

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

在Java中使用synchronized关键字来解决余量错乱的问题:

/**
 * @author peiqi
 * @date 2022/4/119:36
 */
public class Ticket {
    private int count = 30;

    public synchronized void sell() {
        if (count > 0) {
            count--;
        }
        System.out.println(Thread.currentThread().getName() + ":卖出一张,还剩下 " + count + " 张票");
    }

    public int getCount() {
        return count;
    }
}

synchronized也叫线程同步锁,表示此方法是锁定的,同一时刻只能由一个线程执行此方法,线程一次都在等待竞争下一个锁,synchronized相当于保护了关键方法,不允许同时执行,必须一个执行

img

synchronized使用场景

使用synchronized的方法意味着满足了两个线程的安全的特性:

  • 原子性:方法全部执行并且执行的过程不会被任何的因素打断
  • 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

synchronized使用场景:

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

悲观锁和乐观锁

java.util.concurrent是java系统提供的并发编程包,使用java.util.concurrent.atomic.AtomicInteger解决车票余量问题

import java.util.concurrent.atomic.AtomicInteger;

public class Ticket {
    private AtomicInteger count = new AtomicInteger(30);

    public void sell() {
        int newCount = 0;
        if (count.get() > 0) {
            newCount = count.decrementAndGet();
        }
        System.out.println(Thread.currentThread().getName() + ":还剩下 " + newCount + " 张票");
    }

    public int getCount() {
        return count.get();
    }
}

此时sell()不再加锁,解决了车票余量重复的问题,AtomicInteger 虽然是一个类,但等同于一个整数(就像 Integer 是 int 的对象)。

AtomicInteger提供了不使用synchronized就可以保证数据操作的原子方法,如decrementAndGet():

在这里插入图片描述

他是三个操作的组合,多线程情况下也不会出现数值重复的错误,保证了数据的正确性,这就是原子性的含义。

线程间都是基于最新结果进行减一的运算,所以不会重复,这样是可见性的体现

但因为sell方法不加锁多条语句执行时可能被其他的线程打断,所以要给sell()整体加上synchronized才能保证多条语句整体的原子性。

AtomicInteger特点

incrementAndGet()decrementAndGet()都没有加synchronized

这就意味着,递增、递减等方法虽然也是多个步骤,但多线程场景下,其它线程不会等待。只是在数据变化的时候,会判断一下是否有其它线程修改了数据,如果有就根据最新的值进行修改。这就是乐观锁。

乐观锁

乐观锁就是不上锁,总是保证基于最新的数据进行更新,由于没有上锁,就提高了性能,如AtomicInteger类的incrementAndGet()decrementAndGet()方法就乐观锁

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

悲观锁

假设其他线程一定会修改数据,所以提前提防,上锁的思想是悲观的所以称之为悲观锁

适用于写数据比重更大的应用场景.

并发容器

场景:多个任务有前后的顺序,但后继任务不必等待所有前置的任务全部完成后在执行,而是每个前置任务完成后,自动执行对应的后继任务,在这种场景下适合用CompletableFuture特性来解决

CompletableFuture

方法调用时,需要等待返回取得返回值就是同步,不等待而继续执行任务就是异步。采取异步方式能够支持多个任务并行执行,这种机制称为并发

img

  1. Register类重构,不必实现多线程接口
import java.util.concurrent.atomic.AtomicInteger;

public class Register {
  private static AtomicInteger count = new AtomicInteger(0);

  // 注册学号
  public Student regId(Student student) {
    student.setId(count.incrementAndGet());
    return student;
  }
}
  1. 并行注册
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.CompletableFuture;

public class StudentIDTest {
  public static void main(String[] args) {
    // 构建学生集合
    List<Student> studentList = new ArrayList<>();
    for (int i = 1; i <= 10; i++) {
      Student s = new Student();
      s.setName("学生" + i);
      studentList.add(s);
    }

    Register reg = new Register();

    studentList.forEach(s -> {
      CompletableFuture.supplyAsync(
          // 每个学生都注册学号
          () -> reg.regId(s)
        )
        // 学号注册完毕后,打印欢迎消息
        .thenAccept(student -> {
          System.out.println("你好 " + student.getName());
        });
    });

    System.out.println("mission complate");
  }
}
  • CompletableFuture.supplyAsync()方法运行一个异步任务并返回结果,所以里面的方法必须要有返回值
  • supplyAsync()方法参数的整个() -> reg.regId(s)表达式语句包装在另一个对象中,它实现了Runnable接口
CompletableFuture.supplyAsync(
          // 每个学生都注册学号
          () -> reg.regId(s)
        )
         // 学号注册完毕后,打印欢迎消息
        .thenAccept(student -> {
          System.out.println("你好 " + student.getName());
        });

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

注册完后使用thenAccept()方法完成后继任务步骤,thenAccept()方法的参数(sutdent)就是前置任务返回的结果,自动执行student->{}后继任务,后继任务本质上也是多线程方式执行的,thenAccept()方法通常用于任务链末尾.

在这里插入图片描述

多步骤任务

supplyAsync()用于开头,thenAccept()用于末尾,各自调用一次即可,中间有多个步骤,可以调用多次thenApply(),由于末尾也要用到之前的实例对象,所以中间的thenApply方法总是要return实例,否则下一个步骤就获取不到:

在这里插入图片描述

  • 多个任务之间是并行的,使用多线程同时执行多个任务
  • 对于一个任务的多个步骤,是串行的,必须先执行完前一个步骤才能执行下一步骤

返回值

supplyAsync(),thenApply(),thenAccept()返回的是CompletableFuture实例对象,可以设置返回值:

CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))
  .thenApply(student -> {
    return dis.assignClasses(student);
  })
  .thenAccept(student -> {
     System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());
  });

定义变量的类型就是CompletableFuture,可以使用泛型CompletableFuture<>表示其中包含的数据类型具体是什么类型.

如果没有调用thenAccept()方法结尾:

CompletableFuture.supplyAsync(() -> reg.regId(s))
  .thenApply(student -> {
    return dis.assignClasses(student);
  });

因为thenApply()lambda表达式返回的是Student对象,所以CompletableFuture实例对象包含的是Student数据,所以泛型返回的是CompletableFuture<Student>

这几个方法返回的CompletableFuture实例包含什么数据类型取决于Lambda表达式返回值的类型

main()方法的问题

List<CompletableFuture> cfs = new ArrayList<>();
studentList.forEach(s -> {
  CompletableFuture<Void> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))
    .thenApply(student -> {
        return dis.assignClasses(student);
    }).thenAccept(student -> {
        System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());
    });

  cfs.add(cf);
});

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

CompletableFuture.allOf()是静态方法作用是收集所有的任务实例对象,allOf()方法只支持数组不支持集合,所以要转换cfs.toArray(new CompletableFuture[] {}),再调用类方法get(),作用就是等待所有的任务线程(allOf()收集)再执行。

在SpringBoot等服务器运行supplyAsync()异步任务编排的时候,就**没有必要使用get()**方法等待所有线程任务执行完毕,服务器往往是常驻程序,不是main()方法执行完就退出程序

get()方法造成main()方法等待,所以是同步的,通过CompletableFuture编排的任务,不会造成main()方法等待这就是异步。

安全布尔值的包装类

java.util.concurrent.atomic.AtomicBoolean 能够以原子的方式操作布尔值

  • new AtomicBooolean(true) 就是true
  • new AtomicBooolean(false) 就是false

取得布尔值使用get()方法

AtomicBoolean ab = new AtomicBoolean(true);
boolean value = ab.get();

实例对象调用compareAndSet()方法就能以原子的方式修改值,compareAndSet(true,false)判断当前值为true时,修改为false,然后返回成功或失败,这是三个步骤

  • 修改成功返回true
  • 修改失败返回false

compareAndSet()方法返回值与参数数值无关

线程池

为了优化做到复用Thread对象,不必每次都创建新对象,可以使用java的线程池

线程池顾名思义像一个池子,里面装满了线程,线程可以被复用,一个线程可以执行A任务也可以执行B任务,线程不必频繁的创建和销毁,线程池也是**有限大的,**其中的线程数也是有限的,能同时运行的任务数也是有限的.

创建线程池的核心代码(基本上属于固定写法):

import org.apache.commons.lang3.concurrent.BasicThreadFactory;
import java.util.concurrent.*;

public class StudentIDTest {

    // 线程工厂
    private static final ThreadFactory namedThreadFactory = new BasicThreadFactory.Builder()
            .namingPattern("studentReg-pool-%d")
            .daemon(true)
            .build();

    // 等待队列
    private static final BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<Runnable>(1024);

    // 线程池服务
    private static final ThreadPoolExecutor EXECUTOR_SERVICE = new ThreadPoolExecutor(
            20,
            200,
            30,
            TimeUnit.SECONDS,
            workQueue,
            namedThreadFactory,
            new ThreadPoolExecutor.AbortPolicy()
    );

    public static void main(String[] args) {
        // 构建学生集合
        for (int i = 1; i <= 2000; i++) {
            Student s = new Student();
            s.setName("学生" + i);
            Register register = new Register(s);
// 传入 Runnable 对象,运行任务
            EXECUTOR_SERVICE.execute(register);
        }
    }

这里的BasicThreadFactory需要依赖库:

<dependency>
  <groupId>org.apache.commons</groupId>
  <artifactId>commons-lang3</artifactId>
  <version>3.10</version>
</dependency>

创建线程工厂

new BasicThreadFactory.Builder()
  .namingPattern("studentReg-pool-%d")
  .daemon(true)
  .build();

namingPattern()方法是定义线程名字的格式,相当于线程名称模板这里的studentReg就是任务名称。

创建线程等待队列实例

性能较好CPU核数多内存大队列可以大一些:new LinkedBlockingQueue<Runnable>(2048)参数表示能排队的任务个数

创建线程池实例

ThreadPoolExecutor构造函数参数如下

在这里插入图片描述

多线程编程要防止线程数过多把系统崩溃,可以采取分批处理的思想

执行线程池对象的execute()方法把实现Runnable接口的实例对象传入即可

// 传入 Runnable 对象,运行任务
  EXECUTOR_SERVICE.execute(register);