07-多线程

多线程

简介:

  • 概述:进程多条执行路径, 统称为:多线程.
    • 进程: 指的是可执行程序文件。例如: *.exe
      • 大白话:进程 是 汽车
    • 线程: 指的是进程的执行单元, 执行路径
      • 大白话: 线程 是 车道
多线程 并行 和 **并发 **的区别是什么?
  • 多线程并行:
    指的是两个或者以上的线程同时执行. 前提:需要多核CPU.
  • 多线程并发:
    指的是两个或者以上的线程同时请求执行,但是同一瞬间CPU只能执行一个,于是就安排它们(多个线程)交替执行,又因为时间间隔非常短,我们看起来好像是同时执行的,其实不是。
多线程并行和并发的区别 图解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Vj5RTdNe-1669233017783)(F:\BigData\java\图片\day13图片\多线程\多线程并行和并发的区别图解.png)]

多线程的两种实现方式:

  • 记忆的话
    1. 一台电脑上可以有多个进程,这些进程之间的数据是相互 隔离
    2. 一个进程可以有多个线程,这些线程 共享进程的数据
    3. 开启线程调用的是 Thread#start() 方法,如果调用 Thread#run()方法,只是普通的方法调用,并没有开启线程
    4. 同一个线程对象重复开启,会报 IllegalThreadStateException(非法的线程状态异常)
多线程 程序执行流 图解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2B1aA6HV-1669233017785)(F:\BigData\java\图片\day13图片\多线程\单线程程序执行流图.png)]

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-WpRXus7L-1669233017785)(F:\BigData\java\图片\day13图片\多线程\多线程程序执行流图.png)]

方式一:继承Thread类

           1. 自定义一个类(例如: MyThread, **线程类**),继承**Thread类**
        2. 重写**Thread类**中的**run()方法**
        3. 把**要执行的代码写到run()方法中**
        4. 在**测试类中创建线程对象**
        5. 开启线程
演示多线程的实现方式一: 继承Thread类
package com.itheima.demo01_thread;

//自定义的线程类, 用来输出一些内容.

//1. 定义一个类(MyThread), 继承Thread类.
public class MyThread extends Thread{
    //2. 重写Thread#run()方法.
    @Override
    public void run() {
        //3. 把要执行的代码放到run()方法中.
        for (int i = 0; i < 100; i++) {
            System.out.println("run...................." + i);
        }
    }
}

package com.itheima.demo01_thread;

//案例: 演示多线程的实现方式一: 继承Thread类.

public class Demo02 {
    public static void main(String[] args) {
        //需求2: 打印100次 run, 通过自定义的线程类实现.
        //1. 创建线程对象.
        MyThread mt = new MyThread();
        //2. 开启线程.
        //mt.run();       //这样写不会报错, 但是只是普通的方法调用而已, 并没有开启线程.
        mt.start();
        //mt.start();    //同一线程不能重复开启, 否则会报 IllegalThreadStateException(非法的线程状态异常).


        //需求1: 打印100次 main
        for (int i = 0; i < 100; i++) {
            System.out.println("main..." + i);
        }

        //System.out    标准的输出流, 用来往控制台输出内容的.
        //System.err    标准的错误流, 用来打印异常信息的.
    }
}

方式二:实现Runnable接口

  1. 自定义一个类(例如: MyRunnable, 资源类), 实现Runnable接口
  2. 重写Runnable接口中的run()方法
  3. 要执行的代码写到run()方法中
  4. 在测试类中: 创建资源类对象, 并将其作为参数传给Thread类的构造, 从而创建线程对象
  5. 开启线程
演示多线程的实现方式二: 实现Runnable接口
package com.itheima.demo02_runnable;


//1. 定义一个类(MyRunnable), 实现Runnable接口
public class MyRunnable implements Runnable {

    //2. 重写Runnable#run()方法.
    @Override
    public void run() {
        //3. 把要执行的代码放到run()方法中.
        for (int i = 0; i < 100; i++) {
            System.out.println("run...................." + i);
        }
    }
}

package com.itheima.demo02_runnable;

//案例: 演示多线程的实现方式二: 实现Runnable接口.

