【JAVA基础之多线程】多线程案例以及自定义线程池

发布于:2024-05-02 ⋅ 阅读:(21) ⋅ 点赞:(0)

 🔥作者主页小林同学的学习笔录

🔥mysql专栏:小林同学的专栏

目录

1.多线程

1.1  概述

1.2  并发和并行

1.3  进程和线程

1.4  多线程的实现

1.4.1  继承Thread类

1.4.2  实现Runnable接口

1.4.3  实现Callable接口

1.4.4  总结

1.5  设置和获取线程名称

1.6  线程常见的成员方法

1.7  线程优先级

1.8  守护线程

1.9  礼让线程

2.0  插入线程  

2.1  线程的生命周期

2.线程同步

2.1  卖票案例

2.2  卖票案例的问题

2.3  同步代码块解决数据安全问题

2.4  同步方法解决数据安全问题

2.5  lock锁

2.6  死锁

3.生产者消费者(等待唤醒机制)

3.1  生产者和消费者模式概述

3.2  生产者和消费者案例

3.3  生产者和消费者案例优化

4.线程池

4.1 概述

4.2 原理

4.3  Executors创建线程池

4.4  自定义线程池


1.多线程

1.1  概述

什么是多线程,以及多线程的作用?

多线程指的是在一个程序中同时执行多个线程的编程技术。每个线程都是程序中独立的执行流程,可以并行地运行,各自执行不同的任务或者处理不同的数据。

多线程的作用主要有以下几个方面:

1. 提高程序的响应速度:通过多线程,程序可以同时执行多个任务,使得在某个任务阻塞或等待的时候,其他任务仍然可以继续执行,提高了程序的整体响应速度和并发能力。

2. 提高资源利用率:多线程可以更充分地利用计算机的多核处理器或者多个计算资源,使得程序在相同时间内能够处理更多的任务或者数据,提高了资源的利用率。

3. 改善用户体验:对于需要处理大量计算或者IO操作的应用程序,使用多线程可以避免阻塞主线程,保持用户界面的流畅性,提升用户体验。

4. 简化编程模型:有些任务需要同时进行多个操作,使用多线程可以简化程序的编写,使得程序结构更清晰,易于维护和扩展。

总的来说,多线程可以使得程序更高效地利用计算资源,提高程序的并发能力和响应速度,从而改善用户体验,是现代软件开发中非常重要的技术之一。

1.2  并发和并行

并行:在同一时刻,有多个指令在多个CPU上同时执行

并发:在同一时刻,有多个指令在单个CPU上交替执行

1.3  进程和线程

进程:是正在运行的程序

  • 独立性:进程是一个能独立运行的基本单位,同时也是系统分配资源和调度的独立单位
  • 动态性:进程的实质是程序的一次执行过程,进程是动态产生,动态消亡的

  • 并发性:任何进程都可以同其他进程一起并发执行

线程:是进程中的单个顺序控制流,是一条执行路径

  • 单线程:一个进程如果只有一条执行路径,则称为单线程程序
  • 多线程:一个进程如果有多条执行路径,则称为多线程程序

1.4  多线程的实现

1.4.1  继承Thread类

成员方法:

实现步骤:

  • 定义一个类MyThread继承Thread类

  • 在MyThread类中重写run()方法

  • 创建MyThread类的对象

  • 启动线程

代码演示:

public class MyThread extends Thread {
    @Override
    public void run() {
        //写逻辑代码
        for (int i = 0; i <= 99; i++) {
            System.out.println(Thread.currentThread().getName() + " "+ i);
        }
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        //创建MyThread类的对象
        MyThread myThread = new MyThread();
        //启动线程
        myThread.start();
    }
}

输出结果:

Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
...
Thread-0 99

1.4.2  实现Runnable接口

Thread构造方法:

实现步骤:

  • 定义一个类MyRunnable实现Runnable接口

  • 在MyRunnable类中重写run()方法

  • 创建MyRunnable类的对象

  • 创建Thread类的对象,把MyRunnable对象作为构造方法的参数

  • 启动线程

代码演示:

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 0; i <= 99; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}
public class RunnableDemo {
    public static void main(String[] args) {
        //创建MyRunnable类的对象
        MyRunnable myRunnable = new MyRunnable();
        //创建Thread类的对象,把MyRunnable对象作为构造方法的参数
        Thread thread = new Thread(myRunnable);
        //启动线程
        thread.start();
    }
}

