day13【线程池、死锁、线程状态、等待与唤醒+1道作业】

day13【线程池、死锁、线程状态、等待与唤醒】

  • 第一章 线程池方式
    • 1.1线程池概念
    • 1.2线程池的好处
    • 1.3线程池的使用
    • 1.4实操--线程池的练习
  • 第二章 死锁
  • 第三章 线程状态
    • 3.1线程状态
    • 3.2线程状态的切换
    • 3.3等待唤醒机制
    • 3.31等待唤醒机制相关方法介绍
    • 3.32等待唤醒案例
  • 第四章 定时器
    • 4.1定时器功能介绍
    • 4.2定时器方法介绍
    • 4.3案例演示
  • 第五章 练习模块
    • 5.1​练习一题
  • 第六章 总结

第一章 线程池方式

线程池的思想
day13【线程池、死锁、线程状态、等待与唤醒+1道作业】_第1张图片
我们使用线程的时候就去创建一个线程,这样实现起来非常简便,但是就会有一个问题:

如果并发的线程数量很多,并且每个线程都是执行一个时间很短的任务就结束了,这样频繁创建线程就会大大降低系统的效率,因为频繁创建线程和销毁线程需要时间。

那么有没有一种办法使得线程可以复用,就是执行完一个任务,并不被销毁,而是可以继续执行其他的任务?

在Java中可以通过线程池来达到这样的效果。

1.1线程池概念

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

由于线程池中有很多操作都是与优化资源相关的,我们在这里就不多赘述。我们通过一张图来了解线程池的工作原理:

1.2线程池的好处

  1. 降低资源消耗。减少了创建和销毁线程的次数,每个工作线程都可以被重复利用,可执行多个任务。
  2. 提高响应速度。当任务到达时,任务可以不需要的等到线程创建就能立即执行。
  3. 提高线程的可管理性。可以根据系统的承受能力,调整线程池中工作线线程的数目,防止因为消耗过多的内存,而把服务器累趴下(每个线程需要大约1MB内存,线程开的越多,消耗的内存也就越大,最后死机)。

1.3线程池的使用

Java里面线程池的顶级接口是java.util.concurrent.Executor,但是严格意义上讲Executor并不是一个线程池,而只是一个执行线程的工具。真正的线程池接口是java.util.concurrent.ExecutorService

要配置一个线程池是比较复杂的,尤其是对于线程池的原理不是很清楚的情况下,很有可能配置的线程池不是较优的,因此在java.util.concurrent.Executors线程工厂类里面提供了一些静态工厂,生成一些常用的线程池。官方建议使用Executors工厂类来创建线程池对象。

Executors类中有个创建线程池的方法如下:

  • public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象。(创建的是有界线程池,也就是池中的线程个数可以指定最大数量)

获取到了一个线程池ExecutorService 对象,那么怎么使用呢,在这里定义了一个使用线程池对象的方法如下:

  • public Future submit(Runnable task):获取线程池中的某一个线程对象,并执行任务

  • public Future submit(Callable task):获取线程池中的某一个线程对象,并执行任务

    Future接口:用来记录线程任务执行完毕后产生的结果。

使用线程池中线程对象的步骤:

  1. 创建线程池对象。
  2. 创建Runnable接口子类对象。(task)
  3. 提交Runnable接口子类对象。(take task)
  4. 关闭线程池(一般不做)。

Runnable实现类代码:

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        //任务
        System.out.println(Thread.currentThread().getName()+":开始执行实现Runnable方式的任务....");
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(Thread.currentThread().getName()+":执行完毕....");
    }
}

线程池测试类:

public class Test1_Runnable {
    public static void main(String[] args) {
        /*
            线程池使用一:任务是通过实现Runnable的方式创建
              1.使用Executors工厂类中的静态方法来创建线程池:
                    public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
              2..提交并执行任务:
                    - public Future submit(Runnable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
         */
        // 1.创建线程池,初始化线程
        ExecutorService es = Executors.newFixedThreadPool(3);// 创建一个线程池对象,该线程池中有3条线程

        // 2.创建任务
        MyRunnable mr = new MyRunnable();

        // 3.提交并执行任务
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);
        es.submit(mr);

        // 4.销毁线程池(一般不操作)
        //es.shutdown();
    }
}

