14 多线程的创建和使用、线程池的创建(黑马Java视频教程)

在这里插入图片描述

多线程

1. 线程Thread

  • 一个程序内部的一条执行流程
    • 程序中只有一条执行流程单线程的程序
    • 从软硬件上实现的多条执行流程的技术 — 多线程
      • 多线程的好处:多个程序并发进行

2. 如何在程序中创建多个线程

⭐创建方式一:继承Thread
1)步骤:

① 创建MyThread类继承Thread

② 重写Thread的run方法:编写该线程要执行的程序

//1、掌握创建线程的方式一:继承Thread类来实现
public class MyThread extends Thread{
    // 2、重写Thread类的run方法
    @Override
    public void run() {
        // 3、在run方法中编写要执行的代码(线程要完成的任务)
        for (int i = 0; i < 10; i++) {
            System.out.print("\nThread1:" + i + "\n");
        }
    }
}

③ 创建MyThread类的对象

④ 启动线程start

// ---------------- 主函数 --------------
// 4、创建线程对象,代表线程
Thread t1 = new MyThread();
// 5、启动线程
t1.start();
// 主线程: main方法本身是由一条主线程负责推荐执行的
for (int i = 0; i < 10; i++) {
    System.out.print("主线程:" + i);
}

在这里插入图片描述

🫣可以看到两个线程是并发执行的

2)优缺点:
  • 优点:编码简单
  • 缺点:线程类已经继承Thread,无法继承其他类(Java的类不支持多继承,只支持多层继承),不利于功能的扩展,线程有执行结果是不能直接返回的
3)注意事项
  • 启动线程必须调用start方法 ---- 调用run方法还是单线程执行(被当成普通方法执行)
  • 不要把主线程任务放在启动子线程之前 ---- 否则一直只有主线程先跑完
⭐创建方式二:实现Runnable接口 — 线程任务

🤔在创建Thread时作为参数传递,然后启动

1)步骤

① 定义一个线程任务类MyRunnable实现Runnable接口

② 重写run方法:编写该线程要执行的程序

// 1、方式二:实现Runnable接口
public class MyRunnable implements Runnable{
    // 2、重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 10; i++) {
            System.out.print("\tMyRunnable:" + i);
        }
    }
}

③ 创建MyRunnable任务对象

④ 把MyRunnable任务对象交给Thread处理

  • 利用Thread提供的构造器将Runnable对象封装成为线程对象:Thead(Runnable target)

⑤ 启动线程start

//3、创建线程任务类的对象代表一个线程任务
Runnable mr = new MyRunnable();
// 4、把线程任务对象交给一个线程对象来处理
// 把线程任务对象作为参数传递给Thread类的构造方法
Thread t2 = new Thread(mr);
// 5、启动线程
t2.start();

2)优缺点
  • 优点:任务类只是实现接口,可以继续继承其他类,实现其他接口,扩展性强
  • 缺点:需要额外多创建一个Runnable对象,且线程有执行结果是不能直接返回的
3)匿名内部类写法

① 创建Runnable的匿名内部类对象

② 再交给Thread线程对象

③ 调用线程对象的start()启动线程

/*        new Thread(new Runnable() {
            @Override
            public void run() {
                for (int i = 0; i < 10; i++){
                    System.out.println("Thread3:" + i);
                }
            }
        }).start();*/

// Lambda表达式
new Thread(() ->{
        for (int i = 0; i < 10; i++){
            System.out.println("Thread3:" + i);
        }
}).start();
⭐创建方式三:实现Callable接口和FutureTask类来实现

💡前两种线程创建方式都存在问题:线程执行完成后不能直接返回结果(void)

1)步骤

① 创建任务对象

  • 实现Callable接口,重写Call方法,封装要做的事情,和要返回的数据
// 1.创建一个实现Callable接口的类
// 泛型接口:声明返回类型
public class MyCallable implements Callable<String> {
    // 传参---成员变量
    private int n;

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

