42讲 多线程(重要)

全文共计27740字

多线程概述

线程操作系统能够进行运算调度的最小单位。它被包含在进程之中,是进程中的实际运作单位。

线程可以简单理解为:应用程序中相互独立且可以同时运行的功能。

进程就是一段程序的执行过程。进程由处理器(CPU)负责执行。进程是线程的容器。程序是指令、数据及其组织形式的描述,进程是程序的实体。

多线程是指从软件或者硬件上实现多个线程并发执行的技术。具有多线程能力的计算机因有硬件支持而能够在同一时间执行多于一个线程,进而提升整体处理性能。

一个进程可以有很多线程,每条线程并行执行不同的任务(多线程)。

举例

想象一位厨师正在为他的女儿烘制生日蛋糕。他有做生日蛋糕的食谱,厨房里有所需的原料:面粉、鸡蛋、韭菜,蒜泥等。

在这个比喻中:

做蛋糕的食谱就是程序(即用适当形式描述的算法),

计算机科学家就是处理器(CPU),

做蛋糕的各种原料就是输入数据,

阅读食谱、取来各种原料以及烘制蛋糕等一系列动作,每一个动作都是一个线程

阅读食谱、取来各种原料以及烘制蛋糕等一系列动作的总和就是进程

厨师在阅读食谱的同时,手可以打散鸡蛋、清洗韭菜等,两个动作互不影响且节约时间,这就是多线程

多线程的作用

  • 充分利用程序运行过程中的等待时间,让CPU在多个线程间进行切换,从而提高程序的运行效率。
  • 可以把占据时间长的程序中的任务放到后台去处理。
  • 用户界面可以更加吸引人,这样比如用户点击了一个按钮去触发某些事件的处理,可以弹出一个进度条来显示处理的进度。

并发与并行

简述

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

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

在这里插入图片描述

区别

并发并行
多个事情,在同一时间段内同时发生多个事情,在同一时间点上同时发生
多个任务之间互相抢占资源多个任务之间不互相抢占资源

只有在多CPU的情况中,才会发生并行。否则,看似同时发生的事情,其实都是并发执行的。

只有一个咖啡机的时候,一台咖啡机其实是在并发被使用的。而有多个咖啡机的时候,多个咖啡机之间才是并行被使用的。img

并发+并行

执行任务的数量恰好等于 CPU 核心的数量,是一种理想状态。但是在实际场景中,处于运行状态的任务是非常多的,尤其是电脑和手机,开机就几十个任务,而 CPU 往往只有 4 核、8 核或者 16 核,远低于任务的数量,这个时候就会同时存在并发和并行两种情况:所有核心都要并行工作,并且每个核心还要并发工作。

一个双核 CPU 要执行四个任务,它的工作状态如下:

img

每个核心并发执行两个任务,两个核心并行的话就能执行四个任务。当然也可以一个核心执行一个任务,另一个核心并发执行三个任务,这跟操作系统的分配方式,以及每个任务的工作状态有关系。

并发和并行可以同时发生

多线程的创建

1 继承Thread类

优点: 编码简单

缺点: 已经继承了Thread类,无法继承其他类(单继承),不利于扩展。线程执行后不可以返回结果(返回值void)。

步骤

  1. 将类声明为 Thread 的子类。
  2. 重写 Thread 类的 run 方法。
  3. 创建该子类的对象。
  4. 调用 start 方法启动线程(启动后还是执行run方法)

注意

  • 为什么不直接调用重写好的 run 方法?

直接调用 run 会被当做一个普通的方法来执行,CPU不会建立一个新线程。 start 方法底层会告知CPU这是一个新线程,所以只有调用了 start 方法才会创建新线程。

  • 为什么主线程任务一定要放在子线程任务后面?

如果主线程在前,CPU察觉不到这是一个“多线程任务”,所以程序会从上往下以此执行,即优先执行完毕主线程任务。这样就没有多线程的效果了。

//1. 将类声明为 Thread 的子类。
public class MyThread extends Thread {
    //重写 Thread 类的 run 方法。
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程运行");
        }
    }
}

创建并启动一个线程:

public class ThreadDemo1 {
    public static void main(String[] args) {
        //2. 创建该子类的对象。
        MyThread mt = new MyThread();
        //3. 调用 start 方法启动线程(启动后还是执行run方法)
        mt.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("主线程运行");
        }
    }
}

到这里,已经形成了多线程:main方法是一个线程,p也是一个线程

运行结果 重新运行可能会有不同的结果

主线程运行
主线程运行
子线程运行
子线程运行
主线程运行
子线程运行

可以看出,每个线程都在执行,且都在往前推进

2 实现Runnable接口

优点: 线程任务类只是实现了一个接口,还可以继续继承其他类和实现其他接口,扩展性强。

缺点: 多一层对象包装。线程执行后不可以返回结果(返回值void)。

步骤1

  1. 创建线程任务类实现 Runnable 接口。
  2. 重写 Runnable 接口中的 run 方法。
  3. 创建线程任务类的对象。
  4. 任务对象作为参数再创建 Thread 对象。
  5. Thread 对象调用 start 方法启动线程。

步骤2

  • 匿名内部类

步骤1例

//1.创建线程任务类实现 Runnable 接口。
public class MyRunnable implements Runnable {
    //2. 重写 Runnable 接口中的 run 方法。
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println("子线程运行");
        }
    }
}

