并发编程学习

第一章——Lambda表达式

简介

格式:f -> {}

无类型参数

类型识别

系统会根据上下文自动识别类别

多参数

(student1, student2) -> {}

用小括号包裹多参数

无参数

() -> {}

必须使用小括号

单条执行语句

可以不加大括号{}包裹

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

有类型参数

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

fruits.forEach((Fruitf) -> {

System.out.println(f.getName());

});

如果只有一个参数,也必须使用小括号包裹

引用外部变量

规范一

引用的局部变量不允许被修改

即使写在表达式后面也不行

List<Fruit>fruits=Arrays.asList(......);

Stringmessage="水果名称:";

fruits.forEach(f-> {

System.out.println(message+f.getName());

});

message="";

规范二

参数不能与局部变量同名

Stringf="水果名称:";

fruits.forEach(f-> {});

双冒号(::)操作符

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

使用::时,系统每次遍历取得的元素会自动作为参数传递给打印当方法

相当于是

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

不同用法

用法一

静态方法调用

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

用法二

非静态方法调用

fruits.forEach(newLambdaTest()::print);

需要调用实例对象

用法三

多参数

Collections.sort(students, (student1, student2) -> {

// 第一个参数的学号 vs 第二个参数的学号

return student1.getRollNo() - student2.getRollNo();

});

将该过程定义为一个方法

private static int compute(Student s1, Student s2) {

... ...

... ...

}

就可以简写为

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

用法四

父类方法

import java.util.Arrays;

import java.util.List;

public class LambdaTest extends LambdaExample {

public static void main(String[] args) {

List<Fruit> fruits = Arrays.asList(

new Fruit("香蕉"),

new Fruit("苹果"),

new Fruit("梨子"),

new Fruit("西瓜"),

new Fruit("荔枝")

);

LambdaTest at = new LambdaTest();

at.print(fruits);

}

public void print(List<Fruit> fruits){

fruits.forEach(super::print);

}

}

class LambdaExample {

public void print(Fruit f){

System.out.println(f.getName());

}

}

第二章——Stream API

简介

主要是对集合中的数据进行各种操作,增加了集合对象的功能

与InputStream完全不同,是不同的概念,经常与Lambda表达式配合使用

流迭代

创建流

直接创建

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

迭代流

Stream提供的迭代方式也叫做forEach()

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

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

集合类的forEach和流的forEach方法无关,仅仅是方法名相同

流数据过滤

filter()方法

List<Pupil> pupils = new ArrayList<>();

// 这里假设小学生数据对象已经存入了

pupils.stream()

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

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

流数据映射

numbers.stream()

.map(num -> {

return num * num;

})

.forEach(System.out::println);

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

优点

映射后的对象类型,可以与流中原始的对象类型不一致。极大的提供了灵活性、扩展性。

特例写法

如果替换语句简单,系统能自动识别需要返回的值

.map(num -> num * num)

流数据排序

students.stream()

// 实现升序排序

.sorted((student1, student2) -> {

return student1.getRollNo() - student2.getRollNo();

})

.forEach(System.out::println);

student1指代后一个元素,student2指代前一个元素
特例写法

.sorted((student1, student2) -> student1.getRollNo() - student2.getRollNo())

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

流数据摘取

用limit来限制取的元素数目

numbers.stream()

.sorted((n1, n2) -> n2 - n1)

.limit(3)

.forEach(System.out::println);

第三章——并行数据

流合并(一)

import java.util.Arrays;

int sum = numbers.stream()

.reduce((a, b) -> a + b)

.get();

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

reduece方法得作用是合并了所有得元素,终止计算出一个结果
get方法返回最终得整数值

reduce()方法的参数

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

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

流合并(二)

Student result = students.stream()

.reduce(

(a, b) -> {

a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());

return a;

}

)

.get();

第一个Student对象由于充当了缓存角色,正确性被破坏了

此时的总分变成一个Student的了

解决办法

自己new一个对象充当缓存角色,而不是使用流中的原始对象

Student result = students.stream()

.reduce(new Student("", 0),

(a, b) -> {

a.setMidtermScore(a.getMidtermScore() + b.getMidtermScore());

return a;

}

);

此时a变量专门指代缓存角色的对象,最后输出的result即带有总分,且无姓名

流收集

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

collect()方法的做哦那个就是收集元素。

因为利用map转化成了String类型,所以返回值类型是List<String>而不是List<Integer>

并行流

不再调用stream()方法,改为调用parallelStream()即可