public class Demo01 {
    public static void main(String[] args) {
        //4. 创建Runnable接口的子类对象, 并将其作为参数传递给Thread类, 创建线程对象.
        //4.1 创建Runnable接口的子类对象
        MyRunnable mr = new MyRunnable();
        //4.2 并将其作为参数传递给Thread类, 创建线程对象.
        Thread th = new Thread(mr);

        //Lambda表达式版本实现.
        /*Thread th = new Thread(() -> {
            //3. 把要执行的代码放到run()方法中.
            for (int i = 0; i < 100; i++) {
                System.out.println("run...................." + i);
            }
        });*/
        //5. 开启线程.
        th.start();

        //需求1: 打印100次 main
        for (int i = 0; i < 100; i++) {
            System.out.println("main..." + i);
        }
    }
}

匿名内部类的方式实现多线程
package com.itheima.demo02_runnable;

//案例: 通过匿名内部类的方式实现多线程.
public class Demo02 {
    public static void main(String[] args) {
        //方式一:  继承Thread类.
        new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("传智播客....  继承Thread类");
                }
            }
        }.start();

        //方式二:  实现Runnable接口, 匿名内部类.    采用面向对象的思想实现.
        //格式: new Thread(Runnable接口的子类对象).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    System.out.println("黑马程序员....  实现Runnable接口");
                }
            }
        }).start();

        //方式三:  实现Runnable接口, Lambda表达式,   采用函数式编程思想实现.
        //因为Lambda表达式这种方式没有显示的重写run()方法, 所以导致 通过start()方法开启线程的时候,
        //找不到对应的run()方法, 然后当做普通的方法调用了, 偶尔会看见抢资源的情况, 但是几率较小.
        new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                System.out.println("博学谷....  Lambda表达式");
            }
        }).start();
    }
}

Thread类中的成员方法总结

构造方法
  • public Thread();
  • public Thread(String name); 指定线程的名字
  • public Thread(Runnable target);
  • public Thread(Runnable target, String name); 执行线程的名字
成员方法
  • public void run(); 里边写的是该线程要执行的代码
  • public void start(); 开启线程
  • public String getName(); 获取当前正在执行的线程对象的名字.
  • public void setName(String name); 设置线程名
  • public static void sleep(long time)线程休眠指定时间, 单位是毫秒.
  • public static Thread currentThread() 返回当前正在执行的线程对象的引用.
  • public int getPriority(); 获取线程的优先级.
  • public void setPriority(int i); 设置优先级.
  • public void join(); 加入线程, 即: 相当于插队, 也叫: 等待这个线程死亡.
  • public void setDaemon(boolean flag) 设置线程的类别, true: 守护线程, false: 非守护线程(默认)
多线程模拟卖票_继承Tread类的方式

出现的问题:

​ 出现负数票和重复值

问题原因:

负数票:

​ 当ticket的值为1的时候,此时不管哪个线程抢到资源,都会越过if判断,然后停留在休眠线程这里,而休眠线程的特点是:在那里睡,到点就在那里醒来,此时四个线程依次醒来,开始抢资源,就会出现如下的情况:

​ 假设窗口1先抢到资源,此时会打印:窗口1正在售卖第1张票,之后执行ticket–,最终ticket = 0

​ 假设窗口2先抢到资源,此时会打印:窗口2正在售卖第0张票,之后执行ticket–,最终ticket = -1

​ 假设窗口3先抢到资源,此时会打印:窗口3正在售卖第-1张票,之后执行ticket–,最终ticket = -2

​ 假设窗口4先抢到资源,此时会打印:窗口4正在售卖第-2张票,之后执行ticket–,最终ticket = -3

重复值:

​ 它的出现和ticket–这行代码相关,它等价于:ticket = ticket - 1 ,这行代码做了3件事

​ 1.读取ticket的值

​ 2.修改ticket的值,即:-1

​ 3.重新赋值

​ 当某个窗口打印完票数之后,还没有来得及执行重新赋值的时候,被别的线程抢走了资源,就会抢走重复值

解决方案:

​ 采用同步代码块解决,即:加锁的思想,让某一个线程在卖票的过程中,不会受到其他线程的影响。

package com.itheima.demo03_tickets;

//自定义的线程类MyThread, 用来实现模拟卖票
public class MyThread extends Thread {
    //1. 定义变量, 记录票数.
    private static int tickets = 100;      //细节1: 这里要加static, 以为4个窗口(线程)共享100张票.

