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表达式{ }内执行的代码还可以引用外部的变量
- 规范一:引用的局部变量不允许被修改,即使写在表达式后面也不行,因为Lambda表达式引用的局部变量具备final的特性,初始化后不允许被修改
- 规范二:参数不能与局部变量同名
双冒号操作符
双冒号
::这是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(流)主要优点是提升开发效率,使代码更加简洁干净,主要作用是对集合中的数据进行各种操作增强集合对象的功能
流迭代
创建流
- 直接创建
import java.util.stream.Stream;
Stream<String> stream = Stream.of("苹果", "哈密瓜", "香蕉", "西瓜", "火龙果");
- 由数组转化
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();
流的迭代也是由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);
}
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依次指代流的每个元素(包括第一个)
- 此时a变量不在是指代流中第一个元素了,专门指代缓存角色的对象(即第一个
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修饰符,否则会造成死递归的错误
饿汉与懒汉模式
- 饿汉式单例:在定义开始便实例自己
public class Singer{
private static Singler singler = new Singler();
private Singler(){}
public static Singler instance(){
return singler;
}
}
- 懒汉单例:在第一次调用时实例化自己
public class Singer{
private static Singler singler = null;
private Singler(){}
public static Singler instance(){
if(singler==null){
singler = new Singler()
}
return singler;
}
}
区别:
- 线程安全:
- 饿汉式天生线程安全,可以直接用于多线程而不会出现问题。
- 懒汉式本身非线程安全,需要人为实现线程安全。
- 资源加载和性能:
- 饿汉式在类创建的同时就实例化一个静态对象出来,不管之后会不会使用这个单例,都会占据一定的内存,造成内存泄漏,但相应的,在第一次调用时速度也会更快。
- 而懒汉式顾名思义,会延迟加载,在第一次使用该单例的时候才会实例化对象出来,第一次调用时要做初始化,如果要做的工作比较多,性能上会有些延迟,之后就和饿汉式一样了。
有时候从技术角度出发为了节省系统资源时,也会使用单例模式,比如在spring任何自动注入实例对象,默认只有一个实例对象
简单工厂模式
程序中的工厂是生产实例对象的地方
为了实现不同条件创建不同对象的需求,核心问题是:
- 减少代码重复,减少相同逻辑代码
- 降低耦合紧密,减少代码之间的相互影响
实现简单工厂:
- 从具体的产品类抽象出接口,java面向接口编程,所以工厂应该生产一种产品而不是一个产品
- 把生产实例对象的过程收敛到工厂类中实现
UML图
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
- 工厂的接口
规定工厂应该提供什么样的产品,包含了所有工厂的方法
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;
}
}
SnacksFactoryBuilder的buildFactory()方法并不是static的,在复杂场景下尽量不要使用static方法
工厂模式结合spring工程
在工厂中减少定义static方法是为了在使用Spring框架时,可以为FactoryBuilder加上@Component注解让框架管理实例,同样任何需要使用工厂的地方只需要使用@Autowired注解让框架自动注入实例即可,这样可以让工厂模式的代码与Spring互为一体,扩展性更好,易于维护。
观察者模式
订阅和通知这种场景比较适合观察者模式
把需要观察的对象类也就是会改变的类继承Observable类,继承了就表示是核心的,需要观察的类和以往的模型设计不一样,需要观察的类要去掉全部属性的setter方法。
Observable提供的setChanged()方法就是标记被观察者对象发送了变化,notifyObservers()就是发出通知,接受通知的类就叫做观察者,观察者需要实现Observer接口,表示为观察者
@Override
public void update(Observable o, Object arg) {
}
- 第一个参数就是被观察者对象
- 第二个参数就是额外的信息,具体就是调用
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();
}
}
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--语句),后果是不可预料的。所以会出现余量打印错乱甚至相同的情况。
解决办法
多个线程操作同一个资源的时候,发生了冲突的现象,叫做线程不安全
在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相当于保护了关键方法,不允许同时执行,必须一个执行
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
方法调用时,需要等待返回取得返回值就是同步,不等待而继续执行任务就是异步。采取异步方式能够支持多个任务并行执行,这种机制称为并发
- 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;
}
}
- 并行注册
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);