    // 2、实现call方法,将此线程需要执行的操作声明在call方法中
    @Override
    public String call() throws Exception {
        int sum = 0;
        // 3、在call方法中编写要执行的代码(线程要完成的任务)
        for (int i = 0; i < n; i++) {
            System.out.println("MyCallable:" + i);
            sum += i;
        }
        // 有返回值
        return "1-" + this.n + "的计算结果为" + sum;
    }
}
  • 把Callable类型的对象封装成FutureTask(线程任务对象)
    • FutureTask对象:线程对象 + 线程执行完后的结果

② 把线程任务对象交给Thread对象

  • 未来任务对象本质上是一个Runnable线程任务对象,所以可以交给Thread线程对象处理

③ 调用Thread对象的start方法启动线程

// 4、创建Callable接口实现类的对象
Callable<String> myCallable = new MyCallable(10);
// 5、创建FutureTask对象,将Callable接口实现类的对象作为参数传递,将Callable对象封装成FutureTask对象
FutureTask<String> f1 = new FutureTask<>(myCallable);
// 6、创建Thread对象,将FutureTask对象作为Thread对象的target对象
Thread t1 = new Thread(f1);
// 7、调用Thread对象的start方法,开启线程,执行run方法
t1.start();

④ 线程执行完毕后,通过FutureTask对象的get方法获取线程任务执行的结果

try {
    // 8、调用FutureTask对象的get方法,获取线程执行完毕后的结果
    // 如果线程没有执行完毕,get方法会阻塞当前线程,让出CPU,直到线程执行完毕,获取线程执行完毕后的结果
    System.out.println(f1.get());
} catch (Exception e) {
    e.printStackTrace();
}

创建两个线程的结果

2)优缺点
  • 优点:
    • 只是实现接口,还可以继承其他的类和接口
    • 可以获取线程执行的结果
  • 缺点:
    • 编码复杂一点

3. 线程的常用方法

1)getName()
  • 线程默认名:Thread-索引
  • 主线程名(main函数):main
System.out.println(Thread.currentThread().getName());   // main
Thread t = new MyThread();
System.out.println(t.getName());    // Thread-0
2)currentThread() 获取当前线程:

哪个线程调用这段代码,就拿到哪个线程

3)setName(String name) 为线程设置名字:

启动前set设置/有参构造器设置

Thread t = new MyThread();
System.out.println(t.getName());    // Thread-0
// setName(String name):设置线程名称
t.setName("线程1");
System.out.println(t.getName());    // 线程1
t.start();      // 启动线程

⚠️启动前set设置线程名字 : 否则线程启动时,可能名字并没有变,只有在主线程中执行到了更改线程名字的代码,该线程的名字才会变更 ------ 该线程前后名字不一致

4)sleep() 线程休眠

5)join() 线程插队
// 线程插队
MyThread t1 = new MyThread();
t1.start();

for (int i = 0; i <= 10; i++) {
    System.out.println(" " + Thread.currentThread().getName() + ":" + i);
    if (i == 1) {
        try {
            t1.join();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}


4. 线程安全(同步与互斥)

4.1 线程安全问题
  • 多个线程,同时操作同一个共享资源的时候,可能会出现业务安全问题

💡1)线程问题出现的条件:
  • 存在多个线程同时运行
  • 同时访问同一个资源
  • 存在修改该同一个资源的行为
2)模拟线程安全问题的场景:取钱

需求

  • 小明和小红是一对夫妻,他们有一个共同的账户,余额是10万元,用程序模拟两人同时取钱10万元

分析:

  • 小明和小红的共同财产 >> 设计一个账户类,创建一个账户对象,代表两人共同财产
  • 模拟两人同时取钱 >> 设计一个线程类,创建并启动两个线程

根据结果给出了可能的程序执行的顺序之一,还有其他情况

4.2 线程同步 > 线程安全问题的解决方案
  • 多个线程先后依次访问共享资源,以避免出现线程安全问题

(只是在互斥访问资源时是先后执行,其他还是同步执行)

常见方案:加锁
  • 每次只运行一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