    //定义构造方法, 用来给线程设置名字.
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    //2. 在run()方法中实现模拟卖票
    @Override
    public void run() {
        //3.一直卖票,  没有票, 就不卖了.
        while(true) {
            // 没有票, 就不卖了.
            if (tickets <= 0) {         //线程1,线程2 线程3, 线程4
                break;
            }

            //细节2: 加入休眠线程, 让程序出错的概率大一些.
            try {
                Thread.sleep(50);   //单位是毫秒,  该方法的特点是: 在哪里睡, 到点后就在哪里醒来.     线程1,线程2 线程3, 线程4
            } catch (InterruptedException e) {
                e.printStackTrace();
            }

            //走到这里, 说明有票, 卖票即可
            System.out.println(getName() + "  售卖出第 "+ tickets-- +" 张票");    //细节3: 这里要加入线程名, 即: 那个窗口卖的票.
        }
    }
}

package com.itheima.demo03_tickets;

//案例: 模拟卖票,  100张票, 由4个窗口售卖.

public class Demo {
    public static void main(String[] args) {
        //1. 创建4个窗口, 即: 4个线程.
        MyThread mt1 = new MyThread("窗口1");
        //mt1.setName("窗口1");
        MyThread mt2 = new MyThread("窗口2");
        MyThread mt3 = new MyThread("窗口3");
        MyThread mt4 = new MyThread("窗口4");

        //2. 开启线程, 卖票即可.
        mt1.start();
        mt2.start();
        mt3.start();
        mt4.start();
    }
}

线程安全

线程同步

  • 概述
    • 多线程环境 并发 操作同一数据, 就有可能引发安全问题, 此时就需要通过同步思想来解决
同步代码块:
  • 格式:

    • synchronized(锁对象) {
          //要加锁的代码
      }
      
  • 细节:

    1. 同步代码块的锁对象可以是任意类型的对象.
    2. 必须使用同一把锁, 否则可能出现锁不住的情况.:
模拟卖票_问题解决
package com.itheima.demo04_synchronized;

//自定义的线程类MyThread, 用来实现模拟卖票
public class MyThread extends Thread {
    //1. 定义变量, 记录票数.
    private static int tickets = 100;      //细节1: 这里要加static, 以为4个窗口(线程)共享100张票.

    //模拟锁对象
    /*private static Object obj = new Object();
    private static String s = new String();*/

    //定义构造方法, 用来给线程设置名字.
    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    //2. 在run()方法中实现模拟卖票
    @Override
    public void run() {
        //3.一直卖票,  没有票, 就不卖了.
        while(true) {
            //************** 一次完整的卖票动作从这里开始..
            synchronized (MyThread.class) {		//实际开发中,我们一般用本类的字节码文件对象,当锁对象
                // 没有票, 就不卖了.
                if (tickets <= 0) {         //线程1,线程2 线程3, 线程4
                    break;
                }

                //细节2: 加入休眠线程, 让程序出错的概率大一些.
                try {
                    Thread.sleep(50);   //单位是毫秒,  该方法的特点是: 在哪里睡, 到点后就在哪里醒来.     线程1,线程2 线程3, 线程4
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //走到这里, 说明有票, 卖票即可
                System.out.println(getName() + "  售卖出第 "+ tickets-- +" 张票");    //细节3: 这里要加入线程名, 即: 那个窗口卖的票.
            }
            //************** 一次完整的卖票动作到这里结束.
        }
    }
}

public class Demo {
    public static void main(String[] args) {
        //1. 创建4个窗口, 即: 4个线程.
        MyThread mt1 = new MyThread("窗口1");
        //mt1.setName("窗口1");
        MyThread mt2 = new MyThread("窗口2");
        MyThread mt3 = new MyThread("窗口3");
        MyThread mt4 = new MyThread("窗口4");

        //2. 开启线程, 卖票即可.
        mt1.start();
        mt2.start();
        mt3.start();
        mt4.start();
    }
}
模拟卖票_实现Runnable接口版
package com.ithiema.api.demo;

//自定义的MyRunnable接口的子类对象, 充当: 资源对象.

public class MyRunnable implements Runnable {
    private int ticket = 100;   //细节1:这里要不要加static?可以不加

    //细节2:这里能不能用Thread类的构造方法?   不能,因为该类和Thread类没有任何关系