Callable测试代码:

  • Future submit(Callable task) : 获取线程池中的某一个线程对象,并执行.

    Future : 表示计算的结果.

  • V get() : 获取计算完成的结果。

public class MyCallable implements Callable<String> {

    @Override
    public String call() throws Exception {
        // 线程需要执行的任务
        System.out.println(Thread.currentThread().getName()+":开始执行任务...");
        Thread.sleep(5000);
        System.out.println(Thread.currentThread().getName()+":任务执行完毕...");
        return "青年节快乐";// 返回任务执行完毕后的结果
    }
}
public class Test2_Callable {
    public static void main(String[] args) throws Exception{
        /*
            线程池使用二: 任务是通过实现Callable的方式创建
                1.使用Executors工厂类中的静态方法来创建线程池:
                        public static ExecutorService newFixedThreadPool(int nThreads):返回线程池对象,通过参数指定线程池中的线程数量
                2.提交并执行任务:
                      public  Future submit(Callable task):通过参数传入任务,获取线程池中的某一个线程对象,并执行任务
                      返回值:
                        Future接口:用来记录线程任务执行完毕后产生的结果。
                             V get(): 可以获取线程执行完任务后返回的结果
         */
        // 1.创建线程池,初始化线程
        ExecutorService es = Executors.newFixedThreadPool(3);

        // 2.创建任务
        MyCallable mc = new MyCallable();

        // 3.提交并执行任务
        Future<String> future = es.submit(mc);
        es.submit(mc);
        es.submit(mc);
        es.submit(mc);
        es.submit(mc);

        System.out.println("获取任务执行完毕后的结果:"+future.get());

        // 4.销毁线程池(一般不操作)
    }
}

1.4实操–线程池的练习

需求

  • 使用线程池方式执行任务,返回1-n的和

分析

因为需要返回求和结果,所以使用Callable方式的任务

实现

public class Demo04 {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(3);

        SumCallable sc = new SumCallable(100);
        Future<Integer> fu = pool.submit(sc);
        Integer integer = fu.get();
        System.out.println("结果: " + integer);
        
        SumCallable sc2 = new SumCallable(200);
        Future<Integer> fu2 = pool.submit(sc2);
        Integer integer2 = fu2.get();
        System.out.println("结果: " + integer2);

        pool.shutdown();
    }
}

SumCallable.java

public class SumCallable implements Callable<Integer> {
    private int n;

    public SumCallable(int n) {
        this.n = n;
    }

    @Override
    public Integer call() throws Exception {
        // 求1-n的和?
        int sum = 0;
        for (int i = 1; i <= n; i++) {
            sum += i;
        }
        return sum;
    }
}

第二章 死锁

什么是死锁

在多线程程序中,使用了多把锁,造成线程之间相互等待.程序不往下走了。

产生死锁的条件

1.有多把锁
2.有多个线程
3.有同步代码块嵌套

死锁代码

public class Test {
    public static void main(String[] args)  {

         new Thread(new Runnable() {
            @Override
            public void run() {
                // 任务
                synchronized ("锁A"){
                    System.out.println("张三线程: 拿到了锁A,等待获取锁B...");
                    synchronized ("锁B"){
                        System.out.println("张三线程: 拿到了锁A,锁B,进入了房间");
                    }
                }
            }
        }).start();

        // 创建并执行李四线程
        new Thread(new Runnable() {
            @Override
            public void run() {
                // 任务
                synchronized ("锁B"){
                    System.out.println("李四线程:拿到了锁B,等待获取锁A...");
                    synchronized ("锁A"){
                        System.out.println("李四线程: 拿到了锁A,锁B,进入了房间");
                    }
                }
            }
        }).start();
    }
}

第三章 线程状态

3.1线程状态

线程由生到死的完整过程:技术素养和面试的要求。

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

这里先列出各个线程状态发生的条件,下面将会对每种状态进行详细解析

