第十二周笔记

1. 线程状态

  1. 新建状态(New):
    当用new操作符创建一个线程时, 例如new Thread( r ),线程还没有开始运行,此时线程处在新建状态。 当一个线程处于新生状态时,程序还没有开始运行线程中的代码。

  2. 就绪状态(Runnable):
    一个新创建的线程并不自动开始运行,要执行线程,必须调用线程的start()方法。当线程对象调用start()方法即启动了线程,start()方法创建线程运行的系统资源,并调度线程运行run()方法。当start()方法返回后,线程就处于就绪状态。

  3. 运行状态(Running):
    当线程获得CPU时间后,它才进入运行状态,真正开始执行run()方法.

  4. 阻塞状态(Blocked):
    线程运行过程中,可能由于各种原因进入阻塞状态:
    (1) 线程通过调用sleep方法进入睡眠状态;
    (2) 线程调用一个在I/O上被阻塞的操作,即该操作在输入输出操作完成之前不会返回到它的调用者;
    (3) 线程试图得到一个锁,而该锁正被其他线程持有;
    (4) 线程在等待某个触发条件;

    所谓阻塞状态是正在运行的线程没有运行结束,暂时让出CPU,这时其他处于就绪状态的线程就可以获得CPU时间,进入运行状态。

  5. 死亡状态(Dead):
    有两个原因会导致线程死亡:
    (1) run方法正常退出而自然死亡。
    (2) 一个未捕获的异常终止了run方法而使线程猝死。

    为了确定线程在当前是否存活着(就是要么是可运行的,要么是被阻塞了),需要使用isAlive方法。如果是可运行或被阻塞,这个方法返回true; 如果线程仍旧是new状态且不是可运行的, 或者线程死亡了,则返回false。
    在这里插入图片描述

1.1 等待唤醒案例分析

在这里插入图片描述等待唤醒案例: 线程之间的通信

Object类中的方法:

  • void wait() 在其他线程调用此对象的notify()方法或notifyAll()方法前,使当前线程等待;
  • void notify() 唤醒在此对象监视器上等待的单个线程,会继续执行wait()方法之后的代码

代码实现:

  • 创建一个顾客线程:告知老板要的包子种类和数量,调用wait方法,放弃cpu执行,进入到WAITING状态(无限等待);
  • 创建一个老板线程:花了5秒做包子,做好包子后,调用notify方法,唤醒顾客吃包子。

注意: 顾客和老板线程必须使用同步代码块包裹起来,保证等待和唤醒只能有一个在执行,同步使用的锁对象必须保证唯一,只有锁对象才能调用wait和notify方法。

public class WaitAndNotify {
    public static void main(String[] args) {
        //创建锁对象,保证唯一
        Object obj = new Object();
        //创建一个顾客线程
        new Thread(){
            @Override
            public void run(){
                //保证等待和唤醒的线程只能有一个在执行,使用同步技术
                synchronized (obj) {
                    System.out.println("告知老板要的包子种类和数量");
                    //调用wait方法,放弃cpu执行,进入到WAITING状态(无限等待)
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    //唤醒之后继续执行wait后的代码
                    System.out.println("开始吃包子");
                }
            }
        }.start();

        //创建一个老板线程
        new Thread(){
            @Override
            public void run() {
                try {
                    Thread.sleep(5000); //花五秒做包子
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (obj) {
                    System.out.println("做好包子后,唤醒顾客吃包子");
                    obj.notify();
                }
            }
        }.start();
    }
}

1.2 Objects类中wait带参方法和notifyAll方式

进入到TimeWaiting(计时等待)有两种方法:

  • 使用sleep(long m)方法,在毫秒值结束之后,线程睡醒进入到Runnable/Blocked状态
  • 使用wait(long m)方法,wait方法如果在毫秒值结束之后,还没有被notify唤醒,就会自动醒来,线程睡醒进入到Runnable/Blocked状态

唤醒的方法:

  • void notify() 唤醒在此对象监视器上等待的单个线程
  • void notifyAll() 唤醒在此对象监视器上等待的所有线程

1.3 线程间通信

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

为什么要处理线程间通信:
多个线程并发执行时,在默认情况下CPU是随即切换线程的,当我们需要多个线程来共同完成一件任务,并且我们希望他们有规律的执行,那么多线程之间需要一些协调通信,用来帮助我们达到多线程共同操作一份数据。

如何保证线程间通信有效利用资源:
多个线程在处理同一个资源,并且任务不同时,需要线程通信来帮助解决线程之间对同一个变量的使用或操作。就是多个线程在操作同一份数据时,避免对同一共享变量的争夺。也就是我们需要通过一定的手段使各个线程能有效的利用资源。这种手段就是——等待唤醒机制

1.4 等待唤醒机制概述

在这里插入图片描述等待唤醒中的方法:

  1. wait: 线程不再活动,不再参与调度,进入wait set中,因此不会浪费CPU资源,也不会去竞争锁了,这时的线程状态即使WAITING。他还要等着别的线程执行一个特别的动作,也即是“通知(notify)”在这个对象上等待的线程从wait set中释放出来,重新进入到调度队列中。
  2. notify/notifyAll: 即选取所通知对象的wait set中的一个(全部)线程释放。

1.5 吃包子 —— 代码实现

包子类:

public class BaoZi {
    boolean flag = false;

    String pi;
    String xian;
}

包子铺类:

public class BaoZiPu extends Thread{
    // 1. 需要创建一个包子变量
    private BaoZi bz;

    // 2. 使用带参构造方法,为这个包子变量赋值
    public BaoZiPu(BaoZi bz){
        this.bz = bz;
    }

    // 3. 设置线程任务:生产包子
    @Override
    public void run() {
        int count = 0;
        //使用同步技术保证两个线程只能有一个正在执行
        synchronized (bz) {
            //对包子状态进行判断
            if(bz.flag == true) {
                //包子铺调用wait方法进入等待状态
                try {
                    bz.wait();
                } catch (InterruptedException 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();
            }
            //包子铺生产好了包子,改包子的状态为tree(有包子)
            bz.flag = true;
            //唤醒吃货线程吃包子
            bz.notify();
            System.out.println("包子已经生产好了,吃货可以开始吃了");
        }
    }
}

吃货类:

public class ChiHuo extends Thread {
    private BaoZi bz;

    public ChiHuo(BaoZi bz){
        this.bz = bz;
    }

    //设计线程任务:吃包子
    @Override
    public void run() {
        synchronized (bz) {
            //对包子的状态进行判断
            if(bz.flag == false) {
                try {
                    bz.wait();
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            //被唤醒之后执行的代码,吃包子
            System.out.println("正在吃包子");
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            //吃完后,修改包子的状态为false
            bz.flag = false;
            //唤醒包子铺线程生产包子
            bz.notify();
            System.out.println("吃货已经吃完包子了,开始生产包子");
        }
    }
}

测试类:

public class DemoBaozi {
    public static void main(String[] args) {
        BaoZi bz = new BaoZi();
        new BaoZiPu(bz).start();
        new ChiHuo(bz).start();
    }
}

2. 线程池

概念: 线程池是一个容纳多个线程的容器,其中的线程可以反复使用,省去了频繁创建线程对象的操作,无需反复创建线程而消耗过多资源。JDK1.5后可直接使用

  • 当程序第一次启动的时候,创建多个线程,保存到一个集合中
  • 当我们想要使用多线程的时候,就可以从集合中取出来线程使用
  • 当我们使用完毕线程,需要把线程归还给线程池

2.1 线程池代码实现

java.util.concurrent.Executors:线程池的工厂类,用来生成线程池

Executors类中的静态方法:
static ExecutorService newFixedThreadPool(int nThreads) 创建一个可重用固定线程的线程池
参数: int nThreads,创建线程池中包含的线程数量;
返回值: ExecutorService接口,返回的是ExecutorService接口的实现类对象,我们可以使用ExecutorService接口接收(面向接口编程)。

用来从线程池中获取线程,调用start方法,执行线程任务
submit(Runnable task)提交一个Runnable任务用于执行
关闭/销毁线程池的方法: void shutdown()。

//2. 创建一个类,实现Runnable接口,重写run方法,设置线程任务
public class RunnableImpl implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName());
    }
}
public class DemoThreadPool {
    public static void main(String[] args) {
        //1. 使用线程池的工厂类Executors里面提供的静态方法newFixedThreadPool产生一个指定线程数量的线程池
        ExecutorService es = Executors.newFixedThreadPool(2);
        //3. 调用ExecutorService中的方法submit,传递线程任务(实现类),开启线程,执行run方法
        es.submit(new RunnableImpl());
        //线程会一直开启,使用完了线程,会把线程归还线程池,线程可以继续使用
        es.submit(new RunnableImpl());
        es.submit(new RunnableImpl());

        //4. 调用ExecutorService中的方法shutdown销毁线程池(不建议执行)
        es.shutdown();
    }
}

3. Lambda表达式

面向对象的思想: 做一件事情,找一个能解决这个事情的对象,调用对象的方法,完成事情。

函数式编程思想: 只要能获取到结果,谁去做的,怎么做的都不重要,重视的是结果,不重视过程。

3.1 冗余的Runnable代码

使用实现Runnable接口的方式来实现多线程程序:

public class Demo01Runnable {
    public static void main(String[] args) {
        //创建Runnable接口的实现类对象
        RunnableImpl run = new RunnableImpl();
        //创建Thread类对象,构造方法中传递Runnable接口的实现类
        Thread t = new Thread(run);
        //调用start方法开启新线程,执行run方法
        t.start();

        //简化代码,使用匿名内部类,实现多线程程序
        Runnable r = new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        };
        new Thread(r).start();