    //3.重写Runnable接口
    @Override
    public void run() {
        //循环卖票
        while (true) {
            //**********一次完整的卖票从这里开始**********
            //synchronized (MyRunnable.class) {   //可以用当前的字节码文件对象
            synchronized (this) {   //细节4:锁对象可以用this吗? 可以
                //越界处理
                if (ticket <= 0)
                    break;
                try {
                    Thread.sleep(30);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }

                //正常卖票逻辑
                //细节3:加入线程名, 即: 那个窗口卖的票,这里能不能直接调用Thread#getName()方法?  不能,因为该类和Thread类没有任何关系
                System.out.println(Thread.currentThread().getName() + "正在售卖第" + ticket-- + "张票");
            }
            //**********一次完整的卖票从这里结束**********
        }
    }
}

public class Demo02 {
    public static void main(String[] args) {
        //创建资源类对象       细节5:能不能直接创建线程对象?    不能,因为MyRunnable是资源类,不是线程类
        MyRunnable mr = new MyRunnable();

        //创建线程类对象
        Thread th1 = new Thread(mr,"窗口1");
        Thread th2 = new Thread(mr,"窗口2");
        Thread th3 = new Thread(mr,"窗口3");
        Thread th4 = new Thread(mr,"窗口4");
        th1.start();
        th2.start();
        th3.start();
        th4.start();
    }
}
多线程的两种实现方式之间的区别是什么?
  • 继承Thread类:
    好处: 代码相对比较简单. //因为是继承Thread类, 所以可以直接用Thread类中的成员.
    弊端: 扩展性相对较差. //因为已经继承了Thread类了, 所以就不能继承其他类了.
  • 实现Runnable接口:
    好处: 扩展性相对较强. //因为是实现接口, 所以不影响类的体系.
    弊端: 代码相对比较繁琐. //因为不是直接继承Thread类, 所以不能直接使用Thread类中的成员.
同步方法:
  • 格式:
    • 就是在定义方法的时候, 在方法的声明上(返回值类型之前)加上 synchronized 即可.
    • 分类:
      静态成员方法: 锁对象是该类的字节码文件对象.
      非静态成员方法: 锁对象是 this
  • 结论:
    • 如果方法中部分内容要加锁, 就用: 同步代码块.
    • 如果方法中所有内容都要加锁, 就用: 同步方法.
package com.ithiema.api.demo;

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

    }

    //方法中部分代码块加锁,用同步代码块
    public static void show1() {
        System.out.println("start");

        //部分内容加锁
        //synchronized (锁对象){
        synchronized (Demo01.class) {
            System.out.println("Hello world!1");
            System.out.println("Hello world!2");
            System.out.println("Hello world!3");
        }
        System.out.println("end");
    }

    //静态方法,内容全部加锁,锁对象默认是:该类的字节码文件对象,即:Demo01.class
    public static synchronized void show2() {
        System.out.println("Hello world!1");
        System.out.println("Hello world!2");
        System.out.println("Hello world!3");
    }

    //非静态方法,内容全部加锁,锁对象默认是:this
    public synchronized void show3() {
        System.out.println("Hello world!1");
        System.out.println("Hello world!2");
        System.out.println("Hello world!3");
    }
}

死锁

注意:该内容只在面试用,实际开发不用,所谓的死锁指的就是同步代码块的嵌套

细节:

  1. 死锁需要两个线程,两把锁
  2. 一个线程先抢A锁,后抢B锁
  3. 另一个线程先抢B锁,后抢A锁,就有可能会出现死锁的情况
  4. 为了让效果更明显,用while(true)改进
演示死锁的代码块
package com.ithiema.api.demo;

public class DeadLock {
    //1.定义两把锁
    private static final String LOCKA = "锁A";
    private static final String LOCKB = "锁B";

    public static void main(String[] args) {
        //2.一个线程先抢A锁,后抢B锁
        //继承Thread类的方式创建线程对象,格式:new Thread(){//重写run()方法}.start();
        new Thread("李四") {
            @Override
            public void run() {
                //为了效果更明显,用while(true)改进
                while (true) {
                    synchronized (LOCKA) {
                        System.out.println(getName() + "获取到" + LOCKA + "等到" + LOCKB);
                        synchronized (LOCKB) {
                            System.out.println(getName() + "抢到" + LOCKB + "成功进到小黑屋");
                        }
                    }
                }
            }
        }.start();

        //3.一个线程先抢B锁,后抢A锁
        //继承Thread类的方式创建线程对象,格式:new Thread(){//重写run()方法}.start();
        new Thread("张三") {
            @Override
            public void run() {
                //为了效果更明显,用while(true)改进
                while (true) {
                    synchronized (LOCKB) {
                        System.out.println(getName() + "获取到" + LOCKB + "等到" + LOCKA);
                        synchronized (LOCKA) {
                            System.out.println(getName() + "抢到" + LOCKA + "成功进到小黑屋");
                        }
                    }
                }
            }
        }.start();

        //实现Runnable接口,格式:new Thread(Runnable接口的子类对象,线程名).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                //为了效果更明显,用while(true)改进
                while (true) {
                    synchronized (LOCKB) {
                        System.out.println(Thread.currentThread().getName() + "获取到" + LOCKB + "等到" + LOCKA);
                        synchronized (LOCKA) {
                            System.out.println(Thread.currentThread().getName() + "抢到" + LOCKA + "成功进到小黑屋");
                        }
                    }
                }
            }
        }).start();
    }
}

