day12_多线程

22 阅读7分钟

day12_多线程

1. 多线程相关概念

1.1 进程

  • 进程是程序执行的过程,包括了动态创建、调度和消亡的整个过程

  • 操作系统会以进程为单位,分配系统资源(CPU时间片、内存等资源),进程是资源分配的最小单位

1.2 线程

  • 线程被包含在进程之中,是进程中的实际运作单位,一个进程内可以包含多个线程
  • 是操作系统调度(CPU调度)执行的最小单位

2. Java多线程的创建

2.1 继承Thread类

  • Java是通过java.lang.Thread类的对象来代表线程的
  • 特点
    • 编码简单
    • 线程类已经继承Thread,无法继承其他类,不利于功能的扩展
  • 实现
  1. 定义一个子类MyThread继承线程类java.lang.Thread,重写run()方法
  2. 创建MyThread类的对象
  3. 调用线程对象的start()方法启动线程
public class Test {
    public static void main(String[] args) {
        new MyThread().start();
    }
}

class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("MyThread start...");
    }
}
  • 注意
    • 只有调用start方法才是启动一个新的线程执行
    • 启动线程必须是调用start方法,不是调用run方法,直接调用run方法会当成普通方法执行,此时相当于还是单线程执行
    • 不要把主线程任务放在启动子线程之前,这样主线程一直是先跑完的,相当于是一个单线程的效果了

2.2 实现Runnable接口

  • 通过实现Runnable接口实现多线程

  • 特点

    • 任务类只是实现接口,可以继续继承其他类、实现其他接口,扩展性强
  • Thread类提供的构造器说明
    public Thread(Runnable target)封装Runnable对象成为线程对象
  • 实现

  1. 定义一个线程任务类MyRunnable实现Runnable接口,重写run()方法
  2. 创建MyRunnable任务对象
  3. MyRunnable任务对象交给Thread处理
  4. 调用线程对象的start()方法启动线程
public class Test {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        Thread thread = new Thread(mr);
        thread.start();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable start...");
    }
}
  • 匿名内部类写法
/*
new Thread(new Runnable() {
    @Override
    public void run() {
        System.out.println("Thread start...");
    }
}).start();
*/

new Thread(() -> {
    System.out.println("Thread start...");
}).start();

2.3 实现Callable接口

  • 利用Callable接口、FutureTask类来实现多线程
  • 特点
    • 线程任务类只是实现接口,可以继续继承类和实现接口,扩展性强
    • 可以返回线程执行完毕后的结果
    • 编码复杂
  • FutureTask的API
FutureTask提供的构造器说明
public FutureTask<>(Callable call)把Callable对象封装成FutureTask对象
FutureTask提供的方法说明
public V get() throws Exception获取线程执行call方法返回的结果
  • 实现
  1. 定义一个类实现Callable接口,重写call()方法
  2. 把Callable类型的对象封装成FutureTask,即线程任务对象
  3. 把线程任务对象交给Thread对象
  4. 调用Thread对象的start()方法启动线程
  5. 线程执行完毕后、通过FutureTask对象的的get方法去获取线程任务执行的结果
public class Test {
    public static void main(String[] args) throws Exception {
        FutureTask<Integer> threadTask = new FutureTask<>(new MyCallable(1, 1));
        new Thread(threadTask).start();
        System.out.println(threadTask.get()); // 2
    }
}

class MyCallable implements Callable<Integer> {
    private Integer a;
    private Integer b;

    public MyCallable(Integer a, Integer b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        int ans = a + b;
        System.out.println(a + " + " + b + " = " + ans);
        return ans;
    }
}

3. Thread

构造方法

Thread提供的常见构造器说明
public Thread(String name)可以为当前线程指定名称
public Thread(Runnable target)封装Runnable对象成为线程对象
public Thread(Runnable target, String name)封装Runnable对象成为线程对象,并指定线程名称

常用方法

Thread提供的常用方法说明
public void run()线程的任务方法
public void start()启动线程
public String getName()获取当前线程的名称,线程名称默认是Thread-索引
public void setName(String name)为线程设置名称
public static Thread currentThread()获取当前执行的线程对象
public static void sleep(long time)让当前执行的线程休眠多少毫秒后,再继续执行
public final void join()让调用当前这个方法的线程先执行完

4. 线程安全

4.1 什么是线程安全

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

出现线程安全的原因:

  1. 存在多个线程在同时执行
  2. 同时访问一个共享资源
  3. 存在修改该共享资源

4.2 线程同步

  • 解决线程安全问题的方案
  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

常见的线程同步方案:加锁

  • 每次只允许一个线程加锁,加锁后才能进入访问,访问完毕后自动解锁,然后其他线程才能再加锁进来
  • 让多个线程实现先后依次访问共享资源,这样就解决了安全问题

4.3 方式一:同步代码块

  • 原理
    • 每次只允许一个线程加锁后进入,执行完毕后自动解锁,其他线程才可以进来执行
  • 作用
    • 把访问共享资源的核心代码给上锁,以此保证线程安全
  • 实现