1)⭐同步代码块:使用synchronized进行加锁
synchronized(lock){

}
  • 作用:

    • 把访问共享资源的核心代码给上锁,以此保证线程安全
  • 原理:

    • 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
  • 注意事项

    • 对于当前同步执行的线程来说,同步锁必须是同一把锁

ctrl+Alt+T

// ------------------ 给资源区上锁 --------------------------------------
public void drawMoney(double money){
// 查看当前是谁来取钱
String name = Thread.currentThread().getName();
// 判断余额是否充足
// 加锁
synchronized ("account") {
    if(this.banlance >= money){
        // 余额充足,取钱
        System.out.println(name + "取钱成功,取了" + money + "元");
        // 更新余额
        this.banlance -= money;
        System.out.println(name + "取钱后余额为:" + this.banlance);
    }else{
        System.out.println(name + "取钱失败,余额不足");
    }
}
}

💡锁对象如果选择一个唯一的对象,那么所有执行到该资源区的进程都会被锁住

  • 使用规范:
    • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
    • 对于静态方法建议使用**字节码(类名.class)**对象作为锁对象
// ---------- 实例方法用this -------------------------
synchronized (this) {
    if(this.banlance >= money){
        // 余额充足,取钱
        System.out.println(name + "取钱成功,取了" + money + "元");
        // 更新余额
        this.banlance -= money;
        System.out.println(name + "取钱后余额为:" + this.banlance);
    }else{
        System.out.println(name + "取钱失败,余额不足");
    }
}

在这里插入图片描述

在这里插入图片描述

2)⭐同步方法
修饰符 synchronized 返回值类型 方法名称(形参列表){
	操作共享资源的代码
}
  • 作用:

    • 访问共享资源的核心方法给上锁,以此保证线程安全
  • 原理:

    • 每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
    • 底层有隐式锁对象(与同步代码块的使用规范相同),范围是整个代码
public synchronized void drawMoney(double money){
	....
}
3)⭐Lock锁

通过创建出锁对象进行加锁和解锁,更灵活、更方便、更强大

  • Lock作为接口,不能直接实例化,需要使用它的实现类ReentrantLock来构建Lock锁对象

常用方法

  • lock()上锁
  • unlock()解锁
    • ⚠️建议放到finally代码块中,确保锁用完后一定会被释放,避免资源区代码出现异常,导致执行不到释放锁的代码
    • ⚠️建议锁对象进行final修饰,防止外部篡改,提高安全性

public  void drawMoney(double money){
    // 查看当前是谁来取钱
    String name = Thread.currentThread().getName();
    System.out.println(name + "来取钱");
    // 判断余额是否充足

    lock.lock();    // 获取锁
    try {
        if(this.banlance >= money){
            // 余额充足,取钱
            System.out.println(name + "取钱成功,取了" + money + "元");
            // 更新余额
            this.banlance -= money;
            System.out.println(name + "取钱后余额为:" + this.banlance);
        }else{
            System.out.println(name + "取钱失败,余额不足");
        }
    } finally {
        lock.unlock();  // 释放锁
    }
}


5. 线程池

5.1 认识

  • 线程池:一个可以复用线程的技术

  • 不使用线程池 >>> 不断为新任务创建新线程处理 >>> 大量的线程开销大,影响系统的性能

工作原理:

固定线程数量,依次执行队列中的线程任务 >>> 餐厅中固定餐桌数,排队上桌就餐的客人

5.2 创建线程池 ExecutorService接口

⭐方式一:
1)使用ExecutorService的实现类ThreadPoolExecutor自创建一个线程池对象

// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
        3,
        5,
        3,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

💡参数五六七理解


2)ExecutorService常用方法

  • MyRunnable类实现Runnable接口
public class MyRunnable implements Runnable{
    // 重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(Thread.currentThread().getName() + "输出:" + i);
        }
    }
}
  • 创建MyRunnable对象,在线程池中添加Runnable任务