多线程的生命周期详解

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-EzSRoYJ4-1669233017786)(F:\BigData\java\图片\day13图片\02. 多线程的生命周期.png)]

请简述下多线程的生命周期是什么?

  • 入门版:

    • 新建, 就绪, 运行(可能会发生阻塞), 死亡.
  • 进阶版:

    • 新建, 就绪, 运行(可能会发生阻塞或者等待), 死亡.
      阻塞:多指IO流阻塞, 大多数人为不可控.
      等待:多指**sleep(), wait()**之类的, 人为可控.

线程进阶

线程优先级

  • 结论:
  1. 目前我们遇到的 线程安全的类 有: StringBuffer, Vector, Hashtable
  2. 线程的调度模型有两种: 1. 分时调度模型. 2. 抢占式调度模型. Java采用的是第二种.
  3. Java中多线程程序的执行特点是:随机性延迟性,原因是因为CPU在做着高效的切换。
  4. 线程的优先级默认为5,范围是:1-10,1最小,10最大。

优先级越高代表着它抢到CPU资源的几率会大一些, 并不一定会第一个执行

涉及到的Thread类中的成员:
成员方法:
  • public int getPriorty(); 获取线程的优先级
  • public void setPriority(int i); 设置优先级
成员常量:
  • public static final int MAX_PRIORITY; 10
  • public static final int MIN_PRIORITY; 1
  • public static final int NORM_PRIORITY; 5,默认优先级
演示:多线程的优先级问题
package com.ithiema.api.demo;

public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "run..." + i);
        }
    }
}


package com.ithiema.api.demo;

public class Demo01 {
    public static void main(String[] args) {
        //创建线程对象
        MyThread mt1 = new MyThread("飞机");
        MyThread mt2 = new MyThread("高铁");
        MyThread mt3 = new MyThread("汽车");

        //设置线程的优先级
        System.out.println(mt1.getPriority());
        System.out.println(mt2.getPriority());
        System.out.println(mt3.getPriority());

        //开启线程
        mt1.start();
        mt2.start();
        mt3.start();
    }
}

加入线程

成员方法:
  • public void join(); 加入线程, 即: 相当于插队, 也叫: 等待这个线程死亡.

细节:如果要设置某个线程为加入线程,则必须在其他线程开启之前设置有效

案例:演示加入线程
package com.ithiema.api.demo;

public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "run..." + i);
        }
    }
}

package com.ithiema.api.demo;

public class Demo02 {
    public static void main(String[] args) {
        //创建三个线程
        MyThread mt1 = new MyThread("康熙");
        MyThread mt2 = new MyThread("四阿哥");
        MyThread mt3 = new MyThread("八阿哥");

        //开启线程
        //设置爱第一个线程为:加入线程
        mt1.start();

        try {
            mt1.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        mt2.start();
        mt3.start();
    }
}

守护线程

细节:每一个线程默认都是非守护线程, 当非守护线程关闭(结束)的时候, 和它相关的守护线程都要自动关闭.

成员方法
  • public void setDaemon(boolean flag); 设置线程的类别, true: 守护线程, false: 非守护线程(默认)
案例:演示守护线程
package com.ithiema.api.demo;

public class MyThread extends Thread {

    public MyThread() {
    }

    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "run..." + i);
        }
    }
}


package com.ithiema.api.demo;

public class Demo03 {
    public static void main(String[] args) {
        //创建两个线程对象
        MyThread mt1 = new MyThread("关羽");
        MyThread mt2 = new MyThread("张飞");

        //设置它们为:守护线程
        mt1.setDaemon(true);
        mt2.setDaemon(true);

        //设置当前进程(主进程)的名字为刘备
        Thread.currentThread().setName("刘备");

        //开启线程
        mt1.start();
        mt2.start();

        //指定刘备的任务
        for (int i = 0; i < 10; i++) {
            System.out.println(Thread.currentThread().getName() + "..." + i);
        }
    }
}