创建并启动一个线程:

public class ThreadDemo2 {
    public static void main(String[] args) {
        //3. 创建线程任务类的对象。
        MyRunnable mr = new MyRunnable();
        //4. 任务对象作为参数再创建 Thread 对象。
        Thread t = new Thread(mr);
        //5. Thread 对象调用 start 方法启动线程。
        t.start();

        for (int i = 0; i < 3; i++) {
            System.out.println("主线程运行");
        }
    }
}

运行结果 重新运行可能会有不同的结果

子线程运行
主线程运行
主线程运行
子线程运行
主线程运行
子线程运行

步骤2例

public class ThreadDemo3 {
    public static void main(String[] args) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 3; i++) {
                    System.out.println("子线程运行");
                }
            }
        }).start();

        for (int i = 0; i < 3; i++) {
            System.out.println("主线程运行");
        }
    }
}

Runnable是函数式接口,可以使用Lambda改写

public class ThreadDemo3 {
    public static void main(String[] args) {
        new Thread(() -> {
            for (int i = 0; i < 3; i++) {
                System.out.println("子线程运行");
            }
        })start();

        for (int i = 0; i < 3; i++) {
            System.out.println("主线程运行");
        }
    }
}

3 实现Callable接口

以上两种方式均存在局限:线程执行后不可以返回结果。但往往很多业务场景需要线程执行后的结果,JDK5以后,Java提供了第三种创建线程方式:实现 Callable<V> 接口。

Callable 的泛型代表结果的类型。

步骤

  1. 定义类实现 Callable<V> 接口,重写 call 方法(V call())。
  2. FutureTask<V> 把实现类对象封装成线程任务对象。
  3. 任务对象作为参数再创建 Thread 对象。
  4. Thread 对象调用 start 方法启动线程。
  5. 线程执行完毕后,通过 FutureTask 提供的 get 方法获取任务执行结果。

FutureTask对象的作用

  • FutureTask 实现了 Runnable,所以可以当作 Thread 构造的参数,

  • FutureTask 提供的 get 方法可以确保返回的是线程执行完毕后的结果。

FutureTask的API

方法说明
FutureTask(Callable<V> callable)封装Callable对象为FutureTask线程对象
V get() throws InterruptedException, ExecutionException获取线程执行完毕后的结果

//1. 定义类实现 Callable 接口(要指定结果的数据类型),重写 call 方法。
public class MyCallable implements Callable<Integer> {
    private int num;

    public MyCallable(int num) {
        this.num = num;
    }

