Lambda表达式

时间:2024-05-10 21:19:50

   定义

基本结构:

f -> { }

lambda表达式在功能上等同于一个匿名方法。

f变量的类型系统会根据上下文自动识别。

所以,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);

如果代码比较复杂,而为了易阅读、易维护,也可以为参数变量指定类型。

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

引用外部的变量

 Lambda 表达式 {} 内的执行语句,除了能引用参数变量以外,还可以引用外部的变量。

List<Fruit> fruits = Arrays.asList(......);
String message = "水果名称:";

fruits.forEach(f -> {
  System.out.println(message + f.getName());
});

message = "";

 Lambda 表达式有规范:引用的 局部变量 不允许被修改。在表达式外实际值赋值一次的写法是允许的。

使用 Lambda 表达式还有一点需要注意:参数不能与 局部变量 同名。

双冒号操作符

names.forEach(n -> {
  System.out.println(n);
});

简化

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

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

::语法省略了参数变量。

静态方法调用:

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

非静态方法调用:

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

fruits.forEach(new LambdaTest()::print);
多参数
Collections.sort(students, SortTest::compute);

系统会 自动 获取上下文的参数,并按上下文定义的 顺序 传递给指定的方法。所谓 顺序 就是 Lambda 表达式 () 中的顺序。 

父类方法

super 关键字的作用是在子类中引用父类的属性或者方法。那么同样:: 语法也可以用 super 关键字调用父类的非静态方法。

  public void print(List<Fruit> fruits){
    fruits.forEach(super::print);
    }
}

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

 流Stream

相较于普通java,stream 的重点不再是对象运用而是数据的计算。

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

在java中Stream是一个接口,所以也有很多个实现类。

创建流

1。直接创建

Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");

2.由数组转化 

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

3.由集合转化 

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);
流数过滤 
List<Pupil> pupils = new ArrayList<>();
// 这里假设小学生数据对象已经存入了

pupils.stream()
    .filter(pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1)
    .forEach(pupil -> {System.out.println(pupil.getName());});
filter()法

 对流中的数据对象进行过滤。

pupil -> pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1

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

数据流操作的过程就好比一个有着多个节点的管道,每个节点完成一个操作,数据流输入这个管道,按照顺序经过各个节点,进行操作。

数据流映射

map()方法

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

numbers.stream()
    .map(num -> {
        return num * num;
    })
    .forEach(System.out::println);

最后用 return 语句返回的对象,就是转换后的对象。 

下面是同一个功能map 和filter的不同实现:

 map

  pupils.stream().map(pupil -> {
            if (pupil.getAverageScore() > 85 && pupil.getViolationCount() < 1) {
                pupil.setMessage(pupil.getName() + "同学您的成绩优秀,恭喜入围");
            } else if (pupil.getAverageScore() >= 80 && pupil.getViolationCount() < 1) {
                pupil.setMessage(pupil.getName() + "同学您的成绩优良,恭喜入围");
            } else {
                pupil.setMessage(pupil.getName() + "同学您未入围,再接再厉");
            }

            return pupil;
        }).forEach(pupil -> {
            System.out.println(pupil.getMessage());
        });

filter 

  pupils.stream().filter(pupil -> pupil.getViolationCount()>1||pupil.getAverageScore()<80).forEach(pupil -> {
            System.out.println(pupil.getName()+"同学您未入围,再接再厉");
        });
        pupils.stream().filter(pupil -> pupil.getAverageScore()>85&&pupil.getViolationCount()<1).forEach(pupil -> {
            System.out.println(pupil.getName()+"同学您的成绩优秀,恭喜入围");
        });


        pupils.stream().filter(pupil -> pupil.getAverageScore()>=80&&pupil.getAverageScore()<=85&&pupil.getViolationCount()<1).forEach(pupil -> {
            System.out.println(pupil.getName()+"同学您的成绩优良,恭喜入围");
        });

映射到新数据 

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

优点:
映射后的对象类型。可以与流中 的原始的对象类型不同。

数据流排序

sort方法:

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

 student1 指代后一个元素,student2 指代前一个元素

返回 非正数 表示两个相比较的元素需要 交换位置 ,返回正数则不需要。