线程状态 导致状态发生条件
NEW(新建) 线程刚被创建,但是并未启动。还没调用start方法。MyThread t = new MyThread()只有线程对象,没有线程特征。创建线程对象时
Runnable(可运行) 线程可以在java虚拟机中运行的状态,可能正在运行自己代码,也可能没有,这取决于操作系统处理器。调用了t.start()方法 :就绪(经典教法)。调用start方法时
Blocked(锁阻塞) 当一个线程试图获取一个对象锁,而该对象锁被其他的线程持有,则该线程进入Blocked状态;当该线程持有锁时,该线程将变成Runnable状态。等待锁对象时
Waiting(无限等待) 一个线程在等待另一个线程执行一个(唤醒)动作时,该线程进入Waiting状态。进入这个状态后是不能自动唤醒的,必须等待另一个线程调用notify或者notifyAll方法才能够唤醒。调用wait()方法时
Timed Waiting(计时等待) 同waiting状态,有几个方法有超时参数,调用他们将进入Timed Waiting状态。这一状态将一直保持到超时期满或者接收到唤醒通知。带有超时参数的常用方法有Thread.sleep 、Object.wait。调用sleep()方法时
Teminated(被终止) 因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。run方法执行结束时

3.2线程状态的切换

day13【线程池、死锁、线程状态、等待与唤醒+1道作业】_第2张图片
我们不需要去研究这几种状态的实现原理,我们只需知道在做线程操作中存在这样的状态。那我们怎么去理解这几个状态呢,新建与被终止还是很容易理解的,我们就研究一下线程从Runnable(可运行)状态与非运行状态之间的转换问题。

3.3等待唤醒机制

子线程: 打印1000次i循环

主线程:打印1000次j循环

规律: 打印1次i循环,就打印1次j循环,以此类推…

假如子线程先执行,打印1次i循环,让子线程进入无限等待,执行j循环,唤醒子线程,主线程就进入无限等待

值班: 2个人值班

什么是等待唤醒机制

这是多个线程间的一种协作机制。就好比在公司里你和你的同事们,你们可能存在在晋升时的竞争(抢占式),但更多时候你们更多是一起合作以完成某些任务。(相互唤醒和等待)

就是在一个线程进行了规定操作后,就进入无限等待状态(wait()),调用notfiy()方法唤醒其他线程来执行,其他线程执行完后,进入无限等待,唤醒等待线程执行,依次类推… 如果需要,可以使用 notifyAll()来唤醒所有的等待线程。

wait/notify 就是线程间的一种协作机制。

3.31等待唤醒机制相关方法介绍

  • public void wait() : 让当前线程进入到等待状态 此方法必须锁对象调用.
  • public void notify() : 唤醒当前锁对象上等待状态的线程 此方法必须锁对象调用.
  • 案例一:
public class Test {
    static Object obj = new Object();
    public static void main(String[] args)  {
        // 步骤1 : 子线程开启,进入无限等待状态, 没有被唤醒,无法继续运行.
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("准备进入无限等待状态...");
                synchronized (obj){
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        }).start();
    }
}
  • 案例二:
public class Test {
    static Object obj = new Object();
    public static void main(String[] args)  {
        new Thread(new Runnable() {
            @Override
            public void run() {
                System.out.println("准备进入无限等待状态...");
                synchronized (obj){
                    try {
                        obj.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                System.out.println("被唤醒了,继续执行");
            }
        }).start();

        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (obj){
                    System.out.println("准备唤醒无限等待线程...");
                    obj.notify();
                }
            }
        }).start();
    }
}

3.32等待唤醒案例

需求

  • 等待唤醒机制其实就是经典的“生产者与消费者”的问题。

  • 就拿生产包子消费包子来说等待唤醒机制如何有效利用资源:

day13【线程池、死锁、线程状态、等待与唤醒+1道作业】_第3张图片
分析

包子铺线程生产包子,吃货线程消费包子。当包子没有时(包子状态为false),吃货线程等待,包子铺线程生产包子(即包子状态为true),并通知吃货线程(解除吃货的等待状态),因为已经有包子了,那么包子铺线程进入等待状态。接下来,吃货线程能否进一步执行则取决于锁的获取情况。如果吃货获取到锁,那么就执行吃包子动作,包子吃完(包子状态为false),并通知包子铺线程(解除包子铺的等待状态),吃货线程进入等待。包子铺线程能否进一步执行则取决于锁的获取情况。

实现

包子类:

public class BaoZi {
    public boolean flag;// 默认值为false,表示没有包子
    public String xian;// 包子馅
}

生成包子类:

public class BaoZiPu extends Thread {