    //计算1+2+...+num
    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num; i++) {
            sum += i;
        }
        return sum;
    }
}
public class ThreadDemo4 {
    public static void main(String[] args) {
        MyCallable mc1 = new MyCallable(100);
        //2. 用 FutureTask 把实现类对象封装成线程任务对象。
        FutureTask<Integer> ft1 = new FutureTask<>(mc1);
        //3. 任务对象作为参数再创建 Thread 对象。
        Thread t1 = new Thread(ft1);
        //4. Thread 对象调用 start 方法启动线程。
        t1.start();
        //5. 线程执行完毕后,通过 FutureTask 提供的 get 方法获取任务执行结果。
        int result;
        try {
            result = ft1.get();
            System.out.println(result);		// 5050
        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        MyCallable mc2 = new MyCallable(200);
        FutureTask<Integer> ft2 = new FutureTask<>(mc2);
        Thread t2 = new Thread(ft2);
        t2.start();
        try {
            result = ft2.get();
            System.out.println(result);		// 20100
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

Thread

Thread 类是所有线程类的父类,实现了对线程的抽取和封装。

构造方法

方法说明
Thread(String name)为当前线程指定名称
Thread(Runnable target)封装Runnable对象为线程对象
Thread(Runnable target, String name)封装Runnable对象为线程对象,并指定线程名称

常用方法

方法说明
String getName()获取当前线程对象的名称。默认名称:Thread-索引
void setName(String name)修改当前线程对象的名称
static Thread currentThread()返回当前线程对象的引用(在哪个线程中调用,就代表哪个线程)
static void sleep(long time)让当前线程休眠指定时间后再继续执行,不会释放锁。单位毫秒
void run()该线程的任务(由虚拟机调用)
void start()启动线程(由程序员调用)

获取/修改线程名

利用成员方法修改线程名

一般不会修改名称,用默认的 Thread-索引 即可。

public class ThreadDemo5 {
    public static void main(String[] args) {
        MyThread mt1 = new MyThread();
        mt1.setName("子线程1");
        mt1.start();

        MyThread mt2 = new MyThread();
        mt2.setName("子线程2");
        mt2.start();

        for (int i = 0; i < 3; i++) {
            // currentThread() 返回当前线程对象的引用(在哪个线程中调用,就代表哪个线程)
            // getName()       获取当前线程对象的名称
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
}

执行结果

main执行
子线程2执行
子线程2执行
子线程1执行
子线程1执行
子线程2执行
main执行
main执行
子线程1执行

利用构造方法修改线程名

public class ThreadDemo5 {
    public static void main(String[] args) {
        new MyThread("子线程1").start();

        new MyThread("子线程2").start();

        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
}

class MyThread extends Thread {
    public MyThread() {
    }

    //提供带有名称的构造器
    public MyThread(String name) {
        super(name);
    }

    @Override
    public void run() {
        for (int i = 0; i < 3; i++) {
            System.out.println(Thread.currentThread().getName() + "执行");
        }
    }
}

线程休眠

public class ThreadDemo6 {
    public static void main(String[] args) throws InterruptedException {
        for (int i = 0; i < 5; i++) {
            if (i == 2) {
                //当 i == 2 时,就让 main 线程休眠(暂停)2秒
                Thread.sleep(2000);
            }
            System.out.println(i);
        }
    }
}

执行结果

0
1
// 暂停2秒
2
3
4

线程的生命周期

线程的生命周期: 通俗来讲就是一个线程从被创建到被回收的过程。

线程的状态: 每个线程的生命周期中都会经历各种状态,且状态间会在合适的契机切换。

  • Java给线程定义了6种状态:
public class Thread {
    ...
    public enum State {
        // 尚未启动的线程的线程状态。
        NEW,

        // 可运行线程的线程状态。处于可运行状态的线程正在Java虚拟机中执行,但它可能正在等待来自操作系统的其他资源,例如处理器。
        RUNNABLE,

        // 受阻塞的线程状态,该线程被阻止等待监视器锁定。
        BLOCKED,

        // 无限等待的线程状态。
        WAITING,

        // 具有指定等待时间的线程状态。
        TIMED_WAITING,

        // 终止线程的线程状态。
        TERMINATED;
    }
}

6种状态的转换

在这里插入图片描述

线程状态说明
NEW(新建)线程刚被创建,但是并未启动
RUNNABLE(可运行)线程已经调用了start方法等待CPU调度
BLOCKED(锁阻塞)线程在执行的时候未竞争到锁对象
WAITING(无限等待)仅当另一个线程调用notify或者notifyAll方法才能够唤醒
TIMED_WAITING(计时等待)调用带有超时参数的方法将进入计时等待状态,到达超时参数被唤醒或提前被其他线程唤醒。带有超时参数的常用方法有Thread.sleepObject.wait
TERMINATED(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡。

线程安全

简述

多个线程同时操作同一个共享资源的时候可能会出现业务安全问题,这种问题成为线程安全问题。

线程安全问题出现的原因

  • 存在多线程并发
  • 同时访问共享资源
  • 存在修改共享资源(只读不能引发线程安全)

模拟

小明和小红有一个共享账户,账号里有10万元。小明和小红在同一时刻打算分别取出10万元。

在这里插入图片描述

结果是两人都取出了10万元,而银行亏损了10万元。

代码演示

账户类

public class Account {
    private String ID;
    private double money;   //账户总金额

    public Account() {
    }

    public Account(String ID, double money) {
        this.ID = ID;
        this.money = money;
    }

    //取钱方法
    public void WithdrawMoney(Menu m, double money) {
        //1.获取取钱人的信息
        String name = m.getName();
        //2.判断金额是否够取
        if (this.money >= money) {
            System.out.println(name + " 取款 " + money + " 元成功");
            //更新金额
            this.money -= money;
            System.out.println(name + " 取款后账户余额:" + this.money);
        } else {
            System.out.println("余额不足," + name + " 取钱失败");
        }
    }
    
    //省略get set方法
}

线程类

public class Menu extends Thread {
    private Account acc;   //要取的账号
    private double money;   //要取的金额

    public Menu(String name, Account acc, double money) {
        super(name);        //取钱的人
        this.acc = acc;
        this.money = money;
    }

    @Override
    public void run() {
        acc.WithdrawMoney(this, money);
    }
}

测试类

public class Demo {
    public static void main(String[] args) {
        //1.创建一个有10万余额的共享账户
        Account account = new Account("1023023", 100000);
        //2.创建两个操作该账户的线程
        new Menu("小明", account, 100000).start();
        new Menu("小红", account, 100000).start();
    }
}

运行结果

小明 取款 100000.0 元成功
小红 取款 100000.0 元成功
小明 取款后账户余额:0.0
小红 取款后账户余额:-100000.0

线程同步

概述

  • 取钱案例出现问题的原因?

多个线程同时进行,都发现账户的余额够取。

  • 如何保证线程安全?

让多个线程实现先后依次访问共享资源。

线程同步就是用来解决线程安全问题的。

线程同步的核心思想就是加锁 :把共享资源进行上锁,每次只允许一个线程访问,当该线程访问完毕后解锁,其他线程才可访问。

当多个线程同时访问同一个共享资源时,锁对象会让其中的一个线程访问资源,让其他线程进行等待。
在这里插入图片描述

当小红线程访问完毕后,余额已经更新成了0元。小明线程再访问时就会余额不足。

在这里插入图片描述

1 同步代码块

用于把出现线程安全问题的核心代码块上锁。

原理: 每次只允许一个线程进入,执行完毕后自动解锁,其他线程可进入

关键字: synchronized

格式: 选中需要“锁起来”的代码 -> ctrl + alt + T -> 选择 synchronized

synchronized (同步锁对象) {
    // 操作共享资源的代码
}

锁对象要求

  • 对于当前同时执行的线程来说是同一个对象(唯一)

  • 建议使用共享资源作为锁对象

  • 实例方法建议使用 this 作为锁对象

  • 静态方法建议使用 字节码(类名.class) 作为锁对象

修改 Account 类中部分代码

public class Account {
    public void WithdrawMoney(Menu m, double money) {
        String name = m.getName();
        
        //使用 synchronized 包裹
        synchronized ("随便一个对象") {
            if (this.money >= money) {
                System.out.println(name + " 取款 " + money + " 元成功");
                this.money -= money;
                System.out.println(name + " 取款后账户余额:" + this.money);
            } else {
                System.out.println("余额不足," + name + " 取钱失败");
            }
        }
        
    }
}

运行结果

小明 取款 100000.0 元成功
小明 取款后账户余额:0.0
余额不足,小红 取钱失败

虽然解决了线程安全问题,但也有其他问题:会影响其他无关线程的执行。

问题描述

在这一时刻,除了小明和小红取钱,也有其他人取钱。其他人用的是他们自己的卡,不会影响小明和小红卡里的余额。但是上述同步锁对象是任意对象, 其他人也要等待小明小红线程结束,这显然是不合理的。

解决思路

当我们使用“银行卡”作为锁对象时,使用不同银行卡的人就不会互相影响。

WithdrawMoney方法的调用者就是银行卡对象

public class Menu extends Thread {
    @Override
    public void run() {
        acc.WithdrawMoney(this, money);
    }
}

所以锁对象就可以使用 this (Account对象的引用)

synchronized (this) {
    if (this.money >= money) {
        System.out.println(name + " 取款 " + money + " 元成功");
        this.money -= money;
        System.out.println(name + " 取款后账户余额:" + this.money);
    } else {
        System.out.println("余额不足," + name + " 取钱失败");
    }
}	

2 同步方法

用于把出现线程安全问题的核心方法上锁。

原理: 每次只允许一个线程进入,执行完毕后自动解锁,其他线程可进入

关键字: synchronized

格式

修饰符 synchronized 返回值类型 方法名(形参) {
    // 操作共享资源的代码
}

同步方法的底层也是有锁对象的,只不过是隐式的。

  • 实例方法默认使用 this 作为锁对象

  • 静态方法默认使用 字节码(类名.class) 作为锁对象

修改 Account 类中部分代码

public class Account {
    //将方法修饰为同步方法
    public synchronized void WithdrawMoney(Menu m, double money) {
        String name = m.getName();
        if (this.money >= money) {
            System.out.println(name + " 取款 " + money + " 元成功");
            this.money -= money;
            System.out.println(name + " 取款后账户余额:" + this.money);
        } else {
            System.out.println("余额不足," + name + " 取钱失败");
        }
    }
}

3 Lock

以上两种创建方式都是底层自动加锁和释放锁的,为了更清晰地表达如何加锁和释放锁,JDK5以后Java提供了新的锁对象 Lock ,更加灵活、方便:可以随时上锁,随时解锁。

  • Lock 是接口,不能被直接实例化。可以采用其实现类 ReentrantLock 来构造 Lock锁对象。
构造说明
ReentrantLock()构造Lock锁的实现类对象

常用方法

方法说明
void lock()将下面的代码上锁
void unlock()释放锁

语法

尽量使用 try...finally 包裹,避免方法体出现异常后无法解锁,出现死锁情况。

class X {
    private final ReentrantLock lock = new ReentrantLock(); //创建Lock锁对象
    // ...
    
    public void m() {
        lock.lock();//上锁
        try {
            // ...
        } finally {
            lock.unlock();//解锁
        }
    }
}

修改 Account 类中部分代码

import java.util.concurrent.locks.ReentrantLock;

public class Account {
    //创建Lock锁对象。使用 private final 修饰,让锁唯一且不可修改。
    private final ReentrantLock lock = new ReentrantLock();
    
    public void WithdrawMoney(Menu m, double money) {
        String name = m.getName();
        //上锁
        lock.lock();
        try {
            if (this.money >= money) {
                System.out.println(name + " 取款 " + money + " 元成功");
                this.money -= money;
                System.out.println(name + " 取款后账户余额:" + this.money);
            } else {
                System.out.println("余额不足," + name + " 取钱失败");
            }
        } finally {
            //解锁
            lock.unlock();
        }
    }
}

线程通信

线程通信

所谓线程通信就是线程间相互发送数据,线程通信通常通过共享一个数据的方式实现。
线程间会根据共享数据的情况决定自己该怎么做,以及通知其他线程怎么做。

线程通信的前提:存在多个线程操作同一个共享资源,且要保证线程安全

等待和唤醒方法

来自 Object 类的等待和唤醒方法

方法说明
void wait()让当前线程进入无限等待状态并释放所占锁,直到被另一个线程唤醒
void wait(long timeoutMillis)让当前线程进入计时等待状态并释放所占锁,可以被提前唤醒
void notify()唤醒正在等待锁对象的某个线程
void notifyAll()唤醒正在等待锁对象的所有线程

注意:

  • 等待和唤醒方法应当使用同步锁对象进行调用!

  • 必须先唤醒再等待

常见模型

生产者与消费者模型

  • 生产者线程负责生产数据,消费者线程负责消费数据。

生产者线程生产完数据后,唤醒消费者,然后等待(挂起)自己;消费者消费完该数据后,唤醒生产者,然后等待(挂起)自己。

案例

小明和小红(消费者) 有三个监护人(生产者),五个人有一个共享账户。当账户有钱的时候,小明或小红去取;当账户没有钱的时候,三个监护人去存。存一次取一次,且存多少取多少。

代码演示

public class Account {
    private String ID;
    private double money;   //账户总金额
    
    public Account() {
    }

    public Account(String ID, double money) {
        this.ID = ID;
        this.money = money;
    }

    //取钱
    public synchronized void wMoney(WithdrawMoney m, double money) {
        String name = m.getName(); //获取取钱人
        try {
            Thread.sleep(1000);
            if (this.money >= money) {
                //钱够取
                this.money -= money;
                System.out.println(name + " 取款成功,共取 " + money + " 元,账户余额:" + this.money + " 元");
                this.notifyAll();
                this.wait();
            } else {
                //钱不够
                System.out.println(name + " 取款失败");
                //唤醒其他线程
                this.notifyAll();
                //进入等待
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    //存钱
    public synchronized void sMoney(SaveMoney s, double money) {
        String name = s.getName(); //获取存钱人
        try {
            Thread.sleep(1000);
            if (this.money == 0) {
                //账户没有钱
                this.money += money;
                System.out.println(name + " 存款成功,共存 " + money + " 元,账户余额:" + this.money + " 元");
                this.notifyAll();
                this.wait();
            } else {
                //账户有钱
                System.out.println(name + " 存款失败");
                //唤醒其他线程
                this.notifyAll();
                //进入等待
                this.wait();
            }
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
// 取钱
public class WithdrawMoney extends Thread {
    private Account acc;   //要取的账号
    private double money;   //要取的金额

    public WithdrawMoney(String name, Account acc, double money) {
        super(name);        //取钱的人
        this.acc = acc;
        this.money = money;
    }

    @Override
    public void run() {
        while (true)
            acc.wMoney(this, money);
    }
}
// 存钱
public class SaveMoney extends Thread {
    private Account acc;   //要存的账号
    private double money;   //要存的金额

    public SaveMoney() {
    }

    public SaveMoney(String name, Account acc, double money) {
        super(name);
        this.acc = acc;
        this.money = money;
    }

    @Override
    public void run() {
        while (true)
            acc.sMoney(this, money);
    }
}
public class Demo {
    public static void main(String[] args) {
        //创建一个共享账号
        Account acc = new Account("124123412", 0);
        //取钱的线程
        new WithdrawMoney("小明", acc, 1000).start();
        new WithdrawMoney("小红", acc, 1000).start();

        //存钱的线程
        new SaveMoney("监护人1", acc, 1000).start();
        new SaveMoney("监护人2", acc, 1000).start();
        new SaveMoney("监护人3", acc, 1000).start();
    }
}

运行结果

小明 取款失败
监护人3 存款成功,共存 1000.0 元,账户余额:1000.0 元
监护人1 存款失败
小红 取款成功,共取 1000.0 元,账户余额:0.0 元
监护人1 存款成功,共存 1000.0 元,账户余额:1000.0 元
监护人2 存款失败
小明 取款成功,共取 1000.0 元,账户余额:0.0 元
监护人2 存款成功,共存 1000.0 元,账户余额:1000.0 元
监护人1 存款失败
小红 取款成功,共取 1000.0 元,账户余额:0.0 元
监护人2 存款成功,共存 1000.0 元,账户余额:1000.0 元
小明 取款成功,共取 1000.0 元,账户余额:0.0 元
...

线程池⭐

引言

用户每发起一个请求,就需要重新 new Thread() 来创建新的线程,而创建新线程的开销很大,严重影响了系统的性能。对此的解决方案是利用线程池

概述

JDK5提供了代表线程池的接口: ExecutorService

线程池是一种可以复用线程的技术。处理过程中将任务添加到队列,然后在创建线程后自动启动这些任务。如果所有线程池线程都始终保持繁忙,但队列中包含挂起的工作,则线程池将在一段时间后创建另一个辅助线程但线程的数目永远不会超过最大值。超过最大值的线程可以排队,但他们要等到其他线程完成后才启动。

在这里插入图片描述

线程池的优点

  • 降低系统资源消耗,通过复用已存在的线程,降低线程创建和销毁造成的消耗;
  • 提高系统响应速度,当有任务到达时,无需等待新线程的创建便能立即执行;
  • 方便线程并发数的管控,统一分配、调优,提供资源使用率;
  • 线程池提供了定时、定期以及可控线程数等功能。

线程池的创建方式

  1. 使用 ExecutorService 的实现类 ThreadPoolExecutor 创建一个线程池对象
  2. 使用 Executors (线程池的工具类)调用方法返回不同特点的线程池对象

ThreadPoolExecutor

ThreadPoolExecutorExecutorService 的一个实现类。

构造方法

方法说明
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue)用给定的初始参数创建新的线程池。
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, RejectedExecutionHandler handler)用给定的初始参数创建新的线程池。
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory)用给定的初始参数创建新的线程池。
public ThreadPoolExecutor (int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue, ThreadFactory threadFactory, RejectedExecutionHandler handler)用给定的初始参数创建新的线程池。
public ThreadPoolExecutor (   int corePoolSize,
                              int maximumPoolSize,
                              long keepAliveTime,
                              TimeUnit unit,
                              BlockingQueue<Runnable> workQueue,
                              ThreadFactory threadFactory,
                              RejectedExecutionHandler handler  )

参数说明

参数说明注意
int corePoolSize指定线程池的线程数量(核心线程)不能小于0
int maximumPoolSize指定线程池可支持的最大线程数(多出的线程叫临时线程)最大数量>=核心线程数量
long keepAliveTime指定临时线程闲置时的最大存活时间不能小于0
TimeUnit unit指定存活时间的单位(秒、分、时、天)时间单位
BlockingQueue workQueue指定任务队列(当线程满了,任务就进任务队列)不能为null
ThreadFactory threadFactory指定用哪个线程工厂来创建线程不能为null
RejectedExecutionHandler handler指定线程忙,任务队列满时,新任务来了时,使用什么策略应对不能为null

核心线程不会“死亡”,临时线程超出最大闲置时间就会“死亡”

策略

策略说明
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出RejectedExecutionException异常。(默认策略)
ThreadPoolExecutor.DiscardPolicy丢弃任务,但是不抛出异常。(不推荐)
ThreadPoolExecutor.Discard0ldestPolicy抛弃队列中等待最久的任务,然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy主线程负责调用任务的run()方法,从而绕过线程池直接执行

示例

ExecutorService pool = new ThreadPoolExecutor(3,
        5,
        10,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(5),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy());

常用方法

来自 ExecutorService 接口

方法说明
void execute(Runnable command)执行任务,没有返回值。用于执行Runnable任务
Future submit(Callable<T> task)执行任务,返回未来任务对象获取线程结果。用于执行Callable任务
void shutdown()任务执行完毕后关闭线程池
List shutdownNow()立刻关闭线程池,停止正在执行的任务,并返回队列中未执行的任务

常见问题⭐

  • 临时线程什么时候创建?

新任务提交时,发现核心线程都在忙、任务队列也满了,并且还可以创建临时线程,此时才会创建临时线程。

  • 什么时候会开始拒绝任务?

核心线程和临时线程都在忙,任务队列也满了,新的任务过来的时候就会开始拒绝任务。

处理Runnable任务

Runnable任务没有返回值。

使用 ExecutorService 接口中的 execute(Runnable command) 方法处理Runnable任务

代码演示

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程正在执行");
        try {
            //模拟线程执行时间
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
import java.util.concurrent.*;

public class RunnablePoolDemo {
    public static void main(String[] args) {
        //1.创建一个线程池:2个核心线程,2个临时线程,2个线程队列位置,默认线程工厂,默认策略
        ExecutorService pool = new ThreadPoolExecutor(2,
                4,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());
        //2.创建 Runnable 任务
        MyRunnable runnable = new MyRunnable();

        //3.在线程池中执行任务
        // ...
    }
}

各情况演示

  1. 任务数 <= 核心线程数 + 线程队列长度

不会创建临时线程

// 4个任务
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-NKI9dQlI-1679066872204)(F:\文档\Notes\Java\images\image-20230111164112254.png)]

  1. 核心线程数 + 线程队列长度 < 任务数 <= 线程总数 + 线程队列长度

根据所需,创建临时线程

// 6个任务
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-8fCMVhEs-1679066872205)(F:\文档\Notes\Java\images\image-20230111164350998.png)]

  1. 任务数 > 线程总数 + 线程队列长度

实行“策略”

// 7个任务
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
pool.execute(runnable);
// [Running, pool size = 4, active threads = 4, queued tasks = 2, completed tasks = 0]

在这里插入图片描述

处理Callable任务

Callable任务可以返回结果。

使用 ExecutorService 接口中的 submit(Callable<T> task) 方法处理Runnable任务

代码演示

import java.util.concurrent.Callable;

public class MyCallable implements Callable<String> {
    private int num;

    public MyCallable(int num) {
        this.num = num;
    }

    @Override
    public String call() throws Exception {
        int sum = 0;
        for (int i = 1; i <= num; i++) {
            sum += i;
        }
        return Thread.currentThread().getName() + "执行 1 - " + num + " 的和,结果为:" + sum;
    }
}
import java.util.concurrent.*;

public class CallablePoolDemo {
    public static void main(String[] args) throws ExecutionException, InterruptedException {
        //1.创建一个线程池:2个核心线程,2个临时线程,2个线程队列位置,默认线程工厂,默认策略
        ExecutorService pool = new ThreadPoolExecutor(2,
                4,
                10,
                TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(2),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.AbortPolicy());

        //2.在线程池中执行 Callable 任务
        Future<String> f1 = pool.submit(new MyCallable(100));
        Future<String> f2 = pool.submit(new MyCallable(200));
        Future<String> f3 = pool.submit(new MyCallable(300));

        //3.获取结果
        System.out.println(f1.get());
        System.out.println(f2.get());
        System.out.println(f3.get());
    }
}

在这里插入图片描述

Executors

Executors 是线程池的工具类。提供了创建不同类型的线程池对象的方法。

常用方法说明
static ExecutorService newCachedThreadPool()线程数量随着任务增加而增加,如果线程任务执行完毕且空闲了一段时间则会被回收掉。
static ExecutorService newFixedThreadPool(int nThreads)创建固定线程数量的线程池,如果某个线程因为执行异常而结束,那么线程池会补充一个新线程替代它。
static ExecutorService newSingleThreadExecutor ()创建只有一个线程的线程池对象,如果该线程出现异常而结束,那么线程池会补充一个新线程。
static ScheduledExecutorService newScheduledThreadPool(int corePoolsize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。

Executors 的底层也是依靠 ThreadPoolExecutor 创建线程池的

大型并发系统环境中,可能存在风险

  1. FixedThreadPoolSingleThreadExecutor

允许的请求队列长度为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)

  1. CachedThreadPoolScheduledThreadPool

允许的创建线程数量为Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM(内存溢出)

代码演示

public class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println(Thread.currentThread().getName() + "线程正在执行");
        try {
            //模拟线程执行时间
            Thread.sleep(100000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ExecutorsPoolDemo {
    public static void main(String[] args) {
        //1.创建一个线程池:2个核心线程,0个临时线程,Integer.MAX_VALUE个线程队列位置
        ExecutorService pool = Executors.newFixedThreadPool(2);

        //2.在线程池中执行 Runnable 任务
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
        pool.execute(new MyRunnable());
    }
}

运行结果

在这里插入图片描述

定时器

1 Timer定时器

1.1 简述

一种工具,用其安排线程在后台中执行的任务。可安排执行一次任务,或者定期重复执行。如闹钟。

存在的问题

  • 多个Timer定时器占用一个线程。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”。

  • 如果意外终止了计时器的任务执行线程,例如出现了异常,那么之后的所有计时器都将导致IllegalStateException

1.2 方法

构造方法说明
Timer()创建Timer定时器对象
常用方法说明
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.3 定时

import java.util.Timer;
import java.util.TimerTask;

public class TimerDemo1 {
    public static void main(String[] args) {
        //创建定时器对象
        Timer timer = new Timer();
        //给予任务:在 2s 后执行,且每隔 1s 执行一次
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务" + new Date());
            }
        }, 2000, 1000);
    }
}

运行结果

// 等待2s
Timer-0正在执行任务Wed Jan 11 18:57:56 CST 2023
Timer-0正在执行任务Wed Jan 11 18:57:57 CST 2023
Timer-0正在执行任务Wed Jan 11 18:57:58 CST 2023
...

1.4 缺陷演示

问题1: 多个Timer定时器占用一个线程。如果完成某个计时器任务的时间太长,那么它会“独占”计时器的任务执行线程。因此,这就可能延迟后续任务的执行,而这些任务就可能“堆在一起”。

public class TimerDemo2 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务A " + new Date());
                try {
                    //模拟任务执行时长
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, 2000, 1000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务B " + new Date());
            }
        }, 2000, 1000);
    }
}

运行结果

Timer-0正在执行任务A Wed Jan 11 18:54:57 CST 2023
Timer-0正在执行任务B Wed Jan 11 18:55:00 CST 2023
Timer-0正在执行任务A Wed Jan 11 18:55:00 CST 2023
Timer-0正在执行任务A Wed Jan 11 18:55:03 CST 2023
Timer-0正在执行任务B Wed Jan 11 18:55:06 CST 2023
Timer-0正在执行任务A Wed Jan 11 18:55:06 CST 2023

任务A和任务B都定为“ 2s 后执行,且每隔 1s 执行一次”,但因为任务A的时间太长,导致任务B没有精准执行。

问题2: 如果意外终止了计时器的任务执行线程,例如出现了异常,那么之后的所有计时器都将无法执行。

public class TimerDemo3 {
    public static void main(String[] args) {
        Timer timer = new Timer();
        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务A" + new Date());
            }
        }, 2000, 1000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务B" + new Date());
                //模拟出现异常
                System.out.println(1 / 0);
            }
        }, 2000, 1000);

        timer.schedule(new TimerTask() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "正在执行任务C" + new Date());
            }
        }, 2000, 1000);
    }
}