线程池

简介:

  • 概述:

    • 实际开放中,当我们需要使用大量生命周期短线程对象时,每次频繁的创建销毁线程对象非常消耗系统资源,针对于这种情况,我们可以创建一个池子,里面放一些线程对象,用的时候从池子里边拿,用完之后再放回去,从而避免了频繁创建销毁大量生命周期短的线程对象,这个池子就叫: 线程池( ).
  • 好处:节约资源,提高效率.

涉及到的成员方法:
  • Executors:创建线程池的工具类
    • public static ExecutorService newFixedThreadPool(int numThread); 获取线程池对象,指定线程个数
  • ExecutorService表示线程池
    • public Future submit(Runnable target);
      提交要执行的任务给线程池对象,由它自动分配线程对象来执行这些任务,并获取结果,任务类型为:Runnable资源类的对象.
    • public Future submit(Callable target);
      提交要执行的任务给线程池对象,由它自动分配线程对象来执行这些任务,并获取结果,任务类型为:Callable资源类的对象.
    • public void shutdown(); 关闭线程池,实际开发中:不关闭
  • Future:记录的是线程任务执行后的结果对象.
    • public Object get(); 获取线程任务执行结束后,具体的返回值

步骤

  1. 创建线程池,指定初始化的线程个数.
  2. 提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
  3. 从结果对象(Future)中获取线程任务执行结束后具体的返回值.
  4. 操作 线程任务的返回值.
  5. 关闭线程池.

细节:如果使用线程池提交任务, 提交的是Callable任务, 则返回值可以自定义, 提交的是Runnable任务, 默认返回: null

案例:线程池_提交Runnable任务
package com.ithiema.api.demo;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;

public class Demo04 {
    public static void main(String[] args) throws Exception {
        //1.创建线程池,指定初始化的线程个数.
        ExecutorService threadPool = Executors.newFixedThreadPool(5);

        //2.提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
        //格式:threadPool.submit(Runnable接口的对象)
        
        //方式一:匿名内部类
        Future future = threadPool.submit(new Runnable() {
            @Override
            public void run() {
                System.out.println("我是匿名内部类的方式,提交Runnable任务");
            }
        });

        //方式二:Lambda表达式
        Future future = threadPool.submit(() -> System.out.println("我是Lambda表达式的方式,提交Runnable任务"));

        //3.从结果对象(Future)中获取线程任务执行结束后具体的返回值.
        Object obj = future.get();

        //4.操作 线程任务的返回值.
        System.out.println("线程任务执行结束后,返回值是:" + obj);

        //5.关闭线程池.
        threadPool.shutdown();  //实际开发中不关
    }
}
案例:线程池_提交Callable任务
Runnable接口 和 Callable接口的区别是什么?
  1. 重写的方法不同.
    Runnable接口是重写 **run()**方法, Callable接口是重写 **call()**方法.
  2. 方法返回值的类型不同.
    Runnable#run(): 返回值是void类型.
    Callable#call(): 返回值类型可以自定义.
  3. 关于异常的处理方式不同.
    Runnable#run(): 有异常只能try, 不能抛.
    Callable#call(): 有异常可try, 可抛.
package com.ithiema.api.demo;

import java.util.concurrent.*;