流数据摘取

可以使用limit进行限制。

numbers.stream()
    .sorted((n1, n2) -> n2 - n1)
    .limit(3)
    .forEach(System.out::println);

流合并

int sum = 0;
for (int i : numbers) {
    sum += i;
}

System.out.println("sum : " + sum);

stream

import java.util.Arrays;

int sum = numbers.stream()
    .reduce((a, b) -> a + b)
    .get();

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

 reduce()方法的作用是合并所有的元素,终止计算出一个结果,终止也就意味着流不再流动。

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

get()方法返回值的类型,系统会根据流元素类型进行推断。

reduce的参数:

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

reduce也可以操作对象

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

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

但是由于第一个student对象充当缓存,导致其正确性被破坏,所以最好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());
  • 第一个参数,是作为缓存角色的对象
  • 第二个参数,是 Lambda 表达式,完成计算,格式是一样的。
    • 那么 a 变量不再指代流中的第一个元素了,专门指代缓存角色的对象,即方法第一个参数对象。
    • b 变量依次指代流的每个元素,包括第一个元素。
    • ab 职责非常清晰了。

 流收集

forEach()方法和reduce()方法都是流的终点。

如果流对集合进行计算后,想要将元素放到一个新的集合中,可以使用collect()方法

import java.util.stream.Collectors;

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

为了能够把最终结果转换为字符串打印,调用了 map() 方法把流中原来的整数映射为字符串("" + a),所以 collect() 方法的返回值类型就是 List<String>,而不是 List<Integer> 

collect() 方法的作用就是收集元素, Collectors.toList() 是一个静态方法,作为参数告诉 collect() 方法存入一个 List 集合。所以 collect() 方法的返回值类型就是 List

collect方法会将转换后的数据收集到一个新的列表中。

String.join()是一个静态方法,用于将一个字符串数组中的元素连接起来,返回一个新的字符串。可以指定连接的分隔符。

并行流

正如上文所述,管道的显著特点是,每个节点是依次执行的,下一个节点必须等待上一个节点执行完毕。这种执行方式,通常叫作 串行

但是串行的性能很难被优化,所以我们可以将串行改为并行计算。

所谓并行,就是利用多线程,变成同时执行。多线程可以充分发掘多核 CPU 的优势。

并行

stream()方法改为调用parallelStream()方法即可。

并行会将总任务分成多个子任务进行执行,执行的子任务的结果最终会被合并为我们所见到的所想要的最终的结果。

不适用并行的场景:

流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算。并且由于并行是多线程的使用,也会导致输出的顺序是无序的,每个线程独立输出数字,而线程的输出时机,是由 CPU 动态决定的,无法确定。

单例模式

(在Java中,单例模式是一种设计模式,用于确保某个类只有一个实例,并提供一个全局访问点。)

有时需要唯一的实例对象,为了保证一个类仅有一个实例,我们需要把构造函数设置为私有的。

也就意味着只有这个类自己才能实例化这个类。

这种可以保证只有一个实例对象的方式,就是 单例 设计模式。

但是我们还需要增加一个方法,允许其他类访问这个单例的实例。

public class ClassMaster {
  private String id;
  // 班主任名称
  private String name;
  private String gender;

  // 唯一实例
//私有的静态实例
  private static ClassMaster instance = new ClassMaster();
//私有的构造方法
  private ClassMaster() {
  }

  // 外部类可以通过这个方法访问唯一的实例
///公共的静态访问方法
  public static ClassMaster getInstance() {
    return instance;
  }
}
spring中的单例

类变量使用 @Autowired 注解,能够实现自动注入实例对象。

实际上,任何自动注入实例对象,默认只有一个实例对象,是单例的。

Spring 会保证只生成一个 UsersServiceImpl 实例,注入到多个 Service 或 Control 中,不会为每个 Service 或 Control 分别 new 出多个 UsersServiceImpl 实现类的实例。

public class Singleton {
    private volatile static Singleton instance;
    
    private Singleton() {}
    
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

工厂模式

生产实例对象,对代码重复,耦合紧密的代码进行实例。工厂模式可以帮助降低代码的耦合度,增加代码的灵活性和可维护性。

工厂模式:

  1. 从具体的产品类抽象出接口。记住 Java 强调面向接口编程。意味着工厂应该生产 一种 产品,不应该生产某 一个 产品。
  2. 把生产实例对象的过程,收拢到工厂类中实现。

工厂模式是一种设计模式,用于创建对象而不需要指定具体的类。在Java中,工厂模式通常分为三种类型:简单工厂模式、工厂方法模式和抽象工厂模式。

1.简单工厂模式:简单工厂模式是最基本的工厂模式,它通过一个工厂类来创建对象。客户端只需要传入一个参数,工厂类就可以根据参数的不同来创建不同的对象

public class SimpleFactory {
    public static Product createProduct(String type) {
        if ("A".equals(type)) {
            return new ConcreteProductA();
        } else if ("B".equals(type)) {
            return new ConcreteProductB();
        }
        return null;
    }
}

public interface Product {
    void doSomething();
}

public class ConcreteProductA implements Product {
    @Override
    public void doSomething() {
        System.out.println("Product A is doing something.");
    }
}

public class ConcreteProductB implements Product {
    @Override
    public void doSomething() {
        System.out.println("Product B is doing something.");
    }
}

public class Main {
    public static void main(String[] args) {
        Product productA = SimpleFactory.createProduct("A");
        productA.doSomething();
        
        Product productB = SimpleFactory.createProduct("B");
        productB.doSomething();
    }
}

工 厂方法模式:工厂方法模式是在简单工厂模式的基础上,将工厂类抽象成接口或抽象类,每个具体的产品都有对应的工厂类来创建。

public interface Factory {
    Product createProduct();
}

public class ConcreteFactoryA implements Factory {
    @Override
    public Product createProduct() {
        return new ConcreteProductA();
    }
}

public class ConcreteFactoryB implements Factory {
    @Override
    public Product createProduct() {
        return new ConcreteProductB();
    }
}

public class Main {
    public static void main(String[] args) {
        Factory factoryA = new ConcreteFactoryA();
        Product productA = factoryA.createProduct();
        productA.doSomething();
        
        Factory factoryB = new ConcreteFactoryB();
        Product productB = factoryB.createProduct();
        productB.doSomething();
    }
}

抽象工厂模式:抽象工厂模式是在工厂方法模式的基础上,将工厂类抽象成一个工厂接口,每个具体的工厂类可以创建一组相关的产品。 

public interface AbstractFactory {
    ProductA createProductA();
    ProductB createProductB();
}

public class ConcreteFactory1 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA1();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB1();
    }
}

public class ConcreteFactory2 implements AbstractFactory {
    @Override
    public ProductA createProductA() {
        return new ConcreteProductA2();
    }

    @Override
    public ProductB createProductB() {
        return new ConcreteProductB2();
    }
}

public interface ProductA {
    void doSomething();
}

public class ConcreteProductA1 implements ProductA {
    @Override
    public void doSomething() {
        System.out.println("Product A1 is doing something.");
    }
}

public class ConcreteProductA2 implements ProductA {
    @Override
    public void doSomething() {
        System.out.println("Product A2 is doing something.");
    }
}

public interface ProductB {
    void doSomething();
}

public class ConcreteProductB1 implements ProductB {
    @Override
    public void doSomething() {
        System.out.println("Product B1 is doing something.");
    }
}

public class ConcreteProductB2 implements ProductB {
    @Override
    public void doSomething() {
        System.out.println("Product B2 is doing something.");
    }
}

public class Main {
    public static void main(String[] args) {
        AbstractFactory factory1 = new ConcreteFactory1();
        ProductA productA1 = factory1.createProductA();
        productA1.doSomething();
        
        ProductB productB1 = factory1.createProductB();
        productB1.doSomething();
        
        AbstractFactory factory2 = new ConcreteFactory2();
        ProductA productA2 = factory2.createProductA();
        productA2.doSomething();
        
        ProductB productB2 = factory2.createProductB();
        productB2.doSomething();
    }
}

命名

 一般来说,工厂类命名为 XXXXFactory,以 Factory 作为后缀可以提高辨识度,易于理解这个类的作用。

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

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

