学习笔记 - 异常和多线程

发布时间 2023-08-31 20:16:50作者: KoiC

异常

概述

异常指的是程序在执行过程中,出现的非正常情况,如果不处理最终会导致JVM的非正常停止。

Java 异常体系

  • java.lang.Throwable:异常体系的父类
    • java.lang.Error:Java 虚拟机无法解决的严重问题。如:JVM 系统内部错误、资源耗尽等严重情况。
    • java.lang.Exception:其它因编程错误或偶然的外在因素导致的一般性问题,需要使用针对性的代码进行处理,使程序继续运行。
      • 编译时异常(受检异常)
      • 运行时异常(非受检异常)

异常处理

Throwable 中的常用方法

  • public void printStackTrace() :打印异常的详细信息,包含异常类型、原因、出现位置,在开发和调试阶段都得使用 printStackTrace。
  • public String getMessage() :获取发生异常的原因。

try-catch-finally

  • try:程序在执行的过程中,一旦出现异常,就会在出现异常的代码处,生成对应异常类的对象,并将此对象抛出。一旦抛出就不执行其后的代码了。
  • catch:针对抛出的异常对象,进行捕获处理。一旦将异常进行了处理,代码就可以继续执行。(可选结构)
  • finally:finally 结构中的代码是一定会执行的。(可选结构)
try{
    .... //可能产生异常的代码
}
catch(异常类型1 e){
    .... //当产生异常类型1时的处置措施
}
catch(异常类型2 e){
    .... //当产生异常类型2时的处置措施
}
finally{
    .... //无论是否发生异常,都无条件执行的语句
}

何时使用 finally :

  • 在开发中,例如输入流、输出流、数据库连接等资源,在使用完之后必须显式的进行关闭操作,避免内存泄漏。

throws + 异常类型

public void test() throws 异常类型1, 异常类型2, ...{
    // 可能存在编译时异常
}

手动抛出异常对象

通过 throw 关键字手动抛出一个异常对象,如果抛出的异常为 Exception 时,必须处理此异常。

throw 之后的语句不可被执行。

自定义异常类

继承任一异常类,可以仿照异常类重载构造器。

多线程

概念

程序、进程和线程

  • 程序(program):为完成特定任务,用某种语言编写的一组指令的集合。即指一段静态的代码,静态对象。
  • 进程(process):程序的一次执行过程,或是正在内存中运行的应用程序。
    • 每个进程都有一个独立的内存空间,系统运行一个程序即是一个进程从创建、运行到消亡的过程。(生命周期)
    • 程序是静态的,进程是动态的。
    • 进程作为操作系统调度和分配资源的最小单位(亦是系统运行程序的基本单位),系统在运行时会为每个进程分配不同的内存区域。
  • 线程(thread):进程可进一步细化为线程,是程序内部的一条执行路径。一个进程中至少有一个线程。
    • 一个进程同一时间若并行执行多个线程,就是支持多线程的。
    • 线程作为 CPU 调度和执行的最小单位。
    • 一个进程中的多个线程共享相同的内存单元,它们从同一个堆中分配对象,可以访问相同的变量和对象。方便高效的同时可能会带来安全隐患

并行和并发

  • 并行:两个或多个事件在同一时刻发生(同时发生)。指在同一时刻,有多条指令在多个 CPU 上同时执行。
  • 并发:两个或多个事件在同一个时间段内发生。即在一段时间内,有多条指令在单个 CPU 上快速轮换、交替执行,使得在宏观上具有多个进程同时执行的效果

线程的创建方式

继承 Thread 类

步骤:

  1. 创建一个继承于 Thread 类的子类;
  2. 重写 Thread 类的 run( ) ---> 将此线程要执行的操作,声明在此方法体中
  3. 创建当前 Thread 的子类对象;
  4. 通过对象调用 start( ) :启动线程的同时调用当前线程的 run( )。

注意:

  • 如果直接通过对象调用 run( ) ,则没有启动多线程;
  • 已经 start( ) 的线程不能再次执行 start( ) ,否则会报异常。

实现 Runnable 接口

步骤:

  1. 创建一个实现 Runnable 接口的类;
  2. 实现接口中的 run( ) ---> 将此线程要执行的操作,声明在此方法体中
  3. 创建当前实现类的对象;
  4. 将此对象作为参数传递到 Thread 类的构造器中,创建 Thread 类的实例;
  5. 通过对象调用 start( ) :启动线程的同时调用当前线程的 run( )。

匿名实现

new Thread(new Runnable(){
    public void run(){
        // 方法体
    }
}).start();

实现方式对比

  • 共同点:
    • 启动线程,使用的都是 Thread 类中定义的 start( )
    • 创建的线程对象,都是 Thread 类或其子类的实例
  • 不同点:
    • 一个是类的继承,一个是接口的实现
    • 建议使用实现 Runnable 接口的方式,有以下优点:
      • 实现接口的方式避免了类的单继承性的局限性
      • 更适合处理有共享数据的问题
      • 实现了代码和数据的分离
  • 联系:
    • Thread 类也实现了 Runnable 的接口(代理模式)

线程的常用结构

构造器

  • public Thread() :分配一个新的线程对象。
  • public Thread(String name) :分配一个指定名字的新的线程对象。
  • public Thread(Runnable target) :指定创建线程的目标对象,它实现了 Runnable 接口中的 run( )。
  • public Thread(Runnable target, String name) :分配一个带有指定目标新的线程对象并指定名字。