public void test() {
	synchronized(同步锁) {
        // 访问共享资源的核心代码
    }
}
  • 注意
    • 对于当前同时执行的线程来说,同步锁必须是同一个对象
    • 建议使用共享资源作为锁对象,对于实例方法建议使用this作为锁对象
    • 对于静态方法建议使用字节码(类名.class)对象作为锁对象

4.4 方式二:同步方法

  • 原理
    • 每次只能一个线程进入,执行完毕以后自动解锁,其他线程才可以进来执行
  • 作用
    • 把访问共享资源的核心方法给上锁,以此保证线程安全
  • 实现
public synchronized void test() {
    // 访问共享资源的代码
}
  • 底层
    • 同步方法其实底层也是有隐式锁对象的,只是锁的范围是整个方法代码
    • 如果方法是实例方法:同步方法默认用this作为的锁对象
    • 如果方法是静态方法:同步方法默认用类名.class作为的锁对象

4.5 方式三:Lock锁

  • 通过Lock创建出锁对象进行加锁和解锁
  • 可以采用它的实现类ReentrantLock来构建Lock锁对象

构造方法

构造器说明
public ReentrantLock()获得Lock锁的实现类对象

常用方法

方法名称说明
void lock()获得锁
void unlock()释放锁
boolean trylock()尝试获取锁

5. 线程通信

  • 当多个线程共同操作共享的资源时,线程间通过某种方式互相告知自己的状态,以相互协调,并避免无效的资源争夺

常用方法

方法名称说明
void wait()当前线程等待,直到另一个线程调用notify()notifyAll()唤醒自己
void notify()唤醒正在等待对象监视器(锁对象)的单个线程
void notifyAll()唤醒正在等待对象监视器(锁对象)的所有线程
  • 注意:上述方法应该使用当前同步锁对象进行调用

6. 线程的生命周期

  • 线程的生命周期
    • 指的是线程从生到死的过程中,经历的各种状态及状态转换
  • Java的线程生命周期有6种,都定义在Thread类的内部枚举类中
线程状态说明
NEW(新建)线程刚被创建,但是并未启动
Runnable(可运行)线程已经调用了start(),等待CPU调度
Blocked(锁阻塞)线程在执行的时候未竞争到锁对象,则该线程进入Blocked状态;。
Waiting(无限等待)一个线程进入Waiting状态,另一个线程调用notify或者notifyAll方法才能够唤醒
Timed Waiting(计时等待)同waiting状态,有几个方法(sleep,wait)有超时参数,调用他们将进入Timed Waiting状态
Terminated(被终止)因为run方法正常退出而死亡,或者因为没有捕获的异常终止了run方法而死亡

image-20230822202742682.png

7. 线程池

7.1 概述

  • 线程池是一个容器,可以保存一些长久存活的线程对象,负责创建、复用、管理线程

优势:

  • 降低资源消耗,重复利用线程池中线程,不需要每次都创建、销毁
  • 便于线程管理,线程池可以集中管理并发线程的数量

使用步骤:

  1. 创建线程池
  2. 创建任务
  3. 提交任务

7.2 线程池的创建

  • 提交Runnable任务
public class Test1 {
    public static void main(String[] args) {
        MyRunnable mr = new MyRunnable();
        ExecutorService pool = Executors.newFixedThreadPool(3);
        pool.submit(mr);
        pool.shutdown();
    }
}

class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("MyRunnable Start...");
    }
}
  • 提交Callable任务
public class Test2 {
    public static void main(String[] args) throws Exception {
        ExecutorService pool = Executors.newFixedThreadPool(3);
        MyCallable mc = new MyCallable(1, 100);
        Future<Integer> future = pool.submit(mc);
        System.out.println(future.get());
        pool.shutdown();
    }
}

class MyCallable implements Callable<Integer> {
    private int a;
    private int b;

    public MyCallable(int a, int b) {
        this.a = a;
        this.b = b;
    }

    @Override
    public Integer call() throws Exception {
        int sum = 0;
        for (int i = a; i <= b; i++) {
            sum += i;
        }
        return sum;
    }
}

7.3 自定义线程池

  • 使用ThreadPoolExecutor自定义线程池
public ThreadPoolExecutor(
	int corePoolSize, // 核心线程数
    int maximumPoolSize, // 最大线程数
    long keepAliveTime, // 临时线程存活时间
    TimeUnit unit, // 临时线程存活时间单位
    BlockingQueue<Runnable> workQueue, // 阻塞等待队列
    ThreadFactory threadFactory, // 创建线程的工厂
    RejectedExecutionHandler handler // 拒绝策略
)
  • 拒绝策略
策略详解
ThreadPoolExecutor.AbortPolicy丢弃任务并抛出异常。是默认的策略
ThreadPoolExecutor.DiscardPolicy 丢弃任务,但是不抛出异常 这是不推荐的做法
ThreadPoolExecutor.DiscardOldestPolicy抛弃队列中等待最久的任务 然后把当前任务加入队列中
ThreadPoolExecutor.CallerRunsPolicy由主线程负责调用任务的run()方法从而绕过线程池直接执行