工厂的工厂

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

工厂结合Spring工程

在使用 Spring 框架的时候,可以为 SnacksFactoryBuilder 加上 @Component 注解,可以让框架管理实例。

@Component
public class SnacksFactoryBuilder {
    public SnacksFactory buildFactory(String choice) {

    }
}

相应的,任何需要使用工厂的地方,只需要使用 @Autowired 注解让框架自动注入实例即可

@Service
public class XxxxxServiceImpl implements XxxxxService {

    @Autowired
    private SnacksFactoryBuilder snacksFactoryBuilder;
}

 观察者模式

观察者模式是一种行为设计模式,用于定义对象之间的一对多依赖关系,当一个对象的状态发生变化时,所有依赖于它的对象都会得到通知并自动更新。

在Java中,观察者模式通常通过两个接口来实现:Subject(主题)和Observer(观察者)。

Subject接口定义了注册、移除和通知观察者的方法,通常包括registerObserver()、removeObserver()和notifyObservers()等方法。

Observer接口定义了观察者对象的更新方法,通常包括update()方法。

具体的观察者类需要实现Observer接口,并在Subject对象中注册自己。当Subject对象的状态发生变化时,会调用notifyObservers()方法通知所有注册的观察者对象,并调用它们的update()方法进行更新。

观察者模式在Java中被广泛应用,例如Swing中的事件处理机制、JavaBeans中的属性变化通知等都使用了观察者模式来实现。

  @Override
    public void update(Observable o, Object arg) {
        System.out.print(this.name + "观察到天气变化为:");
        System.out.print(o.toString());
        System.out.println(" " + arg);
    }

 多线程

普通工序按顺序执行,称之为单线程。相当于一个进程中只有一个线程。

每个统筹方法工序,是一个个线程。统筹方法工序的工作模式,称之为多线程

如果没有多线程的话,一个个用户只能按照顺序访问首页,程序响应了张三的请求,才能响应李四的请求。越往后的用户越慢,这是无法接受的。

实现

1.线程类

继承Thread类实现线程类

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 类后,需要重写父类的 run() 方法,注意必须是修饰为 public void,方法是没有参数的。

加上 @Override 注解,会让系统自动检查 public void run() 方法定义有没有写错

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

 Thread.sleep(200) 模拟取钱的过程。sleep() 方法(注意是静态方法)的作用是让线程睡眠、暂时不再继续执行,交出 CPU,让 CPU 去执行其他的任务。sleep() 方法的参数是毫秒数,200 表示 200 毫秒,超过这段时间后再继续执行程序。

2.运行线程

线程需要调用 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();
    }
}

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

不仅不能确定系统什么时刻调用 run() 方法,也不能确定调用顺序一定是代码中 start() 方法的书写顺序。

实现Runnable接口

继承 Thread 类定义多线程程序后,缺点就比较明显了,无法再继承其它类,因为 Java 是单继承的,只允许继承一个类,这会导致程序的可扩展性大大降低。

所以定义多线程程序,优先采用第二种方式:实现 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 接口中只有一个待实现的 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))相当于调度器,触发线程任务执行,线程类的实例(new Person())就相当于任务。任务是不能自己启动的,需要被调度。 

  1. Runnable接口是一个功能接口,只包含一个run()方法,用于定义线程执行的任务。Thread类是一个具体的线程类,实现了Runnable接口,并且包含了其他线程操作方法,如start()、sleep()等。

  2. 实现Runnable接口可以避免单继承的局限性,一个类可以实现多个接口,但只能继承一个类。而Thread类是一个具体的类,只能继承一个类。

  3. 使用Runnable接口可以更好地实现线程的重用,多个线程可以共享同一个Runnable实例,而Thread类创建的线程每次都需要创建一个新的Thread实例。

  4. Runnable接口可以作为参数传递给Thread类的构造方法,实现了解耦,更好地实现了面向对象的设计原则。而Thread类直接继承Thread类,耦合性较高。

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

线程安全

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

在 Java 中,可以用 synchronized 关键字来解决余量错乱的问题。synchronized 加载方法上,紧跟着 public :

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

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

