异常

语法错误不叫异常,异常是指程序执行执行过程中发生的不正常行为

异常分两类:

  • Error:Java虚拟机无法解决的问题,如JVM资源耗尽、内部错误等

    • 该错误一般不编写代码进行处理

    • 栈溢出,java.lang.StackOverflowError

      • Exception in thread "main" java.lang.StackOverflowError
        
      • 问题代码

        public class Error {
            public static void main(String[] args) {
                main(args);//w
            }
        }
        
    • 堆溢出,java.lang.OutOfMemoryError

      •         Animal[] aaa = new Animal[99999*8888];
                Animal[] aaaa = new Animal[99999*8888];
                Animal[] aaaaa = new Animal[99999*8888];
                Animal[] aaaaaa = new Animal[99999*8888];
                Animal[] aaaaaaaa = new Animal[99999*8888];
        
  • Exception:ikˈsepSH(ə)n,中文:除外、例外,因为编程错误或者外部因素导致的问题,可以编写代码处理

    • 空指针异常java.lang.NullPointerException

      • 		   Integer a = null;
                Integer b = null;
                System.out.println(a.equals(b));
        //此段代码出现了空指针异常,对象并没有进行初始化(new),并且equals方法是非静态方法
        
        int[] a = null;
        a[0] = 1;
        //也出现了空指针异常
        
    • 数组越界java.lang.ArrayIndexOutOfBoundsException

      • 		int[] a = new int[10];
                a[10] = 1;
        
        //两个都越界
        
                String s = "01234";
                System.out.println(s.charAt(5));
        
    • 强制类型转换类型时的不匹配java.lang.ClassCastException

      •  Object s = new Integer(1);
         String ss = (String) s;
        
    • 数字格式异常java.lang.NumberFormatException

      • String s = "abc";
        int a = Integer.parseInt(s);
        //类型不匹配
        
    • 输入类型不匹配异常java.util.InputMismatchException

      • Scanner input = new Scanner(System.in);
        int a = input.nextInt();
        
        //此时输入一个不是整型的数会抛出异常
        
    • 算数异常java.lang.ArithmeticException

      • int a = 10, b = 0;
        int c = a / b;
        //除以的是0
        

处理

  • 什么都不处理,任由其自动的抛出异常并终止程序
  • 在编写时考虑异常情况,给出错误的提示和错误的处理

所有的异常都继承于 java.lang.throwable类,throwable中文为抛出异常,该类有两个子类,java.lang.Error类和java.lang.Exception

try-catch-finally

catch中文是抓住

是将可能出现异常的代码集中在一起与不会发生异常的区分开

此时是抓抛模型

如果这段代码有异常,就会在异常处生成一个对应的异常对象,将此对象抛出,其后的代码不再执行

先使用try尝试这段代码,如果有对应的异常就catch抓住执行相应的处理

####格式

try{
    可能有异常的代码;
}catch(异常类型1 变量名1){
    语句;
}catch(异常类型... 变量名...){
    语句;
}catch(异常类型n 变量名n){
    语句;
}finally{ //finally可以不写
    一定会执行的代码;//无论有没有异常,都会执行
}

变量名可以随意写,可直接处理的的异常都继承于java.lang.Exception类,因此最大的异常为Exception,只能捕获一个异常

  • catch中如果多个异常没有字符类关系,那么书写顺序随意
  • 如果有多个异常有子父类关系,那么异常的子类必须写在异常的父类之上,否则会报错
  • try中声明的变量是局部的,只能在try中使用,同理,catch中也是一样的
  • 当catch中出现异常时,如果没有finally语句 会直接抛出异常,如果有finally语句,先抛出异常,再执行finally语句 然后再退出程序
  • 即使catch中有return语句,那么仍会执行finally语句
  • 像是数据库连接之类的资源JVM不会自动回收,因此无论发生什么情况,都需要手动关闭,finally作用就是这个

常用的异常处理方式

方法含义
异常类型变量.getMessage()返回字符串,显示异常的基本原因
异常类型变量.printStackTrance()trance中文痕迹,作用是不直接抛出异常,而是显示异常,即相当于直接抛出异常信息而不停止程序

throws

throws中文为扔掉

向上抛异常,把异常交给上层处理,并没有处理掉异常

方法定义() throws 可能的异常1,...,n;

例子

