线程状态、等待唤醒机制、线程池及Lambda表达式

线程状态

当线程被创建并启动以后,它既不是一启动就进入了执行状态,也不是一直处于执行状态。在API中java.lang.Thread.State 这个枚举中给出了线程生命周期的六种状态:
在这里插入图片描述

Timed Waiting(计时等待)

在我们写卖票的案例中,为了减少线程执行太快,现象不明显等问题,我们在run方法中添加了sleep语句,这样就
强制当前正在执行的线程休眠(暂停执行),以“减慢线程”。sleep方法的使用需要记住下面几点:

  1. 进入 TIMED_WAITING 状态的一种常见情形是调用的 sleep 方法,单独的线程也可以调用,不一定非要有协
    作关系。
  2. 为了让其他线程有机会执行,可以将Thread.sleep()的调用放线程run()之内。这样才能保证该线程执行过程
    中会睡眠
  3. sleep与锁无关,线程睡眠到期自动苏醒,并返回到Runnable(可运行)状态。
//实现一个计数器,计数到100,在每个数字之间暂停1秒,每隔10个数字输出一个字符串
package ThreadStatusTeat;

public class StatusWaiting {
    public static void main(String[] args) {
        Runnable r= new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i <100 ; i++) {
                    if (i%10==0){
                        System.out.println("----------"+i);
                    }
                    System.out.println(i);
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }

            }
        };
        new Thread(r).start();
    }
}

LOCKED(锁阻塞)

Blocked状态在API中的介绍为:一个正在阻塞等待一个监视器锁(锁对象)的线程处于这一状态。
比如,线程A与线程B代码中使用同一锁,如果线程A获取到锁,线程A进入到Runnable状态,那么线程B就进入到Blocked锁阻塞状态。这是由Runnable状态进入Blocked状态。除此Waiting以及Time Waiting状态也会在某种情况下进入阻塞状态

Waiting(无限等待)

Wating状态在API中介绍为:一个正在无限期等待另一个线程执行一个特别的(唤醒)动作的线程处于这一状态。

package ThreadStatusTeat;

public class WaitingTest {
    public static Object obj = new Object();
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    synchronized (obj){
                        try {
                            System.out.println("获取到锁对象。调用wait方法,进入waiting状态,释放锁对象");
                            obj.wait(3000);//计时等待,三秒时间到自动醒来
                            obj.wait();//无限等待
                        } catch (InterruptedException e) {

                            e.printStackTrace();
                        }
                        System.out.println(Thread.currentThread().getName()+"从waiting状态醒来,获取到锁对象,继续执行了");
                    }
                }
            }
        },"等待线程").start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true){
                    System.out.println(Thread.currentThread().getName()+"---等待三秒钟");
                    try {
                        Thread.sleep(3000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (obj){
                        System.out.println(Thread.currentThread().getName()+"获取到锁对象,调用notify方法,释放锁对象");
                        obj.notify();
                    }
                }
            }
        },"唤醒线程").start();

    }
}

通过上述案例我们会发现,一个调用了某个对象的 Object.wait 方法的线程会等待另一个线程调用此对象的
Object.notify()方法 或 Object.notifyAll()方法。
其实waiting状态并不是一个线程的操作,它体现的是多个线程间的通信,可以理解为多个线程之间的协作关系,
多个线程会争取锁,同时相互之间又存在协作关系。
当多个线程协作时,比如A,B线程,如果A线程在Runnable(可运行)状态中调用了wait()方法那么A线程就进入
了Waiting(无限等待)状态,同时失去了同步锁。假如这个时候B线程获取到了同步锁,在运行状态中调用了
notify()方法,那么就会将无限等待的A线程唤醒。注意是唤醒,如果获取到锁对象,那么A线程唤醒后就进入
Runnable(可运行)状态;如果没有获取锁对象,那么就进入到Blocked(锁阻塞状态)。

线程状态图如下:
在这里插入图片描述

等待唤醒机制

线程间通信

概念:多个线程处理同一个资源,但是处理的动作(线程的任务)却不相同

比如:线程A用来生成包子的,线程B用来吃包子的,包子可以理解为同一资源,线程A与线程B处理的动作,一个
是生产,一个是消费,那么线程A与线程B之间就存在线程通信问题。

线程间通信目的:
多个线程并发执行时, 在默认情况下CPU是随机切换线程的,当我们需要多个线程来共同完成一件任务,并且我们
希望他们有规律的执行, 那么多线程之间需要一些协调通信,以此来帮我们达到多线程共同操作一份数据。
如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。 就
是多个线程在操作同一份数据时, 避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效
的利用资源。而这种手段即—— 等待唤醒机制