    BaoZi bz;

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

    @Override
    public void run() {
        // 生产包子
        // 加锁
        while (true) {
            synchronized (bz){
                // 如果有包子,就进入无限等待
                if (bz.flag == true){
                    // 进入无限等待
                    try {
                        bz.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                // 来到这里,说明一定没有包子
                System.out.println("包子铺线程:开始生产包子...");
                // 设置包子馅
                bz.xian = "猪肉大葱";
                // 生产完包子,改变包子的状态
                bz.flag = true;
                System.out.println("包子铺线程:生产好了包子,唤醒吃货来吃...");
                // 唤醒吃货线程
                bz.notify();
            }
        }
    }
}

消费包子类:

public class ChiHuo extends Thread {

    BaoZi bz;

    public ChiHuo(BaoZi bz) {
        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.xian);
                // 吃完包子,改变包子的状态
                bz.flag = false;
                System.out.println("吃货线程:吃完包子了=====================");
                // 唤醒包子铺线程
                bz.notify();
            }
        }
    }
}

测试类:

public class Test {
    public static void main(String[] args) {
        /*
            实现:
                包子铺线程生产包子,生产完后就进入无限等待,唤醒吃货线程
                吃货线程吃包子,吃完了包子就进入无限等待,唤醒包子铺线程
         */
        // 创建包子对象
        BaoZi bz = new BaoZi();

        // 创建包子铺线程对象
        BaoZiPu baoZiPu = new BaoZiPu(bz);

        // 创建吃货线程对象
        ChiHuo ch = new ChiHuo(bz);

        // 启动包子铺线程
        baoZiPu.start();

        // 启动吃货线程
        ch.start();
    }
}

第四章 定时器

4.1定时器功能介绍

​ 定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情

4.2定时器方法介绍

1.构造方法

public Timer():构造一个定时器

2.成员方法

void  schedule(TimerTask task, long delay)
                              在指定的延迟时间之后执行指定的任务。
void schedule(TimerTask task, long delay,  long period)
                              在指定的延迟时间之后开始执行任务,每隔指定毫秒重新执行任务。
void schedule(TimerTask task, Date time)
                              在指定日期时间执行指定任务。
void  schedule(TimerTask task, Date firstTime,  long period)
                              在指定日期时间执行指定任务,每隔指定毫秒重新执行任务。

4.3案例演示

public class Test {
    public static void main(String[] args) throws ParseException {
        /*
            定时器:
                - 功能介绍
                    定时器,可以设置线程在某个时间执行某件事情,或者某个时间开始,每间隔指定的时间反复的做某件事情
                - 方法介绍
                    1.创建定时器:
                        public Timer():构造一个定时器
                    2.设置定时器:
                        void  schedule(TimerTask task, long delay)
                                                       在指定的延迟时间之后执行指定的任务。
                        void schedule(TimerTask task, long delay,  long period)
                                                      在指定的延迟时间之后开始执行任务,每隔指定毫秒重新执行任务。

                        void schedule(TimerTask task, Date time)
                                                      在指定日期时间执行指定任务。
                        void schedule(TimerTask task, Date firstTime,  long period)
                                                      在指定日期时间执行指定任务,每隔指定毫秒重新执行任务。
                       void cancel()  取消定时器
                - 案例演示
         */
        // 1.创建定时器
        Timer timer = new Timer();

        // 2.设置定时器
        //  void  schedule(TimerTask task, long delay)
        // 设置3秒后,定时器开始执行指定任务,只执行一次
       /* timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
                // 执行完任务后,取消定时器
                timer.cancel();
            }
        },3000);*/

        // void schedule(TimerTask task, long delay,  long period)
        // 设置3秒后,定时器开始执行指定任务,每隔1秒重复执行任务
        /*timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
            }
        },3000,1000);*/

        // void schedule(TimerTask task, Date time)
       /* // 设置2020-05-04 14:19:00执行指定的任务
        String str = "2020-05-04 14:19:00";
        // 把String对象转换为Date对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
                timer.cancel();
            }
        },date);*/

       // void schedule(TimerTask task, Date firstTime,  long period)
        // 设置2020-05-04 14:22:00执行指定的任务,每隔1秒重复执行
        String str = "2020-05-04 14:22:00";
        // 把String对象转换为Date对象
        SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
        Date date = sdf.parse(str);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println("起床啦...");
            }
        },date,1000);
    }
}