流中的每个数据元素之间,有逻辑依赖关系的时候,不适合使用并行计算

并行流的性能意外

原因

  1. 硬件太差。CPU核数很低

  1. 任务简单。因为多线程的管理也是会消耗CPU、内存等资源的,可能比任务本身的开销更大

第四章——常用设计模式

作用

有助于提示代码质量,也有助于提示程序员的设计能力

单例模式、

问题

班主任类

public class ClassMaster {

private String id;

// 班主任名称

private String name;

private String gender;

}

public class Classes {

public static void main(String[] args) {

// 创建班主任实例

ClassMaster classMaster = new ClassMaster();

// 创建班主任实例2

ClassMaster classMaster2 = new ClassMaster();

}

}

在Classes可以创建多个ClassMaster实例。这就违背了要求,没有做到班主任类只有一个实例对象。

解决

public class ClassMaster {

private String id;

// 班主任名称

private String name;

private String gender;

private ClassMaster() {

}

}

将构造函数设置为私有的,除了ClassMaster自己,其他任何类都不能实例化ClassMaster对象,就能保证ClassMaster的实例是全局唯一的。

public class ClassMaster {

private String id;

// 班主任名称

private String name;

private String gender;

// 唯一实例

private static ClassMaster instance = new ClassMaster();

private ClassMaster() {

}

}

访问实例

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;

}

}

像@Autowired也是单例模式的体现

简单工厂模式

工厂实现

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;

}

}

抽象工厂模式

多种产品

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的,因为在复杂场景下尽量不要使用类方法,实例方法可以被继承,扩展性较好

工厂模式结合Spring工程

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

@Component

public class SnacksFactoryBuilder {

public SnacksFactory buildFactory(String choice) {

}

}

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

观察者模式

观察对象

import java.util.Observable;

public class WeatherData extends Observable {

// 城市

private String cityName;

// 时间

private String time;

// 温度

private String temp;

// 城市固定了就不变了

public WeatherData(String cityName) {

this.cityName = cityName;

}

// 打印天气信息

public String toString() {

return cityName + "," + LocalDate.now().toString() + " " + time + ",气温:" + temp + "摄氏度。";

}

public String getCityName() {

return cityName;

}

public String getTime() {

return time;

}

public String getTemp() {

return temp;

}

}

继承了Ovservable。表示被观察的类

watchData是被观察者。

因为要使数据变化后发起通知,所以增加一个新的方法专门处理

import java.util.Observable;

public class WeatherData extends Observable {

/**

* 一个城市的气温在某个时刻发生了变化

*/

public void changeTemp(String time, String temp) {

if(time == null || temp == null) {

// 输入数据为空是有问题的,不处理

return;

}

// 与原数据不同,说明发生了变化

if(!time.equals(this.time) || !temp.equals(this.temp)) {

// 标记变化

super.setChanged();

this.time = time;

this.temp = temp;

// 发出通知,参数是额外的信息

super.notifyObservers("温度变化已通知");

}

}

}

父类Observable提供的方法setChanged()就是标记被观察者对象发生了变化

notifyObservers()就是发出通知,父类提供的。

观察者

import java.util.Observable;

import java.util.Observer;

public class WeatherObserver implements Observer {

private String name;

@Override

public void update(Observable o, Object arg) {

System.out.print(this.name + "观察到天气变化为:");

System.out.print(o.toString());

System.out.println(" " + arg);

}

public String getName() {

return name;

}

public void setName(String name) {

this.name = name;

}

}

要实现update()方法。第一个参数是被观察者对象,第二个就是额外的信息。具体说就是调用super.notifyObservers()传入的参数对象,传入什么对象,arg就是什么值

运行程序

public class WeatherTest {

public static void main(String[] args) {

// 在天气变化后发邮件的观察者

WeatherObserver w1 = new WeatherObserver();

w1.setName("天气邮件观察者");

// 在天气变化后发短信的观察者

WeatherObserver w2 = new WeatherObserver();

w2.setName("天气短信观察者");

// 城市天气数据

WeatherData weatherData = new WeatherData("余杭");

// 添加观察者

weatherData.addObserver(w1);

weatherData.addObserver(w2);

// 气温变化

weatherData.changeTemp("11:08", "32.8");

// 气温变化

weatherData.changeTemp("14:46", "29.3");

}

}

必须先调用addObserver()方法把观察者对象实例添加到被观察者实例中。