        //继续简化
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName());
            }
        }).start();
    }
}

对于Runnable的匿名内部类用法,可以分析出:
Thread类需要Runnable接口作为参数,其中的抽象run方法是用来指定线程任务的核心;为了指定run的方法体,不得不需要Runnable接口的实现类;为了省去定义一个RunnableImpl实现类的麻烦,不得不使用匿名内部类;为了覆盖重写抽象run方法,所以方法名称,方法参数,方法返回值不得不再写一遍,且不能写错;而实际上,方法体才是关键所在。

3.2 Lambda的更优写法

JDK1.8中加入了Lambda表达式的新特性

public class DemoLambda {
    public static void main(String[] args) {
        //使用Lambda表达式实现多线程
        new Thread(() -> {
                System.out.println(Thread.currentThread().getName());
            }
        ).start();
    }
}

3.3 Lambda标准格式

Lambda标准格式由三部分组成:一些参数,一个箭头,一段代码。

格式: (参数列表) -> {一些重写方法的代码}

格式说明:

  • ():接口中抽象方法的参数列表,没有参数,就空着;有参数就写出参数,多个参数之间使用逗号隔开;
  • ->:传递的意思,把参数传递给方法体{}
  • {}:重写接口中的抽象方法的方法体。

3.4 Lambda表达式的练习

无参数无返回值的情况下:

  • 给定一个厨子Cook接口,内含唯一的抽象方法makeFood,且无参数,无返回值;
  • 使用Lambda的标准格式调用invokeCook方法,打印出“吃饭啦”的字样。
public interface Cook {
    public abstract void makeFood();
}
public class DemoCook {
    public static void main(String[] args) {
        invokeCook(new Cook() {
            @Override
            public void makeFood() {
                System.out.println("吃饭啦");
            }
        });

        //使用Lambda表达式
        invokeCook(() -> {
                System.out.println("吃饭啦");
            }
        );
    }

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

有参数无返回值的情况下:

  • 使用数组存储多个Person对象
  • 对数组中的Person对象使用Arrays的sort方法通过年龄进行升序排序
public class Person {
    String name;
    int age;

    public Person() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    public Person(String name, int age) {
        this.name = name;
        this.age = age;
    }

    @Override
    public String toString() {
        return "Person{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }
}
public class DemoArrays {
    public static void main(String[] args) {
        Person[] arr = {
                new Person("猪猪侠", 15),
                new Person("小菲菲", 14),
                new Person("超人强", 16)
        };
        Arrays.sort(arr, new Comparator<Person>() {
            @Override
            public int compare(Person o1, Person o2) {
                return o1.getAge() - o2.getAge();
            }
        });
        
        //使用Lambda表达式
        Arrays.sort(arr, (Person o1, Person o2) -> {
                    return o1.getAge() - o2.getAge();
                }
        );
        for (Person person : arr) {
            System.out.println(person);
        }

    }
}

有参数有返回值的情况下:

  • 给定一个计算器Calculator接口,内含抽象方法calc可以将两个int数组相加得到和值;
  • 使用Lambda的标准格式调用invokeCalc方法,计算1+2。
public interface Calculator {
    public abstract int calc(int a, int b);
}
public class DemoCalculator {
    public static void main(String[] args) {
        invokeCalc(1, 2, new Calculator() {
            @Override
            public int calc(int a, int b) {
                return a + b;
            }
        });

        //使用Lambda表达式
        invokeCalc(1, 2, (int a, int b) -> {
                return a + b;
            }
        );
    }

    public static void invokeCalc(int a, int b, Calculator c){
        int sum = c.calc(a, b);
        System.out.println(sum);
    }
}

3.5 Lambda省略格式和使用前提

Lambda表达式:凡是可以推导出来的,都可以省略;

可以省略的内容:

  1. 参数列表:括号中参数列表的数据类型,可以省略不写;
  2. 参数列表:括号中的参数如果只有一个,那么类型和()都可以省略;
  3. 一些代码:如果{}中的代码只有一行,无论是否有返回值,都可以省略({},return,分号),{}、return、分号必须一起省略。

例如上面代码中分别可以省略成:

invokeCook(() -> System.out.println("吃饭啦"));

Arrays.sort(arr, (o1, o2) -> o1.getAge() - o2.getAge());

invokeCalc(1, 2, (a, b) -> a + b);

使用前提:

  1. 使用Lambda必须具有接口,且要求接口中有且仅有一个抽象方法;
  2. 使用Lambda必须具有上下文推断,也就是方法的参数或局部变量类型必须为Lambda对应的接口类型,才可以使用Lambda作为该接口的实例。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值