public class Error {
    public static void main(String[] args) {
        int a = 10, b = 0;
        try {
            test();
        } catch (ArithmeticException e) {
            System.out.println("出现除以0的异常");
        }
        catch (Exception e) {
            System.out.println("异常");
        } finally {
            System.out.println("必须执行的");
        }
        System.out.println("dddd");
    }
    public static void test() throws Exception{ //此时的异常抛给main方法了
        System.out.println(0 / 0);
    }
}

子类重写的方法抛出异常的类型不大于父类被重写的方法抛出的异常类型

如果父类中被重写的方法没有使用throws方式处理异常,子类也不能使用throws方式处理异常

手动抛出异常

throw new 现有的异常类();
//现有的异常类支持重载,并且可以提示信息
throw new 现有的异常类("提示信息");

如果throw new Exception(),必须设置该方法可以throws一个异常

例如

public static void fun(int x) throws Exception{
        throw new Exception("异常");
}

//则其他函数调用这个方法时,必须使用try进行环绕

自定义异常类

  • 新建一个类
  • 使这个类继承于现有的异常类
  • 提供全局常量static final long serialVersionUID
  • 提供重载的构造器,根据需要编写

多线程

多个并行执行的线程称为多线程,一个main方法也可以看作是一个线程

一个进程中的多个线程可以共享相同的内存

CPU的并行:多个CPU同时执行多个任务(多个人同时做不同的事)

CPU的并发:一个CPU同时执行多个任务(多个人同时做一件事)

一个Java程序至少启用3个线程,一个是main()主线程,垃圾回收线程,异常处理线程

  • 同时执行多个任务
  • 需要实现一些等待任务
  • 需要一些后台运行程序

Java的多线程通过java.lang.Thread类实现的,thread中文为线,THred

创建方法

继承于Thread类

  • 创建一个继承于Thread的一个子类
  • 重写Thread类的run()方法,即将这个线程要做的事写在run()方法中
  • 创建子类的对象
  • 通过创建的对象调用start()方法(也是可以在主线程中直接调用run()方法的,但这样调用没有启动多线程,相当于普通的方法调用,此时程序还是按行执行)
    • start()方法的作用(对于一个线程来说,start()方法只能启动一次)
      • 启动当前线程
      • 调用当前线程的run()方法
public class test extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0)
                System.out.println(i);
        }
    }
}

class Main{
    public static void main(String[] args) {
        test aaa = new test();
        aaa.start();
        System.out.println("hello");
    }
}