等待唤醒机制

概念:

这是多个线程间的一种协作机制。谈到线程我们经常想到的是线程间的竞争(race),比如去争夺锁,但这并不是故事的全部,线程间也会有协作机制。
就是在一个线程进行了规定操作后,就进入等待状态(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个方
    法。

生产者与消费者问题

等待唤醒机制其实就是经典的“生产者与消费者”的问题。
就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:
包子铺线程生产包子**,吃货线程消费包子。
1、当包子没有时(包子状态为false),吃货线程等待,
2、包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。
3、接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。
涉及
包子资源类**、吃货线程类包子铺线程类,代码演示:
包子资源类:

package ThreadStatusTeat;

public class Baozi {
    String pier;
    String  xianer;
    boolean flag=false;

}

吃货线程类:

package ThreadStatusTeat;

public class ChiHuo extends Thread{
    private Baozi bz;
    public ChiHuo(Baozi bz,String name){
        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();
            }

        }
    }
}

包子铺线程类:

package ThreadStatusTeat;

public class BaoZiPu extends Thread{
    private Baozi bz;
    public BaoZiPu(Baozi bz,String name){
        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.xianer="牛肉大葱";
                }
                count++;
                bz.flag=true;
                System.out.println("包子做好了:"+bz.pier+bz.xianer);
                System.out.println("吃货来吃吧");
                //唤醒等待线程
                bz.notify();
            }
        }
    }
}

测试类:

package ThreadStatusTeat;

public class BaoZiDemo {
    public static void main(String[] args) {
        Baozi bz =new Baozi();
        ChiHuo ch = new ChiHuo(bz,"吃货");
        BaoZiPu bzp = new BaoZiPu(bz,"包子铺");
        ch.start();
        bzp.start();
    }
}

线程池

线程池:其实就是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,
无需反复创建线程而消耗过多资源

线程池的作用

Java里面线程池的顶级接口是java.util.concurrent.Executor ,但是严格意义上讲Executor 并不是一个线程
池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService 。
在java.util.concurrent.Executors 线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官
方建议使用Executors工程类来创建线程池对象。
Executors类中有个创建线程池的方法
public static ExecutorService newFixedThreadPool(int nThreads) :返回线程池对象。(创建的是有界线
程池,也就是池中的线程个数可以指定最大数量)
使用线程池对象的方法
public Future<?> submit(Runnable task) :获取线程池中的某一个线程对象,并执行
Future接口:用来记录线程任务执行完毕后产生的结果。线程池创建与使用。
使用线程池中线程对象的步骤:
1、创建线程池对象
2、创建Runnable接口子类对象(task)
3、提交Runnable接口子类对象(take task)
4、关闭线程池(一般不做)
Runnable实现类代码:

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());
        System.out.println("教我游泳,教完后,教练回到了游泳池");
    }
}

线程池测试类:

import java.util.concurrent.Executor;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {
    public static void main(String[] args) {
        //1、创建线程池对象
        ExecutorService service = Executors.newFixedThreadPool(2);
        //2、创建Runnable实例对象
        MyRunnable r = new MyRunnable();
        //之前的做法
        Thread t =new Thread(r);
        t.start();
        //3、从线程池中取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        //再从线程池中取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        //再从线程池中取线程对象,然后调用MyRunnable中的run()
        service.submit(r);
        //注意submit方法调用结束后,程序并不终止,是因为线程池控制了线程的关闭
        //将使用完的线程又归还到了线程池
        //service.shutdown();关闭线程池

    }
}

Lambda表达式

函数式编程思想

面向对象过分强调“必须通过对象的形式来做事情”,而函数式思想则尽量忽略面向对象的复杂语法——强调做什么,而不是以什么形式做。

Lambda的更优写法

传统写法

当需要启动一个线程去完成任务时,通常会通过java.lang.Runnable 接口来定义任务内容,并使用
java.lang.Thread 类来启动该线程。代码如下:

public class Demo01Runnable {
public static void main(String[] args) {
// 匿名内部类
Runnable task = new Runnable() {
@Override
public void run() { // 覆盖重写抽象方法
System.out.println("多线程任务执行!");
}
};
new Thread(task).start(); // 启动线程
}
}

本着“一切皆对象”的思想,这种做法是无可厚非的:首先创建一个Runnable 接口的匿名内部类对象来指定任务内容,再将其交给一个线程来启动。

Lambda的更优写法

借助Java 8的全新语法,上述Runnable 接口的匿名内部类写法可以通过更简单的Lambda表达式达到等效:

public class Demo02LambdaRunnable {
public static void main(String[] args) {
new Thread(()> System.out.println("多线程任务执行!")).start(); // 启动线程
}
}

从代码的语义中可以看出:我们启动了一个线程,而线程任务的内容以一种更加简洁的形式被指定。不再有“不得不创建接口对象”的束缚,不再有“抽象方法覆盖重写”的负担

语义分析

仔细分析上述代码中的语义, Runnable 接口只有一个run 方法的定义:

public abstract void run();

即制定了一种做事情的方案(其实就是一个函数):
无参数:不需要任何条件即可执行该方案。
无返回值:该方案不产生任何结果。
代码块(方法体):该方案的具体执行步骤。
同样的语义体现在Lambda 语法中,要更加简单:

()->System.out.println("多线程任务执行!")

前面的一对小括号即run 方法的参数(无),代表不需要任何条件;
中间的一个箭头代表将前面的参数传递给后面的代码;
后面的输出语句即业务逻辑代码。

Lambda标准格式

Lambda省去面向对象的条条框框,格式由3个部分组成:
一些参数
一个箭头
一段代码
Lambda表达式的标准格式为:

(参数类型 参数名称)> { 代码语句 }

格式说明:
小括号内的语法与传统方法参数列表一致:无参数则留空;多个参数则用逗号分隔。
-> 是新引入的语法格式,代表指向动作。
大括号内的语法与传统方法体要求基本一致。

练习:使用Lambda标准格式(无参无返回)

给定一个厨子Cook 接口,内含唯一的抽象方法makeFood ,且无参数、无返回值。如下:

public interface Cook {
void makeFood();
}

下面的代码中,通过Lambda的标准格式调用invokeCook 方法,打印输出“吃饭啦!”字样:

package LambdaTest;

public class InvokeCookDemo {
    public static void main(String[] args) {
        /*Cook cook = new Cook() {
            @Override
            public Cook makeFood() {
                System.out.println("吃饭啦!");
                return null;
            }
        }.makeFood();*/
        invokeCook(()->{
            System.out.println("吃饭啦!");
        });


    }

    private static void invokeCook(Cook cook) {
        cook.makeFood();
    }
}

练习:使用Lambda标准格式(有参有返回)

给定一个计算器Calculator 接口,内含抽象方法calc 可以将两个int数字相加得到和值:

package LambdaTest;
//给定一个Calculate接口,内含抽象方法calc可以将两个int 数字相加得到和值
public interface Calculate {
    public abstract int calc(int a, int b) ;

}

在下面的代码中,通过Lambda的标准格式调用invokeCalc 方法,完成120和130的相加计算:

package LambdaTest;

public class InvokeCalculateDemo {
    public static void main(String[] args) {
        /*new Calculate() {
            @Override
            public void calc(int a, int b) {
                int result = a+b;
                System.out.println(result);
            }
        }.calc(100,200);*/
        invokeCalc(120,130,(int a, int b)->{

            return a+b;
        });



    }
    private static void invokeCalc(int a,int b,Calculate calculate){
        int result = calculate.calc(a,b);
        System.out.println("结果是:"+result);
    }
}

Lambda省略格式

Lambda强调的是“做什么”而不是“怎么做”,所以凡是可以根据上下文推导得知的信息,都可以省略。例如上例还可
以使用Lambda的省略写法:

public static void main(String[] args) {
invokeCalc(120, 130, (a, b)> a + b);
}

省略规则
在Lambda标准格式的基础上,使用省略写法的规则为:

  1. 小括号内参数的类型可以省略;
  2. 如果小括号内有且仅有一个参,则小括号可以省略;
  3. 如果大括号内有且仅有一个语句,则无论是否有返回值,都可以省略大括号、return关键字及语句分号。
    eg:仍然使用前文含有唯一makeFood 抽象方法的厨子Cook 接口,使用Lambda的省略格式调用
    invokeCook 方法,打印输出“吃饭啦!”字样:
public static void main(String[] args) {
invokeCook(()> System.out.println("吃饭啦!"));
}

Lambda的使用前提

Lambda的语法非常简洁,完全没有面向对象复杂的束缚。但是使用时有几个问题需要特别注意:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法。
    无论是JDK内置的Runnable 、Comparator 接口还是自定义的接口,只有当接口中的抽象方法存在且唯一
    时,才可以使用Lambda。
  2. 使用Lambda必须具有上下文推断。
    也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才能使用Lambda作为该接口的实例。
    备注:有且仅有一个抽象方法的接口,称为“函数式接口”。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值