输出结果:

Thread-0 0
Thread-0 1
Thread-0 2
Thread-0 3
Thread-0 4
Thread-0 5
...
Thread-0 99

1.4.3  实现Callable接口

成员方法:

实现步骤:

  • 定义一个类MyCallable实现Callable接口

  • 在MyCallable类中重写call()方法

  • 创建MyCallable类的对象

  • 创建Future的实现类FutureTask对象,把MyCallable对象作为构造方法的参数

  • 创建Thread类的对象,把FutureTask对象作为构造方法的参数

  • 启动线程

  • 再调用get方法,就可以获取线程结束之后的结果。

代码演示:

public class MyCallable implements Callable<Integer> {
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= 100; i++) {
            sum = sum + i;
        }
        return sum;
    }
}
public class CallableDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //创建MyCallable对象
        MyCallable myCallable = new MyCallable();
        //把返回值结果存放到Future对象里
        FutureTask<Integer> futureTask = new FutureTask<>(myCallable);
        //创建线程对象
        Thread thread = new Thread(futureTask);
        //开启线程
        thread.start();
        //取出FutureTask存放对象的结果
        Integer result = futureTask.get();
        System.out.println(result);
    }
}

输出结果:

5050

1.4.4  总结

三种实现方式的对比

  • 实现Runnable、Callable接口

    • 好处: 扩展性强,实现该接口的同时还可以继承其他的类

    • 缺点: 编程相对复杂,不能直接使用Thread类中的方法

  • 继承Thread类

    • 好处: 编程比较简单,可以直接使用Thread类中的方法

    • 缺点: 可以扩展性较差,不能再继承其他的类

1.5  设置和获取线程名称

成员方法:

代码演示:

public class MyThread02 extends Thread{

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

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

public class ThreadDemo02 {
    public static void main(String[] args) {
        MyThread02 my1 = new MyThread02();
        MyThread02 my2 = new MyThread02();

        //void setName(String name):将此线程的名称更改为等于参数 name
        my1.setName("高铁");
        my2.setName("飞机");

        //Thread(String name)
//        MyThread02 my3 = new MyThread02("高铁");
//        MyThread02 my4 = new MyThread02("飞机");

        my1.start();
        my2.start();

//        my3.start();
//        my4.start();
        //static Thread currentThread() 返回对当前正在执行的线程对象的引用
        System.out.println(Thread.currentThread().getName());
    }
}

输出结果:

高铁:0
高铁:1
飞机:0
飞机:1

1.6  线程常见的成员方法

1.7  线程优先级

线程调度:

  • 两种调度方式

    • 非抢占式调度模型:所有线程轮流使用 CPU 的使用权,平均分配每个线程占用 CPU 的时间片

    • 抢占式调度模型:优先让优先级高的线程使用 CPU,如果线程的优先级相同,那么会随机选择一个,具有随机性,优先级高的线程获取的 CPU 时间片相对多一些

  • Java使用的是抢占式调度模型

成员方法:

IDEA默认优先级为5,如果想要一个线程运行多一点,可以考虑把优先级调高,这种是比较随机的,不能说优先级越高,运行的次数一定越多

代码演示:

public class MyThread03 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName()+":"+i);
        }
    }
}
public class ThreadDemo03 {
    public static void main(String[] args) {
        MyThread03 my1 = new MyThread03();
        MyThread03 my2 = new MyThread03();

        //void setName(String name):将此线程的名称更改为等于参数 name
        my1.setName("高铁");
        my2.setName("飞机");
        //设置优先级
        my1.setPriority(1);
        my2.setPriority(10);
        //开启线程
        my1.start();
        my2.start();
    }
}

输出结果:

飞机:0
飞机:1
飞机:2
飞机:3
飞机:4
飞机:5
飞机:6
高铁:0
飞机:7
...

1.8  守护线程

概念:

守护线程是一种特殊类型的线程,它在程序运行时在后台提供服务,不会阻止程序的终止。当所有的非守护线程都执行完毕时,守护线程也会陆续被销毁。

成员方法:

代码演示:

public class MyThread extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
}
public class MyThread02 extends Thread{
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            System.out.println(getName() + "  " + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        //创建线程对象
        MyThread myThread = new MyThread();
        MyThread02 myThread02 = new MyThread02();
        //设置线程名称
        myThread.setName("非守护");
        myThread02.setName("守护");
        //设置守护线程
        myThread02.setDaemon(true);
        //开启线程
        myThread.start();
        myThread02.start();
    }
}

输出结果:

守护  0-7
非守护  0
守护  8
非守护  1
守护  9
非守护  2
守护  10
非守护  3
守护  11
守护  12
守护  13
非守护  4
守护  14
非守护  5
守护  15
非守护  6
守护  16
守护  17
守护  18
非守护  7
守护  19
非守护  8
守护  20
非守护  9
守护  21-30

可见如果非守护线程执行完,守护线程会陆续结束

1.9  礼让线程

概念:

当一个线程执行时,它可以选择性地暂停自己的执行,以便给其他线程更多的执行机会。这种暂停的行为称为“礼让”,它允许其他线程在多线程环境中更公平地分享 CPU 时间。

2.0  插入线程  


概念:让某个线程先执行完,再执行其他线程

2.1  线程的生命周期

 一个完整的线程生命周期包含以下几个阶段:

  1. 新建(New):线程被创建后,处于新建状态。

  2. 就绪(Runnable):线程被启动后,处于就绪状态,表示线程已经准备好运行,但还未被调度执行。

  3. 运行(Running):线程进入运行状态后,开始执行其任务。

  4. 阻塞(Blocked):线程可能被阻塞,等待某些条件满足或资源释放。

  5. 等待(Waiting):线程可能进入特定的等待状态,等待其他线程的通知。

  6. 超时等待(Time Waiting):线程在有超时时间的等待状态中。

  7. 终止(Terminated):线程执行任务完毕或出现异常,进入终止状态

需要注意的是运行状态并不属于生命周期里面,只不过为了更好的理解,一旦线程拿到

执行权,虚拟机就把线程由系统来管理,因此实际上生命周期只有6个

2.线程同步

写线程一般有以下套路:

  • 1.循环
  • 2.同步代码块(也就是上锁)
  • 3.判断共享数据是否到了尾部(到了尾部就结束线程break;)
  • 4.判断共享数据是否到了尾部(没有到尾部,执行核心逻辑)

2.1  卖票案例

  • 案例需求

    某电影院目前正在上映国产大片,共有100张票,而它有3个窗口卖票,请设计一个程序模拟该电影院卖票

  • 实现步骤

    • 定义一个类SellTicket实现Runnable接口,里面定义一个成员变量:private int tickets = 100;

    • 在SellTicket类中重写run()方法实现卖票,代码步骤如下

    • 票卖没了,线程停止

    • 有票则卖票,并告知是哪个窗口卖的

    • 卖了票之后,总票数要减1

    • 定义一个测试类SellTicketDemo,里面有main方法,代码步骤如下

    • 创建SellTicket类的对象

    • 创建三个Thread类的对象,把SellTicket对象作为构造方法的参数,并给出对应的窗口名称

    • 启动线程

代码演示:

public class SellTicket implements Runnable{
    //这里不用static,因为SellTicket对象只创建一次,不存在数据共享问题
    int tickets = 100;
    @Override
    public void run() {
        while (true) {
            if (tickets == 0) {
                break;
            } else {
                 try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                --tickets;
                System.out.println(Thread.currentThread().getName() + "  窗口卖票还剩" + tickets + "张");
            }
        }
    }
}

public class SellTicketDemo {
    public static void main(String[] args) {
        SellTicket sellTicket = new SellTicket();
        //启动三个线程
        Thread thread = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        Thread thread3 = new Thread(sellTicket);
        //给三个线程设置名称
        thread.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        //开启线程
        thread.start();
        thread2.start();
        thread3.start();
    }
}

输出结果:

窗口3  窗口卖票还剩98张
窗口2  窗口卖票还剩98张
窗口1  窗口卖票还剩98张
窗口3  窗口卖票还剩97张
...
窗口1  窗口卖票还剩-1张
窗口2  窗口卖票还剩0张
窗口3  窗口卖票还剩-2张

运行之后会发现出现出现重复的票数,以及出现负数的票数

重复票数:

假设线程一得到执行权,然后走run()方法,会休眠100ms,休眠的间隙,执行了--tickets,还没来得及打印,线程二也进来run()方法,然后也休眠100ms,执行了--tickets,最后再打印,因此出现重复

出现负数票数:

假设票数剩下最后一张,因为存在休眠,三个线程同时执行了run()方法,执行完--tickets并打印,

因此出现了负数票数

2.2  卖票案例的问题

  • 卖票出现了问题

    • 相同的票出现了多次

    • 出现了负数的票

  • 问题产生原因

    线程执行的随机性导致的,可能在卖票过程中丢失cpu的执行权,导致出现问题

2.3  同步代码块解决数据安全问题

  • 安全问题出现的应该满足以下条件

    • 是多线程环境

    • 有共享数据

    • 有多条语句操作共享数据

  • 如何解决多线程安全问题呢?

    • 基本思想:让程序没有安全问题的环境

  • 怎么实现呢?

    • 把多条语句操作共享数据的代码给锁起来,让任意时刻只能有一个线程执行即可

    • Java提供了同步代码块的方式来解决

  • 同步代码块格式:

    • synchronized(锁对象) { 
      	操作共享数据的代码 
      }

      synchronized(锁对象):就相当于给代码加锁了,锁对象就可以看成是一把锁

特点:

  • 1.锁默认打开,有一个线程进去了,锁自动关闭
  • 2.里面的代码全部执行完毕,线程出来,锁自动打开

同步的好处和弊端:

  • 好处:解决了多线程的数据安全问题

  • 弊端:当线程很多时,因为每个线程都会去判断同步上的锁,这是很耗费资源的,无形中会降低程序的运行效率

代码演示:

public class SellTicket implements Runnable{
    private static int tickets = 100;
    @Override
    public void run() {
        while (true) {
            synchronized (SellTicket.class){
                if (tickets > 0) {
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        throw new RuntimeException(e);
                    }
                    --tickets;
                    System.out.println(Thread.currentThread().getName() + "  窗口卖票还剩" + tickets + "张");
                } else {
                    break;
                }
            }
        }
    }
}

注意:

1.synchronized(锁对象)要在循环的里面,否则会出现一个窗口卖完100张票才会出来

2.synchronized(锁对象),要确保锁对象的唯一性,如果锁对象不唯一,则出现锁不住,跟没锁一样,一般锁对象用该类的字节码文件对象,可以保证锁对象的唯一性

3.如果锁对象为this,表示哪个线程获取到执行权就是哪个线程对象,这种锁对象不唯一

2.4  同步方法解决数据安全问题

同步方法锁对象不能自己确定,java已经确定好了,分为非静态同步和静态同步

①.同步方法的格式

同步方法:就是把synchronized关键字加到方法上

修饰符 synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

同步方法的锁对象是什么呢?

this

②.静态同步方法

同步静态方法:就是把synchronized关键字加到静态方法上

修饰符 static synchronized 返回值类型 方法名(方法参数) { 
	方法体;
}

同步静态方法的锁对象是什么呢?

类名.class,也称为当前类的字节码文件对象

特点:

  • 1.同步方法是锁住方法里面的所有方法
  • 2.锁对象不能自己指定

代码演示:

public class SellTicket02 extends Thread{
    //这里需要数据共享,因为创建了三个SellTicket02对象
    static int tickets = 100;
    @Override
    public void run() {
        while (true) {
            if(method()) break;
        }
    }

    //this
    public synchronized boolean method(){
        //如果没票了就返回true
        if (tickets == 0) {
            return true;
        } else {
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            --tickets;
            System.out.println(Thread.currentThread().getName() + "  窗口卖票还剩" + tickets + "张");
        }
        return false;
    }
}
public class SellTicketDemo02 {
    public static void main(String[] args) {
        SellTicket02 sellTicket = new SellTicket02();
        //启动三个线程
        Thread thread = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        Thread thread3 = new Thread(sellTicket);
        //给三个线程设置名称
        thread.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        //开启线程
        thread.start();
        thread2.start();
        thread3.start();
    }
}

2.5  lock锁

lock锁可以由程序员控制,通过手动加锁以及手动释放锁来提高代码的灵活性

Lock是接口不能直接实例化,这里采用它的实现类ReentrantLock来实例化