运行结果

在这里插入图片描述

2 ScheduledExecutorService

2.1 简述

ScheduledExecutorService 是在 JDK5被引入并发包,目的是为了弥补Timer的缺陷, ScheduledExecutorService 内部为线程池。

ScheduledExecutorService的优点

  • 基于线程池,某个任务的执行情况不会影响其他定时任务的执行。

2.2 方法

常用方法说明
static ScheduledExecutorService newScheduledThreadPool(int corePoolsize)创建一个线程池,可以实现在给定的延迟后运行任务,或者定期执行任务。(来自Executors线程工具类)
ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long delay, long period, TimeUnit unit)延迟delay时间后触发任务,且周期触发

2.3 定时

public class TimerDemo4 {
    public static void main(String[] args) {
        //1.创建 ScheduledExecutorService 线程池对象
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        //2.给予任务:在 2s 后执行,且每隔 1s 执行一次
        pool.scheduleAtFixedRate(new Runnable() {
            @Override
            public void run() {
                System.out.println(Thread.currentThread().getName() + "执行任务");
            }
        }, 2, 1, TimeUnit.SECONDS);
    }
}

运行结果

pool-1-thread-1执行任务
pool-1-thread-1执行任务
pool-1-thread-1执行任务
pool-1-thread-2执行任务
...