第五章 练习模块

5.1​练习一题

请用“等待唤醒”机制编写一个程序,要求:

​ 第一个线程:遍历1–1000所有的数字,在遍历过程中,如果发现这个数字能同时被

​ 2,3,5,7整除,立即wait()退出等待,让第二个线程进入。

​ 第二个线程:运行后,将一个计数器 + 1,之后再唤醒等待的线程。

​ 主线程中:休息2秒,让两个线程全部执行完毕,打印“计数器”的结果。

​ 注意:第二个线程使用的计数器,要定义在线程外部。

public class H31 {
    final static Object lock = new Object(); // 锁

    static int count = 0; // 计数值

    static int flag = 0;
    // 0 表示遍历线程运行
    // 1 表示计数线程运行且该让计数加1
    // 2 表示计数线程运行且退出循环

    public static void main(String[] args) throws InterruptedException {
        new Thread() {
            @Override
            public void run() {
                for (int i = 1; i <= 1000; i++) {
                    synchronized (lock) {
                        // 还没轮到自己,休息
                        while (flag == 1) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        if (i % 2 == 0 && i % 3 == 0 && i % 5 == 0 && i % 7 == 0) {
                            // 满足条件
                            System.out.println(i);
                            // 通知计数线程运行,自己下次阻塞
                            flag = 1;
                            lock.notify();
                        }
                    }
                }
                // 循环结束,通知计数线程运行且退出循环
                synchronized (lock) {
                    flag = 2;
                    lock.notify();
                }

            }
        }.start();

        new Thread() {
            @Override
            public void run() {
                synchronized (lock) {
                    // 不断循环进行计数
                    while (true) {
                        // 还没轮到自己,休息
                        while (flag == 0) {
                            try {
                                lock.wait();
                            } catch (InterruptedException e) {
                                e.printStackTrace();
                            }
                        }
                        if (flag == 2) {
                            // 退出
                            break;
                        } else if (flag == 1) {
                            // 计数加1
                            count++;
                            // 唤醒遍历线程
                            flag = 0;
                            lock.notify();
                        }
                    }
                }
            }
        }.start();

        // 休息2秒,让两个线程全部执行完毕,打印“计数器”的结果
        Thread.sleep(2000);
        System.out.println(count);

    }
}

答案
210
420
630
840
4

第六章 总结

  • 能够描述Java中线程池运行原理
    创建线程池的时候,在线程池中初始化一些线程,当有任务加入的时候,就会随机分配一个空闲线程去执行,
    那么该线程就处于占用状态,当线程池中的所有线程都处于占用状态的时候,新添加的任务就得等待,
    当有线程执行完任务,处于空闲状态的时候,就会来执行等待的任务,依次类推…

  • 能够描述死锁产生的原因
    一条线程获取了其他线程的锁,并不释放,造成其他线程获取不到锁,出现死锁
    死锁产生的条件
    多条线程
    多把锁
    同步代码块嵌套

  • 能够说出线程6个状态的名称
    新建,可运行,锁阻塞,无限等待,计时等待,被终止

  • 能够理解等待唤醒案例
    实现等待唤醒机制:
    1.使用锁对象调用wait()方法进入无限等待状态
    2.其他线程使用锁对象调用notify()方法或者notifyAll()方法唤醒等待线程
    3.调用wait()和notify(),notifyAll()方法的锁对象一致
    分析等待唤醒的程序:
    1.线程的调度依然是抢占式调度
    2.线程通过所对象调用wait()方法进入了无限等待,那么该线程就不会争夺cpu,也不会霸占锁对象
    3.线程从无限等待状态被唤醒之后,需要拿到锁对象才可以从唤醒的位置继续往下执行,否则进入锁阻塞
    课后扩展: 多条线程的有效通信(有规律执行)
    线程1执行—>线程2执行—>线程3—>线程1…以此类推

    练习: 1.线程的使用 2.死锁 3.等待唤醒机制吃包子案例(并分析程序运行) 4.定时器
    理解: 线程池工作原理 等待唤醒机制

你可能感兴趣的:(day13【线程池、死锁、线程状态、等待与唤醒+1道作业】)