// 使用线程池对象来执行任务:看会不会复用线程
Runnable target = new MyRunnable();
pool.execute(target);   // 提交第一个任务 创建线程 自动启动线程处理这个任务
pool.execute(target);   // 提交第二个任务 创建线程 自动启动线程处理这个任务
pool.execute(target);   // 提交第三个任务 创建线程 自动启动线程处理这个任务
pool.execute(target);   // 提交第四个任务 workQueue任务队列 -- 复用线程

🤔创建的线程池的核心线程数corePoolSize是3,任务队列workQueue的容量是3,所以当任务队列未满时,核心线程处理完当前线程任务后就会复用线程,接着处理队列中的线程任务

  • 关于只有一个Runnable实现类对象,但可以提交多个线程任务的理解
    • 在这里插入图片描述
3)模拟线程池创建临时线程的场景

💡临时线程的创建时机

  • 新任务提交时
  • 核心线程都在忙
  • 任务队列也满了
  • 此时还可以创建临时线程

①修改MyRunnable类的run方法中调用sleep() 方法,模拟线程忙碌

  • 区分线程任务和线程
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MyRunnable implements Runnable{
    private String name;    // 线程任务名 : 标记当前处理的是哪个任务

    // 重写run方法
    @Override
    public void run() {
        for (int i = 0; i < 5; i++) {
            System.out.println(this.name + "输出:" + i);  // 输出线程任务名+输出内容
            try {
                Thread.sleep(1000000000); //休眠1000000秒
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}

②通过for循环创建多个线程任务对象,并加入到线程池中

  • 核心线程3个,任务队列容量3个:当核心线程忙碌且任务队列满时,才创建临时线程 >>> 第七个线程任务加入时才会创建
// 1、使用线程池的实现类ThreadPoolExecutor声明七个参数来创建线程池对象
ExecutorService pool = new ThreadPoolExecutor(
        3,
        5,
        3,
        TimeUnit.SECONDS,
        new ArrayBlockingQueue<>(3),
        Executors.defaultThreadFactory(),
        new ThreadPoolExecutor.AbortPolicy()
);

// 2、使用线程池对象来执行任务:看会不会复用线程
for (int i = 1; i <= 8; i++) {
    pool.execute(new MyRunnable("Task:" + i));
}

③结果解析

由结果可以看出,临时线程执行的不是任务队列中的,而是新加入的。

在这里插入图片描述

4)模拟任务拒绝的场景

💡任务拒绝策略

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


🤔核心线程3个、任务队列容量3个,临时线程数2个 >>> 超出 3 + 3 + 2 = 8个线程任务后,就会开始拒绝任务。由于pool创建时,采取的时AbortPolicy策略,所以会抛出异常。

⭐方式二:
  • 使用Executors(线程池的工具类)调用方法返回不同特点的线程池对象

在这里插入图片描述

ExecutorService pool = Executors.newFixedThreadPool(5); // 线程池大小固定,不会自动调整
// new ThreadPoolExecutor(nThreads, nThreads, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>());

ExecutorService pool2 = Executors.newCachedThreadPool();    // 线程池大小不固定,会根据任务数量自动调整
// new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L, TimeUnit.SECONDS,new SynchronousQueue<Runnable>());

ExecutorService pool3 = Executors.newSingleThreadExecutor();    // 线程池大小固定,只有一个线程
// new ThreadPoolExecutor(1, 1, 0L, TimeUnit.MILLISECONDS, new LinkedBlockingQueue<Runnable>(), threadFactory));

ExecutorService pool4 = Executors.newScheduledThreadPool(5);    // 线程池大小固定,可以定时
  • 💡底层都是ExecutorService的实现类:ThreadPoolExecutor

在这里插入图片描述

6. 并发/并行

6.1 进程(动态)
  • 正在运行的程序(软件)是一个独立的进程
  • 线程是属于进程的,一个进程中可以同时运行很多个线程
6.2 并发
  • CPU分时轮询的执行线程

在这里插入图片描述

6.3 并行
  • 同一时刻同时在执行
    在这里插入图片描述
6.4 多线程
  • 并发和并行同时进行的
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值