Day07 等待与唤醒、线程池、λ表达式
一、 等待唤醒机制
1.1线程间的通信
线程间的通信指的就是,在两个及两个以上的线程对同一个数据(资源)进行不同的操作。例如有线程AB、线程A生产包子、线程B购买包子,都是对包子操作,但是操作不一样。
通信存在的目的:
多线程并发执行的时候,CPU会在各个(优先级相同的)线程中随机切换执行,为了让多线程有规律的执行,需要协调通信。
1.2 等待唤醒机制
等待唤醒机制用于处理线程间的通信,因为多线程处理同一数据的时候。是有先后次序的,如包子铺的案例,只有生产线程生产出东西之后购买线程才能执行操作。所以要采用一些协作机制:wait
、notify
、notifyAll
来对各线程进行协调。
-
wait:使线程进入Waiting状态,不能再被CPU执行,不参与调度,进入等待队列,当收到其他线程的notify指令被唤醒时,在抢到锁的情况下,进入调度队列(ready set),否则,进入锁阻塞的状态(Blocked)
-
notify:随机唤醒一个处于等待队列的线程
-
notifyAll :唤醒所有处于等待队列的线程
注意点:
- wait和notify方法只能由同一个锁对象发出
- wait和notify方法是属于Object类的对象,所以可以由任何对象发出
- 由于要通过锁对象才能使用wait和notify方法,所以只能在同步代码块和同步函数中使用
1.3生产者与消费者的问题
采用以下例子来说明等待唤醒机制,线程A:生产包子。线程B:消费包子
自定义包子类:
class Bz{
private boolean isHave =false;
public boolean isHave() {
return isHave;
}
public void setHave(boolean isHave) {
this.isHave = isHave;
}
}
自定义消费者类CHIHUO:
class CHIHUO implements Runnable{
Bz bz;
public CHIHUO(Bz bz) {
this.bz = bz;
}
public void run() {
while(true) {
//采用包子类对象bz作为密码锁
synchronized(bz) {
if(bz.isHave() == false) {
//当包子补存在的时候,进入等待状态,等待唤醒
try {
System.out.println("老板来个包子!");
bz.wait();
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
//包子有了,那么包子铺线程在等待状态,吃完之后唤醒包子铺
System.out.println("那我开始吃包子啦!");
bz.setHave(false);
bz.notify();
System.out.println("__________________");
}
}
}
}
自定义包子铺类:
class BZP implements Runnable{
Bz bz;
public BZP(Bz bz) {
this.bz = bz;
}
public void run() {
while(true) {
synchronized(bz) {
//区别是,包子铺在存在包子的时候,进入等待状态
if(bz.isHave() == true) {
try {
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//不存在包子时,做包子,并唤醒吃包子的线程,
System.out.println("做好了,来吃吧");
bz.setHave(true);
bz.notify();
}
}
}
}
测试:
public static void main(String[] args) {
Bz bz = new Bz();
BZP bzp = new BZP(bz);
CHIHUO ch = new CHIHUO(bz);
Thread t1 = new Thread(ch);
Thread t2 = new Thread(bzp);
t1.start();
t2.start();
}
/*老板来个包子!
做好了,来吃吧
那我开始吃包子啦!
__________________
老板来个包子!
做好了,来吃吧
那我开始吃包子啦!
__________________
老板来个包子!
做好了,来吃吧
那我开始吃包子啦!
__________________
*/
二、 线程池
2.1 线程池的思想
在一个任务过程中,线程随着执行体的开始于结束,频繁的创建与死亡,浪费资源。线程池的思想是创建一些能够重复使用的线程,它不在任务执行完被销毁,而是由线程池来决定。
2.2 线程池的概念
Java中线程池是容纳线程的一种容器,容器中的线程可以反复使用,不用再自行创建线程。
线程池的优点:
- 合理利用资源,不用再频繁的创建销毁线程,线程可以重复利用
- 提升响应速度,要求执行任务时,不用再创建线程,可以立刻执行
- 提高线程的可管理性,根据系统性能,调整线程池中线程的数量
2.3 线程池的简单使用
线程池的接口:java.util.concurrent.ExecutorService
,此类中提供了静态工厂,生成一些常用的线程池。
public static ExecutorService newFixedThreadPool(int nThreads)
:返回一个有界的线程池对象,支持同时运行的线程数为nThreads。- 提交任务的方法:
public Future<?> submit(Runnable task)
,向线程池提交任务 - 使用步骤:
- 创建线程池
- 定义线程执行任务(实现Runnable 接口中的run()方法)
- 将任务提交到线程池
- 关闭线程池(一般不做)
public static void main(String[] args) {
Test t1 = new Test("线程1");
Test t2 = new Test("线程2");
Test t3 = new Test("线程3");
ExecutorService pool = Executors.newFixedThreadPool(2);
pool.submit(t1);
pool.submit(t2);
pool.submit(t3);
}
}
class Test implements Runnable{
String name;
public Test(String name) {
this.name = name;
}
@Override
public void run() {
while(true) {
System.out.println(name+"正在工作");
}
}
}//输出结果中,只有两种情况:线程1正在工作、线程2正在工作,因为线程池的最大容量为2,线程3一直在等待状态。
三、λ(Lambda)表达式
3.1 函数式编程的思想
和面向对象编程的思想不同的是,函数式编程抛起了繁琐的定义,创建步骤,以最快的方式,达到目的。
以洗衣服为例:面向对象编程可能要准备洗衣机类、定义清洗的方法、再创建洗衣机对象,再通过对象调用洗衣服函数。
对于函数式编程,只要知道怎么洗就好了,过程相对不重要。
3.2 Lamabda 写法与传统写法比较
例如我们想创建一个线程,让它重复打印“λ”。
第一种方式:定义类,再创建线程,然后调用线程对象启动线程
public static void main(String[] args) {
Thread test = new Thread(new Lambda());
test.start();
}
}
class Lambda implements Runnable{
public void run() {
System.out.println("λ");
}
}
第二种方式:如果只用启动一次,采用匿名对象+匿名内部类,更加轻便
public static void main(String[] args) {
new Thread(new Runnable() {
public void run() {
System.out.println("λ");
}
}).start();;
}
第三种:采用Lambda表达式:直观的感觉,就是很清爽
public static void main(String[] args) {
new Thread(()->System.out.println("λ")).start();
}
相比于实现同一个目的来说,相比于定义类的繁琐步骤、匿名内部类的复杂语法,Lambda表达式显得更加轻便。
3.3 Lambda表达式的标准格式
包括三部分:
-
参数 :无则留空,多个用逗号隔开
-
箭头 :代表动作指向
-
代码语句:与传统语法要求一致
(参数类型 参数名称) -> {代码语句}
3.4 练习 :无参无返回的Lamba格式
给定一个厨子Cook
接口,内含唯一的抽象方法makeFood
,且无参数、无返回值。如下:
public interface Cook {
void makeFood();
}
在下面的代码中,请使用Lambda的标准格式调用invokeCook
方法,打印输出“吃饭啦!”字样:
public class Demo05InvokeCook {
public static void main(String[] args) {
// TODO 请在此使用Lambda【标准格式】调用invokeCook方法
}
private static void invokeCook(Cook cook) {
cook.makeFood();
}
}
解:
public static void main(String[] args) {
invokeCook(()->{System.out.println("吃饭咯"):});
}
private static void invokeCook(Cook cook) {
cook.makeFood();
}
interface Cook{
void makeFood();
}
这段Lambda表达式:
() -> System.out.println("吃饭咯")
,小括号代表了参数是无参,箭头指向输出,{}里为要执行的语句块。
3.5 有参返回
当需要对一个对象数组进行排序时,Arrays.sort
方法需要一个Comparator
接口实例来指定排序的规则。假设有一个Person
类,含有String name
和int age
两个成员变量:
public class Person {
private String name;
private int age;
// 省略构造器、toString方法与Getter Setter
}
public static void main(String[] args) {
Person p1 = new Person("憨憨一号",66);
Person p2 = new Person("憨憨二号",25);
Person p3 = new Person("憨憨三号",77);
ArrayList<Person> list = new ArrayList<Person>();
Collections.addAll(list, p1,p2,p3);
Collections.sort(list,(Person p ,Person pp)-> {return p.getAge()-pp.getAge();});
for(Person p : list) {
System.out.println(p);
}
3.6 Lambda的简化写法
在标准写法的基础上:
- 小括号内如果只有一个参数,小括号可以省略
- 小括号内的参数类型可以省略(因为可以根据上下文推导出参数的类型)
- 如果大括号内,有且仅有一个语句,无论是否又返回值,可以省略大括号、return关键字、语句分号;
利用简化写法,再做前面的厨子练习:
invokeCook(() -> System.out.println("吃饭了") );
//省略了语句分号,大括号
3.7 Lambda的使用前提
- 必须有接口,且接口中有且仅有一个抽象方法
- 必须具有上下文推断,方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
有且仅有一个抽象方法的接口,称为“函数式接口”;