等待唤醒机制
就是在一个线程进行了规定操作后,就进入等待状态(wait()),等待其它线程执行完它们的指定代码过后,再将其唤醒(notify());在有多个线程进行等待时,如果需要,可以用notifyAll()方法来唤醒所有的等待线程
wait:线程不再活动,不在参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时线程状态即是WAITING.它还要等着别的线程执行一个特别的动作,也即“notify”.
notify:选取所通知对象的wait set中的一个线程释放
注意:
哪怕只通知一个等待的线程,被通知线程也不能立即恢复执行,因为它当初中断的地方是在同步块内,而此刻它已经不持有锁,所以需要再次尝试去获取锁。成功后才能在当初调用wait方法之后的地方恢复
总结如下:
1.如果能获取锁,线程就从WAITING状态变成RUNNABLE状态
2.否则,从wait set 出来,又进入entry set,线程就从WAITING状态又变成了BLOCKED状态
调用wait和notify方法需要注意的细节
1.wait方法与notify方法必须由同一个锁对象调用,因为:对应的锁对象可以通过notify唤醒使用同一个锁对象调用的wait方法后的线程
2.wait方法与notify方法是属于Object类的方法的,因为:锁对象可以是任意对象,而任意对象的所属类都是继承了Object类的
3.wait方法与notify方法必须要在同步代码块或者是同步函数中使用。因为:必须要通过锁对象调用这2个方法
就拿生产包子、消费包子来说明等待唤醒机制如何有效利用资源:
//包子资源类
public class BaoZi{
String pier;
String xianer;
boolean flag = false;//包子资源 是否存在 包子资源状态 .
}
//吃货线程类
public class ChiHuo extends Thread{
private BaoZi bz;/*这句话只是声明了一个变量,变量名为bz,变量类型为BaoZi,
就是说bz能够引用BaoZi类型的对象,注意只是能够
只有用bz=new BaoZi() 才说明新建了一个BaoZi对象,并把它赋值给变量bz,也就是说现在的
bz才实际上引用了一个BaoZi类型的对象,在堆中开辟了空间。
*/
public ChiHuo(String name,BaoZi bz){
super(name);
this.bz=bz;
}
@Override
public void run(){
while(true){
synchronized(bz){
if(bz.flag==false){//没包子
try{
bz.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
System.out.println("吃货正在吃"+bz.pier+bz.xianer+"包子");
bz.flag=false;
bz.notify();
}
}
}
}
//包子铺线程类
public class BaoZiPu extends Thread{
private BaoZi bz;
public BaoZiPu(String name,BaoZi bz){
super(name);
this.bz=bz;
}
@Override
public void run(){
int count=0;
//造包子
while(true){
synchronized(bz){
if(bz.flag==true){
try{
bz.wait();
}catch(InterruptedException e){
e.printStackTrace();
}
}
//没有包子,做包子
System.out.println("开始做包子");
if(count%2==0){
bz.pier="冰皮";
bz.xianer="五仁";
}else{
bz.pier="薄皮";
bz.xinaer="牛肉";
}
count++;
bz.flag=true;
System.out.println("包子做好了:"+bz.pier+bz.xainer);
System.out.println("吃货来吃吧");
bz.notify();
}
}
}
}
//测试类
public class Demo{
public static void main(String[] args){
//等待唤醒案例
BaoZi bz = new BaoZi();
BaoZiPu bzp = new BaoZiPu("包子铺",bz);
ChiHuo ch = new ChiHuo("吃货",bz);
ch.start();
bzp.start();
}
}
线程池
我们使用线程的时候就去创建一个线程,这样实现起来非常简单,但是就会有一个问题:如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频发的创建线程和销毁线程需要时间。
在Java中通过线程池来达到线程重复使用的效果,就是执行完一个任务,并不销毁线程,可以被复用。
线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多的资源;
合理利用线程池的三个好处:
1.降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务
2.提高响应速度。当任务到达时,任务可以不需要等到线程创建就能立即执行
3.提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目。防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)
Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService.
要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Excutors线程工厂类里提供了一些静态工厂,生成一些常用的线程池。
Executors类中有个创建线程池的方法如下:
public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法:
public Future<?> submit(Runnable task):获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用
使用线程池中线程对象的步骤:
1.创建线程池对象
2.创建Runnable接口子类对象。
3.提交Runnable接口子类对象。
4.关闭线程池(一般不做)
public class MyRunnable implements Runnable{
@Override
public void run(){
System.out.println("我要一个教练");
try{
Thread.sleep(2000);
}catch(InterruptedException e){
e.printStackTrace();
}
System.out.println("教练来了:"+Thread.currentThread().getName());
}
}
//测试类
public class ThreadPoolDemo{
public static void main(String[] args){
//创建线程池对象
ExecutorService service = Executors.newFixedThreadPool(2);//包含2个线程对象
//创建Runnable实例对象
MyRunnable r = new MyRunnable();
//从线程池中获取线程对象,然后调用MyRunnable中的run()
service.submit(r);
//再获取两个线程对象
service.submit(r);
service.submit(r);
//注意:**submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭**
}
}
Lambda表达式
通过对比来感受一下Lambda表达式:
public class DemoRunnable{
public static void main(String[] args){
//内部类
Runnable task = Runnable(){
@Override
public void run(){
System.out.println("多线程任务执行");
}
};
new Thread(task).start();
}
}
使用Lambda表达式优化上面的代码;
public calss DemoLambdaRunnable{
public static void main(String[] args){
new Thread(()->System.out.println("多线程任务执行")).start();
//小括号代表Runnable接口run抽象方法的参数为空,大括号代表run的方法体
}
}
Lambda的参数和返回值
要求:
使用数组存储多个Person对象
对数组中的Person对象使用Arrays类中的sort方法通过年龄进行升序排序
下面举例演示java.util.Comparator接口的使用场景代码,其中抽象方法定义为:
public abstract int compare(T o1,T o2);
当需要对一个数组对象进行排序时,Arrays.sort()方法需要一个Comparator接口实例来指定排序的规则。
//Person类
public class Person{
private String name;
private int age;
//省略构造方法、toString方法与Getter Setter
}
import java.util.Arrays;
import java.util.Comparator;
public class DemoComparator{
public static void main(String[] args){
//本来年龄乱序的对象数组
Person[] array ={new Person("古力娜扎",19),new Person("迪丽热巴",18),
new Person("马儿扎哈",20)};
/*
或者这样创建Person类的数组
Person[] array = new Person[3];
array[0] = new Person("古力娜扎",19);
...
*/
//内部类
Comparator<Person> comp = new Comparator<>(){
@Override
public int compare(Person o1,Person o2){
return o1.getAge()-o2.getAge();
}
};
Arrays.sort(array,comp);//第二个参数为排序规则,即Comparator接口实例
}
}
下面我们来分析上述代码真正要做的是什么
1.为了排序,Arrays.sort方法需要排序规则,即Comparator接口的实例,抽象方法compare是关键
2.为了指定compare的方法体,不得不需要Comparator接口实现类
3.为了省去定义一个ComparatorImpl实现类的麻烦,不得不使用匿名类
4.必须覆盖重写抽象compare方法,所以方法名称、方法参数、方法返回值不得不再写一遍,且不能错
5.实际上,只有参数和方法体才是关键
Lambda写法:
import java,util.Arrays;
public class DemoCompaaratorLambda{
public static void main(String[] args){
Person[] array = {new Person("古力娜扎",19),new Person("迪丽热巴",18),
new Person("马儿扎哈",20)};
Arrays.sort(array,(Person a,Person b) -> {return a.getAge()-b.getAge();});
}
}
Lambda格式的有参有返回
//接口
public interface Calculator{
int calc(int a,int b);
}
//测试类
public class DemoCalculatorLambda{
public static void main(String[] args){
invokeCalc(130,120,(int a,int b) -> {return a+b;});
}
private static void invokeCalc(int a,int b,Calculator calculator){
int result = calculator.calc(a,b);
System.out.println("结果是:"+result);
}
}
Lambda省略格式
在Lambda标准格式的基础上,使用省略写法的规则为:
1.小括号内参数的类型可以省略
2.如果小括号内有且仅有一个参数,则小括号可以省略
3.如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号
Lambda的使用前提:
1.使用lambda必须是接口,且要求接口中有且仅有一个抽象方法。
无论是JDK内置的Runnable、Comparator接口还是自定义的接口,只有当接口中的抽象方法存在且唯一时,才可以使用Lambda
2.使用Lambda必须具有上下文推断
也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例
备注:有且仅有一个抽象方法的接口,称为“函数式接口”