public class Demo04 {
    public static void main(String[] args) throws Exception {
        //1.创建线程池,指定初始化的线程个数.
        ExecutorService threadPool = Executors.newFixedThreadPool(10);

        //2.提交任务给线程池对象,由它分配线程来执行该任务,并返回执行对象.
        //格式:threadPoll.submit(Callable接口的子类任务)
        //方式一:匿名内部类
        Future future = threadPool.submit(new Callable<Integer>() {     //这里泛型可以自定义
            @Override
            public Integer call() throws Exception {
                System.out.println("匿名内部类的方式,提交Callable任务");
                return 10;
            }
        });
        
        //方拾二:Lambda表达式
        Future future = threadPool.submit(() -> {
            System.out.println("Lambda表达式的方式,提交Callable任务");
            return 10;
        });

        //省略模式写法
        Future future = threadPool.submit(() -> 10);

        //3.从结果对象(Future)中获取线程任务执行结束后具体的返回值.
        Integer num = (Integer) future.get();
        //4.操作 线程任务的返回值.
        System.out.println("返回值" + num);
        //5.关闭线程池.
    }
}
多线程记忆点-总结:
  1. 一台电脑上可以有多个进程, 这些进程之间的数据是相互 隔离 的.
  2. 一个进程可以有多条线程, 这些线程 共享该进程的 数据.
  3. 开启线程调用的是 Thread#start()方法, 如果调用Thread#run()方法, 只是普通的方法调用, 并没有开启线程.
  4. 同一个线程对象重复开启, 会报 IllegalThreadStateException(非法的线程状态异常)
  5. 线程的调度模型有两种: 1. 分时调度模型. 2. 抢占式调度模型. Java采用的是第二种.
  6. Java中多线程程序的执行特点是: 随机性和延迟性, 原因是因为CPU在做着高效的切换.
  7. 线程的优先级默认为5, 范围是: 1-10, 1最小, 10最大.
  8. 如果要设置某个线程为加入线程, 则必须在其他线程开启之前设置有效.
  9. 每一个线程默认都是非守护线程, 当非守护线程关闭(结束)的时候, 和它相关的守护线程都要自动关闭.
  10. 如果使用线程池提交任务, 提交的是Callable任务, 则返回值可以自定义, 提交的是Runnable任务, 默认返回: null

Lock锁

简介:
  • 概述:

    • 它是一个接口,也是JDK1.5的特性,叫:互斥锁,能让我清晰的看到在哪里加锁,在哪里释放锁的操作.
      它的常用子类是ReentrantLock,可以实现同时管理多个线程,让线程有规律的执行,即:多线程的等待唤醒机制.
  • 名词解释:多线程的等待唤醒机制

    • 概述:
      • 目前我们写的多线程程序打印的结果都是"成片"的数据, 这是因为多线程的执行具有随机性和延迟性,
        这样做不太方便我们管理线程, 如果说我们想实现 线程1一次, 线程2一次, 线程3一次, 线程1一次, 线程2一次, 线程3一次…
      • 这种情况, 就必须使用: 多线程的等待唤醒机制了, 该某个线程执行, 我们就把它唤醒, 不该某个线程执行, 如果它抢到资源了, 我们就让它等待.
        这个就是:多线程的等待唤醒机制, 它描述的是 3个或者3个以上的线程有规律的执行.
实现方式:

方式1:采用 wait() + notifyAll() 组合实现.

Object类中的方法:

​ public void wait(); 让当前线程处于等待状态 张三, 李四, 王五.
​ public void notify(); 随机唤醒某一个线程, 注意: 唤醒是随机的.
​ public void notifyAll(); 唤醒所有等待的线程.

  • 方式2:采用 Lock锁机制实现, 它能实现精准唤醒.
成员方法:
  • public void lock(); 加锁
  • public void unlock(); 解锁
Lock锁优化
package com.ithiema.api.demo;

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

public class MyRunnable implements Runnable {
    private int ticket = 1000;

    //创建Lock锁对象
    private Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {

            lock.lock();    //加锁
            if (ticket <= 0)
                break;

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

            System.out.println(Thread.currentThread().getName() + " " + ticket--);
            lock.unlock();      //解锁

        }
    }
}

生产者和消费者

设计模式简介

  • 概述:

  • 所谓的设计模式不独属于任何的单一语言,它是前辈们的经验和总结,是一系列解决问题的思路和方案。

  • 分类:

    • 创建型

      • 特点:顾名思义,需要创建对象
      • 例如:
        • 单列设计模式,原型设计模式,工厂方法设计模式,抽象工厂设计模式,建造者设计模式
    • 结构型

      • 特点:就是用来表述事物之间的关系的
      • 例如:
        • 装饰设计模式,适配器设计模式…
    • 例:

      装饰类										被装饰的类
      BufferedReader br = new BufferedReader(new FileReader("day13/data/1.txt"))
      
    • 行为型

      • 特点:描述事物能够做什么
      • 例如:
        • 模板方法设计模式,观察者,访问者,中介者(也叫消费者)…
  • 结论:设计模式一共有23种,分为3大类,即:创建型(5种),结构型(7种),行为型(11种)。

生产者和消费者案例

步骤:

生产者消费者案例中包含的类:

奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作

生产者类(Producer):实现Runnable接口,重写run()方法,调用存储牛奶的操作

消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作

测试类(BoxDemo):里面有main方法,main方法中的代码步骤如下

①创建奶箱对象,这是共享数据区域