代码演示:

public class SellTicket03 extends Thread {
    //这里需要数据共享,因为创建了三个SellTicket02对象
    static int tickets = 100;
    //这里为了确保锁的唯一性,加上static
    static Lock lock = new ReentrantLock();

    @Override
    public void run() {
        while (true) {
            try {
                //手动上锁
                lock.lock();
                if (tickets == 0) {
                    break;
                } else {
                    Thread.sleep(100);
                    --tickets;
                    System.out.println(Thread.currentThread().getName() + "  窗口卖票还剩" + tickets + "张");
                }
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
    }
}
public class SellTicketDemo03 {
    public static void main(String[] args) {
        SellTicket03 sellTicket = new SellTicket03();
        //启动三个线程
        Thread thread = new Thread(sellTicket);
        Thread thread2 = new Thread(sellTicket);
        Thread thread3 = new Thread(sellTicket);
        //给三个线程设置名称
        thread.setName("窗口1");
        thread2.setName("窗口2");
        thread3.setName("窗口3");
        //开启线程
        thread.start();
        thread2.start();
        thread3.start();
    }
}

注意:

1.为了确保锁的唯一性,加上static,没有static锁不住

static Lock lock = new ReentrantLock();

2.把释放锁放在finally代码快里面,因为如果break跳出循环,不会执行到unlock()释放锁,

锁会一直存在,导致程序一直处于执行状态

2.6  死锁

  • 概述

    死锁是指两个或多个进程在竞争系统资源时,由于彼此之间的互相等待对方已经占有的资源而导致的一种僵局状态,这些进程无法继续执行,也无法释放已经占有的资源。

  • 避免死锁

    不要进行锁的嵌套

代码演示:

public class MyThread extends Thread{
    //唯一锁对象
    static Object objA = new Object();
    static Object objB = new Object();
    //死锁导致都在等待释放锁,因此程序不能停止
    @Override
    public void run() {
        while (true){
            if("线程A".equals(getName())) {
                synchronized (objA) {
                    System.out.println("线程A拿到了A锁,准备拿B锁"); //拿到A锁
                    //没拿到B锁,出现阻塞
                    synchronized (objB){
                        System.out.println("线程A拿到了B锁,顺利执行完一轮");
                    }
                }
            } else if ("线程B".equals(getName())) { //拿到B锁
                if("线程B".equals(getName())) {
                    synchronized (objB) {
                        System.out.println("线程B拿到了B锁,准备拿A锁");
                        //没拿到A锁出现阻塞
                        synchronized (objA) {
                            System.out.println("线程B拿到了A锁,顺利执行完一轮");
                        }
                    }
                }
            }
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        MyThread thread = new MyThread();
        MyThread thread1 = new MyThread();

        thread.setName("线程A");
        thread1.setName("线程B");

        thread.start();
        thread1.start();
    }
}

3.生产者消费者(等待唤醒机制)

3.1  生产者和消费者模式概述

概述:生产者消费者模式是一个十分经典的多线程协作的模式

所谓生产者消费者问题,实际上主要是包含了两类线程:

一类是生产者线程用于生产数据

一类是消费者线程用于消费数据

成员方法:

3.2  生产者和消费者案例

案例需求:

  • 桌子类(Desk):定义表示面条数量的变量,定义锁对象变量,定义标记桌子上有无面条的变量

  • 生产者类(Cooker):实现Runnable接口,重写run()方法,设置线程任务

    1.判断是否有面条,决定当前线程是否执行

    2.如果有面条,就进入等待状态,需要等待唤醒,如果没有包子,继续执行,生产面条

    3.生产面条之后,更新桌子上面条状态,唤醒消费者消费面条

  • 消费者类(Foodie):实现Runnable接口,重写run()方法,设置线程任务

    1.判断是否有面条,决定当前线程是否执行

    2.如果没有面条,就进入等待状态,需要等待唤醒,如果有面条,就消费面条

    3.消费面条后,更新桌子上面条状态,唤醒生产者生产面条

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

    创建生产者线程和消费者线程对象

    分别开启两个线程

代码实现:

public class Desk {

    //是否有面条  0:代表没有  1:代表有
    public static int foodFlag = 0;

    //面条的最大个数 10 , 吃完就退出线程
    public static int count = 10;

    //锁对象
    public static Object lock = new Object();
}



public class Foodie extends Thread{
    @Override
    public void run() {
        /**
         *         1.循环
         *         2.同步代码块(也就是上锁)
         *         3.判断共享数据是否到了尾部(到了尾部就结束线程break;)
         *         4.判断共享数据是否到了尾部(没有到尾部,执行核心逻辑)
         */
        while (true){
            synchronized (Desk.lock){
                //达到最大限度,退出线程
                if(Desk.count == 0){
                    break;
                }else{
                    //先判断桌子是否有面条
                    if(Desk.foodFlag == 0){
                        try {
                            //如果没有,就等待
                            Desk.lock.wait();//让当前线程跟锁一起绑定
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //有面条就开吃,数量-1
                        Desk.count--;
                        System.out.println("吃货在吃面条,还能再吃" + Desk.count + "碗");
                        //吃完之后,唤醒厨师继续做
                        Desk.lock.notifyAll();//唤醒该锁对象下的所有线程
                        //修改桌子的状态
                        Desk.foodFlag = 0;
                    }
                }
            }
        }
    }
}





public class Cook extends Thread{
    @Override
    public void run() {
        while (true){
            synchronized (Desk.lock){
                //退出线程
                if(Desk.count == 0){
                    break;
                }else{
                    if(Desk.foodFlag == 1){
                        try {
                            //如果桌子有食物就等待
                            Desk.lock.wait();
                        } catch (InterruptedException e) {
                            throw new RuntimeException(e);
                        }
                    }else{
                        //如果没有,则制作食物
                        System.out.println("厨师制作了一碗面条");
                        //修改桌子上的状态
                        Desk.foodFlag = 1;
                        //唤醒等待的消费者开吃
                        Desk.lock.notifyAll();
                    }
                }
            }
        }
    }
}



public class ThreadDemo {
    public static void main(String[] args) {
        Cook cook = new Cook();
        Foodie foodie = new Foodie();

        cook.setName("厨师");
        foodie.setName("吃货");

        cook.start();
        foodie.start();
    }
}


输出结果:

厨师制作了一碗面条
吃货在吃面条,还能再吃9碗
厨师制作了一碗面条
吃货在吃面条,还能再吃8碗
厨师制作了一碗面条
吃货在吃面条,还能再吃7碗
厨师制作了一碗面条
吃货在吃面条,还能再吃6碗
厨师制作了一碗面条
吃货在吃面条,还能再吃5碗
厨师制作了一碗面条
吃货在吃面条,还能再吃4碗
厨师制作了一碗面条
吃货在吃面条,还能再吃3碗
厨师制作了一碗面条
吃货在吃面条,还能再吃2碗
厨师制作了一碗面条
吃货在吃面条,还能再吃1碗
厨师制作了一碗面条
吃货在吃面条,还能再吃0碗

3.3  生产者和消费者案例优化

用阻塞队列来优化

常见BlockingQueue:

  • ArrayBlockingQueue: 底层是数组,有界
  • LinkedBlockingQueue: 底层是链表,无界.但不是真正的无界,最大为int的最大值

BlockingQueue的核心方法:

  • put(Object): 将参数放入队列,如果放不进去会阻塞
  • take(): 取出第一个数据,取不到会阻塞

代码演示:


public class Cook02 extends Thread{

    ArrayBlockingQueue<String> queue;

    public Cook02(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            //不断的把面条放在阻塞队列中,这里不需要加锁,因为put()方法里面有加锁,如果在加锁,就是锁嵌套
            try {
                queue.put("面条");
                System.out.println("厨师放了一碗面条");
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}




public class Foodie02 extends Thread{

    ArrayBlockingQueue<String> queue;

    public Foodie02(ArrayBlockingQueue<String> queue) {
        this.queue = queue;
    }

    @Override
    public void run() {
        while (true){
            try {
                //不断从阻塞队列中获取面条
                String food = queue.take();
                System.out.println(food);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
    }
}




public class ThreadDemo02 {
    public static void main(String[] args) {
        /**
         * 细节:生产者和消费者必须使用同一个阻塞队列
         * 保证put()和take()方法都是对应一把锁
         */
        //这里参数设置为1,表示队列最多只能有一个
        ArrayBlockingQueue<String> queue = new ArrayBlockingQueue<>(1);

        //创建线程对象,并把阻塞队列对象传递过去
        Cook02 cook02 = new Cook02(queue);
        Foodie02 foodie02 = new Foodie02(queue);

        cook02.start();
        foodie02.start();
    }
}

4.线程池

4.1 概述

线程池是一种提高多线程处理效率的技术,通过重复利用已创建的线程,减少了线程创建和销毁的开销,提高了程序的性能和响应速度。线程池将线程的管理和任务的分配分离开来,通过维护一个线程队列,动态调整线程数量,实现任务的并发执行

4.2 原理

线程池的原理主要是通过预先创建一定数量的线程并将其放入线程池中,当有任务需要执行时,线程池会从池中取出一个空闲线程来执行任务,任务执行完毕后,线程不会被销毁而是继续保留在线程池中等待下一个任务的到来。

线程池的原理主要包括以下几个方面:

  1. 线程复用:线程池中的线程可以被重复利用,避免频繁创建和销毁线程的开销。
  2. 控制线程数量:线程池可以控制线程数量的上限,避免系统资源被无限制占用,确保系统的稳定性。
  3. 任务队列:线程池通常会维护一个任务队列,用于存储等待执行的任务,当线程池中没有空闲线程时,新任务会被放入任务队列中等待执行。
  4. 线程调度:线程池的管理器会负责调度线程的执行顺序,决定哪个线程执行哪个任务。

4.3  Executors创建线程池

概述 : JDK对线程池也进行了相关的实现,在真实企业开发中我们也很少去自定义线程池,而是使用JDK中自带的线程池。

成员方法:

static  newCachedThreadPool() 创建一个默认的线程池

static  newFixedThreadPool(int n) 创建一个指定最多线程数量的线程池

代码演示:

public class MyRunnable implements Runnable{
    @Override
    public void run() {
        for (int i = 1; i <= 10; i++) {
            System.out.println(Thread.currentThread().getName() + "  " + i);
        }
    }
}
public class Demo {
    public static void main(String[] args) {
        //创建线程池对象
        //线程最大限度为int最大值,执行完放回线程池
        //ExecutorService pool = Executors.newCachedThreadPool();
        //线程最大限度为3个线程,执行完放回线程池
        ExecutorService pool = Executors.newFixedThreadPool(3);

        //提交任务
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());
        pool.submit(new MyRunnable());


        //销毁线程池,一般不会销毁线程池
        //pool.shutdown();
    }
}

4.4  自定义线程池

先来了解核心线程和临时线程:

  • 核心线程是系统中始终存在的线程,它负责处理系统中常见的任务和操作。核心线程一般不会被销毁,除非系统关闭或者手动进行资源释放。
  • 临时线程则是在需要时动态创建的线程,用于处理系统中临时性的任务或者处理大量的并发任务。临时线程一般在任务完成后被销毁,以释放资源和减轻系统负担。

临时线程发挥作用时机:

核心线程都被拿完了,而且队列等待也排满了,此时临时线程才会发挥作用,发挥完作用,如果处于空闲状态就会被销毁,而多余的要执行任务会被拒绝

  实现逻辑:

任务拒绝的相关策略:


代码实现:

public class MyThreadPool {
    public static void main(String[] args) {
        /**
         * 参数说明:
         * 1.核心线程数
         * 2.最大线程数(包含临时线程)
         * 3.临时线程,空闲线程最大存活时间值
         * 4.时间单位
         * 5.任务队列
         * 6.创建线程工厂
         * 7.任务拒绝策略
         */
        ThreadPoolExecutor poolExecutor = new ThreadPoolExecutor(
                3,              //核心线程数
                6,              //最大线程数(包含临时线程)
                60,             //临时线程,空闲线程最大存活时间值
                TimeUnit.SECONDS,   //时间单位
                new ArrayBlockingQueue<>(3),    //任务队列
                Executors.defaultThreadFactory(), //创建线程工厂,底层是new Thread();
                new ThreadPoolExecutor.AbortPolicy()  //任务拒绝策略,是一个内部类
        );
    }
}

总结:

①.当核心线程满时,再提交任务就会排队

②.当核心线程满,队伍满时,会创建临时线程

③.当核心线程满,队伍满,临时线程满时,会触发任务拒绝策略