观察者模式让观察者和被观察者双方的耦合度降到最低(称之为解耦)

第五章——并发编程

继承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() + " 取钱完毕");

}

}

运行线程

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属性,所以可以调用 setName()方法为线程设置名字,通过 getName()就指代是哪个线程在运行

实现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 + " 取钱完毕");

}

}

线程启动

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

}

}

线程安全

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

public class Ticket {

public synchronized void sell() {

count--;

System.out.println(Thread.currentThread().getName() + ":卖出一张,还剩下 " + count + " 张票");

}

}

利用 synchronized关键字来解决余量错乱的问题,也叫做同步锁,同一时刻只能由一个线程执行此方法

对于判断语句,最好在synchronized修饰的方法中写,在run里面写容易出现多个线程同时判断条件成立,就都会执行

synchronized使用场景

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

  1. 原子性:方法全部执行且执行的过程不会被任何因素打断

  1. 可见性:当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看到修改的值

缺点:滥用 synchronized会使得性能不高,多个线程需要等待

比较适合的场景
  1. 写操作的场景。例如用户修改个人信息、点赞、收藏、下单等

  1. 尽量精确锁住最小的代码块,把最关键的写操作抽象成独立的方法加锁。

悲观锁和乐观锁

AtomicInteger提供了不使用 synchronized就能保证数据操作原子性的方法。例如 decrementAndGet()方法

但仍有可能出现负数的情况。这是因为条件判断语句、操作语句、打印信息语句组合起来,就不具备原子性了,因为 sell()不加锁,多条语句执行时就可能被其它线程打断了。

所以还得整体加 synchronized才能保证多条语句整体的原子性

乐观锁

乐观锁其实是不上锁,由于没有上锁,就提高了性能。

像 AtomicInteger类的 incrementAndGet()和 decrementAndGet()方法就是典型的乐观锁实现。

悲观锁

适合写数据比重更大的应用场景。一般来说写数据的整体消耗时间更长些,是可以接受的。

并发容器(一)

利用 CompletableFuture特性来解决。

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

}

}

Stream parallelStream()侧重点是流的元素的计算操作;而 CompletableFuture的任务可以比较宽泛。

并发容器(二)

CompletableFuture.supplyAsync(() -> reg.regId(s))

.thenApply(student -> {

return dis.assignClasses(student);

})

.thenAccept(student -> {

System.out.println("姓名:" + student.getName() + ",学号:" + student.getId() + ",班级号:" + student.getClassId());

});

多步操作,可以调用多次thenApply()。由于末尾也要用到 student实例对象。所以位于中间的 thenApply()方法,总是要 return学生实例对象,否则下一个步骤就获取不到了。

扩展知识点:返回值

supplyAsync() 是静态方法,返回值是 CompletableFuture实例对象。

CompletableFuture<Student> cf = CompletableFuture.supplyAsync(() -> reg.regId(s))

.thenApply(student -> {

return dis.assignClasses(student);

});

这个cf的类型是Student是因为return返回的是Student数据

main()方法的问题

可能线程任务还没执行完毕,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();

}

将任务收集起来,然后等待所有的线程执行完毕。

类方法get(),作用就是等待所有的任务线程 allOf()收集的都执行完毕,再继续执行。

需要强调的是

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

再次理解同步与异步

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

安全的布尔值包装类

AtomicBoolean ab = new AtomicBoolean(true);

boolean value = ab.get();

必须要有默认值

如果是多个值的话就得用{new AtomicBoolean(true),}在里面写多个

线程池

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

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

线程池创建

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

}

}

BasicThreadFactory的pom依赖

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

构建函数的参数表示能排队的任务个数

创建线程池实例

使用线程池执行任务

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()等待一段时间

for (int i = 1; i <= 2000; i++) {

... ...

... ...

}

try {

Thread.sleep(3000);

} catch (InterruptedException e) {

e.printStackTrace();

}

线程池与并发容器

CompletableFuture内部也用到了线程池,实际上是把任务放入内部的默认线程池里执行

CompletableFuture.supplyAsync(

() -> reg.regId(s),

EXECUTOR_SERVICE

)

第二个参数传入的是构建好的线程池对象

线程池可以运行任务,但是不利于编排。

基础多线程

线程池

并发容器

一般学习阶段使用supplyAsync(() -> {})就够了。遇到任务并发度高的就需要指定线程池。或者一开始使用CompletableFuture默认线程池,任务执行慢的时候再开始考虑指定线程池,并调整线程池参数。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值