②创建生产者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用存储牛奶的操作

③创建消费者对象,把奶箱对象作为构造方法参数传递,因为在这个类中要调用获取牛奶的操作

④创建2个线程对象,分别把生产者对象和消费者对象作为构造方法参数传递

⑤启动线程

奶箱类(Box)

package com.ithiema.api.demo;

//奶箱类(Box):定义一个成员变量,表示第x瓶奶,提供存储牛奶和获取牛奶的操作
public class Box {
    //1.定义变量,记录第几瓶奶
    private int milk;

    //2.定义变量,记录奶箱放奶状态
    private boolean state = false;      //true:有奶,false:无奶

    //3.定义方法,表示放奶瓶
    public synchronized void put(int milk) {
        //3.1 判断奶箱状态,有奶就等待
        if(state){
            //等待
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //3.2 走这里,说明肯定无奶,就放奶
        System.out.println("送奶工正在放入第" + milk + "瓶奶");
        this.milk = milk;

        //3.3 修改奶箱状态
        state = true;

        //3.4 唤醒消费者来消费(取奶)
        this.notify();      //随机唤醒一个等待的线程
    }

    //4.定义方法,表示取奶瓶
    public synchronized void get() {
        //4.1 判断奶箱状态,无奶就等待
        if(!state){
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        //4.2 走这里,说明肯定有奶,就取奶
        System.out.println("消费者正在获取第" + milk + "瓶奶");

        //4.3 修改奶箱状态
        state = false;

        //4.4 唤醒生产者,放奶
        this.notify();
    }
}

生产者类(Producer,资源类)

package com.ithiema.api.demo;

//生产者类(Producer,资源类):实现Runnable接口,重写run()方法,调用存储牛奶的操作
public class Producer implements Runnable {

    //1.定义变量,记录:奶箱(共享数据区)
    private Box b;

    //2.定义构造方法,传入具体的奶箱对象
    public Producer(Box b) {
        this.b = b;
    }

    //3.重写run()方法
    @Override
    public void run() {
        for (int i = 1; i <=  31; i++) {
            b.put(i);
        }
    }
}

消费者类(Customer)

package com.ithiema.api.demo;

//消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
public class Customer implements Runnable{

    //1.定义变量,记录:奶箱(共享数据区)
    private Box b;

    //2.定义构造方法,传入具体的奶箱对象
    public Customer(Box b) {
        this.b = b;
    }

    //3.重写run()方法
    @Override
    public void run() {
        while (true)
            b.get();
    }
}

测试类

package com.ithiema.api.demo;

public class Demo01 {
    public static void main(String[] args) {
        //1.创建奶箱类,表示:共享数据区
        Box b = new Box();

        //2.创建生产者类和消费者类对象,它们表示:资源类对象
        Producer p = new Producer(b);
        Customer c = new Customer(b);

        //3.创建两个线程,分别封装上述的两个,资源类对象
        Thread th1 = new Thread(p);
        Thread th2 = new Thread(c);

        //4.开启线程
        th1.start();
        th2.start();
    }
}

定义变量,记录:奶箱(共享数据区)
private Box b;

//2.定义构造方法,传入具体的奶箱对象
public Producer(Box b) {
    this.b = b;
}

//3.重写run()方法
@Override
public void run() {
    for (int i = 1; i <=  31; i++) {
        b.put(i);
    }
}

}


**消费者类(Customer)**

~~~~java
package com.ithiema.api.demo;

//消费者类(Customer):实现Runnable接口,重写run()方法,调用获取牛奶的操作
public class Customer implements Runnable{

    //1.定义变量,记录:奶箱(共享数据区)
    private Box b;

    //2.定义构造方法,传入具体的奶箱对象
    public Customer(Box b) {
        this.b = b;
    }

    //3.重写run()方法
    @Override
    public void run() {
        while (true)
            b.get();
    }
}
~~~~

**测试类**

~~~java
package com.ithiema.api.demo;

public class Demo01 {
    public static void main(String[] args) {
        //1.创建奶箱类,表示:共享数据区
        Box b = new Box();

        //2.创建生产者类和消费者类对象,它们表示:资源类对象
        Producer p = new Producer(b);
        Customer c = new Customer(b);

        //3.创建两个线程,分别封装上述的两个,资源类对象
        Thread th1 = new Thread(p);
        Thread th2 = new Thread(c);

        //4.开启线程
        th1.start();
        th2.start();
    }
}
~~~
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值