2.4 优点演示

  1. 当完成某个计时器任务的时间太长时:
public class TimerDemo5 {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        pool.scheduleAtFixedRate(() -> {
            System.out.println(Thread.currentThread().getName() + "执行任务A " + new Date());
            try {
                //模拟任务执行时长
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, 2, 1, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "执行任务B " + new Date())
                , 2, 1, TimeUnit.SECONDS);
    }
}

运行结果

在这里插入图片描述

即使任务A执行时间很长,也不会影响其他任务按时完成。

  1. 当意外终止了计时器的任务执行线程时:
public class TimerDemo6 {
    public static void main(String[] args) {
        ScheduledExecutorService pool = Executors.newScheduledThreadPool(2);

        pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "执行任务A " + new Date())
                , 2, 1, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(() -> {
            System.out.println(Thread.currentThread().getName() + "执行任务B " + new Date());
            //模拟任务出现异常
            System.out.println(1 / 0);
        }, 2, 1, TimeUnit.SECONDS);

        pool.scheduleAtFixedRate(() -> System.out.println(Thread.currentThread().getName() + "执行任务C " + new Date())
                , 2, 1, TimeUnit.SECONDS);
    }
}

运行结果

在这里插入图片描述

任务B出现异常被终止,不会影响其他任务的按时执行。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
### 回答1: 多线程在电商项目中的应用非常广泛,可以提高系统的并发处理能力和用户的响应速度,同时也可以减少资源占用和提高系统的可扩展性。 以下是多线程在电商项目中的几个应用场景: 1. 商品搜索:在电商网站中,商品搜索是一个非常常见的功能。搜索时需要从数据库中查询符合条件的商品信息,如果没有使用多线程,查询操作可能会阻塞主线程,导致用户的搜索请求得不到及时响应。可以使用多线程来实现异步查询,让查询操作在后台线程中执行,从而提高搜索速度和用户体验。 2. 购物车:购物车是电商网站中必不可少的功能之一。如果每次用户在购物车中添加、删除商品时都要向数据库中更新购物车信息,那么系统的并发处理能力就会受到限制。可以使用多线程来实现异步更新购物车信息,从而提高系统的并发处理能力和用户响应速度。 3. 订单处理:在电商网站中,订单处理是一个非常重要的功能。如果订单处理过程中涉及到多个操作,例如扣款、发货、更新库存等,可以将这些操作放在不同的线程中执行,从而提高系统的并发处理能力和用户响应速度。 4. 评论管理:在电商网站中,用户可以对商品进行评价和评论。如果每次用户提交评论时都要向数据库中插入新的评论信息,那么系统的并发处理能力就会受到限制。可以使用多线程来实现异步插入评论信息,从而提高系统的并发处理能力和用户响应速度。 总之,多线程可以提高电商网站的并发处理能力和用户响应速度,同时也可以减少资源占用和提高系统的可扩展性,是电商项目中非常重要的一种技术手段。 ### 回答2: 在电商项目中,多线程技术的应用可以提高系统的响应速度以及并发处理能力。具体来说,多线程可以应用于以下几个方面: 1. 并发订单处理:在电商项目中,同时可能有多个用户下单购买商品,这就需要系统能够同时处理多个订单。通过多线程技术,可以将不同订单的处理任务交给不同的线程来执行,实现订单的并发处理,加快订单完成速度。 2. 购物车处理:用户可能同时往购物车中添加多个商品,而购物车中的商品数量、总价等信息需要实时更新。通过多线程技术,可以将购物车的更新任务交给不同的线程来执行,实现购物车信息的并发处理,避免用户在购物车操作时的卡顿等问题。 3. 数据库读写:电商系统中的大量数据需要频繁读写,而数据库操作通常是比较耗时的操作。通过多线程技术,可以将数据库的读写任务交给不同的线程来执行,提高数据库操作的并发性能,减少用户等待时间。 4. 广告推荐:电商网站通常会根据用户的浏览历史、购买记录等信息进行个性化推荐广告。通过多线程技术,可以将广告推荐任务交给不同的线程来执行,实现广告推荐的并发处理,提高广告推荐的实时性和准确性。 综上所述,多线程在电商项目中的使用可以提高系统的并发处理能力和响应速度,从而提升用户的购物体验。但需要注意的是,多线程的使用也需要合理的资源管理和线程同步机制,以避免线程安全问题和资源竞争等潜在风险。 ### 回答3: 多线程是指在一个程序中同时运行多个线程,每个线程可以独立执行不同的任务,从而提高程序的并发性和执行效率。在电商项目中,多线程可以应用于以下方面: 1. 商品数据处理:电商平台通常有大量的商品数据需要处理,例如商品抓取、图片处理、数据清洗等。使用多线程可以将这些任务分割为多个子任务并行处理,提高数据处理速度和效率。 2. 并发访问控制:电商平台需要应对大量用户的并发访问,例如多个用户同时浏览商品、提交订单等。使用多线程可以实现并发访问控制,确保每个用户的请求能够及时响应,提高用户体验。 3. 数据库操作:电商平台的数据库通常面临着大量的读写操作,例如用户信息的存储、订单的更新等。使用多线程可以将数据库操作分担至多个线程,提高数据库的并发处理能力和数据访问速度。 4. 后台任务处理:电商平台需要进行后台任务的处理,例如订单的消息通知、库存的更新等。使用多线程可以将这些后台任务并行处理,提高任务处理的效率和实时性。 需要注意的是,在多线程的实际应用中,需要考虑线程安全、资源共享、线程间通信等问题,以确保多线程的正确性和可靠性。同时,合理的线程管理和调度也是保证多线程应用在电商项目中高效运行的重要因素。

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值