以上代码的在main()方法的执行过程

  • 初始化3个线程main()主线程,垃圾回收线程,异常处理线程
  • new一个对象,被aaa引用(->13
  • 使用aaa创建一个新的线程(->14
  • 线程aaa中的runmain()中的后续语句同时执行(所以hello输出的地方不唯一,可能在前可能在后)

如果一个线程中的run()的语句只运行一次,可以使用匿名子类的方式

class Main {
    public static void main(String[] args) {
        Thread aaa = new Thread() {
            @Override
            public void run() {
                for (int i = 0; i < 100; i++) {
                    if (i % 2 == 0)
                        System.out.println(i);
                }
            }
        };
        aaa.start();
        System.out.println("hello");
    }
}

常用方法

方法名含义
对象名.start()启动线程
对象名.run()线程启动后的操作
对象名.getName()获取线程名称
Thread.currentThread()中文当前,是一个静态方法,ˈkərənt,作用时返回一个线程(返回值为Thread),通常配合其他方法进行使用,例如获取当前线程的名称可以使用currentThread().getName()
对象名.setName(字符串)设置线程名称
对象名.yield()释放当前CPU的执行权,只要一释放,执行权可能被另外的线程拿到,也可能自己接着拿到执行权。为非静态方法,中文为屈服,yild
对象名.join()该方法会向上抛出异常,所以要使用try-catch进行环绕,将调用join()的线程暂停并让步,让对象名线程先执行,直到对象名线程执行完毕,前提时对象名被执行过start
对象名.stop强制结束当前线程,已过时
对象名.sleep(长整型)静态方法,向上抛出异常,所以要使用try-catch进行环绕,可以直接通过Thread.sleep(数)调用,所有线程都会暂停相应的毫秒数
对象名.isAlive()查看这个线程是否存活,如果存活返回true,否则返回false
public class test extends Thread {
    @Override
    public void run() {
        for (int i = 0; i < 100; i++) {
            if (i % 2 == 0) {
                System.out.println(getName() + ":" + i);
//                System.out.println(currentThread( ) +":"+ i);
            }

        }
    }

    public static void main(String[] args) {
        test aaa = new test();
        aaa.setName("测试1");
        System.out.println(aaa.getName());
        aaa.start();
    }
}

主线程中无法直接使用多线程的一些方法,可以使用Thread.current().方法()

例如给主线程命名

 public static void main(String[] args) {
        Thread.currentThread().setName("主线程");
        System.out.println(Thread.currentThread().getName());
}

Thread中有一个可以直接设置名字的构造器,因此可以在继承的Thread子类中写一个构造器

public 类名(){

}

public 类名(String name){
    super(name);
}

之后调用时:类名 变量名 = new 类名(字符串);

调度

Java调度方法:同优先级的线程组成队列,先到的先服务

高优先级的采用抢占策略

Java线程的优先级最低是1,最高是10,默认是5,以下是优先级的定义常量


    /**
     * The minimum priority that a thread can have.
     */
    public static final int MIN_PRIORITY = 1;

    /**
     * The default priority that is assigned to a thread.
     */
    public static final int NORM_PRIORITY = 5;

    /**
     * The maximum priority that a thread can have.
     */
    public static final int MAX_PRIORITY = 10;

Java中的线程优先级由Thread类中的私有整形成员变量 priority决定,中文为优先

可以使用变量.getPriority()获取优先级,或者变量.setPriority()进行设置优先级

优先级比较高的大概率能先执行完

实现Runnable接口的方式

runnable中文是可运行的

  • 创建一个实现Runnable接口的类
  • 实现Runnable类中的抽象方法run()
  • 创建实现类的对象
  • 将对象作为参数传到Thread类的构造器中,创建Thread类的对象
  • 通过Thread类创建的对象调用start方法
class test2 implements Runnable {//创建一个实现```Runnable```接口的类
    @Override
    public void run() {//重写run方法
        for (int i = 0; i < 100; i++) {
            if(i % 2 == 0){
                System.out.println(i);
            }
        }
    }

    public static void main(String[] args) {
        test2 aaa = new test2();//创建实现类的对象
        Thread aaaa = new Thread(aaa);//使用参数创建Thread类
        aaaa.start();//调用start
    }
}

以上代码中,第12、13行代码可以合并,合并之后Thread aaaa = new Thread(new test2());

在这种实现类的内部,如果要获取线程名字,不能直接使用Thread内置方法,应该使用Thread.current().方法名

线程的生命周期

Java中线程的生命周期状态在Thread.state类中定义

状态:

  • NEW新建
    • new一个对象
  • RUNNABLE执行中
    • 调用start()方法后运行run()方法
  • BLOCKED
    • 阻塞sleep()方法后
    • 执行join()方法后
    • 等待同步锁
    • wait()方法
    • 线程挂起方法(过时)
  • WAITING
  • TIME_WAITING
  • TERMINATED 结束
    • 执行完run()方法
    • 执行stop() 方法
    • 异常未处理

image-20210729185747436

线程的同步

多线程可能会导致程序不稳定,例如

a = 100;
if(a > 10)
    a /= 10;

假设a为static的变量,如果多个线程共同执行会遇到以下情况

  • 线程1到达if 判断成功,进入语句内遭遇阻塞
  • 线程2在线程1阻塞的时候,也到达if 判断成功
  • 理想状态多线程执行完后a = 10,但如果出现以上情况,此时的a = 0

即有共享数据时,两个线程会出现问题

问题原因:某个线程在操作过程中,尚未执行完必须要执行的一段程序,此时其他程序参与进来。也操作代码,导致的问题

解决方法:当某个线程在操作时,其他线程不许参与进来,(即使发生阻塞)直到执行完必须的代码

Java中通过同步机制解决线程问题

同步代码块

synchronized,中文为同步,读音sing·kruh·nized

用法

synchronized(同步监视器){
    //同步的代码
}

同步监视器被称为锁,任何一个类的对象都可以充当锁,多个对象只能用同一把锁,即用同一个地址

通常使用Object当锁

Object obj = new Object();
synchronized(obj){
        //同步的代码
}

好处是解决了安全问题,缺点是相当于一个单线程了

过程:

  • 多个线程一块抢这个锁
  • 抢到后,锁住,执行锁住的语句
  • 执行完之后,解锁,继续重复抢锁的过程

同步方法

如果操作共享数据的代码都在一个方法中,可以将这个方法声明为同步方法

写法:

权限修饰符 synchronized 返回值 方法名(参数列表){
    //语句
}

死锁

不同线程分别占用对方需要的同步资源不放弃,都等待对方放弃自己的同步资源,形成了线程死锁,出现死锁后,不会出现异常,也不会给出提示,所有线程都处于堵塞状态,无法继续

一般嵌套锁会出现这个问题

Lock锁

JDK5.0之后定义的锁

使用ReentrantLock类,中文可重入锁,读音ree·en·truhnt

权限 ReentrantLock 变量 = new Reentrant(参数);

参数为布尔型,默认不写为false

如果写true,代表创建一个公平的锁,即按照先来后到 先进先出 的顺序进行执行线程,假设有三个线程,线程1出来后,不会再让线程1执行相关的代码,而是让其他的依次执行

使用方法

try{
    lock.lock();//锁住
    
    //以下为需要锁住的代码
}finally{
    lock.unlock();//解锁
}

synchronizedreentrant的异同:

  • 相同:都可以解决线程安全的问题
  • 不同:
    • synchronized是执行完代码之后自动的释放同步监视器
    • Reentrant是手动的启动同步和手动的关闭同步

线程的通信

即多个线程有条不紊的按照一定的顺序交替执行

使用前提是在同步代码块和同步方法中,使用lock无法使用wait()和唤醒方法

以下方法都是定义在java.lang.Object类中

wait()方法

必须使用try-catch环绕

使当前线程无限期的堵塞,并释放锁,让其他线程进来

notify()和notifyAll()方法

中文为通知,notify()代表唤醒另一个线程,如果有多个被wait()的 就唤醒优先级高的,notifyAll()代表唤醒其他所有被wait()的线程

sleep()方法和wait()方法的异同

相同点:

  • 都可以使当前线程进入阻塞状态
  • 都需要try-catch进行环绕

不同点:

  • 两个方法的定义位置不同,sleep()定义在Thread类中,wait()定义在object类中
  • 调用范围不同,sleep()在任何需要位置调用,wait()只能用在同步代码块和同步方法中
  • 如果两个都放在同步监视器中,sleep()不会释放锁,wait()会释放锁

JDK5新增的多线程创建的方式

Callable接口和线程池

Callable接口

中文是可调用的,kaa·luh·bl

要比Runnable强大

创建方法

class 类名 implements Callable{
    public Object call() throws Exception{
        //重写call方法
    }
}
Future接口

future中文是未来

Callable接口实现的类要想使用,需要借助Future接口,它有一个唯一的实现接口,FutureTask,同时它也实现了RunnableFutureTask两个接口

  • 实现Callable接口
  • 使用实现类new一个对象
  • 使用FutureTask 变量名2 = new FutureTask(实现类的对象)
  • new一个Thread类的对象,将变量名2传递进去,即Thread 变量名3 = new Thread(变量名2)
  • 调用变量名3.start()
  • 如果想要获取call()方法的返回值使用try-catch环绕并调用变量名2.get()方法
class Call implements Callable {//实现```Callable```接口

    @Override
    public Object call() throws Exception {
        for (int i = 0; i < 100; i++) {
            System.out.println(i);
        }
        return null;
    }

    public static void main(String[] args) {
        Call c = new Call();//使用实现类```new```一个对象
        FutureTask f = new FutureTask(c);//new 一个Future类的对象,并把实现类对象传进去
        Thread t = new Thread(f);//new一个Thread类,将Future类的对象传递进去
        t.start();
    }
    
    	//查看call的返回值
        try {
            System.out.println("返回值为:" + f.get());
        } catch (InterruptedException e) {
            e.printStackTrace();
        } catch (ExecutionException e) {
            e.printStackTrace();
        }
}

使用线程池创建多线程

经常创建、销毁线程会占用特别大的资源

思路是提前创建好多个线程,放入到线程池,需要时直接获取,使用完毕再放回到线程池

Executor类 ɪɡˈzekjətər

创建方式:

  • ExecutorService 变量名1 = new Executor.newFixedThreadPool(线程池数量)
  • 使用传统的方法定义一个派生类(继承于Thread)或者实现Runnable接口(实现run()方法)
  • 变量名1.execute.(实现类/派生类),该方法有多个重载,包括FatureTask
  • 可将线程池做一个关闭操作,即变量名1.shutdown()

该方法创建的多线程便于管理

Q.E.D.


念念不忘,必有回响。