先判断 sell() 方法有没有被上锁:

  • 如果上锁,说明有其他线程正在调用 sell() 方法,必须等其他线程对 sell() 方法调用结束后才可以执行 sell() 方法;
  • 如果没有上锁,则执行 sell() 方法。开始执行时对此方法加锁,不允许其他线程执行,方法执行完毕后解锁。

乐观锁 

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

AtomicInteger 虽然是一个类,但等同于一个整数(就像 Integer 是 int 的对象)。调用 new AtomicInteger() 构造函数实例化对象的时候,可以指定任意的整数值。 

decrementAndGet()方法

decrementAndGet()方法是一个原子操作,用于将变量的值减1,并返回减1后的值。这个方法通常用于多线程环境中,确保对变量的修改是原子的,避免出现竞态条件。

但是由于条件判断语句、操作语句、打印信息语句组合起来,就可能不具备原子性,仍就可能出现负数,为了保证多条语句的整体的原子性仍需要使用synchronized

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

也可以通过 AtomicInteger的使用来保证没有重复情况

import java.util.concurrent.atomic.AtomicInteger;

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

    private Student student;

    public Register(Student student) {
        this.student = student;
    }

    @Override
    public void run() {
        student.setId(count.incrementAndGet());
        System.out.println("姓名:" + student.getName() + ",学号:" + student.getId());
    }
}

incrementAndGet() 也是不使用 synchronized 就能保证数据操作原子性的方法:

incrementAndGet()方法会取得当前的值并加一然后返回新值。

incrementAndGet() 和 decrementAndGet() 都没有加 synchronized 关键字说明递增、递减等方法虽然也是多个步骤,但多线程场景下,其它线程不会等待。只是在 数据变化 的时候,会 判断 一下是否有 其它 线程 修改 了数据,如果有就根据最新的值进行修改。

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

synchronized 关键字是把整个方法执行前就上锁,假设 其他线程 一定会修改 数据,所以提前防范。上锁的思想是悲观的,所以称之为悲观锁。

乐观锁

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

悲观锁

适合写数据比重更大的应用场景。

并发容器

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() 方法运行一个异步任务并且返回结果,所以 regId() 方法必须有返回值。

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

thenAccept() 方法的参数(student)就是前置任务的返回结果,系统会在前一个任务完成后,自动执行 student -> {} 后继任务。所以本质上,后继任务也是多线程方式执行的。thenAccept() 方法通常用于任务链的末尾。thenAccept() 方法的参数(student)就是前置任务的返回结果,系统会在前一个任务完成后,自动执行 student -> {} 后继任务。所以本质上,后继任务也是多线程方式执行的。thenAccept() 方法通常用于任务链的末尾。

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

supplyAsync() 用于开头thenAccept() 用于末尾,各自调用一次即可。中间有多个步骤,可以调用多次 thenApply() 。由于末尾也要用到 student 实例对象,所以位于中间的 thenApply() 方法

supplyAsync() 是静态方法,返回值是 CompletableFuture 实例对象,再调用 thenApply() 或 thenAccept() 实例方法,返回的也是 CompletableFuture 实例对象。

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

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

 当数据量较大致使并行程序未完全进行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[] {})) 

线程池

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

线程池并不是无限大的(因为计算机的CPU、内存等资源毕竟有限),所以线程池中存在的线程数也是 有限 的,这就意味着能同时运行的任务数是 有限 的,其它过剩的任务就需要 排队 。待任务完成、有空闲的线程后,才能继续执行任务。

创建线程池

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) {

  }
}

依赖库

<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” 改掉。

创建线程等待队列实例

new LinkedBlockingQueue<Runnable>(2048)。构造函数的参数表示能排队的任务个数。

new LinkedBlockingQueue<Runnable>(512)

new LinkedBlockingQueue<Runnable>(1024)

创建线程实例

ThreadPoolExecutor 构造函数参数较多。

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

使用线程池运行任务

public class StudentIDTest {
  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);
    }
  }
}

 等待线程池执行

使用 main() 函数运行多线程程序,往往会遇到一个问题:多线程程序还没有运行完,main() 函数就退出了。

我们可以将程序等待一段时间以此不使得程序退出。