常用方法

  • start() :启动线程,调用线程的 run()
  • run() :将线程要执行的操作,声明在 run() 中;
  • currentThread() :获取当前执行代码对应的线程;
  • getName()setName() :获取和设置线程名;
  • sleep() :静态方法,可以使当前线程睡眠指定时间;
  • yield() :释放 CPU 的执行权;
  • join() :在线程 a 中通过线程调用该方法,会使线程 a 进入阻塞状态,知道线程 b 执行结束后线程 a 才继续执行;
  • isAlive() :判断当前线程是否存活。

过时的方法:

  • stop() :强行结束线程(可能会遗留类似数据库连接的资源),不建议使用;
  • void suspend() / void resume() :可能造成死锁,不建议使用。

线程的优先级

getPriority() / setPriority() :获取和设置线程的优先级

Thread 类内部声明的三个常量:

  • MAX_PRIORITY(10):最高优先级
  • MIN_PRIORITY(1):最低优先级
  • NORM_PRIORITY(5):普通优先级,默认优先级( main 线程具有普通优先级)

线程的生命周期

jdk5之前的

image

jdk5之后的

image

线程安全问题

当使用多线程访问同一个资源时,若多个线程只有读操作,那么不会发生线程安全问题。但是如果多个线程中对资源有读和写的操作,就容易出现线程安全问题。

同步代码块

synchronized(同步监视器){ 
    // 同步监视器俗称锁,拿到锁的线程才可以执行被同步的代码,同步监视器为唯一的对象
    // 需要被同步的代码(即操作共享数据的代码)
}

注意:

  • 在实现 Runnable 接口的方式中,同步监视器可以考虑使用 this
  • 在继承 Thread 类的方式中,同步监视器要慎用 this ,可以考虑使用 当前类.class

同步方法

如果操作共享数据的代码完整的声明在了一个方法中,那么我们就可以将此方法声明为同步方法

权限修饰符 synchronized 返回类型 方法名(){
    // 方法体
}

注意:

  • 非静态的同步方法,默认同步监视器是 this
  • 静态的同步方法,默认同步监视器是 当前类.class
  • 可用来解决单例模式中懒汉式的线程安全问题

优缺点

  • 好处:解决了线程安全问题;
  • 弊端:在操作共享数据时,多线程其实是串行执行的,意味着性能低。

死锁

不同的线程分别占用对方需要的同步资源不放弃,都在等待对方放弃自己需要的同步资源,就形成了线程的死锁。

触发死锁的原因

  • 互斥条件
  • 占用且等待
  • 不可抢夺(或不可抢占)
  • 循环等待

Lock 锁

  1. 创建 Lock 的实例,需要确保多个线程共用一个 Lock 实例,可以考虑将此对象声明为 static final ;
  2. 执行 lock() 方法,锁定对共享资源的调用;
  3. 调用 unlock() 方法,释放对共享数据的锁定。

线程的通信

当我们需要多个线程共同完成一件任务,并且我们希望它们有规律的执行,那么多线程之间需要一些通信机制,可以协调它们的工作,以此实现多线程共同操作一份数据。

三个方法

  • wait() :线程一旦执行此方法,就进入等待状态。同时,会释放对同步监视器的调用;
  • notify() :一旦执行此方法,就会唤醒被 wait() 的线程中优先级最高的那个线程。(如果被 wait() 的多个线程的优先级相同,随机唤醒一个)。被唤醒的线程从当初被 wait 的地方继续执行。
  • notifyAll() :一旦执行此方法,就会唤醒所有被 wait 的线程。

注意:

  • 三个方法必须要在同步代码块或同步方法中使用;
  • 三个方法的调用者,必须是同步监视器;
  • 这三个方法声明在 Object 类中。

对比 wait 和 sleep

  • 相同点:
    • 一旦执行,当前线程都会进入阻塞状态
  • 不同点:
    • 声明位置:
      • wait() :声明在 Object 类中;
      • sleep() :声明在 Thread 类中,静态的;
    • 使用场景不同:
      • wait() :只能使用在同步代码块或同步方法中;
      • sleep() :可以在任何需要使用的场景;
    • 使用在同步代码块或同步方法中:
      • wait() :一旦执行,会释放同步监视器;
      • sleep() :一旦执行,不会释放同步监视器;
    • 结束阻塞的方式:
      • wait() :到达指定时间自动结束阻塞 或 通过被 notify 唤醒结束阻塞;
      • sleep() :到达指定时间自动结束阻塞;

创建线程的其它方式

实现 Callable

与之前的方式相比:

  • call() 可以有返回值,更加灵活;

  • call() 可以使用 throws 的方式处理异常,更灵活;

  • Callable 使用了泛型参数,可以指明具体的 call() 的返回值,更灵活。

如果在主线程中获取分线程 call() 的返回值,则此时的主线程是阻塞状态的。

使用线程池

提前创建好多个线程,放入线程池中使用时直接获取,使用完放回池中。可以避免频繁创建、销毁,实现重复利用。

优点:

  • 提高了程序执行的效率(因为线程已经提前创建好了);
  • 提高了资源的复用率(因为执行完的线程并未销毁,而是可以继续执行其它的任务)
  • 可以设置相关的参数,对线程池中线程的使用进行管理。
// 1.提供指定线程数量的线程池
ExecutorService service = Executors.newFixedThreadPool(10);
ThreadPoolExecutor service1 = (ThreadPoolExecutor) service;
// 设置线程池的属性
service1.setMaximumPoolSize(50);
// 2.执行指定的线程的操作。需要提供实现 Runnable 或 Callable 接口实现类的对象
service.execute(new NumberThread()); // 适用于Runnable
service.execute(new NumberThread1());

//service.submit(Callable callable); // 适用于 Callable

// 3.关闭连接池
service.shutdown();