一 等待唤醒机制
1.1 线程之间通信
概念:多个线程在处理同一个资源,但是处理的动作(线程的任务)却不相同。
为什么要处理线程间通信:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
1.2 等待唤醒机制
等待唤醒机制:是多个线程间的一种协作机制。就是在一个线程进行了规定操作后,就进入等待状态(wait()), 等待其他线程执行完他们的指定代码过后 再将其唤醒(notify());在有多个线程进行等待时, 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。
wait/notify 就是线程间的一种协作机制。
等待唤醒中的方法
等待唤醒机制3个方法的含义如下:
1. wait:线程不再活动,不再参与调度,进入 wait set 中,因此不会浪费 CPU 资源,也不会去竞争锁了,这时的线程状态即是 WAITING。它还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象
上等待的线程从wait set 中释放出来,重新进入到调度队列(ready queue)中
2. notify:则选取所通知对象的 wait set 中的一个线程释放;例如,餐馆有空位置后,等候就餐最久的顾客最先
入座。
3. notifyAll:则释放所通知对象的 wait set 上的全部线程。
注意:
哪怕只通知了一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以她需要再次尝试去获取锁(很可能面临其它线程的竞争),成功后才能在当初调用 wait 方法之后的地方恢复执行。
总结如下:
如果能获取锁,线程就从 WAITING 状态变成 RUNNABLE 状态;
否则,从 wait set 出来,又进入 entry set,线程就从 WAITING 状态又变成 BLOCKED 状态
调用wait和notify方法需要注意的细节
1. wait方法与notify方法必须要由同一个锁对象调用。因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程。
2. wait方法与notify方法是属于Object类的方法的。因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的。
3. wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法。
//做包子的(包子铺):是一个线程类,可以继承Thread设置线程任务(run)
/*注意:
包子铺线程和包子线程关系是 通信(互斥)
必须同时同步技术保证两个线程只能有一个在执行
锁对象必须保持唯一,可以使用包子对象作为参数传递进来
包子铺和吃货的类就需要把包子对象作为参数传递进来
1 需要在成员位置创建一个包子变量
2 使用带参数的构造方法,为这个包子变量赋值
*/
public class BaoZiPu extends Thread {
private BaoZi bz;
public BaoZiPu(BaoZi bz) {
this.bz = bz;
}
@Override
public void run() {
//定义一个变量
int count = 0;
//让包子铺一直做包子
while (true) {
synchronized (bz) {
if (bz.zhuangtai == true) {
try {
//有包子的时候等待
bz.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
if (count % 2 == 0) {
bz.pi = "薄皮";
bz.xian = "鲜肉";
} else {
bz.pi = "厚皮";
bz.xian = "青菜";
}
count++;
System.out.println("包子铺正在生产:" + bz.pi + bz.xian + "的包子");
//生产需要3秒时间
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
bz.zhuangtai = true;
bz.notify();
System.out.println("包子铺包子做好了" + bz.pi + bz.xian + "可以吃包子了");
}
}
}
}
public class GuKe extends Thread{
//创建包子变量用于锁
private BaoZi bz;
//使用带参数的构造方法,为包子变量赋值
public GuKe(BaoZi bz) {
this.bz = bz;
}
//设置线程任务
@Override
public void run() {
super.run();
//使用死循环,让顾客一直吃包子
while(true){
synchronized (bz){
//看有没有包子
if(bz.zhuangtai == false){
try{
bz.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pi+bz.xian+"的包子");
bz.zhuangtai = false;
bz.notify();
System.out.println("顾客吃完"+bz.pi+bz.xian+"的包子");
System.out.println("===========================");
}
}
}
}
二 线程池
2.1 概述
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。
合理利用线程池能够带来三个好处:
1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用
2. 提高响应速度。任务可以不需要的等到线程创建就能立即执行。
3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存
2.2 线程池的使用
Java里面线程池的顶级接口是java.util.concurrent.Executor ,但是严格意义上讲Executor 并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口java.util.concurrent.ExecutorService
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads)
:返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法如下:
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
1. 创建线程池对象。使用线程工厂类Execuyors里面提供的静态方法
2. 创建Runnable接口子类对象。实现Runnable接口,重写run方法
3. 提交Runnable接口子类对象。调用submit方法,传递线程任务(实现类),开启线程执行run方法
4. 关闭线程池(一般不做)。
三 Lambda 表达式
3.1 Lambda 表达式概述
面向对象的思想:
做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情.
函数式编程思想:
只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程
3.2 冗余的Runnable代码
对于Runnable 的匿名内部类用法,可以分析出几点内容:
Thread 类需要Runnable 接口作为参数,其中的抽象run 方法是用来指定线程任务内容的核心;
为了指定run 的方法体,不得不需要Runnable 接口的实现类;
为了省去定义一个RunnableImpl 实现类的麻烦,不得不使用匿名内部类;
必须覆盖重写抽象run 方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能写错;而实际上,似乎只有方法体才是关键所在。
3.3 Lambda优化写法
public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(() ‐> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}
匿名内部类的好处与弊端
一方面,匿名内部类可以帮我们省去实现类的定义;另一方面,匿名内部类的语法——确实太复杂了!
Runnable 接口只有一个run 方法的定义:
public abstract void run();
即制定了一种做事情的方案(其实就是一个函数):
无参数:不需要任何条件即可执行该方案。
无返回值:该方案不产生任何结果。
代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda 语法中,要更加简单:
前面的一对小括号即run 方法的参数(无),代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码。
3.3 Lambda标准写法
Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数
一个箭头
一段代码
Lambda表达式的标准格式为: (参数类型 参数名称) ‐> { 代码语句 }
格式说明:
1,小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
2,-> 是新引入的语法格式,代表指向动作。传递,把参数传递给方法体
3,大括号内的语法与传统方法体要求基本一致。
public class TestCalculator {
public static void main(String[] args) {
invokeCal(200,20,(int a,int b)->{
return a+b;
});
}
public static void invokeCal(int a,int b,Calculator C){
int sum = a + b;
System.out.println(sum);
}
}
Lambda表达式:是可推导,可以省略
凡是根据上下文推导出来的内容,都可以省略书写
可以省略的内容:
1,(参数列表):括号中参数列表的数据类型,可以省略
2,(参数列表):括号中的参数如果只有一个,那么类型是()都可以省略
3,(一些代码):如果{ }中的代码只有一行,无论是否有返回值,都可以省略({ },return,分号) 注意:要省略{ },return,分号必须一起省略
invokeCal(200,20,(int a,int b)->{
return a+b;
});
//省略后
invokeCal(200,20,(a, b)-> a+b)
注意:
1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable 、Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda。
2. 使用Lambda必须具有上下文推断。也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
备注:有且仅有一个抽象方法的接口,称为“函数式接口”。